開発プロセス●●●○○

デザインパターン

Design Patterns ・ でざいんぱたーん

繰り返し現れる設計課題への定石解に名前を付けたカタログ。設計の共通語彙になる。

✦ 30秒で試す — 航海名前を1つ言うだけで、設計の方針が丸ごと伝わる書き直すと、クラスの数はどうなる?予想してみる

概要

デザインパターンは、ソフトウェア設計で繰り返し現れる課題に対する「定石の解き方」に名前を付けて整理したカタログです。「オブジェクトの生成を1か所に集約したい」「状態の変化を複数の画面に通知したい」といった課題は、アプリの種類が違っても構造としては同じ形で何度も現れます。先人が磨いてきた解法を「Singleton」「Observer」のような名前で呼べるようにしたものがデザインパターンです。

最大の価値は、コードの再利用ではなく語彙の共有にあります。「ここは Strategy パターンで差し替え可能にしよう」の一言で、クラス構成・意図・トレードオフまで含めた設計方針がチームに伝わる。設計レビューやコードリーディングの場面で、パターン名は圧縮率の高い共通言語として機能します。フレームワークやライブラリのドキュメントにも Factory、Adapter、Proxy といった名前は頻出するため、読み解きの前提知識にもなっています。

なぜ生まれたか

発想の源流はソフトウェアではなく建築にあります。建築家クリストファー・アレグザンダーは1970年代、「良い建物や街には繰り返し現れる構造がある」として、それらを名前付きの解法集にまとめる「パターンランゲージ」を提唱しました。個々の名人芸に閉じていた設計知識を、名前・課題・解法・帰結のセットとして記述すれば、他者が学び組み合わせられる — この考え方をソフトウェアに持ち込んだのが、1994年の『Design Patterns』(通称 GoF 本。著者4人が Gang of Four と呼ばれたことに由来)です。

当時のオブジェクト指向開発では、経験者は「継承より委譲」「実装ではなくインターフェースに依存する」といった勘所を体で知っていましたが、それを新人に伝える体系的な言葉がありませんでした。GoF は23個のパターンとしてこの暗黙知を明文化し、結合度と凝集度を改善する具体的な手筋を、誰でも参照できる形にしたのです。

詳細

3つの分類と代表パターン

GoF の23パターンは、扱う課題の種類で3つに分類されます。生成に関するパターンは「オブジェクトをどう作るか」を扱います。インスタンスを1つに限定する Singleton、生成手順をサブクラスに委ねる Factory Method、複雑な組み立てを段階的に行う Builder などです。構造に関するパターンは「クラスやオブジェクトをどう組み合わせるか」を扱います。互換性のないインターフェースを変換して繋ぐ Adapter、本物の代理を立てて呼び出しを仲介する Proxy、木構造(データ構造の一種)を、葉と枝の区別なく同じ操作で扱う Composite などです。振る舞いに関するパターンは「オブジェクト間の協調と責任分担」を扱います。状態変化を購読者たちへ通知する Observer、アルゴリズムを差し替え可能な部品に切り出す Strategy、処理の骨格だけ親が決めて詳細を子に委ねる Template Method などが代表です。

パターンが目指すもの

23個の根底に流れる原則は「変わるものと変わらないものを分離し、変わる部分を差し替え可能にする」ことです。Strategy を例に、適用前と適用後の構造を比べてみます。ネットショップの会計処理が、配送方法ごとに送料の計算を切り替える場面です。

適用前 — 分岐が本体に埋まる適用後 — 差し替えられる部品に会計処理(Checkout)送料を計算するif 通常便 → 計算Aif 速達便 → 計算Bif 冷凍便 → 計算Cif 置き配 → 計算D変わる部分(配送方法ごとの計算)が本体に埋まり、1つ足すたびに本体を開いて直し、全体を再テストする会計処理(Checkout)送料の計算は部品に任せる使う送料計算(interface)calc(order): 金額実装する通常便速達便冷凍便置き配本体はインターフェースだけを知り、中身を知らない。新しい配送方法は、部品を1つ足して渡すだけ
Strategy パターンの適用前と適用後 — 変わる部分(配送方法ごとの計算)を本体から切り離し、インターフェース越しに差し替える

