0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

良い設計と悪い設計を分けるもの ── 設計概念とパターンの活用

0
Posted at

はじめに

設計レビューで「なぜこの構造にしたのか?」と聞かれて、
「なんとなく」「AIが出力したから」くらいしか答えられなかった経験はないでしょうか。
自分は何度かあります。
良い設計と悪い設計の違いはセンスで決まるのか——正直しばらくそう思っていました。

でも実際には、設計判断の背後には半世紀以上かけて蓄積された普遍的な概念と、
先人が検証してきたパターンの体系があります。
「センス」ではなく「言葉」で設計を説明するための道具が、
ちゃんと存在しているんです。

この記事では、その「言葉」となる設計概念と、
知恵の蓄積であるパターンの2つを整理してみます。

TL;DR

  • 良い設計は「関心の分離でモジュールに分け、情報隠蔽で独立性を高め、
    段階的に詳細化し、リファクタリングで進化させる」という概念の組み合わせで作られる
  • パターンは「暗記するもの」ではなく、
    「問題・文脈・力を整理し、検証済みの解法を検索・適用する思考法」
  • 設計概念は判断の拠り所、パターンは知恵の蓄積。
    両方を使いこなすことで設計の質と効率が上がる

設計はなぜ必要か ── 要件と実装をつなぐ意思決定

要件が固まったら、すぐコードを書き始めたい。そう感じる場面は実務でもよくあります。
設計工程は地味で、成果物が見えにくい。一方コードは動く。
だから「設計を飛ばして、書きながら考える」という誘惑が生まれる。

このセクションでは、設計を飛ばすと何が起きるのか、
そして設計が果たす本質的な役割を整理します。

設計を飛ばすとどうなるか ── 技術的負債の構造

ユースケースが揃った段階で、勢いでコーディングに入りたくなることは確かにあります。
ただ、コンポーネント同士の関係を考えずに書き始めると、
後から変更が困難な構造になりやすい。

ここで出てくるのが 技術的負債(テクニカルデット) という考え方です。
「目先の速さを優先して手っ取り早い解決策を選ぶことで、後から支払うことになる追加コスト」を指します。

ローンに似ています。

返済タイミング       利子の総額
-----------------  --------
すぐ返す             少ない
放置して後で返す      大きく膨らむ

ソフトウェアをインクリメンタルに作る以上、技術的負債をゼロにするのは不可能です。
重要なのは「ゼロにする」ことではなく、意識的に管理すること。
定期的なリファクタリングで返済していく姿勢が大切になります。

そして設計には、もう1つ大きな役割があります。
それは「品質を育む場」としての役割です。

コードが動き始めてしまうと、品質の問題に気づいてもなかなか後戻りできません。
一方で設計の段階なら、まだ手戻りのコストは低い。
設計はテストやコード生成の前に、
品質を評価して改善できる「最後の安価なタイミング」として機能します。

設計モデルが果たす役割

設計は、要件モデル(シナリオ・クラス・振る舞い)を「構築可能な青写真」に変換するプロセスです。入力と出力をシンプルに描くと、こんな流れになります。

[要件モデル]                       [設計モデル]                  [構築]
- シナリオ              ────→   1. データ/クラス設計    ────→   コード生成
- クラス                        2. アーキテクチャ設計           テスト
- 振る舞い                      3. インターフェース設計          デプロイ
                               4. コンポーネント設計

4つの設計モデルそれぞれの役割は次のとおりです。

設計モデル 何を決めるか
データ/クラス設計 分析クラスを設計クラスに変換し、必要なデータ構造を定義する
アーキテクチャ設計 主要な構造要素間の関係、スタイル、パターンを定義する
インターフェース設計 外部システム・人間・内部コンポーネント間の情報の流れを定義する
コンポーネントレベル設計 アーキテクチャの構造要素を手続きレベルの記述に落とし込む

設計はこれら4要素を一気に決めるのではなく、2つの軸で進めます。
プロセス次元(タスクの進行)と抽象次元(抽象度の段階的な低下)です。

最初は粗い全体像から始めて、反復のたびに具体度を上げていくイメージです。

ここで一緒に意識したいのが品質属性です。
性能、信頼性、保守性、セキュリティなど、ソフトウェアに求められる品質面の特性を指します。

設計プロセスを通じてこれらが達成されているかを評価し続けること
——それが、品質を「育む」という言葉の具体的な意味になります。

