VOLUME 02 / CHAPTER 01

割引を変えたら、帳票が壊れた

凝集度と結合度を、実際に伝わる変更から理解する。SOLIDとパターンを、分割の目的と守る契約に結び付ける。

出典照合日: · 独自解説

架空事例:青葉機材の予約システムを実装する過程を扱う。クラスや処理の分け方は比較用の設計例であり、この構成をすべての業務へ推奨するものではない。

小さな修正のはずだった

長期貸出しの割引率を変更したところ、予約票に表示する金額が合わなくなった。画面で計算する金額は正しい。保存した金額も正しい。帳票だけが以前の割引規則で再計算していた。

修正担当者は、帳票の計算式も直した。これで今回の不具合は解消する。しかし、次の料金変更でも、二つの場所を同時に直す必要がある。この問題は、計算式の難しさではない。一つの業務上の判断が、複数の場所に分かれていることにある。

逆の問題もあった。予約を確定する一つの関数が、料金計算、データ保存、メール文の生成、印刷レイアウトの選択を全部行っていた。メールの文面を変更するためにも、機材の確保を扱うコードの近くを編集する必要がある。

似たコードを集めれば解決するのか。大きな関数を細かくすれば解決するのか。どちらも、それだけでは足りない。何を一緒に変え、何を独立して変えたいかを、業務の意味から考える必要がある。

凝集度は、名前を一つにすることではない

「予約管理」という名前を付ければ、関連するすべての処理を一か所へ集めてもよいように見える。しかし、予約という一語の中には、契約条件の判断、機材確保、通知、表示という異なる変更理由がある。

同じ目的に密接に関係する処理をまとめるのが、高い凝集度を目指す考え方である。一方、異なるモジュールが互いのどの情報へ依存するかに着目するのが結合度である。この二つの基本は、コンプリートテキスト第6章で整理されている。1

本事例では、料金規則の変更は料金計算に集め、画面と帳票には計算結果を渡す案を考える。ただし、表示したい金額が「現在の規則で計算した見積額」なのか、「契約時に確定した金額」なのかは分けなければならない。計算を共通化した結果、過去の契約まで新しい規則で再計算してしまえば、別の不具合になる。

共通化する前に、二つの処理が同じ意味を持つかを調べる。同じ掛算が書かれているだけでは、同じ責務とはいえない。逆に、画面と帳票で式の書き方が異なっていても、同じ確定金額を求めているなら、その判断を分散させない方が整合を保ちやすい。

渡すデータが少なくても、依存は強いことがある

帳票の生成処理へ、予約レコード全体を渡していたとする。帳票側は必要な項目を自由に取り出せる。呼出し側は楽だが、保存用レコードの構造を変えると、帳票の内部まで影響する可能性がある。

そこで、予約番号、確定金額、表示用の明細など、帳票の目的に必要な情報を渡す案に変える。何を必要としているかが、インタフェースに現れる。これは、引数の個数を必ず減らすという話ではない。呼出し先が、相手の内部構造を知る必要を減らす話である。

また、引数が一つだけでも、その値が「1なら保存、2なら計算、3なら通知」を指示するなら、呼出し先の処理分岐を呼出し側が詳しく知っている。制御結合の問題は、値のサイズではなく、その値で相手の振る舞いを操作する関係にある。共有の大域データを介する共通結合、相手の内部へ直接依存する内容結合とも区別する。1

データを渡す形式を整えただけで、意味上の結合がなくなるわけでもない。「確定金額」という項目の意味を変えれば、JSONの形が同じでも、受け取る側の解釈は変わる。構造の依存と、業務上の契約への依存を分けて見る。

開放・閉鎖原則は、変更を禁止しない

割引には、通常料金、長期利用、会員向けなどの計算方法がある。種類が増えるたびに、予約確定処理の内部へ条件分岐を追加していた。料金規則が変わると、予約を保存する処理まで読み直す必要がある。

そこで、料金計算の共通の呼出し方を決め、個々の計算方法を差し替える案を考える。既存の予約処理を必要以上に書き換えずに、計算の種類を追加する。この方向が、開放・閉鎖原則の具体的な使い方になる。1

ただし、将来あらゆる変更に対応できる万能のインタフェースを、最初から作ることではない。すべての料金計算が機種、期間、数量だけで決まる前提なら、その条件に沿った境界を作れる。将来、料金が別の情報に依存するようになれば、境界自体の見直しが必要になることもある。

開放・閉鎖原則を守ったから、一度書いたコードを永久に変更しないという結論にはならない。どの変化を想定し、その変化をどこへ閉じ込めるかという設計である。何に対して拡張可能なのかが説明できなければ、「柔軟な設計」は評価できない。

単一責任原則も、一つのクラスに一つのメソッドしか置かない規則ではない。責任の違う変更を混ぜないための観点である。契約上の料金規則と、帳票の字体が変わる理由を分けることには意味がある。行数だけで分割しても、その違いは保存されない。

差し替えられる型でも、約束を破れば代替にならない

料金計算の共通インタフェースが、有効な数量1〜100について見積額を返すと約束していたとする。ある実装だけが、数量20を超えると例外を投げる。型として代入できても、利用側は安心して差し替えられない。共通の契約で受け付ける入力を、その実装だけが拒否するからである。

