Stratified Design
Stratified Designは、書籍『Grokking Simplicity』によれば、「関数を抽象度ごとに層へ分け、上位層の関数は下位層の関数を呼び出すだけで構成する設計手法」である。主なメリットは次の3点である。 変更容易性
下位層を差し替えるだけで振る舞いを変更できる。各層が実装詳細を抱え込む設計では、変更箇所の特定が難しくなる。
読みやすさ
ざっくりした仕様を把握したければ、上位層の関数を読むだけで良い
再利用性
下位層を他の処理でもそのまま利用できる。
https://gyazo.com/490a89ecbfe69ff3dc170cf4bed05ee6
Abelson & Sussman が書いていること
分量は 10 ページほどで、SICP から抜粋した二例 (picture language と数式微分器) で要点を示す。論文の主張は、ほぼ次の3点である。
層は特化された言語: 各層はその層固有のプリミティブ・組み合わせ手段・抽象化手段を備える。上位層はその語彙だけで構成され、下位の実装詳細には触れない。
Each level of stratified design may be thought of as a specialized language with a variety of primitives and means of combination appropriate to that level.
closure 性: 部品を組み合わせた結果が同じ層の部品として再び扱える。同じ層の中で組み合わせを任意に繰り返せる。
層ごとの差し替え可能性: 下位層の表現を入れ替えても上位層は影響を受けない。冒頭の変更容易性はこの可換性に由来する。
We can vary the pieces at any level.
Grokking Simplicity は、これらを関数型プログラミング向けの設計指針として説明し直している。
例題: ナントカPay
ここまでの3点が実装上どう違いを生むかを、ナントカPay という例で確認する。ナントカPayの要求は以下の通りとする。
ウォレットにコインをチャージして、利用したり送金したりできます。
銀行口座を登録しておくと、そこからウォレットにチャージできます。これは必須ではありません。
銀行口座を登録する場合は、本人確認が必要です。本人確認は免許証またはマイナンバーで行います(外部API利用)
口座番号が実在するか確認する必要があります。(外部API利用)
メールアドレスがブラックリストに載っていれば登録できません。(ブラックリストはテキストファイルに1行ずつNGメールアドレスが記載されている)
メールアドレスは全ユーザで一意でなくてはなりません。
これまで一度も登録したことのないユーザであれば、ウォレットに500コインを付与します。
必要な機能に分解すると以下のようになる。
https://gyazo.com/ad45ca6891cf974c8c40b03ac9ba74c5
実装上の責務に基づくレイヤードアーキテクチャ
典型的なサービス/ドメイン/インフラ層でレイヤー分けした場合、全部がデータベースや外部API、ファイル読み込みに依存した処理なので、サービス層も外部I/Oのオーケストレーションになり、ドメイン層に残る処理が少ない。結果として、ドメイン層はスカスカになりがちである。
https://gyazo.com/fc1bcf81521bb149ecb1145d76e2ccb9
Stratified Designに基づくStrata分割
Stratified Design では抽象度で整理する。抽象度が低い層ほど実装技術に近く、変化は少ない。一方、抽象度が高い層ほど業務要件に近く、変更を受けやすい。層をまたいで直接呼ぶ経路はなく、各層はその上の層にとっての「特化した言語」(DSL)として働く。
https://gyazo.com/2298c0f5f4df19c613195d775104e9cd
二つの分割は同じ「層」という語を使いながら別の基準で切っている。次節以降で、その違いがどこから来たかを掘る。
POSAのレイヤードアーキテクチャとの比較
抽象度による層という考え方の歴史
抽象度で層を分ける発想は Stratified Design が最初ではない。
Parnas On the Criteria... (CACM 1972): フローチャート順ではなく「変更されうる設計決定を隠蔽する」軸でモジュールを分けるべきだと論じた。Parnas 自身は「抽象度の階層」に懐疑的だった。 Buschmann ら POSA Vol.1 (1996) Layers パターン: 分割基準は明示しないが、OSI 参照モデルを主例に置いており、抽象度系譜の上にある。
これに対して、現場で「レイヤードアーキテクチャ」として流通しているものの多くは責務軸である。
Eckerson の三層クライアントサーバ (1995) を起源とする プレゼンテーション / アプリケーション / データ
Evans『Domain-Driven Design』(2003) の UI / Application / Domain / Infrastructure
ナントカPay の例でも、典型的なサービス/ドメイン/インフラ層で分けるとほぼ全ての処理がインフラ層に寄ってしまう。
Cockburn の Hexagonal Architecture (Ports and Adapters) (2005) は抽象度に基づく層構造とは異なる発想である。本人インタビューによれば、考案動機は「上下や左右の長方形で描く反射を断ちたかった」点にあり、層構造そのものを拒否している。中心の六角形を取り囲むポートとアダプタは UI 側もデータベース側も対称に扱うことが目的で、「データベースを特別なものと考えるのをやめさせたかった」とも述べている。にもかかわらず実装解説では「外側から内側への層」として紹介されることが多く、ここでも層の呼び名が独り歩きしている。 なぜ責務の種類を「層」と呼ぶようになったのか
Stratified Designの背景と責務軸のアーキテクチャは、同じ「層」という語を使いながら別の基準で切っている。後者があえて層と呼ばれる必要はなかったはずだが、歴史的経緯がある。
抽象度層の流れ (1968〜): Dijkstra THE → OSI 参照モデル (1984) → POSA Layers (1996)。下位層が上位層に抽象を提供する意味で「層」と呼んだ。
三層クライアントサーバの登場 (1990年代前半): Open Environment Corporation の John J. Donovan や Wayne Eckerson らが プレゼンテーション / アプリケーション / データ の物理的な三分割を「three-tier」として広めた。本来 tier は物理配置で、層 (layer) と区別されていた ("tier is used to refer to the physical hardware structuring mechanism, while layer is sometimes used to refer to a conceptual software logic structuring mechanism")。 論理側への流入: 三つの物理ノードに割り当てる責務が UI・業務処理・データ永続化と固定されたため、論理側でも同じ三組を「層」と呼ぶ用法が広まった。
Web 領域での定着: Fowler『Patterns of Enterprise Application Architecture』(2002) が presentation / domain / data source の三層を中心に据え、Buschmann の Layers パターンを参照しつつ「責務で分けた組織化の単位」として layer を使った。Evans『DDD』(2003) も UI / Application / Domain / Infrastructure を「layer」と呼び、PoEAA の用法を継承。
つまり実務での「レイヤードアーキテクチャ」は、抽象度層の語彙をクライアントサーバ三層モデル経由で輸入した結果である。この過程で、layer という語だけが残り、抽象度による層という本来の意味は薄れていった。Cockburn が 2005 年に Hexagonal でレイヤーの概念を拒否したのも、この呼び名の混乱への反発である。
なお、本来は物理配置を指す tier と論理分割を指す layer は別語だが (Layer と Tier の違い を参照)、英語圏でも区別は早期に崩れた。三層クライアントサーバ全盛期は物理ノードと論理責務がほぼ一対一 (UI=ブラウザ / 業務=アプリサーバ / データ=DBサーバ) で、tier と layer を別語で呼ぶ実害がなかった。Sun J2EE や Microsoft DNA などの製品マーケティングが「N-tier」と「presentation/business/data layer」を同じ図のキャプションで入れ替えて使い、PoEAA も logical 側を扱う立場から一貫して layer を採用した結果、三分割を layer と呼ぶ用法が世代を超えて定着した。コンテナ化で物理配置が論理から独立してから、ようやく区別の必要が再浮上している。 「Stratified vs Layered」の対比を読むときは、後者が指す実体が抽象度層 (Dijkstra 系譜) なのか責務層 (PoEAA 系譜) なのかで意味がまったく変わる。本来別物が同じ語で呼ばれている。
類似のパターン
関数粒度で抽象レベルを揃える原則は、1988 年の論文と独立に Smalltalk / リファクタリング界隈からも出てきた。
Kent Beck Implementation Patterns (2007) Composed Method: 「メソッドの本文は、同じ抽象レベルにある他メソッドの呼び出しだけで構成する」。Stratified Design の関数粒度版。 Robert C. Martin『Clean Code』(2008) Stepdown Rule:
We want every function to be followed by those at the next level of abstraction.
「一つの関数のなかで抽象レベルを混在させるな」も併記し、後に Single Level of Abstraction Principle (SLAP) として広まった。Principles Wiki は出典を Clean Code とするが、原理自体はそれより古い可能性があると注釈する。 時代もコミュニティも違うが、いずれも「一つの関数の中で抽象レベルを混在させない」という点で共通している。Stratified Design を関数粒度で実装するときの作法として読むなら、Composed Method / Stepdown Rule / SLAP のいずれを経由しても構わない。