左では、配送方法という「よく変わる軸」が会計処理の本体に if 分岐として埋め込まれているため、置き配を足すには本体を開いて直すしかなく、そのたびに通常便や速達便の計算まで再テストの対象になります。右では、本体は「送料を計算してくれる何か」というインターフェースだけを知っていて、具体的な計算は差し替え可能な部品に分かれています。新しい配送方法はクラスを1つ足して本体に渡すだけで加わり、既存の部品にも本体にも触れません。同じ発想で、Observer は通知先を、Factory は生成対象を、それぞれ本体から切り離します。これはSOLID原則、とりわけ開放閉鎖原則(拡張に開き修正に閉じる)や依存性逆転原則の具体的な実現手段であり、依存性注入もこの系譜に連なる技法です。つまりパターンは暗記対象のカタログではなく、「疎結合・高凝集(結合度と凝集度)を実の設計でどう達成するか」の実例集として読むのが本来の使い方です。

パターンは今も現役の技術基盤に息づいています。ORM の内部には Unit of Work や Identity Map といったパターンが、GUI やフロントエンドの状態管理には Observer の系譜が、MVC のようなアーキテクチャパターンにも複数の GoF パターンが組み込まれています。名前を知っていると、これらのライブラリの設計意図が透けて見えるようになります。

落とし穴 — パターン病

デザインパターンを覚えたての時期に陥りがちなのが、パターンを使うこと自体が目的化する「パターン病」です。if 文2つで済む処理に Strategy と Factory を導入し、クラスが5個に増えて読みにくくなる — 課題がまだ存在しないのに解法だけ先に適用してしまう倒錯です。パターンには必ず「どんな課題のときに使い、どんな代償があるか」が定義されており、課題側が現れていないなら適用しないのが正解です。実務では、まず素直に書き、変更の圧力が実際にかかった箇所をリファクタリングでパターンの形へ育てていく進め方が健全です。

もう一つ知っておきたいのは、パターンの必要性が言語の進化とともに変わることです。GoF は C++ と Smalltalk を前提に書かれており、たとえば Strategy は、関数を値として渡せる言語なら関数型プログラミングでいう高階関数(関数を受け取る関数)で済みます。上の図の「送料計算」も、calc 関数を1つ渡せば足ります。Iterator は多くの言語で構文(for-of や foreach)に吸収され、Singleton はモジュールシステムがあれば専用クラスは不要です。Peter Norvig は1996年の講演「Design Patterns in Dynamic Languages」で、GoF の23パターンのうち16は Lisp や Dylan のような動的言語では不要になるか、ずっと単純になると指摘しました。ここから「パターンとは言語の表現力の穴を人力で埋めるレシピである」という見方が広まり、どの言語で何が構文に昇格済みかを意識すると、パターンを盲目的に移植する失敗を避けられます。それでも、課題と解法に共通の名前を付けて設計を語るという営みそのものは、言語がどれだけ進化しても価値を失いません。

試問

設計の現場で出会う8つの課題です。それぞれ、計算を差し替えられる部品に切り出す Strategy、変化を登録した相手に知らせる Observer、形の合わない口を変換してつなぐ Adapter のどれで解くのがよいか、それとも定石を持ち込まず素直に書くのがよいかを選びます。

✦ 命題挑戦中

8つの設計課題を、Strategy・Observer・Adapter・定石は要らない、のどれで扱うかで仕分けよ

Strategy計算・手順を差し替えられる部品に切り出す
Observer変化を、登録した相手たちに知らせる
Adapter形の合わない口を変換してつなぐ
定石は要らない素直に書けば済む

項目を選んでから、入れる箱を選びます