Clean Architecture読了しました

目次
  1. SOLID原則
  2. Single responsibility principle(SRP)
  3. Open-closed principle(OCP)
  4. Liskov substitution principle(LSP)
  5. Interface segregation principle(ISP)
  6. Dependency inversion principle(DIP)
  7. クリーンアーキテクチャ
  8. データベース/フレームワークは詳細

先日、Robert C. Martin氏の有名な著書 Clean Architecture を読み終えました。 良著というのは前々から聞いていたのですがEngineering Managerとしてのインプットに時間を割いていた都合で読む機会を逸しておりました。

翻訳版の言い回しが微妙に理解し辛かったりといった部分は多少あったのですが、言語やフレームワークに依存せずプログラミングに広く適用できる設計ノウハウがまとまっており今後のプログラミングに広範囲に適用できるという意味で非常に有用なインプットになりました。

ここからは個人的に印象深かった部分をピックアップしてまとめたり、感想を書き下してみます。

SOLID原則

「第Ⅲ部 設計の原則」では関数やクラス、データ構造の組み方を示す原則であるSOLID原則に関して説明されています。 読了前から概念はネットサーフィンで知ってはいましたが、改めて有用な原則だと思いました。

Single responsibility principle(SRP)

「モジュール(関数やデータをまとめた凝集性のあるもの)はたった1つのアクター(変更を求める人たちの総称)に対して責務を負うべきである」という原則です。「どのモジュールもたった1つのことだけを行うべき」という意味ではないので注意が必要です。

例えば、全く異なる性質の2つの機能がたまたま同じロジックを必要としていたので、2機能から同じロジックを参照するように実装してしまった、という事例はこの原則に違反していると言えます。 このような違反をすると、プロダクトが成長過程でそれぞれの機能で異なる追加要件が必要になった場合に同じロジックの実装を維持できなくなってしまい、2機能を跨いだ改修が必要になってしまうでしょう。

こういった事例は割と実務で違反しちゃってる実装を見るケースがあり、特に機能実装の初期段階で意識して実践すべき原則だなーと思います。

Open-closed principle(OCP)

「ソフトウェアの構成要素は拡張に対して開いていて、修正に対しては閉じていなければいけない」という原則です。

例えばポリモーフィズムを利用した実装はこの原則を満たす例と言えます。 上位のロジックはスーパークラスに依存させ、そのスーパークラスを継承したサブクラスを増やして機能拡張ができる構造にする、これにより上位のロジックは修正しなくても良い状態になるのでOCPを満たしていることになります。

OCPを実現する場合、上位のロジックの実装時に将来の拡張を見据えた適切な単位での抽象化が特に重要なポイントになりそうです。

Liskov substitution principle(LSP)

「S が T の派生型である場合、プログラムで T 型のオブジェクトが使われている箇所は S 型のオブジェクトで置換可能であるべき」という原則です。

例えば、TというスーパークラスとSというサブクラスの2つがあり、Tを参照するロジックが書かれていた場合、そのロジックのTをSに置き換えても「振る舞いが変わらない」場合は原則を満たしていると言えます。

継承を使うときは単にスーパークラスをオーバーライドしたメソッドを実装するだけではなく、「振る舞い」もスーパークラスと同じであるように実装しましょうね、ということを示唆している原則です。

Interface segregation principle(ISP)

「クライアントは使用しないインターフェースに依存するべきはない」という原則です。

これを満たすために、クライアントにとって必要最低限の単位になるよう適切にインターフェースを分けておく設計が必要になります。依存が少なければ将来あるインターフェースに変更があったとしても、修正箇所は小さく抑えることができるでしょう。

Dependency inversion principle(DIP)

この原則は「具象ではなく抽象に依存すべき」というものです。

特にこの原則の対象となるのはプログラムにおいて変化のしやすい具象の要素で、java.lang.stringといった安定した要素は除きます。抽象であるインターフェースに依存したロジックを組むことでそのロジックは未来の変更の影響を受けにくくなります。変化しやすい具象は抽象を介して取り扱うようにすることで、変更容易性を維持していきましょうというのがこの原則が言っているポイントです。

クリーンアーキテクチャ

例の有名な多重の円で表現された設計のアプローチです。(図はこちらを参照)

クリーンアーキテクチャでは以下の4つの要素で構成されており、上にある要素ほどより上位の方針とされています。

  • Entities
    • 企業のビジネスルールをカプセル化したもの
  • Use Cases
    • アプリケーション固有のビジネスルール
  • Interface Adapters
    • Entities, Use Cases向けのフォーマットをDB, Webなどの外部エージェントに便利なフォーマットに変換するもの
    • MVCアーキテクチャもこのレイヤーが保持する
  • Frameworks & Drivers
    • DB, Web Framework

そしてコードの依存はより上位の方針に向かっていなければいけない、というのがこのアーキテクチャのルールとされています。

このアーキテクチャではより変化しにくいものが上位の要素に配置され逆に変化の大きいものが下位の要素に配置される形になっていますが、実際よく修正することになるのって、Controller, Service, RepositoryといったInterface Adaptersのレイヤーと機能固有のロジックであるUse Casesのレイヤーが多いなという実感があったこともあり、納得感がありました。Use Casesを変更を受けてInterface Adapters側も変更をしなくちゃならない、というケースはよくあることだと思います。

また、Interface Adaptersのレイヤーの中にEntities, Use Casesが紛れ込んでいるコードはよく見かける気がしていて、Use Casesに当たるロジックをうまく維持しながらその両端にあるInterface Adaptersなロジックを修正する、みたいなことって結構今までやってきていたなという印象です。明確にUse Casesのロジックとして別コンポーネントに切り出すべきなんでしょうね。

今後のリファクタリングや機能追加時にはこのクリーンアーキテクチャの構造を適用できないか検討してみたいところです。

データベース/フレームワークは詳細

後半の章では、MySQLのようなRDBMSやSpringといったフレームワークは詳細であり、これらの詳細に強く依存しないように実装すべき、という主張がされています。

データベースへの依存はインターフェースを噛ませたりして抽象化しやすい仕組みではあるので同意なのですが、フレームワークに関しては実際問題特定のフレームワークに強く依存したコードにならざるを得ないケースが往々にしてあるだろうなという感想を持ちました。 特にフロントエンドの巷のフレームワークは記法が独特になりがちで強い依存は避けられないと思います。 (クリーンアーキテクチャのユースケースに当たるコードはフレームワークに依存しない記法にしやすいかもしれませんが)

結局、フレームワーク依存を避けた設計はフレームワーク採用による恩恵(生産性に大きく寄与する、可読性を維持しやすい、etc…)とのトレードオフなのだと思います。