設計の品質ガイドライン

では、良い設計とは具体的に何でしょうか。
判断の出発点として、まず以下の3つの条件を押さえておくと整理しやすいです。

  • 要件モデルの全明示的要件を実装し、暗黙的要件にも対応していること
  • コード生成者・テスター・保守者にとって読みやすく理解しやすいガイドであること
  • データ・機能・振る舞いの各ドメインから完全な描写を提供すること

そして、これらを支える具体的な品質ガイドラインを8項目で整理すると次のようになります。

# ガイドライン 一言要約 怠るとどうなるか
1 アーキテクチャの確立 認識可能なスタイル/パターンに沿い、進化的に実装可能な構造を持つ 構造が場当たり的になり、拡張のたびに既存箇所が壊れる
2 モジュール性 論理的に分割された要素・サブシステムから構成される 巨大な一枚岩になり、変更影響が読めない
3 表現の独立性 データ・アーキテクチャ・インターフェース・コンポーネントを別々に表現できる 表現が混ざり、変更時にどこを直せばいいか分からなくなる
4 適切なデータ構造 認識可能なデータパターンから導かれた構造を採る データ操作が冗長・非効率になる
5 機能的独立性 各コンポーネントが独立した機能特性を持つ 1つの変更が他のコンポーネントに広く波及する
6 インターフェースの単純化 コンポーネント間・外部環境との接続の複雑さを下げる 結合が強くなり、テストや交換が困難になる
7 反復可能な手法 要件分析の情報をもとに反復可能な方法で導かれる 設計が属人化し、再現性が下がる
8 効果的な記法 意味が伝わる記法で表現される 読み手によって解釈が割れ、後工程で不整合が発生する

このガイドライン8項目は「偶然には達成されない」性質のものです。
基本的な設計原則の適用、体系的な方法論、徹底的なレビュー
——この3つの組み合わせによってはじめて達成されます。

ここから先のセクションでは、
これらのガイドラインを達成するための「設計概念」を見ていきます。

設計概念 ── 良い設計の判断基準を手に入れる

品質ガイドラインが「良い設計の条件」だとすれば、
設計概念はその条件を達成するための「思考の道具」です。
ここでは半世紀以上の歴史を通じて磨かれてきた設計概念を、
3つの問いで整理し直してみます。

観点 問い 主な概念
分割する どう分けるか? 関心の分離、モジュール性、抽象化
独立させる 分けたものをどう独立させるか? 情報隠蔽、機能的独立性、凝集度・結合度
進化させる どう育てていくか? 段階的詳細化、リファクタリング、設計クラス

並列に並べると独立した道具に見えますが、
実際の設計判断の中では、これらは問いに対する答えの束として連動して現れます。

複雑さに立ち向かう ── 分割の技法

ソフトウェアの複雑さに立ち向かう、最も基本的な戦略は「分割」です。
1つの巨大な問題を、独立に解ける小さな問題に分けていく。
これは「分割して統治せよ」という古典的な戦略の系譜にあります。

この戦略を支えるのが 関心の分離(Separation of Concerns) という考え方です。
複雑な問題を、独立に解ける小さな問題に分割する設計概念です。

その背景には、2つの問題を同時に解こうとしたときの体感的な複雑さは、
それぞれを個別に解いたときの複雑さの合計より大きい——という経験則があります。

ということは、分割するだけで「全体の複雑さの体感量」を下げられるわけです。

関心の分離を実現する最も一般的な形がモジュール性です。
ソフトウェアを独立した名前付きの部品(モジュール)に分割し、
それを統合して全体を作る。

モノリシックな(巨大な一枚岩の)プログラムは、
変数が何百個も並び、制御パスが網目のように走っているような状態です。
これでは人間の頭で全体を追いきれません。

ここで気をつけたいのが、「分ければ分けるほど良い」わけではないという点です。

image.png

モジュール数を増やすほど1モジュールあたりの開発コストは下がりますが、
モジュール間を統合するコストは増えていきます。
合計の開発コストが最小になる「ちょうどよいモジュール数」が存在するわけです。

正確な値を予測する方法は今のところありませんが、
「適度さがある」と意識するだけでも判断の精度は上がります。

ちなみにモジュール化する具体的なメリットは次のあたりです。

  • 開発計画を立てやすい
  • インクリメンタルに開発できる
  • 変更を局所化できる
  • テストとデバッグが効率化される
  • 長期保守の副作用が抑えられる