リスコフ置換原則を理解する入口は、基底型が利用側へ約束したことを、代替する型が破らないことである。1 この事例では、共通契約の数量範囲が本当に妥当か、特定条件の不成立を結果として表すべきか、別の契約へ分けるべきかを検討する。継承関係を描くだけでは解決しない。

インタフェース分離原則も、同じ利用者に不要な約束を背負わせないために使う。料金を照会したいだけの処理が、料金を登録・削除する操作まで持つ巨大なインタフェースへ依存する必要があるかを問う。ただし、メソッド数が多いから必ず違反とはいえない。利用側の役割と、何に依存させているかが問題になる。

これらの原則を、単にクラスを増やす技法として覚えると、構成は複雑になるのに変更しやすくならない。原則が守ろうとしている、利用側の契約へ戻って考える。

依存方向と、組み立てる場所

予約処理が、特定DB製品向けの保存クラスを自分で生成していた。保存方式を変更したいとき、予約処理も修正しなければならない。

予約処理が必要とする保存の契約を定め、その契約を具体的な保存実装が満たす構成なら、業務上の方針が保存方式の詳細を直接知る必要を減らせる。高水準の方針と低水準の詳細を抽象的な契約へ関係付けるのが、依存性逆転原則の狙いである。1

では、具体的な保存実装を、誰が予約処理へ渡すのか。アプリケーションを組み立てる場所で生成し、コンストラクタなどを通して渡す方法がある。Dependency Injectionは、このように利用する依存対象を外から与えるための仕組みとして使える。Fowlerの解説は、依存対象を設定することと利用することを分離する点を重視している。2

DIとDIPは同じ略語の言い換えではない。具体的なクラスを外から渡しても、その製品固有の操作へ業務処理が依存していれば、依存方向の問題は残り得る。一方、小さな構成をコードで明示的に組み立てるだけでも、必要な分離は実現できる。専用のDIコンテナを導入したこと自体を、設計品質の証明にしない。

パターンは形ではなく、変えたいものから選ぶ

料金の計算方法を交換するならStrategyが候補になる。予約の状態によって操作の振る舞いを切り替えるならStateが候補になる。どちらも内部のオブジェクトを差し替える形に見えるが、解こうとしている問題が異なる。

既存の点検サービスが、こちらの必要な呼出し形式と違うならAdapterを検討する。複雑な点検サブシステムに単純な窓口を提供したいならFacadeである。代理を通してアクセスを制御するProxyと、既存の対象へ振る舞いを付加するDecoratorも、似た構造を持ち得るが意図は同じではない。これらの区別は、原教材第6章のパターンの比較を、設計の選択へ移したものである。1

本事例で、料金の種類が二つしかなく、追加の可能性も低いなら、単純な分岐の方が説明しやすい場合もある。Strategyを使わなければ誤り、という規則ではない。変更の頻度、差し替えたい範囲、独立に確認したい性質を考え、分離の費用に見合うか判断する。

新しい抽象化を入れるたびに、読む人は呼出し先を追う必要がある。分割が少なすぎれば影響が広がり、分割しすぎれば関係を理解する負担が増える。シラバス8-4の方式評価が求める、要件への追跡、一貫性、運用・保守の実現可能性は、この負担も含めて考えるための観点になる。3

料金を変えても、帳票を直さなくてよくなった。ただし、それを判断するには、実際に同じ業務上の金額を表示していることを確かめなければならない。分割後のクラス図が美しく見えることより、どの変更がどこへ伝わらなくなったかを説明できることが重要である。

知識を答案へ戻す

午前Ⅱ。 凝集度は内部のまとまり、結合度はモジュール間の依存を見る。SOLIDは、責任、拡張、置換、利用インタフェース、依存方向という異なる問いで見分ける。パターンは図の形より意図を読む。

午後Ⅰ。 変更が複数箇所へ及ぶという本文があれば、同じ判断の重複か、異なる責務の混在かを区別する。単に「共通化する」「疎結合にする」と答えず、何をどこへまとめ、何を渡すのかを、与えられた事実から記述する。

午後Ⅱ。 「SOLIDを採用した」ではなく、料金規則の変更と帳票の変更を分けるため、確定金額を共通の業務処理で決め、表示側はその結果を利用する構成にした、という目的と判断を説明する。本章の事例を本人の経験として扱うものではない。

立ち止まって考える:似た計算式は、必ず一つへ共通化するべきか

同じ事実を計算しているかを先に確認する。現在の規則による見積額と、過去の契約時に確定した金額は意味が異なる。同じ式に見えても変更理由が違う場合があり、共通化によって必要な履歴まで変えてはいけない。

出典と照合範囲

[1] コンプリートテキスト第6・7・12章:凝集度、結合度、SOLID、代表パターン、契約を明示する設計を基礎とした。料金・帳票・保存の分離は本章の独自事例と推論である。

[2] Martin Fowler, Inversion of Control Containers and the Dependency Injection pattern(2004):依存対象を提供する仕組みと、設定を利用から分離する観点を参照した。掲載されている古いJava製品の選定を推奨するものではない。著者公開本文

[3] IPA「SAシラバス Ver.5.2」:8-1、8-4、10-2。方式、構成要素への割当て、インタフェース、方式評価の位置付けを照合した。公式PDF