hufoo(フーフー)

2026.01.19
制作

保守性を高める3つの設計原則(DRY・関心の分離・単一責任)とコード整理の手順

保守性を高める3つの設計原則(DRY・関心の分離・単一責任)とコード整理の手順

私は個人でWEBサイトを制作する機会が多いです。

これまでは、運用や保守を伴わない単発のプロジェクトを中心に動いていたこともあり、「機能と見た目」を実装することが最優先で、コードの綺麗さはあまり意識していませんでした。

もちろん、要件を見て「最低限変更が入るであろう所」だけは保守性を考えて作っていましたが、正直なところ、その恩恵を感じることはありませんでした。

そんな時、過去に自分が制作したプログラムに機能追加をする機会がありました。 「自分が書いたコードだから簡単だろう」と思っていたのですが、いざ開いてみると全く整理されておらず、大まかな概要を把握するだけでもかなりの時間を要してしまったのです。

書いた本人が把握するのにもこんなに分かりづらく、ストレスが溜まるコード。 これをもし、他の人に見られているかもしれないと思うとぞっとしました。

それ以来、今では保守性の高いコードを意識して書くようになりましたが、気を抜くと当時の私が顔を出してきます。

そうならないように、今回は今の私が意識している**「DRYの法則」「関心の分離」「単一責任の原則」**について、自戒を込めて書いていこうと思います。

