レイヤードアーキテクチャ
POSAでの定義
レイヤードアーキテクチャを、体系だって書いたのは「Pattern-Oriented Software Architecture, Volume 1, A System of Patterns」だろう。まずはその原典に立ち返って、レイヤードアーキテクチャとは何かをみてみる。
コンテキスト
ソースコードの変更がシステム全体に波及させたくない。それが1つのコンポーネントに閉じられ、他に影響を与えないようにすべきだ。
インタフェースは安定している。標準化団体によって規定されている場合もある。
システムの一部は交換可能である。コンポーネントはシステムの他の部分に影響を与えることなく、実装を入れ替えることができる。
現在設計しているシステムと同様の下位レイヤの課題をもつ他のシステムを、将来構築することがあるかもしれない。
理解のしやすさと保守性のために同じ責務はグルーピングしておきたい。が、コンポーネントの凝集度は高くしたい。
「標準」のコンポーネント細分化はない。
複雑なコンポーネントはさらなる分割が必要だ。
ソリューション
システムの構成要素を抽象度でレイヤ分けする。
レイヤの設計には順序があるわけではない。
レイヤの中では、すべての構成要素が同じ抽象度で動作しなければならない。
あるレイヤが提供するサービスは、その下位レイヤが提供するサービスで構成される。
https://gyazo.com/7bcaea1a75fda3017c33f59cb74bdbb2
実装
1. 抽象化の基準を定義する
基準が複数入り混じることがある。よくあるレイヤリングの例は以下のようなもので、低レイヤはハードウェアとの距離が抽象化の基準であるのに対し、高レイヤは概念的な複雑さが抽象化の基準である。
ユーザに見える要素
特定のアプリケーションモジュール
共通サービスのレベル
OSとのインタフェース
OS
ハードウェア
2. 抽象化基準にしたがって、抽象レベルの数を決める。
3. レイヤーに名前を付けて、それぞれにタスクを割り当てる。
トップダウンで決めに行く方が簡単。
最上位レイヤはクライアントが認識するシステム全体のタスクになる。
他のすべてのレイヤのタスクは、上位レイヤのヘルパーになる。
ボトムアップアプローチは、かなりの経験とドメインの先見性が要求される。
4. サービスを決める
いかなるコンポーネントも複数のレイヤにまたがってはならない。
レイヤJが提供する関数の引数、戻り値、エラーの型は、以下のいずれかでなくてはならない。
プログラミング言語の組み込み型
レイヤJで定義された型
共有データ定義モジュールから取得した型 (ただしこれは厳密なレイヤリング原則を緩和していることになる)
より多くのサービスを低レイヤよりも高レイヤに配置する方が良い。(再利用の逆ピラミッド)
5. レイヤリングを洗練させる
1〜4までのステップを繰り返し、新しいコンポーネントの追加がレイヤリングの原則を違反することがなく安定したレイヤリングができていることを確認する。
トップダウンとボトムアップ両方での検証を繰り返す(ヨーヨー開発)
6. 各レイヤのインタフェースを決める
レイヤJ+1はレイヤJのインタフェースを通してのみ提供サービスを使うようにする(ブラックボックスアプローチ)
7. 個々のレイヤを構造化する
ここまでレイヤ間にフォーカスしてきたが、レイヤの内部でも適切にコンポーネントを分割すべきだ。
8. 隣接するレイヤ間のやりとりの仕方を決める
プッシュモデル: 上位レイヤが下位レイヤのサービスをコールする。
プルモデル: 下位レイヤが自らの判断で、上位レイヤから利用可能な情報を取得する。依存の逆転を防ぐためにコールバックを使う。
9. 隣接するレイヤを切り離す
レイヤJの変更は、インタフェースが不変である限り、レイヤJ+1に影響を与えてはいけない。そうなるように設計する。
10. エラー処理戦略を設計する
あるレイヤで発生したエラーが、そのレイヤで処理するか、上位レイヤに渡すかを決める。
この設計コストが他のアーキテクチャパターンと比べて高い。
変化系
緩和レイヤーシステム
POSAで既に紹介されているが、レイヤ間のインタラクションを、上位レイヤは隣接した下位レイヤだけでなく、他の下位レイヤーすべてのサービスを利用できる、と緩和したもである。変更の影響の波及が大きくなることを代償に、性能面でのメリットをえることができる。
継承によるレイヤリング
これもPOSAにのっている変化系。下位レイヤを上位レイヤが実装継承する。
IDDD のDPO(Domain Payload Object)は、ドメイン層のオブジェクトを、アプリケーション層が実装継承するので、このパターンと言える。 オープンレイヤ
緩和レイヤーシステムが緩め過ぎなところ、もう少し制約を設けたもの。
あるレイヤが呼び出しをバイパスしてもよいレイヤを「Open」として位置づける。レイヤリングを保ったまま、Sinkholeアンチパターンを避けるための方法だが、乱用には注意。
https://gyazo.com/625f7e11d21b93ea7f758624cca83d8f
上図は、一昔前のWebアプリケーションによく見かけたレイヤ定義で、共通のビジネスロジックを置く層を設けてサービス層としているが、全部がサービス層を経由してデータベースにアクセスするのは、開発量が増すので、コントローラ層から直接DAOを呼ぶのも許容する。サービス層はバイパスされることがあるので「Open」と定義される。
こういったWebアプリケーションの多くがそうであったように、Openなレイヤを作ってしまうと、どういうときにバイパスしてよいかの判断に迷うことになる。Openなレイヤの責務が明確であればよいが、上図のように曖昧だと単にコントローラ層に共通して現れるコード群を切り出して置くだけのレイヤになりがちで、POSAに書かれているようなレイヤリングの目的とはズレてくる。(すなわち、そのような場合はレイヤを分ける意味はない)
レイヤをまたいだ実装
DDDのドメイン層とインフラストラクチャ層で、Repositoryはドメイン層の責務であるものの、実装しようとするとインフラストラクチャに依存するので、ドメイン層ではRepositoryのインタフェースだけ提供し、インフラストラクチャ層でその実装をもつ。
ここを指してDIPだとの解説記事が多いが、DIPは各レイヤに見られる原則で、レイヤをまたぐ箇所がドメイン-インフラストラクチャ間だ、と理解した方が正確だろう。
https://gyazo.com/a06b9a61eb65b9862b62408b07d65135
アンチパターン
Sinkholeアンチパターン
問題領域がシンプルすぎて、あるレイヤーが上位レイヤからもらったデータを下位レイヤに受け渡すだけの責務だけしか負わないこと。
いわゆるマスターメンテナンス機能みたいなアプリケーションを、クリーンアーキテクチャの参照実装で作ろうとすると、ユースケースレイヤがSinkholeになる。
「Fundamentals of Software Architecture」(2020)では、あるレイヤーへのリクエストのうち20%くらいがSinkholeならば、それを許容すべきだし、それが80%を越えるようならば、レイヤリングが不適切もしくはレイヤードアーキテクチャが不適切と考え見直すべきだとしている。
NygardのBad Layering
パッケージディレクトリが
model
controller
form
fragment
mapper
dto
logic
のように分かれていて、モデルにFooクラスができたら(もしくはコントローラにFooControllerクラスができたら)、以下のようなクラスが各パッケージに作られる。
Foo
FooController
FooForm
FooFragment
FooMapper
FooDTO
FooLogic
これは何かレイヤを定義しているようで、それが壊れてしまっている兆候を表している。
DTO
正しいレイヤリングをするのであれば、DTOには注意しなければならない。
元来PofEAAでは、リモート呼び出しの回数を減らすために、Domainオブジェクトを分解/収集したオブジェクトを作り、それを受け渡しするものとしてDTOを定義している。
例
典型的なやつ
"Design Patterns and Best Practices in Java"より
プレゼンテーション層
UIを表現する。HTMLやJavascript、それらのテンプレートエンジン。
コントローラ層
UIからのリクエストを受け付ける。それらのクリーンナップ、バリデーション、認証認可などの責務をもつ
サービス層(アプリケーション層)
レコードの追加、email送信、ファイルのダウンロード、帳票の生成などの責務をもつ。
小規模のアプリケーションでは、コントローラ層と統合されることもある。
ビジネス層
業務関連のルールの責務をもつ
たいしたビジネスルールが存在しない場合は、サービス層と統合されることもある。
データアクセス層
データの取得、必要なフォーマットでのデータ表現、データのクリーニング、データの保存、データの更新など、データに関連するすべての操作を管理する責務をもつ。
"Analysis patterns"より
時代背景的には、WebアプリケーションというよりはクラサバのGUIアプリケーションが暗に対象とされている。
ドメイン層
アプリケーションとデータソースに非依存である。
アプリケーションロジック層
ドメインモデルを選択し、単純化する責務。
ユーザインタフェースのコードは含まないが、ドメイン層との繋ぎ(ファサード)の集合を提供する。
ドメイン層の型からユーザインタフェースの型へ変換する。
プレゼンテーション層
アプリケーションファサードからGUIや帳票へ情報を整形して出し入れする責務。
ユーザインタフェースのみに関心があり、ドメイン層の知識はもたない。
データインタフェース層
データソース層とドメイン層間でデータをやりとりする責務を持つ。
レイヤードアーキテクチャじゃなくても…
Rails
Railsは密結合と過度な抽象概念の排除によって、コード量と理解しやすさを追求したフレームワークといえる。Scaffoldで吐き出されるコードは、Nygard Bad Layeringそのものであるが、そもそもレイヤーが分かれていないのでレイヤリング上の問題(再利用性や変更時の影響波及)は該当しない。
レイヤリングされていないことによってその恩恵を受けれないが、これは別のテクノロジーで解決するという目論見である。(これがRubyの言語自体がもつ柔軟性とされている)。
変更容易性とレイヤリング
レイヤードアーキテクチャはレイヤーが低いほど、Volatilityが低い、すなわち変更されにくいことを前提としている。したがって、上位レイヤの変更は、下位レイヤに影響を与えることが無いことがメリットになる。
https://gyazo.com/a4d0cd00ce6ea3e243ccfc45901606d3
上位レイヤが下位レイヤのサービスを呼び出すときは、上位レイヤは下位レイヤの型に値を変換してからサービスを呼び出し、戻り値を上位レイヤで扱う型に変換する。下位レイヤは提供するサービスのインタフェースのみを上位レイヤには公開し、詳細は見せない。これにより下位レイヤが変更されたときの上位レイヤへの影響があるのは、サービスのインタフェースや公開する型が変わったときのみになる。
下位レイヤほどボラティリティが低いので、提供するサービスも下位レイヤ主導で決められ、比較的安定した下位レイヤの型に変換するTypeMapperの役割もぐちゃぐちゃになりにくい。
https://gyazo.com/f5bbd24de348a6edadb9e1f67707a7f3
古典的Webアプリ
このレイヤードアーキテクチャの基本形と比して、古典的なWebアプリケーションがぐちゃぐちゃになりやすかったのは、下位レイヤであるサーバサイドのWeb層が、INもOUTも型変換の責務をになうので、ボラティリティの高いブラウザのレイヤ、すなわち画面の修正の変更影響をもろに受けることにある。このためレイヤードアーキテクチャの「上位レイヤの変更は、下位レイヤに影響を与えることが無いこと」が捨て去られる。
https://gyazo.com/9de0d6d1421aaa67a08b799482b7f84b
さらには、提供するサービスの仕様が上位レイヤ主導で決められる(画面ドリブンの開発)ので、安定的なレイヤーから設計を積み上げるのが難しく、同じValueに対してTypeMapperが重複してたくさん生み出されるもとにもなる。
近代的フロントエンドによる歪さの解消
近代的なフロントエンド技術を使うと、この歪んだレイヤードアーキテクチャは解消され、基本形に近くなる。レイヤー間でやり取りする型やサービスのインタフェース、すなわちSwagger/OpenAPIのようなもの、は双方の取り決めとして存在することが多い点が、下位レイヤが主導で決める基本形との違いになる。
https://gyazo.com/c026f5cb2c4af6e9380737b65c41caca
ドメイン層とインフラ層の依存関係の逆転について
ヘキサゴナルアーキテクチャやクリーンアーキテクチャの示す特徴的な点は、従来ビジネスロジックを支えるレイヤであったインフラ層が、逆にドメイン層に依存するところである。
https://gyazo.com/32337d78095b0d919e72ae9b93aa2824
インフラ層よりもドメイン層の方がボラティリティが低ければ、変更容易性を高めることになるだろう。が、そうでなければ結局ドメイン層の変更にとともにインフラ層も手が入ることになり、効果は薄れる。