そして、もう1つ重要な概念が抽象化です。
「詳細を隠して本質だけを見せる技法」のことで、
モジュールの「見せ方」を制御する役割を果たします。
抽象化には2種類あります。

  • 手続き的抽象化: 特定の機能を持つ一連の命令に名前を付けて、内部の詳細を隠す
  • データ抽象化: データオブジェクトの属性の集合に名前を付ける

たとえば「ログインする」という手続き名は、
認証サーバーへの通信、トークン管理、セッション作成
といった一連の手順を1つの言葉に畳み込んでいます。これが手続き的抽象化です。

一方、「ユーザー」というデータ抽象化は、
ID・名前・メールアドレスといった属性の集合を1つの概念として扱います。

抽象化は段階的に降りてきます。
最も高い抽象レベル(問題の言語で解を述べる)から始まり、中間レベル(問題指向と実装指向の混合)を経て、最も低い抽象レベル(直接実装可能な形)に至る。
設計はこの階段を一段ずつ降りていく作業ともいえます。

ここまでに出てきた3つの概念は、
独立した別々の道具ではなく因果関係を持って積み重なるレイヤーとして機能します。

[関心の分離]   ← 「複雑な問題は分割して解く」という根本思想
     │
     ↓ 実現手段として
[モジュール性] ← 独立した名前付きの部品に分ける
     │
     ↓ 部品の見せ方を制御するのが
[抽象化]       ← 部品の境界とインターフェースを定義する

関心の分離が根底にあり、モジュール性がその実現手段、抽象化がモジュールの境界と見せ方を定義する。この3つは順に重ねていくレイヤーのような関係になっています。

なお、ソフトウェアの「全体構造とその構造が提供する概念的整合性」を扱うアーキテクチャも設計の重要な概念です。
ただし、すでに設計モデルの1要素として位置づけ済みなので、
本記事では概観に留め、深入りはしません。

部品の「質」を測る ── 独立性の評価基準

モジュールに分割できたとして、その分割が「良い分割」かどうかをどう判断するか。
ここで鍵になるのが機能的独立性という考え方です。

機能的独立性は、関心の分離・モジュール性・抽象化、
そしてこのあと出てくる情報隠蔽の帰結として生まれる特性です。
各モジュールが「単一の目的」に集中し、
他のモジュールとの過度な相互作用を避ける状態を指します。

なぜ独立性が重要なのか、を整理しておきます。

  • 開発が容易: 機能がコンパートメント化されているため、
    インターフェースが単純で並行作業しやすい
  • 保守が容易: 副作用が限定的で、エラー波及が低減し、再利用も視野に入る
  • テストが容易: 単体で切り出して検証できる

情報隠蔽 ── 余計なことを見せない

独立性を支える代表的な原則が 情報隠蔽(Information Hiding) です。
モジュールの設計判断(アルゴリズムとデータ)を外部から見えないようにし、
モジュール間の通信を「必要な情報だけ」に限定する考え方です。

[モジュールA]                       [モジュールB]
┌────────────────┐                 ┌────────────────┐
│ 公開IF          │ ←── 必要な ──→   │ 公開IF         │
│ ────────────── │     情報だけ      │ ──────────────│
│ アルゴリズム     │                  │ アルゴリズム    │
│ 内部データ       │ (外部から        │ 内部データ      │
│ 実装の選択       │   見えない)      │ 実装の選択      │
└────────────────┘                 └────────────────┘

情報隠蔽の最大のメリットは、変更時のエラー波及を防げる点にあります。
あるモジュールの内部実装を変えても、外部から見た振る舞いが変わらなければ、
他のモジュールへの影響を心配しなくて済む。

テスト時や保守時に、変更が予期しない場所に飛び火するリスクを大きく下げてくれます。

凝集度と結合度 ── 独立性を測る2つの基準

独立性は感覚で評価するものではなく、定量的に測れる基準があります。
それが凝集度結合度です。

何を測るか 理想
凝集度(Cohesion) モジュール内の機能的な強さの度合い 高い(1つのことに集中している)
結合度(Coupling) モジュール間の相互依存の度合い 低い(依存関係が単純)

具体例で見てみます。

