0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Clean Architectureを紐解く - 設計の原則 -

Posted at

第7章 SRP: 単一責任の原則

著者見解

単一責任の原則(Single Responsibility Principle: SRP)とは「ひとつのモジュールは、たったひとつのアクター(利害関係者)に対してのみ責務を負うべきである」という考え方です。ここでいうモジュールとはソースファイル単位を指し、アクターとはそのソースに変更を求めうるユーザーやチーム、部署などをまとめたものです。

たとえば給与システムの中に Employee クラスがあり、そこに calculatePay()(給与計算)、reportHours()(勤怠報告)、save()(データ保存)という三つのメソッドを定義していたとします。会計部門は給与計算の結果を CFO に、勤怠管理部門は報告データを COO に、インフラ部門はデータベース操作を CTO に提出します。つまりこれら三つのメソッドは、それぞれ異なるアクターの要望に応えなければなりません。にもかかわらず同一の Employee クラスにまとめてしまうと、あるアクター向けの要件変更が別のアクター向け機能に影響を及ぼす危険があります。

現場でよくある一例として、給与計算と勤怠報告の両方で基本労働時間を求める処理を regularHours() という共通メソッドに切り出していたとしましょう。ここを会計部門の要望で変更したところ、勤怠管理部門が気づかずに間違った時間データを使い続け、支払いミスや数百万ドル規模の損害につながった、という悲劇です。このように、別々のアクター向けコードを1つのソースファイルに詰め込むこと自体が、無用な結合と意図せぬ副作用を招きます。

さらにマージの問題も深刻です。CTO 配下の DBA チームがデータベーススキーマ変更のために Employee クラスを修正し、一方で COO 配下の人事チームも勤怠レポート改修のため同じクラスを編集すると、別々の開発チームが同時並行で同一ファイルを更新せざるをえなくなります。バージョン管理ツールがある程度自動で衝突を解消してくれるとはいえ、ヒューマンエラーやマージし忘れ、意図しない上書きといったリスクは完全には排除できません。

ではどうすればよいか。最も基本的な解決策は、「アクターごとにクラスを分離する」ことです。まず EmployeeData のような純粋データ保持クラスを用意し、そこに名前や時間数といったフィールドだけを持たせます。給与計算の責務を担う PayrollProcessor、勤怠報告を担う HoursReporter、データ保存を担う EmployeeSaver といった具合に、それぞれのアクターに紐づく機能を別々のクラスに実装すれば、コードの重複や衝突を避けられます。

ただし実際の呼び出し箇所では、これら三つのクラスを都度インスタンス化し、適切にデータを渡す手間が増えてしまいます。そこで登場するのが Facade パターンです。EmployeeFacade という薄い中間層を設け、そこに calculatePay(employeeData)reportHours(employeeData)save(employeeData) といったメソッドを定義します。Facade 内ではそれぞれの専用クラスを生成し、内部で処理を委譲するだけ。利用者はひとつのクラス(EmployeeFacade)さえ覚えておけばよいので、運用上の煩雑さを抑えながらも SRP を保てます。

「メソッドが一つしかないクラスをいくつも作るなんて設計が細かくなりすぎる」と感じるかもしれません。しかし各クラス内では、その責務を遂行するための private メソッドや補助処理が多数含まれるのが通常です。結果として、関連する処理をまとめたひとつのスコープ(クラス)として機能し、外部に漏れる情報は最小限になるためむしろ可読性や保守性が向上します。

なお、SRP は関数やクラスレベルだけでなく、コンポーネントレベルでは「共通閉鎖の原則(Common Closure Principle)」、さらにシステム全体のアーキテクチャでは「変更の軸を意識して境界を引く」といった思想に発展します。言い換えれば、変更理由(アクター)ごとにコードを分割し、責務のぶつかり合いやマージ作業を減らすことが、安定したソフトウェアを作るための鍵となるのです。

筆者所感

個人的には、SRPを「アクターごとにクラスを分離する」というコンテキストで考える場合は、クラス単位という狭い単位で考えるより、「ドメイン単位」で考えるほうが現実的だと考えています。ドメインを分けておけば、共有カーネルなど特別な取り決めがない限り相互依存が生じにくく、自然に責務の分離ができます。もしA部門の人がたまたまB部門の業務を行うなら、その時はBというアクターとして振る舞えばよく、コード上の境界が混乱するわけではありません。