DRY原則 (Don't Repeat Yourself)

同じコードを2度書かない。共通化し、一箇所から参照する。

これは大前提ですよね。プログラミングがプログラミングたる所以とも言えます。

新卒で働いた会社の先輩から口酸っぱく言われたのは、今ではいい思い出です。

でも納期に追われていると、とりあえず形にすることがゴールになってしまって、

「すまん!あとで分ける!とりあえずコピペ!」

をしてそのまま忘れ去り、納品に至ってしまうことも……あったりなかったり。

DRY原則のメリット

なんといってもバグ修正や仕様変更が楽です。

複数個所で利用されていても、おおもとの1カ所を直すだけで済むんですから。

DRY原則が守られていないと、一括置換で何とかしようとして事故る……なんてこともなくなります。

  • バグ修正が楽
    • 1箇所直せば全部直る
  • 仕様変更が楽
    • 1箇所変えれば全部変わる
  • 一貫性が保てる
    • 同じロジックが必ず同じ動作をする

DRY原則の注意点

コードを書いていると、「全く違う使い方だけど処理は同じ」や「だいたい同じようなことをしている処理」に出くわします。

それを無理やり共通化してしまうことを**「過度なDRY」**といいます。

「処理が同じでも、意味が違うなら分けるべき」と心得て、共通化できても利用目的が違う場合は分けておきましょう。

【NG】処理は同じだけど、意味が違うものを共通化してしまっている

// 処理は同じだけど、意味が違うものを共通化してしまっている
function calculate10Percent(price: number) {
  return price * 0.1;
}

const tax = calculate10Percent(300);    // 税金
const point = calculate10Percent(3000); // ポイント

【OK】意味ごとに分ける

function calculateTax(price: number) {
  return price * 0.1;  // 税率10%
}

function calculatePoint(price: number) {
  return price * 0.1;  // ポイント還元率10%
}

const tax = calculateTax(300);
const point = calculatePoint(3000);

関心の分離 (Separation of Concerns)

異なる責務を別々の場所に分ける

これを最初に聞いたときは、「関心ってなんだよ」「責務って怖い」と身構えてしまいました。

でも、きちんと把握してみると「なるほど、エンジニアしてるなー」と腹落ちする内容でした。

かなりざっくりですが、私は以下のように脳内変換しています。

「関心 = 責務 = やってること」

表で見るとイメージが湧きやすいと思います。(Next.jsをベースに考えています)

関心 置き場所
型定義 types/ Recipe, Material
データ取得 lib/api/ fetchRecipes
計算・変換 lib/utils/ formatDate
状態管理 lib/hooks/ useMenuAdd
見た目 components/ Button
ページ構成 app/ or pages/ page.tsx

冒頭でお話しした「過去のコードの概要を把握するのに時間がかかった」のは、まさにこの分離ができておらず、全てがごちゃ混ぜになっていたからでした。

要は、**「機能ごとに整理整頓しておきましょう」**ということです!

関心の分離のメリット

  • 見通しが良い
    • 「APIを修正したい」となったら lib/api/ を見ればOK!ファイルへのアクセスが迷子になりません。
  • 影響範囲が限定される
    • APIの処理を変えても、計算処理(utils)には影響しません。1ファイルに全部書いていると、修正のたびに「どこか壊れてないか?」と怯えることになります。
  • テストしやすい
    • 機能が独立しているため、他の機能に依存せずにテストできます。準備が少なくて済みます。
  • チーム開発しやすい
    • 「私はUIを作るから、APIの接続お願い!」といった業務分担がスムーズになります。

私個人としては「見通しが良い」ことにかなりメリットを感じていますが、本格的にこれをやり始めたきっかけは**「チーム開発」**でした。

プレイングPMとして参加した案件で、「さすがに必要だよな……」と思い、付け焼刃で学んで導入してみたんです。

すると、びっくりするくらい制作がスムーズに進みました。

「うわ、これを知らずにやってたのか……」と、今までの自分の無知さを全力で恥じた瞬間でもありました。

単一責任の原則 (Single Responsibility Principle)

1つのファイル(関数・コンポーネント)は、1つの責務だけを持つ

正直に告白すると、私はこの原則を理解するのにかなり時間がかかってしまいました。 というのも、「関心の分離」との違いがよく分からなかったからです。

「関心の分離をしていれば、結果的に単一責任の原則もできているんじゃないか?」 頭の固い私はそう思ってしまい、なかなか腹落ちさせることが出来ませんでした。

ただ、そこで立ち止まっていても仕方がないので、私はざっくりとこう理解することにしました。

「その関数の役割を一言で説明できるか?」

「これは注文処理をして、メールも送って、ログも残す関数です」と説明に「〜して、〜する」という接続詞が入るならアウト。 「これは税金の計算をする関数です」と言い切れるならOK。

これくらいシンプルなルールを設けることで、迷わず実装できるようになりました。

単一責任の原則のメリット

  • 読みやすい
    • 1つの関数が10行〜30行くらいで収まるので、パッと見て何をしているか把握できます。
  • テストしやすい
    • 1つの機能だけテストすればいいので、準備が少なく手軽に実施できます。
  • 再利用しやすい
    • 「税計算」だけが切り出されていれば、他の場所でも使い回せます。
  • 修正しやすい
    • 影響範囲が限定的なため、修正によるデグレ(バグの再発)を恐れる必要が減ります。

単一責任の原則の例

【NG】1つの関数が複数の責務を持つ

function processOrder(order: Order) {
  // 責務1: バリデーション
  if (!order.items.length) throw new Error("Empty order");
  if (!order.address) throw new Error("No address");

  // 責務2: 合計計算
  const subtotal = order.items.reduce((sum, item) => sum + item.price, 0);
  const tax = subtotal * 0.1;
  const total = subtotal + tax;

  // 責務3: データベース保存
  await db.orders.create({ ...order, total });

  // 責務4: メール送信
  await sendEmail(order.email, "ご注文ありがとうございます");

  // 責務5: ログ記録
  console.log(`Order ${order.id} processed`);

  return { total };
}

【OK】1関数1責務 メインの関数は、専門家たち(個別の関数)に指示を出すだけにします。

// 各機能は「一言で説明できる」サイズに切り出す
function validateOrder(order: Order): void {
  if (!order.items.length) throw new Error("Empty order");
  if (!order.address) throw new Error("No address");
}

function calculateOrderTotal(order: Order): number {
  const subtotal = order.items.reduce((sum, item) => sum + item.price, 0);
  const tax = subtotal * 0.1;
  return subtotal + tax;
}

async function saveOrder(order: Order, total: number): Promise<void> {
  await db.orders.create({ ...order, total });
}

async function sendOrderConfirmation(email: string): Promise<void> {
  await sendEmail(email, "ご注文ありがとうございます");
}

function logOrderProcessed(orderId: string): void {
  console.log(`Order ${orderId} processed`);
}

// メイン処理: 各関数を呼び出すだけ(指揮者)
async function processOrder(order: Order) {
  validateOrder(order);
  const total = calculateOrderTotal(order);
  await saveOrder(order, total);
  await sendOrderConfirmation(order.email);
  logOrderProcessed(order.id);
  return { total };
}

DRY・関心の分離・単一責任の関係

clean-code-principles-practice-01.png

先ほどの章でも触れましたが、私は頭が固く、勉強段階ではなかなか理解できない箇所もありました。しかし、実際にコードを書きながら実践してみると、「あ、こういうことか!」とすぐに納得することが出来ました。

特につまづいていた「関心の分離」と「単一責任の原則」の違いについては、自分の中でこう定義することで上手く運用できるようになりました。

関心の分離 = 「置き場所のルール」単一責任の原則 = 「中身のチェック」

「どこに置くか」を決めるのが関心の分離、「その中身が太りすぎていないか」を確認するのが単一責任の原則。こう考えると、迷いがなくなります。

実際に3つの手法を実践する手順

私は、**「まずは1ページに全部書いてから、後で分ける」**という制作方法を採用しています。 そのため、作り始めの段階では page.tsx が500行を超えるような「魔境」と化すことがよくあります。

そこから、下記の手順で整理を実施していきます。

  1. まずは動くコードを書く(魔境を作る)
  2. DRY原則:「同じコード書いてない?」
  3. 関心の分離:「これはどの責務?(どこに置く?)」
  4. 単一責任の原則:「この関数、一言で説明できる?」

具体的なイメージはこんな感じです。

Step 1:【DRY原則】1ファイルに全部入っている状態 まずは動くことが最優先。この時点では汚くてOKです。

// pages/recipes.tsx - 500行の巨大ファイル

function RecipesPage() {
  // API呼び出し、計算、状態管理、UIが全部ここに...
  // 重複コードもちらほら...
}

Step 2:【関心の分離】ざっくり分ける(置き場所を決める) 役割ごとにファイルを移動させます。

  • lib/api/recipes.ts ← API呼び出しをごそっと移動
  • lib/utils/nutrition.ts ← 計算系をごそっと移動
  • lib/hooks/useRecipes.ts ← 状態管理を移動
  • pages/recipes.tsx ← UIだけ残る

Step 3:【単一責任の原則】中身を整理する(中身をチェック) ざっくり分けただけでは、移動先のファイル内が整理されていないことがあります。最後にここを整えます。

  • fetchRecipes()
  • fetchRecipeById()
  • saveRecipe()
  • deleteRecipe()
  • updateRecipeWithValidation() ← 「更新」と「検証」が混ざってる!分けよう。
    • → validateRecipe()
    • → updateRecipe()

おわりに

ここまで読んでいただいた方は薄々お気づきかもしれませんが、こういった設計原則や概念は、本や記事で読んだだけでは「ふーん、なるほど」で終わってしまうことがほとんどです。

私自身、頭では分かったつもりになっていても、実際に現場で冷や汗をかきながら手を動かしてみて初めて、「あ、あの時先輩が言っていたのはこういうことか!」と腹落ちしました。

もちろん、無理をする必要はありません。もし納期に余裕があるときは、ぜひ下記のことからやってみてください。

  • 「今日はコピペせずに共通化してみようかな」
  • 「この関数、ちょっと長すぎるから分けてみようかな」

過去の私のような悲劇が、世界から一つでも減ることを祈っています!

SNS・メールを登録して
更新情報を受けとる