クリーンアーキテクチャ本を読むためのポイント
先日のClean Architectureは全てのプログラマにお奨めしたい良著という記事では、ASCII DWANGOから出ているClean Architecture 達人に学ぶソフトウェアの構造と設計(以下、Clean Architecture本と呼ぶ)が、アーキテクチャパターンとしてのクリーンアーキテクチャ The Clean Architecture(日本語翻訳版) を採用するかどうかに関わらず、ありとあらゆるプログラマにお勧めしたい良著であると書きました。
Clean Architecture本は主に設計(実装面もある程度含む)において、メンテナンスしやすいものを作り上げるために必要な知見をコンパクトにまとめた本です。この本で押さえておくべき重要な概念は「知識」とその知識を利用する「依存関係」です。
この記事では、前回よりもさらに掘り下げて、Clean Architecture本を読む上で押さえておくべきポイントをまとめました。
・ Clean Architecture本の内容を正しく理解したい人
・ The Clean Architectureを実践したい人
に向けた記事です。是非、本を読む前、読みながら、読んだ後、それぞれで参照してみてください。
内容がわかりくい!とか、ここが間違ってる!とかがあれば、是非お気軽にコメントなり頂ければ幸いです。
ダーティーな世界は何がつらいのか
クリーンではない、つまり、ダーティな状況では、ソースコードの見通しが悪く、改修コストも増加します。
よく「技術的負債が貯まった状況」という言い方がされることもあります。
・ 依存関係が不明瞭(複雑な依存関係ゆえにいじると随所に影響が出る、どこを調べればいいのかわからない、バグが怖い、変更が怖い)
・ 循環的複雑度の高いコード(分岐の多すぎる関数、コードの長すぎる関数、多すぎるメソッドなど)
・ ユニットテストを書きたいけど、ユニットテストが書けない(実コードとユニットテストの相性が悪すぎる)
・ ユニットテストがあっても複雑怪奇
このようなコードはダーティーな状態です。
こういうシステムをメンテナンスしなければいけない、あるいは新規で開発していく時にダーティーにならないための設計指針として、Clean Architecture本を読んでみましょう。
本の構成
第I部 イントロダクションと第II部 構成要素から始めよ:プログラミングパラダイムは、それぞれ軽く読めば大丈夫だと思います。関数型言語を触ったことがない方は一度触れておくといいかもしれません。
※筆者の考えですが、純粋関数を取り扱った言語を触ってみると、プログラミングとは「変換である」という考えを理解できるようになると思います。
第III部 設計の原則を理解することがこの本の肝と呼べるかもしれません。
第IV部(コンポーネントの原則)、第V部(アーキテクチャ)は、第III部で書かれているSOLID原則だけでは足りない考えを補いつつ、グループ分けについて書かれています。
筆者の個人的な考えですが、この本は「第24章 部分的な境界」より前が、本編だと思っています。The Clean Architectureを実践したいと思ったときに、24章以後は役に立つような立たないような悩ましい文章やコードを多く含んでいると思っています。
※あくまで筆者の考えです。他の方は違う考えをお持ちかもしれません。
また、いくつかの概念は著者のボブおじさんが、Javaのシステムを開発していく上での知恵という要素が大きく関わっています。コンポーネントなどの考え方はコンパイルやデプロイの都合と関わり合っている部分も大きいため、自分が実際に利用する言語や環境、デプロイの仕組みなどに応じて、適宜読み替えて頭の中で組み立て直すと、理解しやすくなるかもしれません。
必ずしもボブおじさんの真似をする必要はありません。大切なエッセンスを適切に選んで、適宜設計について考えてみましょう。
グループ
この記事でグループという言葉は境界線を引かれた集まりのことを指します。(Clean Architecture本で登場する言葉ではありません)
クラス、モジュール、レイヤー、コンポーネント、ライブラリ、パッケージ、システム、または他のありとあらゆる単位のグループがあります。
ここらへんは言語や設計思想によっても言葉が違ってくるものです。
知識と、知識の露出と、依存
前回の記事にも書きましたが、知識とはあるグループに含まれた、ありとあらゆる情報すべてです。ソースコードのパス(ディレクトリ名、ファイル名)や変数・関数・クラスなどの名前、実際のコード、そこにしか登場しないマジックナンバー、言葉や概念などすべてを指します。
すべての知識を内側と外側の両方に区別なく出している状態だと、グループである意味がありません。すべての知識にアクセスできるなら簡単に密結合できてしまうからです。大体の汚いシステムも最初は綺麗な状態だったはずです。それが汚くなっていくのは密結合のしやすさに大きな原因があります。
そのため、あるグループが持つ情報のうち、外向けの情報を絞り込んで露出すべきです。その方法にはC言語のヘッダファイルや、オブジェクト指向言語でよくみかける interface や JavaDocのついた public メソッドなど、様々な方法があります。
※どこまでを「露出している」とみなすかどうかは、人員・文化などによっても異なるかもしれないので、必ずしもこうすべき!という明確な答えはありません。ある程度の指針としてinterfaceについて語られてはいますが…。
依存とは別のグループの知識を利用することです。AとBの2グループがあるとして、AとBがお互いに知識を使わないのであれば、依存していません(依存関係がなく、独立しています)。AがBの知識を利用していればAがBに依存しているという関係であり、AとBが双方の知識を利用していればAとBが相互に依存しあっています。
クリーンかどうかは、依存関係の複雑さで決まります。汚いシステムとは、依存関係が散らかって汚くなっているため、メンテナンス性が落ちたシステムなのです。
ポイント1: モジュールをシンプルに保つ
第7章のSRP:単一責任の原則では、モジュールに関するアクターの視点を基準として単一の責任を負うとしています。
ここでいう責任とは、露出している知識に沿った機能を実現する責任であり、責任を負うことを責務と呼びます。プログラミングにおいて責務は重要な概念であり、設計時に「そのモジュールの持つ責務とは何か」を考え続けることが大切です。
単に「単一責任」「単一責務」とだけ考えると、SRPは理解しづらいものになるため、アクターという概念が重要になります。ペルソナと言うほうが馴染みがあるかもしれません。
たとえばAmazonマーケットプレイスのようなECのシステムであれば、アクターには、ユーザー、出店者といった属性で分けた人間が該当するでしょう。さらに発送管理をするバッチ処理のような、人間以外のアクターもあるでしょう。
ECの「カート」モジュールであれば、それはユーザーしか使うことがなく、ユーザーに関連した責務しか負わなくても大丈夫なはずです。出店者や発送管理バッチ処理など、他のアクターを理由としてカートモジュールに変更が生じるのはSRP原則に反しているため、設計を見直すべきでしょう。
もちろん複数のアクター視点で利用したいモジュールもあるはずです。たとえば、ある商品がセール期間中に行うべき処理は、ユーザーと出店者双方にまたがるため、ユーザーと出店者という2つのアクターから参照されるモジュールとして作る事になるでしょう。
このときユーザーか出店者のどちらか1つのアクターからしか参照されないモジュールと混ざると大体悲しいことになるので、分けられるかぎり分けるようにしたいものです。
第8章 OCP: オープン・クローズドの原則は、なるべく既存のソースコードを変更せずに済むようにし、機能追加はObserverパターンや他のデザインパターン、あるいはプラグインなど、既存のソースコードの外側に拡張していくような仕組みで設計しましょうという原則です。
ソースコードを変更しなくて済むならそれに超したことはありません。
バグフィックスで修正するためにソースコードに変更を加えるは仕方の無い普通の作業ですが、機能追加のためにソースコードを変更したくはありません。
なぜなら、機能追加のためにソースコードの変更をしようとすると、その変更が及ぼす影響範囲の調査など、意識しなければいけないことが増えるからです。
ここではソースコードを引き合いに出しましたが、単位はソースコードに限りません。モジュール、システム、ディレクトリ、ソースコード、クラスなど、様々な粒度で考えるべきものです。
やり過ぎるとenterprise fizzbuzzみたいな目に遭うので、拡張するための、フックとなるポイントはほどほどのタイミングや粒度で設置すべきです。利用する見込みが全くないフックを設置する意味はなく、むしろ露出する知識の増加による悪影響の方が大きいでしょう。
ポイント2: 依存関係を一方通行にする、依存を循環させない
第14章 コンポーネントの結合では、非循環依存関係の原則(ADP)で、
コンポーネントの依存グラフに循環依存があってはいけない。
とありますが、コンポーネントに限った話ではありません。どういう粒度にせよ、できうる限り、相互依存や循環した依存が生じないように設計すべきです。
ポイント3: 知識の露出を最小限にする
第9章のLSP(リスコフの置換原則)と第10章のISP(インターフェース分離原則)はともに、余分な知識の露出を減らすための原則であり、第11章のDIP(依存関係逆転の原則)はそれを実現するための考え方とデザインパターンです。
オブジェクト指向のカプセル化は不完全になりがちです。そもそもメンバーをprivateにしたとしても、そのソースコードを見れば、その中身について知ることができてしまいます。
そこでISPの原則です。インターフェースだけを露出しておいて、その実装については分離します。
LSPは、インターフェースの知識のみで動くようにして、実装といった派生先の情報を参照してはいけないという原則です。
もちろん厳密にいうと、ソースコードを司る神であるプログラマ諸氏はすべてのソースコードにアクセスできるでしょう。
言語によってやり方は異なりますが、古き良きCであれば、ヘッダーファイルだけ参照できるようにすれば知識の露出を最小限にしていると言えるでしょう。requireやimportという考え方がある言語では、インポート先のファイルに、余分な知識が混じっていないか?を基準にするといいでしょう。
ただし、次に挙げるポイントで「詳細と概要を分ける」というものを挙げていて、これを実現するためには、大きな矛盾が生じるために、DIP(依存関係逆転の原則)を使います。
ポイント4: 詳細と概要を分ける
RDBやフレームワーク、ウェブ関連、APIコールなどはすべて「詳細」であるとしています。
たとえば、RDBを前提にしすぎたシステムを構築すると、スケールアウトをしたくなったときに困りがちです。性能向上をしたいときに、札束でぶん殴ってスケールアップをしてもいいですが、RDB以外の選択肢もあればなんとかなったのかもしれません。
検討した結果、その時点でRDBを使うという判断を下すのはかまいません。ただし、RDBに固有のコードを随所に書くと、何かがあって別の仕組みに切り替えたくなったときに、切り替えが困難になります。
そこで、RDBを叩くためのレイヤー(詳細)と、RDBの詳細な知識を使わず、より抽象化したコードを使ったレイヤー(概要)を分離しておきます。
こうすれば、いざ乗り換えようとしたときに、RDBを叩く詳細を、クラウドのマネージドNoSQLサービスを叩く詳細でリプレイスするだけで乗り換えが可能になります。
こうすることで変更に強いシステムになります。
「概要」は「詳細」の持つ知識に一切依存してはいけません。
ところが、実際にプログラムが動くとき、「詳細」なしでは動作しないはずです。どうやって抽象化された「概要」が実際に動くコードになるのでしょうか?
先ほど出てきたDIP(依存関係逆転の原則)はまさにこのことを取り扱います。
The Clean Architectureではports & adaptersというデザインパターンを使います。「概要」側に出島としてのインターフェース(ports)を定義して、そのインターフェースの中身(adapters)を「詳細」側が実装します。
概要側が「詳細側でこういう要件を満たすコードを書いてよ」という要望をportsという形で定義するのです。このportsは、LSP/ISPを守るために概要内の知識のみで定義します。たとえばSQLの考え方など、詳細に沿った知識を使ってはいけません。
詳細側は、出島であるそのportsに書かれた知識と、自分たちの詳細な知識を使い、要件を満たすコードを書きます。
もっともここまでの説明だけでは実際にコードを動作させることはできないため、依存性の注入(DI)や他の方法で初期化をしておく必要があるでしょう。Clean Architecture本でも実際にどうするかは宙ぶらりんになっているところがあります。
これは言語、環境などで異なってくるため、たとえば、ウェブのJavaScriptでいえばindex.jsなどといったエントリーポイントにあたるコード上で初期化を行うのがおそらく手頃なところでしょう。
The Clean Architectureを採用するなら、interface & adapters層と、usecase層をそれぞれ初期化し、実際にusecase層を駆動できるようにお膳立てをします。
コンパイル型言語によっては、コンパイル時の都合やデプロイ方法に合わせて、DIのように再コンパイルがなるべく生じない仕組みが好まれることもあります。
ポイント5: テストしづらいコードから、テストしやすいコードを分離する
ポイント4と似たようなものですが、詳細をさらに分割するための考え方です。
第23章のプレゼンターとHambleオブジェクトでは、ユニットテストしづらいコードをさらに切り分けるHambleオブジェクトパターンを紹介しています。
例えばユニットテストしづらいコードの代表格としてViewが挙げられています。ウェブやモバイルなどのViewはユニットテストしづらいものです。
しかしあるViewを実現するすべての要素が、テストしづらいものだけで成り立っているか?というとそうではありません。
Viewの中でもユニットテストしやすい部分はあるはずです。このとき、Viewの中で部分的にユニットテストを書くのでは無く、別の単位として切り出すのです。
Clean Architecture本では、Viewから切り出したテスタブルなコンポーネントをPresenterと呼び、Viewはテストしづらい要素だけが残っていることになります。
まとめ
Clean Architecture本は、
・ モジュールをシンプルに保つ
・ 依存関係を一方通行にする、依存を循環させない
・ 知識の露出を最小限にする
・ 詳細と概要を分ける
・ テストしづらいコードから、テストしやすいコードを分離する
という5点と、そこからさらにコンポーネント・レイヤー・アーキテクチャといったグループ分けについて考えを掘り下げた本です。
The Clean Architectureを実践するためにはこの5点を押さえておけば大丈夫です。
例の有名な図(円を用いたコンセプト図)や名前(PresenterとかInterface & AdaptersとかUsecaseとかEntity)は脇道に過ぎません。無視するのは良くないですが、そこに捕らわれる必要性はありません。
ディレクトリの掘りすぎ、テンプレートめいたコードの量産、インターフェース切りすぎなどをやってしまうとめんどいプロダクトになりますが、The Clean Architectureの原則だけを守って、あとはバランスよく設計をすれば、そんなにめんどいプロダクトにはなりにくいでしょう。
今回の記事ではクリーンアーキテクチャについて、前回の記事よりも、踏み込んだ解説を行いました。自然言語で概念を説明するのにはどうしても限界があるため、次の記事ではTypeScriptのコードを例とした解説を行います。