同時に実務では、アクター単位の分離だけでなく「機能ごとの分離」(ビジネスロジック専用クラス、出力整形専用のPresenter/ViewModel、永続化を担当するRepositoryなど)やユースケースごとの分離(給与の計算、賞与の計算など)も並行して行われます。こうした分割とアクター(変更理由)に基づく分割は相補的であり、両者を組み合わせることでより頑健な設計になります。

最後に設計手法としては、Facadeを使って具象実装を直接生成して委譲するより、DIコンテナでインターフェイスに具象を注入する(依存性逆転を守る)ほうが柔軟でテストしやすいと考えています。

第8章 OCP: オープン・クローズドの原則

著者見解

オープン・クローズドの原則(Open–Closed Principle, OCP)は、1988年にBertrand Meyer が提唱した「ソフトウェアの構成要素は拡張に対して開かれており、修正に対して閉じられていなければならない」という考え方です。言い換えれば、新しい機能を追加するときに既存のコードをできるだけ書き換えずに済むように設計せよ、ということです。この原則を無視してしまうと、ちょっとした要件変更でも大規模なリファクタリングが必要になり、結果としてバグを生み出しやすいチーム開発や運用コストの肥大化を招きます。

たとえば、ある企業の財務情報をウェブ上で表示するシステムを考えてみましょう。このシステムは現在、数値のスクロール表示や、負の数値を赤字でハイライトする機能を備えています。そこへ「同じレイアウトで白黒プリンターに出力したい」という要望が届きました。印刷用にはページごとのヘッダー・フッターの付与や、負の数値を括弧で囲むといった別のフォーマット処理が必要です。ここで既存のコードを直接書き換えると、ウェブ表示部分に思わぬ影響を与えるリスクがあります。オープン・クローズドの原則に従うならば、新しい印刷機能は「拡張」で実現し、ウェブ表示ロジックは「修正」する必要がない状態を目指すのです。

では、具体的にどうすれば既存コードをほとんど触らずに印刷機能を追加できるのでしょうか。まず、システムの内部設計を「会計データの生成」「ウェブ表示用のフォーマット」「印刷用のフォーマット」という三つの責務に切り分けます。このとき単一責任の原則(SRP)を適用し、会計処理と表示処理を別々のモジュール(あるいはクラス)に実装しておきます。次に、依存関係逆転の原則(DIP)を用いて、上位の「ビジネスルール」モジュールが下位の「フォーマッタ」モジュールに依存しないように設計します。具体的には「フォーマッタインタフェース」を定義し、ウェブ用フォーマッタと印刷用フォーマッタはこのインタフェースを実装する形にします。

こうすると、新しい印刷機能を追加したいときには「印刷用フォーマッタ」の実装クラスをひとつ用意し、既存のビジネスルールモジュールには手を加えずに拡張できます。ビュー(View)やプレゼンター(Presenter)、コントローラー(Controller)も同様に層ごとに責務を分離し、依存関係は必ず一方向に流れるようにします。たとえば、Controller は Presenter のインタフェースにしか依存せず、Presenter は View のインタフェースにしか依存しない。さらに各コンポーネントは階層的に配置し、上位レベルほど業務ロジックに近く、下位レベルほど入出力やインフラストラクチャに近い役割を担います。

このように設計すれば、たとえ印刷機能の要件が大きく変わっても、印刷用フォーマッタまわりの新規実装だけで対応でき、ウェブ表示や会計処理部分にまで波及的な変更を行う必要はありません。結果として既存コードは「修正に対して閉じ」、新しい機能は「拡張に対して開かれた」まま、高い安定性と柔軟性を両立できるのです。アーキテクトはこの原則を念頭に置き、機能の変更理由(変更軸)ごとにコンポーネントを階層的に配置し、インタフェースを介した一方向依存の境界を引くことで、OCP を具現化していきます。

筆者所感

新機能追加時に既存コードの修正を最小限にするという OCP の方針には賛成です。一方で、設計上の問題や技術的負債を解消するには、ドメインの分割・統合やモジュール再編成など、大きなリファクタリングが避けられないこともあります。そうしたときは躊躇せず大胆に修正すべきですが、リスク管理も同時に行う必要があります。レビュワーは通常より深くレビューし、設計意図や依存関係の変化を確認することが必須です。

第9章 LSP: リスコフの置換原則

著者見解

リスコフの置換原則(Liskov Substitution Principle, LSP)は、1988年にBarbara Liskovが提唱した「派生型は基底型と置き換えても、プログラムの振る舞いを変えてはならない」という考え方です。具体的には、あるプログラム P が基底型 T のオブジェクト o₂ に依存して動作しているとき、そこに派生型 S のオブジェクト o₁ を代入しても、P の正しさや期待される動作が損なわれてはならない、ということを意味します。