[高凝集・低結合 ── 理想的な状態]
┌────────────────────┐    最小限の      ┌────────────────────┐
│  ユーザー認証        │ ←──────────→    │  パスワード検証      │
│ - ログイン処理       │   インターフェース │ - ハッシュ照合       │
│ - ログアウト処理     │                 │                    │
└────────────────────┘                 └────────────────────┘
   1つの責務に集中                         1つの責務に集中

[低凝集・高結合 ── 危険信号]
┌─────────────────────────────────────┐
│  UserManager                        │
│ - ログイン処理                        │ ←─┐
│ - メール送信                          │   │ あちこちから
│ - PDF生成                            │ ←─┤ 内部実装に
│ - データベース直接操作                  │   │ 依存される
│ - 売上集計                            │ ←─┘
└─────────────────────────────────────┘
   無関係な機能の寄せ集め

理想的にはモジュールは1つのことだけを行う(高凝集)。
複数の無関係な機能を詰め込んだモジュール(低凝集)は避ける。
結合度は、インターフェースの複雑さ・参照のポイント・通過するデータに依存するため、
可能な限り低く抑える。

目指すべきは「高凝集・低結合」。
これが独立性の高いモジュールの条件です。
設計レビューでよく聞く「このクラス、責務が多すぎませんか?」というコメントは、
凝集度が低くなっていることへの違和感を言葉にしたもの、
と捉えると腑に落ちやすいです。

設計を育てる ── 進化の技法

設計は一度で完成するものではなく、反復的に育てていくものです。
最初から完璧を目指して手が止まるより、
粗いものから始めて段階的に洗練していくほうが、結果的に良いものになる。
そのための技法を見ていきます。

段階的詳細化 ── 抽象から具体へ降りていく

段階的詳細化(Stepwise Refinement)は、トップダウンの設計戦略です。
機能のマクロなステートメント(手続き的抽象化)を段階的に分解し、
最終的にプログラミング言語のレベルまで具体化していきます。

[高抽象レベル]
  「ユーザーの注文を処理する」
        │
        ↓ 分解
[中抽象レベル]
  「在庫を確認する」「決済を実行する」「配送手配を行う」
        │
        ↓ さらに分解
[低抽象レベル]
  「在庫テーブルからSKUごとの在庫数を取得し、
   注文数量と比較して不足していれば例外を返す」
        │
        ↓ 実装に落とす
[コード]
  def check_stock(sku: str, qty: int) -> None: ...

抽象化と詳細化は相補的な概念です。
抽象化は詳細を隠して全体像を見せ、詳細化は隠された詳細を順に明らかにする。
この2つを行き来しながら設計を進めていきます。

「いきなり全部書く」のではなく「段階的に具体化する」。
この考え方は、設計だけでなく文章を書くときや問題を解くときにも応用が効きます。
汎用的な思考法だなと自分は感じています。

リファクタリング ── 振る舞いを変えずに構造を直す

リファクタリングは、外部から見た振る舞いを変えずに内部構造を改善する技法です。

[Before: 低凝集なコンポーネント]
┌──────────────────────────────────┐
│  ReportComponent                 │
│ - 売上レポート生成                  │
│ - 在庫レポート生成                  │
│ - 顧客レポート生成                  │
└──────────────────────────────────┘
   3つの関連の薄い機能が同居

           ↓ リファクタリング

[After: 高凝集な3つのコンポーネント]
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Sales        │ │ Inventory    │ │ Customer     │
│ Report       │ │ Report       │ │ Report       │
└──────────────┘ └──────────────┘ └──────────────┘
   各コンポーネントが1つの責務に集中

冗長性の除去、未使用の設計要素の排除、非効率なアルゴリズムの改善、不適切なデータ構造の修正。こうした改善を、外部の振る舞いを保ったまま内部に加えていく。
アジャイル開発で特に重要視される技法です。

振る舞いを変えないつもりでも、副作用は起こり得ます。
リファクタリングはテストの裏付けがあって初めて安全に行えるもの、
という前提を忘れないようにしたいところです。
リファクタリング支援ツールには、コード変更を分析して
「振る舞いの変化を検出するテストスイートを生成する」ような機能を備えるものもあります。

設計クラス ── 実装を導く具体度に持っていく

設計クラスは、分析クラス(問題領域の概念を表す高い抽象度のクラス)に、
実装に必要な技術的詳細を加えて洗練したものです。

分析クラスがユーザーから見える要素を記述するのに対し、
設計クラスは実装を導く技術的詳細を提供します。

良い設計クラスには、次の4つの特性があります。