たとえばライセンス料を計算する License クラスを考えてみましょう。基底型 License は calcFee() というメソッドを提供し、これを呼び出すのがBilling アプリケーションだとします。そこから派生して PersonalLicense と BusinessLicense の二種類を用意し、それぞれ異なる計算アルゴリズムを実装しているとします。このとき Billing アプリケーションはどちらのライセンスも License 型として扱い、同じ calcFee() の呼び出しで正しく動作します。つまり PersonalLicense と BusinessLicense は、License を置き換えても問題が起きないため、LSP を満たしていると言えます。

一方でよく知られている違反例が「長方形–正方形問題」です。Rectangle クラスには幅と高さを独立に設定する setW() と setH() があり、これを使って面積を計算すると期待通り動きます。ところが派生型として Square クラスを定義し、「幅と高さは必ず同じ値になる」という制約を加えてしまうと、次のようなコードで不正が起きます。

Rectangle r = new Square();
r.setW(5);
r.setH(2);
assert r.area() == 10;  // しかし Square では幅と高さが揃うため、area() は 4 や 25 になるおそれがある

このように、ユーザー(ここではプログラム P)が「これは Rectangle だ」と信じて使っているのに、実際には Square が入ってしまうと前提が崩れ、プログラム全体の正しさが失われます。これが LSP 違反の典型例です。

アーキテクチャの視点で考えると、LSP を守らない設計は「特定の実装にだけ対応する例外処理やパッチ」を生み出します。たとえばタクシー配車サービスを複数社まとめて扱うシステムを想像してください。各社は RESTful インターフェイスを通じて「ドライバー呼び出し用の URI」を提供し、我々のプラットフォームはどの会社からも同じ方法でタクシーを手配できることを前提にしています。ところがある大手タクシー会社だけが仕様を誤解して別のエンドポイント形式を返した場合、我々はその会社向けに特別な変換ロジックをコードベースに追加せざるを得なくなります。結果として「標準的な配車フロー」と「例外的な配車フロー」の双方を維持しなければならず、システム全体の複雑度と保守コストが跳ね上がります。これはまさに「派生型(ここでは一社だけの特殊実装)が基底型(REST インターフェイス仕様)を置き換えられない」ために起きる障害であり、LSP 違反がアーキテクチャにもたらす悪影響を如実に示しています。

まとめると、リスコフの置換原則を守るとは、「どの派生型を渡しても同じように動作する」という基底型の契約(インターフェイス)が常に成立するように設計することです。こうすることで、後から新しい実装を追加したり、別のサービスと統合したりするときに、既存コードへの侵入的な修正や特別対応を最小限に抑え、システムと拡張性を高めることができるのです。

筆者所感

インターフェイスで定めた契約(引数・戻り値・例外・副作用)から実装が逸脱しないことが重要です。実装クラスで勝手に別の戻り値を返したり独自の例外を投げたりすると、利用側の前提が崩れて予期せぬ不具合につながります。特に例外は型チェックだけでは検出されにくいので、注意が必要です。

第10章 ISP: インターフェイス分離の原則

著者見解

インターフェイス分離の原則(Interface Segregation Principle, ISP)は、「クライアントは自身が利用しないメソッドに依存してはならない」という考え方です。静的型付け言語の代表である Java を例にとると、たとえ User1 が OPS クラスの中で op1() しか呼び出していなくても、コンパイラから見ると User1 のコードは OPS のすべてのメソッド(op2()op3())に依存していることになります。その結果、たとえば op3() の実装を変更しただけで、User1 のアプリケーションも再コンパイル・再デプロイが必要になってしまうわけです。

この無駄な結合を防ぐには、OPS クラスを一つの大きなインターフェイスとして提供するのではなく、User1, User2, User3 それぞれの利用形態に応じた小さなインターフェイスに分割します。具体的には、User1 用に U1Ops インターフェイス(op1() のみ定義)を用意し、OPS にはそれを実装させる。このようにすれば、User1 はもはや OPS 全体ではなく U1Ops のみを見ていればよく、OPS 側で op2()op3() を変更しても User1 への影響はまったくありません。