特性 判定基準 違反時の兆候
完全性と充分性 クラス名から合理的に期待される全属性・全メソッドを備え、過剰なメソッドを持たない クラス名と実態が乖離する、または不要な機能が混入する
原始性 各メソッドはクラスの1つのサービスに集中し、同じことを達成する別の方法を提供しない 同等のメソッドが複数あって使い分けが曖昧
高凝集 小さく焦点の絞られた責務を持ち、属性とメソッドをそれらの責務の実現に集中させる 責務が肥大化、無関係な属性・メソッドが混在
低結合 クラス間のコラボレーションを必要最小限にとどめる あちこちのクラスを直接呼び出している

低結合の指針として有名なのがデメテルの法則です。
「友人にだけ話しかけよ(don't talk to strangers)」というスローガンで、
メソッドは隣接クラスのメソッドにのみメッセージを送るべき、
という考え方を示しています。

a.getB().getC().doSomething()のように内部の内部を辿って操作する書き方は、
デメテルの法則に違反する典型例です。

ここまで扱った概念——関心の分離、モジュール性、抽象化、情報隠蔽、機能的独立性、凝集度・結合度、段階的詳細化、リファクタリング——はいずれも、
特定の設計手法に依らず普遍的に適用される原則として機能します。

設計レビューのチェックポイントとしてもそのまま使えるはずです。

パターンという知恵の体系 ── 車輪を再発明しない

ここまでで設計概念を手に入れ、
設計レビューで「なぜこの構造にしたのか」を説明する語彙はかなり揃いました。
ただ実際の設計では、
「この問題、似たような形で誰かが解決済みなのではないか?」
と感じる場面が必ず出てきます。
新規の問題に毎回ゼロから挑むのは、車輪の再発明そのものです。

そこで登場するのがパターン——設計概念を適用した結果の、検証済みの解法カタログです。

パターンとは何か ── 問題・文脈・力・解決策

パターンは、ある文脈の中で繰り返し現れる問題と、
その解決策の核心を記述したものです。
建築の世界で生まれた概念をソフトウェアに持ち込んだもので、
構成要素は次の4つに整理できます。

  • 文脈(context): 問題が存在する環境
  • 力の体系(system of forces): 問題の解釈と解決策の適用を左右する要件・制約・限界
  • 解決策(solution): 力の体系を最もよく満たす提案
  • 結果(consequences): 解決策を適用した場合のトレードオフ

「問題は1つでも、解決策は文脈と力次第で変わる」というのがパターン的思考の核心です。
同じ問題でも、性能を優先する文脈と保守性を優先する文脈では、
最適なパターンが異なる。

そして、パターンには種類があります。
設計の各レベルに対応するパターンが整理されています。

種類 何を扱うか 適用場面
アーキテクチャパターン 大局的な設計問題を構造的アプローチで解決 システム全体の骨格設計
データパターン データ指向の問題とモデリング解決策 データベース・データ構造設計
コンポーネントパターン(=設計パターン) サブシステム・コンポーネントの開発と通信 クラス・コンポーネントレベル
インターフェース設計パターン UIの問題とエンドユーザーの特性を考慮した解決策 ユーザーインターフェース設計
WebAppパターン Webアプリケーション構築に関する問題 Webサービス開発
モバイルパターン モバイルプラットフォーム固有の問題 モバイルアプリ設計
イディオム 特定言語のアルゴリズムやデータ構造の実装 実装レベルの定石

オブジェクト指向設計の文脈でよく出てくる「Gang of Four(GoF)パターン」は、
この中のコンポーネントパターン領域を3つに分類したものです。

  • 生成パターン: オブジェクトの生成・合成・表現に関する問題
    (Factory Method、Abstract Factory、Builder など)
  • 構造パターン: クラスやオブジェクトの組織化・統合方法
    (Adapter、Composite、Proxy、Decorator、Facade など)
  • 振る舞いパターン: オブジェクト間の責任割当と通信方法
    (Chain of Responsibility、Command、Iterator、Mediator、Visitor など)

なお、Pipes and Filtersは「フィルタを直列に繋いでデータを流す」構造を扱います。
GoFのコンポーネントレベルの構造パターンではなく、
システム全体の骨格を扱うアーキテクチャパターンとして分類されるのが一般的です。

また「パターン」という言葉は、
現象を記述するだけで解決策を提供しない用法でも使われます。
ソフトウェア設計で扱うのは検証済みの解法までセットで提示するパターンであり、
本記事の「パターン」もこの意味で使っています。

パターンの構造と探し方

パターンを使いこなすには、パターンが「どう書かれているか」を知る必要があります。パターン記述には共通のテンプレートがあり、これを理解しておくと検索もしやすくなります。

┌─────────────────────────────────────────────────┐
│  パターンテンプレート                               │
├─────────────────────────────────────────────────┤
│  パターン名:    本質を短く表現する名前               │
│  問題:          パターンが対処する問題              │
│  動機:          問題の具体例                       │
│  文脈:          問題が存在する環境                  │
│  力:            制約・限界の体系                   │
│  解決策:        問題に対する詳細な提案               │
│  意図:          パターンの目的と機能                │
│  コラボレーション: 他パターンとの関係                 │
│  結果:          実装時のトレードオフ                │
│  実装:          実装時の特記事項                   │
│  既知の使用例:  実際のアプリでの使用例                │
│  関連パターン:  関連パターンへの相互参照              │
└─────────────────────────────────────────────────┘

パターン名はとくに重要です。
検索性に直結するため、本質を短く表現する命名が求められます。

「パターンを知っている」とは「カタログのエントリを読んだことがある」だけでは不十分で、名前を聞いて問題の像が浮かぶ状態になっているのが望ましいです。

フレームワーク ── パターンとの違い

パターンと混同しやすい概念にフレームワークがあります。
フレームワークは、パターンが組み込まれる実装レベルの骨組み、
いわば再利用可能な「ミニアーキテクチャ」です。

観点 パターン フレームワーク
抽象度 高い(コード非依存) 低い(コードで具現化)
適用範囲 どのドメインでも適用可能 特定ドメインに属する
大きさ 小さい設計要素 より大きなアーキテクチャ要素
内包関係 フレームワークに内包される パターンを内包できる

フレームワークは「プラグポイント」(フックやスロット)と呼ばれる拡張点を持っていて、
そこに問題固有のクラスや機能を組み込むことで、
自分のアプリケーションに適合させていく仕組みになっています。

WebアプリケーションフレームワークやUIフレームワークを使ったことがあれば、
感覚的には馴染みやすいはずです。

AIによるパターン発見・推奨

パターンには「ドキュメント不足のため情報が失われやすい」という構造的な課題があります。コード上にパターンが実装されていても、明示的に「これは○○パターンです」と書かれていない限り、後から読み手が認識するのは難しい。

この課題に対する取り組みは、時代によって主役が変わってきました。

時期 主なアプローチ 何ができるか
初期 機械学習でソースコード解析 パターンの存在を認識・検出
近年 LLMを組み込んだIDE・AIコーディングアシスタント コードベースの読解、パターンの示唆、置き換え提案

レビューの前段階で「第一の読み手」として機能してくれるイメージです。

AIアシスタントから受け取れるフィードバックの例:

  • 「このコードは Strategy パターンに置き換えられそうです」
  • 「ここは Builder パターンの適用箇所に見えます」
  • 「このクラスは The Blob の兆候があります(責務が多すぎる)」

パターンで考える ── 設計プロセスへの組み込み

パターンを「カタログとして知っている」ことと、
「設計プロセスの中で使いこなせる」ことの間には、けっこう距離があります。
パターンベース設計は、ある種の「新しい考え方」を要求するのです。

基本的な進め方は、文脈(全体像)から始めて、問題の階層を抽出し、
各問題に対してパターンを検索・適用していく流れになります。

パターンで考える6ステップ

この流れで重要なのは、
設計が固まるまでステップ2〜4を繰り返す点(ステップ5の分岐)です。
1回で全レベルが決まるわけではなく、
抽象度の階段を行ったり来たりしながら設計を形作っていきます。

低レベルの検討で見えた制約を高レベルへ反映するために、
ステップ5の判断ノードが「もう一周必要か」を問い直す役割を果たします。

パターンベース設計の8タスク

先ほどの6ステップを実務のタスク粒度に落とし込むと、次の8タスクに展開できます。
「全体像→部分→繰り返し→洗練」という流れは6ステップと同じで、
各ステップで具体的に何をするかをより細かく書き下したものです。

  1. 要件モデルから問題階層を抽出する
  2. 問題ドメインのパターン言語
    (パターンの集合体で、テンプレート化され相互関連を示す)を探す
  3. アーキテクチャパターンを探索し、コラボレーションパターンも確認する
  4. コンポーネントレベルの問題にパターンを適用する
  5. 全ての大きな問題が対処されるまで、上記2〜4を繰り返す
  6. UIの問題に対してUIパターンリポジトリを検索する
  7. 候補パターンの問題・文脈・力を比較して適合性を確認する
  8. 品質基準をガイドとして設計を洗練する

パターン整理表

候補パターンが増えてくると、
何をどこに当てはめるつもりなのかを管理しきれなくなります。
そこで使えるのがパターン整理表
——問題種別とパターン種別をマトリクスにして候補を整理する手法です。

問題種別 \ パターン種別 データベース アプリケーション 実装 インフラ
データ/コンテンツ (候補パターン名) (候補パターン名)
アーキテクチャ (候補パターン名) (候補パターン名)
コンポーネント (候補パターン名) (候補パターン名)
UI (候補パターン名) (候補パターン名)

パターン言語やリポジトリから見つけた候補を、問題ステートメントに紐づけて記録する。
パターン名はパターン記述のURLへのハイパーリンクにしておくと、
後から参照しやすくなります。

スプレッドシートで運用するくらいの軽さで十分機能する道具です。

パターンベース設計でよくある失敗

最後に、パターンベース設計でよく起きる失敗パターンを整理しておきます。

  • 問題とその文脈・力を十分に理解する前にパターンを選んでしまう
  • 間違ったパターンを選んだ後、それに気づかず強引に当てはめる
  • パターンが対処していない力(制約)を見落とす
  • パターンを文字通り適用し、自分の問題空間に合わせた適応を怠る

これらの失敗を防ぐ最大の武器は、結局のところレビューです。
第三者の目で「文脈と力が本当にこのパターンに合っているか」を確認してもらう
——これがパターン適用の精度を上げる近道になります。

各レベルのパターン概観

パターンの思考法と活用プロセスを掴んだところで、
実際にどの設計レベルにどんなパターンがあるのかをコンパクトに概観しておきます。

詳細なカタログ化は本記事のスコープ外です。
「パターンは多様なレベルで存在する」という実感を持てれば十分です。

┌──────────────────────────────────────────────────────┐
│  アーキテクチャレベル                                   │
│   全体構造(並行性、永続化、分散処理 など)                 │
│   例: e-commerce、Microkernel、Pipes and Filters      │
└──────────────────────────────────────────────────────┘
                       │ 影響を与える
                       ↓
┌──────────────────────────────────────────────────────┐
│  コンポーネントレベル                                   │
│   部分問題(情報検索、データ変換、状態管理 など)            │
│   例: SearchBox、AdvancedSearch、SearchResults        │
└──────────────────────────────────────────────────────┘
                       │
                       ↓
┌──────────────────────────────────────────────────────┐
│  UI/UXレベル                                          │
│   繰り返し問題(ナビゲーション、ページレイアウト等)          │
│   例: Card stack、Shopping cart、Top-level navigation │
└──────────────────────────────────────────────────────┘
                       │
                       ↓
┌──────────────────────────────────────────────────────┐
│  デバイス特性に応じたパターン                             │
│   モバイル/Web固有問題(チェックイン、マップ等)             │
│   例: Sign-up flows、Popovers、Maps                   │
└──────────────────────────────────────────────────────┘

各レベルで具体例を見ていくと、パターンの粒度感が掴みやすいです。

レベル 扱う問題
アーキテクチャ システム全体の構造的な問題(並行性、永続化、分散など) e-commerce、Microkernel、Pipes and Filters
コンポーネント 部分問題(情報検索、データ変換、状態管理など) SearchBox、AdvancedSearch、SearchResults
UI/UX ユーザーインターフェースの繰り返し問題 Card stack、Shopping cart、Top-level navigation
デバイス特性 モバイル/Web固有の問題(チェックイン、マップなど) Sign-up flows、Popovers、Maps

たとえば「製品情報を検索する機能」を設計する場合、コンポーネントレベルでは次のようなパターンの中から、文脈に合うものを選んでいくイメージです。

  • SearchBox: 小さな検索ボックス
  • AdvancedSearch: 複合条件での検索
  • SearchResults: 検索結果の表示

デバイス特性のレベルでは、近年クロスプラットフォームフレームワーク(FlutterやReact Nativeなど)の普及もあり、
Web/モバイルをまたいで共通化できる部分と、
デバイス特性ごとに固有の配慮が必要な部分の両面で議論されることが増えています。

各レベルのパターン適用で必ず確認したいのが、
パターンの「結果(consequences)」フィールドです。
ここにセキュリティやパフォーマンスのトレードオフが記載されているため、
見落とすと後から痛い目を見ることになります。

そして、1つのソフトウェアには複数レベルのパターンが共存します。
アーキテクチャパターンを選んだ時点で、共存可能なコンポーネント・UIパターンの候補が制約される。
レベルをまたいだパターン同士の整合性を意識する必要があります。

アンチパターンから学ぶ ── よくある「やってはいけない設計」

パターンが「こうすればうまくいく」を教えてくれるのに対し、
アンチパターンは「こうするとうまくいかない」を教えてくれます。
自分の設計にアンチパターンの兆候がないか確認することも、
設計品質を上げる有効な手段です。

アンチパターンとは、
頻繁に使われるが通常はソフトウェアの品質に悪影響を及ぼす設計の解法を指します。
パターンとアンチパターンでは、記述の方向が逆になっています。

観点 パターン アンチパターン
記述の方向 ボトムアップ トップダウン
出発点 解決策 繰り返し現れる悪い設計・開発慣行
構成要素の追加順 解決策 → 力・症状・文脈 症状と悪影響 → 改善手順

代表例を見ていきます。

The Blob ── 巨大な万能クラス

The Blobは、大量の属性・操作を持つ単一のクラスを指します。

項目 内容
症状 凝集度のない属性・操作の寄せ集め。単一のコントローラクラスが全機能を集中
原因 OOアーキテクチャの不在、抽象化スキルの不足、既存クラスに少しずつ機能を追加し続けた結果
改善の方向 振る舞いを他のデータオブジェクトに分散させ、The Blobの複雑さを低減する(リファクタリング)

「機能追加のたびに既存クラスにメソッドを足し続けた結果、気づけばそのクラスが肥大化している」——これは新規開発でも保守でもよく見る構図で、凝集度が低くなっていることの典型的な症状です。

その他の代表的なアンチパターン

アンチパターン名 症状 対策の方向性
Big Ball of Mud 認識可能な構造がないシステム 段階的にモジュール境界を引き直す
Stovepipe System 関連性の薄いコンポーネントの寄せ集め コンポーネント間の関係を整理し、共通基盤を抽出
Lava Flow 削除が困難な冗長・低品質コードの残留 影響範囲を計測したうえで段階的に除去
Spaghetti Code コード構造の濫用による理解困難なプログラム 制御フローを整理し、適切な抽象化を導入
Copy and Paste Programming 汎用解の代わりに既存コードをコピーして使い回す 共通部分を関数・モジュールとして抽出
Silver Bullet 特定の技術的解決策がすべての問題を解決すると思い込む トレードオフ分析を必ず行う

アンチパターンはリファクタリング判断の道具としても有効です。
レビューで「このコード、Lava Flow になっていませんか?」と問えるようになると、
「なんとなく汚い」という抽象的なコメントが、共有可能な語彙に変わります。

おわりに

ここまで設計概念とパターンを見てきましたが、これらは知識として知っているだけだと、
ただの語彙集で終わってしまいます。
明日からのコードレビューでどう使えるか、3つの場面で整理してみます。

場面 投げかけられる問い
自分のコードを見るとき 「このクラスは1つのことに集中しているか(凝集度)」「モジュール間の依存はどれくらいか(結合度)」「実装詳細を隠せているか(情報隠蔽)」
他者のコードをレビューするとき 「なぜこの設計にしたのか」を設計概念の言葉で説明してもらえるか。自分の設計についても同じ問いに答えられるか
リファクタリングを判断するとき 「The Blobの兆候はないか」「ここでパターンが使えそうか」

なんとなくの違和感が、具体的な改善ポイントに変わる
——これが概念とパターンを手にする一番の効用かなと思っています。

設計概念とパターンは「点」ではなく「束」で機能します。
今回掴んだ概念を土台に、より粒度の細かい設計レベル(アーキテクチャ、コンポーネント、UXなど)に降りていくと、
具体的な技法として血肉になっていくはずです。

設計判断を言語化できるエンジニアは、自分の選択を説明でき、他者の選択も評価できる。
この記事がその第一歩になれば嬉しいです。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?