同じ原則はシステムアーキテクチャのレベルでも当てはまります。たとえばシステム S が、特定のデータベース D 向けに機能を絞ったフレームワーク F を利用しているとします。しかし F は D 中のごく一部の機能しか使っておらず、残りの機能は S にとって不要です。それにもかかわらず F が D の全機能に依存していると、D のいずれかの変更が F の再ビルド・再デプロイを引き起こし、結果として S まで巻き添えにしてしまいます。これを防ぐには、まず F 側が D の必要最小限の機能だけを表すインターフェイスに依存し、S もまた F の必要な部分だけを参照するように設計します。こうして「使わない機能への依存」を断ち切ることで、下流コンポーネントの変更が上流コンポーネントに波及するのを防ぎ、システム全体の安定性・保守性を向上させることができるのです。

筆者所感

インターフェイスを通じて依存を絞ることは、再ビルド/再デプロイの問題以上に「意図しない依存関係の混入」を防ぐ意味で重要です。たとえモノリスで境界ごとのデプロイを分けていなくても、巨大なインターフェイスを公開しておくと、別のユースケース向けのメソッドが便利に見えて他チームや新参の開発者に使われがちです。その結果、設計意図が崩れ、将来的な変更で思わぬ波及が起きます。このような事態を防ぐには、役割ごとに小さなインターフェイスを用意して必要最小限だけを公開するのが有効です。

第11章 DIP: 依存関係逆転の原則

著者見解

依存関係逆転の原則(Dependency Inversion Principle, DIP)は、「高水準モジュールも低水準モジュールも、具体的な実装ではなく抽象(インタフェースや抽象クラス)に依存すべきだ」という考え方です。つまり、むやみに具象クラスを直接参照すると、実装の変更に伴って上位のコードまで修正が必要になり、システムの柔軟性や拡張性が損なわれます。DIP を守れば、具象の「どう実現するか」よりも抽象の「何を提供するか」が先行し、変更の影響範囲を抑えられます。

たとえば Java で文字列操作に必ず String クラスを使うように、プラットフォームやランタイムが提供する非常に安定したクラスは例外的に直接依存しても問題になりません。しかし、システム固有のビジネスロジックや外部サービス連携など、将来変更の可能性が高い部分を具象に直接結びつけると、変更のたびに依存元も書き換えなくてはいけないリスクがあります。そこで、依存の向きを「具象→抽象」に揃え、抽象要素のほうがむしろ安定して変わりにくい、という関係を築くのが DIP の狙いです。

ただし、オブジェクト指向言語ではどうしても具象クラスを生成(new)する瞬間だけは、具体的な型を知らなければなりません。この「最後の依存」を吸収する手段としてよく使われるのが Abstract Factory パターンです。以下に簡単な例を示します。

// 抽象インタフェース
public interface Service {
    void execute();
}

// 具象実装
public class ConcreteService implements Service {
    @Override
    public void execute() {
        // 具体的な処理
    }
}

// サービスを生成する抽象ファクトリ
public interface ServiceFactory {
    Service makeService();
}

// 具象ファクトリは具象実装を生成する
public class ConcreteServiceFactory implements ServiceFactory {
    @Override
    public Service makeService() {
        return new ConcreteService();
    }
}

// このアプリケーションクラスは、抽象にしか依存しない
public class Application {
    private final ServiceFactory factory;

    public Application(ServiceFactory factory) {
        this.factory = factory;
    }

    public void run() {
        Service svc = factory.makeService();
        svc.execute();
    }
}

この例では、ApplicationServiceFactoryService の抽象インタフェースだけを参照し、具象クラス ConcreteService にはまったく依存していません。新たに別のサービス実装を加えたいときには、ServiceServiceFactory の契約を変えずに済むため、既存コードはそのまま稼働し続けられます。具象ファクトリがどの実装を返すかは起動時の設定や依存性注入コンテナが握っておけばよく、アプリケーション本体は変更不要です。

まとめると、依存関係逆転の原則とは「具象→抽象」という依存の一方向化を徹底し、インタフェースや抽象クラスを安定した契約として据え置くことで、具象の変更が上位モジュールに波及しないようにする設計手法です。これにより、新機能の追加や差し替えを最小限の変更で実現し、長期的に堅牢で拡張しやすいコードベースを保つことができます。

筆者所感

依存性逆転の典型例は永続化です。ビジネスロジックが直接 DB の具象実装に触れると、DB を変えたり実装を改めたりするたびにビジネス側まで手を入れなければならなくなります。そこでリポジトリなどのインターフェイスを先に定義し、具象実装側がそのインターフェイスに合わせて実装する形にすると、ビジネスロジックは「何を受け取り何を渡すか」だけを意識すればよくなります。結果として、DB を MySQL から PostgreSQL に切り替えるような変更があったとしても、実装クラスだけを差し替えれば済み、ビジネスロジックは触らずに済みます。

0
0
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
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?