アプリケーションの階層を再考し、
こんにちは、びしょ〜じょです。
11月26日といえば1、後醍醐天皇、Laz、おれの誕生日でした。みんなは何人わかるかな?
# ここに欲しいものリストを貼る
1. はじめに
世の中にはレイヤードアーキテクチャないしはDDDの実践を謳うソフトウェアプロジェクトが多く存在する。 しかし、その実態のいくつかは、部分的または全くもって、うまくいってない。
なるほど一見してわかりやすいディレクトリ構成…? よくみたら抽象度がまばらでまるで迷宮。 レイヤードのためのレイヤー構造。 依存関係がぐちゃぐちゃでドメインロジックやビジネスロジックが渾然一体となっている。 ドメインドリブンとはドメインドリブンのことなのだが。 新規実装はどこに何を置けばいいですか? 変な位置にあるこの関数の処遇は誰に聞けばいいですか?
なぜこんなことに…いや、そもそもレイヤードな構造はなぜ在り、これらは何に失敗しているのか? というのを自分でgRPCサーバを実装していたら思った。 筆者は個人的にはライブラリか言語処理系しか書いてこなかったんで、アーキテクチャを真面目に考えたりすることも無かったんでよかった。 よかったんで書きます。
本 連載 では、ソフトウェア開発におけるレイヤードアーキテクチャの意義を、人間の認知負荷の観点から再考し、OCamlを用いて実践、さらに発展的な手法、考察をおこなう。 まず、レイヤードアーキテクチャが脳の認知負荷を下げることを確認し、次いでDDDにおけるレイヤー分割の判断基準をフローチャートで整理して混乱がおこらないようにする。 実装ではopaque typesとモジュールを利用してドメインの生成を絞り、エフェクトハンドラを利用してレイヤー境界である依存関係を値から文脈の世界に追い出す。 実装を経てアーキテクチャ全体を見たときに、extensible interpreterと構造が共通していることに気づく。 最後に、副次効果としてテスト容易性が得られることを確認する。
ゴチャゴチャ日本語を書くパートが長くなったんで2分割した。 連載と言っているがこれと実装の2回を想定している。 本項では前半部分、レイヤードアーキテクチャの意義とDDDにおけるレイヤー分割のフローチャートまでを示す。
一応想定しているアプリケーションとしては、ビジネスアプリケーションというかWebアプリケーションになる。 言語処理系はパイプラインアーキテクチャになりそうだし、OSなども全く違う作りだろう。 とはいえアーキテクチャをちゃんとアプリケーションに沿って真面目に考えましょうとか認知負荷のあたりは後述するが普遍的な話やと思います。
2. そもそも何がDDDやねんと
2-1. レイヤードアーキテクチャの存在理由
抽象化する理由はなにか? それは いま 集中したいこと とどうでもいいことを分けるため。
例えばFunctor。コンテナの構造には興味がなく、中身aがbに変化することにだけ興味がある。
class Functor f where
-- コンテナfがListなのかMaybeなのかに関心なし
fmap :: (a -> b) -> f a -> f b
add1 :: Functor f => f Int -> f Int
add1 = fmap (+1)
--- ここまで中身に興味なし ---
add1 [1,2,3] -- [2,3,4]
add1 $ Just 3 -- Just 4
レイヤードアーキテクチャも同様に、レイヤー外部のことに関心を持たず、"いま"集中したいことにだけをレイヤーごとに記述する。 アプリケーション中のエンティティについて考えるとき、DBの都合には関心がない。
2-1-1. 認知負荷、凝集度と結合度
なぜ集中するために抽象化して関心のないものを削ぎ落とすのか? 理由の一つに、人間のワーキングメモリが一度に保持できる情報のチャンクは 7つ 前後2と非常に小さいことが挙げられる。 例えば新たな仕事先で巨大なコードがまったく抽象化されずに書かれていたら、実際に開発に着手できるようになるまで大変な時間がかかるだろう。 一つの関数に、まずDB操作が始まり、業務仕様があり、エラー制御があり、ワーオDBの中間テーブルのオブジェクトをまた操作してる…そしてAPIレスポンスにパッキングして、とフラットに書かれていたら、文字通り 脳がパンクする のだ。 あまりにも大きな認知負荷で全くコードが読めない。読む気にならない。
これを解決するための構造的特性が、ソフトウェアアーキテクチャにおける 高凝集・疎結合 だ3。 凝集度はコンポーネントが一つの目的に集中している度合いを指し、結合度はコンポーネント間の依存関係の強さを指す。 上記のようなベチャッとした平たいコードはいろんな関心が集まっているので凝集度が低い。 全てが一つのコンポーネントに詰め込まれているので、他のコンポーネントからの依存が強く結合度も高まる。 何も考えず単にフラットなコードというのは、単にソフトウェアアーキテクチャ的観点からも品質が低いことが指摘できる。
まず凝集度について。 凝集度にも種類があり、単にファイルが分割されただけでその中身のエントリーの関係性が薄い場合(偶発的凝集)は意味をなさない。 目指すべきは機能的凝集であり、一つのコンポーネントが一つの目的に強く関連するエントリーを持つことである。
次いで結合度に思いを馳せる。 抽象度が高く、外部への依存が少ないコンポーネントは、他のコンポーネントからの依存関係が弱くなる。 こちらも抽象化・隠蔽に失敗して内部データや実装がリークする内部結合から、プリミティブ型などそれ以上の詳細がない型だけが行き来するデータ結合を目指したい。
結合度が低いとレイヤーの移動で抽象度や対象が適切に切り替わって垂直方向に対する脳の負荷が下がり、高凝集だと至ったレイヤーでどのコンポーネントを参照すればよいか迷子にならず水平方向で脳への負荷が下がる。 ビジネスロジックでドメインオブジェクトを触るときに疎結合のおかげでDBでの表現について("今は")考える必要がなくなり、コンポーネントが高凝集だと特定の外部サービスの抽象化されたAPI呼び出しがどこにあるか迷わなくなる。 つまりレイヤードアーキテクチャは、抽象度という視点でコードを縦横に割り、高凝集なチャンクにまとめ、疎結合な境界線を引くことで、ハードウェア性能の高くない人間一般の脳でもその認知を超える巨大なシステムが扱えるようになる道具なのだ。
2-1-2. 分割の軸としてのDDD
「では高凝集疎結合にしよう!」となったんですが、具体的にはどうやって? 結局ここでうまくレイヤーやコンポーネント分けに失敗すると脳に悪いアーキテクチャが生まれてしまう。 そこでDDDを使う。 まずアプリケーションにおける ドメイン 、つまりプロダクトの扱うエンティティ、その間の制約、計算方法、など抽出された根幹部分を 絶対的な軸 として用意し、そこからの相対としてビジネスロジックや外部との接続を定義していく。 ここを軸にすると、DDDの依存の方向と同様に2つの概念が得られる:
-
Usecases … ドメインに関係や動きを生む
ドメインは在り方、あるいは生まれ方や変わり方と消え方、つまりルールだけがあるので、これらをインタラクトさせる必要がある。 ユースケースにはドメインの動きを記述する。 例えば"ドメインのルールで値をチェックし、それらを保存する"とか。
-
Infrastructure … アプリケーション外部との接続
でもユースケースはピュアに書きたい。 だってドメインの動きを書くだけなのにDBのコネクションエラーや外部APIのエラーなんて"今は"関係ないはずでしょ? そこでユースケースはドメインの動きの記述に 集中 し、実際の外部との接続はインフラ層と呼ぶ場所に任せる。 外部とはDB、Web API、ファイルシステム、などなど。
ドメインを軸にすると決めることで、それを動かすためのユースケースが生まれ、ユースケースがドメインの動きのモデリングに集中するために実際に外部サービスを呼び出す場としてインフラ層が生まれる。 ドメインを中心においた場合の解の一つとしてDDDが得られる。
2-1-3. ソフトウェアアーキテクチャの存在理由: 認知摩擦の解消、コンウェイの法則
ここまで個々人の脳の認知負荷を考えてきた。 個人的な感覚の開発風景からチーム開発に話を移してみると、いろんな人間の脳が出てきて、いろんなキャパシティの脳が人間というインターフェイスでコミュニケーションを取り合い、ときに認知力や認識方法の違いで摩擦を感じ、疲労する。 ソフトウェアアーキテクチャを決定し、"どこに何を配置するか"に制約をつくることで、チーム全体が共有するメンタルモデルを統一し、摩擦の発生を抑えられる。 ソフトウェアアーキテクチャがレイヤードであることに重要さは必ずしもないが、ソフトウェアアーキテクチャが定まっており 厳密に実施されている ことは重要である。
このロジックはどこに書くのか? —— ある人はドメイン、また別の人はインフラに置くべきだと考える。 この認知のズレがコミュニケーションに摩擦を生み、全体的な開発スピードが低下していく。 ソフトウェアアーキテクチャによってどこに何を配置するかを強制・実践していくことで、皆が同意できる自明な場所にて解決され、上記のような議論が発生しなくなる。 厳密な実践を続け、形骸化した謎の三層アーキテクチャにしないことまでが重要になる。
このコードの構造と組織の認知の関係性は、まさにコンウェイの法則の指摘するそれである。 上記で述べたように、ソフトウェアの構造がカオスであれば組織間のコミュニケーションパスもまたカオスになるという、ある種の セカイ系的な 繋がりがある。 手元にあるソフトウェアの構造の解決で、コミュニケーションコストの解決がなされるわけだ。 ハッキングから今晩のおかずまで同一コンポーネントに記載された凝集度の低い場所からは、様々な内容の議論がチーム内のあちこちで発生するだろう。 結合度の高い箇所で実装に関する議論を始めたら、バックエンド、フロントエンド、SRE、更にはPMを引っ張り出して話をする必要があるかもしれない。
このコミュニケーションコストの問題は、往々にして組織運営などで解決する傾向が見られる4。 コミュニケーションコストのために更にミーティングが増える。 しかし根本的な問題というのは議論が必要なソフトウェア側にあるため、ミーティングのためのミーティングはそのことを看破できる場たりえるが、そうでなければただただコミュニケーションコストが肥大化するだけだ。 確かに問題を認識していてもリアーキテクチャは腰が重い。 実装も時間がかかる。 しかしやらない場合、増えたミーティングの時間もなくならない。 マイクロサービスのような高階のアーキテクチャならば尚更、配置の議論の複雑さが高まるだろう。
"どうあるべき"が掲げられ、それに沿って作られているのが望ましいが、歴史的経緯により偶発的に積み上げられた実装の地層も多く存在する。 それでも、"本来 どうあるべき"という眼差しを改めて向けることはできるし、その新たな眼差しを共有できれば、実装も変わりコミュニケーションコストも少しずつ減っていくんではないか。 読者の皆さんとこの眼差しを共有するため、本項ではとりあえず次章のフローチャートを提案する。
2-1-4. ちなみに: レイヤーやDDDは必然ではないし、適さない場合もある
当然だがレイヤードアーキテクチャは必然のものではない。
まずDB操作が始まり、業務仕様があり、エラー制御があり、ワーオDBの中間テーブルのオブジェクトをまた操作してる…そしてAPIレスポンスにパッキングして を一つの関数でおこなうことは、例えばシェルスクリプトでは、適当なハッシュテーブルをインメモリDBと考えれば、よく書くだろう。
これでワークする理由は、コード全体が小さいから(そう、ですよね…?)。
他にもいくつかの理由でレイヤードアーキテクチャたりえない場合がある。たとえば:
- 単純なCRUD
- データ変換
- ハイパフォーマンスが必要な場合
1.はシェルスクリプトと概ね同じ理由で、コード全体が小さく脳への負荷が小さい場合。
2.は、単にデータ形式を変換するだけであり、ビジネスロジックが存在しない場合。それぞれの変換レイヤーがデータの内容を知っていればよいため(ここでfmap を思い出す)。
3.は、レイヤー分割による抽象化オーバーヘッドが性能に悪影響を与える場合。
"今"解きたい問題が脳への負荷だったので解法の一つとしてレイヤードアーキテクチャを選択したにすぎない。 そしてDDDもまたそうであり、主題としてドメインを固定したゆえにDDDになっただけである。 例えばすでにデータベースにデータがあり単純な構造ならばトランザクションスクリプトパターンが適しているかもしれないし5、既存とは毛色の機能を追加したい場合は別のアプリケーションに切り出してマイクロサービスなどといった高階のアーキテクチャに話が移っていく。 プロダクトの提供したい価値や業界によってはもっと別のアーキテクチャ特性にフォーカスしたい場合もある。 大事なのは、そのアーキテクチャで何を解決したいかを明確にすること。 DDDの実施ができてない場合は、ただの理解不足なだけではなくDDD自体がアプリケーションや解決したい問題にフィットしていない可能性もありえる。
2-2. フローチャートで脳とプロジェクトを整理する: オニオンアーキテクチャ
先に述べたようにDDDには3つの概念、domains, usecases, infrastructureがある。 ここですこし構造を整理する。 突然ですが application というレイヤーを用意します。 その中にusecaseを置く。 Applicationでは"アプリケーションが何を達成するか"を記述する。 なぜそんなことを? アプリケーションはときに外部サービスとの通信が必要になる。 ここでもその外部サービスが何をしてくれるかにのみ関心があるため、外部サービスのインターフェイスを記述する。 この外部サービスとdomainにあるrepositoryを操作して、usecaseにアプリケーションの振る舞いが書かれる。 これにより、applicationを覗くだけで"何が必要で、何ができるか"が一目瞭然となり、脳にいいですねえ。
Usecaseないしはapplicationの立ち位置がスッキリしたので、infrastructureもスッキリさせる。 Applicationはアプリケーションのこと だけ を考えたい、いわば 内側 の世界だった。 ともすれば、infrastructureはアプリケーションの 外側 の世界にあたる。 外界には、こちら(アプリケーション)から外界に働きかける場合と、外界から働きかけを受ける場合の2つのふれあい方がある。 働きかける場合は、例えばDBにデータを保存したり、外部APIを呼び出したりする場合。 これらはinfrastructureに置く。 働きかけを受ける場合というのは、gRPCリクエストを受け取ったりHTTPリクエストを受け取ったりする場合。 これらは interface に置く。 なるほどたしかに、外界から見ればprotobufやAPI schemaはインターフェイスですね。 またインターフェイスかよって思うかもしれないが、OCamlerはこれ以外のinterfaceをsignatureと考えてくれればスッキリするんで、静かに。
アプリケーションの内・外、さらに内側の階層…という構造から、これは(オニオンアーキテクチャ <: DDD)と呼ばれることもある。 依存の方向性がすべて内側にのみ向かっており、最内のドメインは何にも依存しない真実のみが記載される。 このルールによって、それぞれはそれらの内側の知識だけあれば書けるようになっており、脳への負荷が下がる。
ということで言い訳が終わり、もう少し解像度をあげるためにフローチャートで見ていく。
START: コード化しようとしている概念
⇓
Q1. それはアプリケーション上の知識やルールか?
(例: データの整合性、計算ロジック、状態遷移のルール)
├─ Yes → Domains: Object (entity) / Value / Repository
│ アプリケーション上の存在の定義
└─ No ↓
Q2. アプリケーションの挙動そのものか?
(例: "登録"というタスクのステップ、"送金"の手順)
├─ Yes → Application: UseCase / Service Intf.
│ 知識と外部への要求を呼び出して処理の手順を記述する
│ 外部サービスのインターフェイス
└─ No ↓
Q3. それは外部からの入力を受け付ける窓口か?
(例: gRPCリクエスト受け取って処理する)
├─ Yes → Interface: gRPC handler / HTTP Controller
│ 外部から入力を受け取り、アプリケーションを実行する
│ UseCaseを呼び出すアダプター
└─ No ↓
Q4. その機能はIOを発火する?
├─ Yes → Infastructure: DB / Service Impl.
│ Repositoryや外部サービスの実装
│ DB、乱数生成器、外部サービスの実際のAPI呼び出しなど
└─ No → Domain Service
このフローチャートで最も重要なことは、とにかく (この)ルールを厳守し続けること である。 例外ができるとアーキテクチャの前提が崩れ、レイヤーを突き抜けていく例外の周辺で認知負荷が上昇し、レイヤードアーキテクチャないしはDDDのうまあじが破壊される。 ルールが厳守されないと、コミュニケーションの摩擦がちょうどその箇所から新たに発生していくだろう。 御プロジェクトに特殊な事項がある場合は、まず実装上も特殊な事項なのかを熟考していただき、その場合DDDに例外を突っ込むのが正しいのか、別のアーキテクチャにすべきかは検討してほしい。 御プロジェクトは多分ユニークだと思いますが、他のプロジェクトも 同様にユニークな 価値提供をしているんで 逆に 実装として特殊なケースは多くない。
これらを反映すると以下のようなディレクトリ構造になる(図2.2)。
/(root)
├─ domains
│ ├─ objects
│ └─ repositories
├─ application
│ ├─ usecases
│ └─ services
├─ infrastructure
│ ├─ db/sql
│ └─ services
├─ interface
│ └─ grpc
└─ main
ログやメトリクスなどのアプリケーションの運用上必要になるメタ的なものは登場していない。 これらはレイヤーをまたいで直で現れる場合がある。メタいので。 いわゆるアスペクト指向プログラミングの領域になり、それらの実践は言語やツールに依存することになる。
3. おわりに
書いてくうちにどんどん日本語がデカくなってしまったので二部構成にした。 実装の話は次回。
コンウェイの法則については筆者はこれまでピンと来ていなかったが、本項をまとめるうちににわかに理解できてきた。 つまり書いてる途中は(書いたあとも)理解が浅かった。 この記事が面白かった。

また一言添えておくと、アーキテクチャを決めると単にコミュニケーションが減るのではなく、 不要な コミュニケーションがなくなるだけだ。 デプロイ可用性やテスト容易性などを強く指向するアーキテクチャならば、SREやQAエンジニアとの連携が必要になるだろう。 ただそれはアプリケーションやプロダクトの運用に対し 本質的な コミュニケーションなので、減ることも減らす必要もない。
ブログ内で何の言及もしていなかったがコメントが追加された。 GitHub sponsorも始めた。 いいから黙って俺に投資しろ 。
-
この記事は11月26日に執筆を開始した ↩
-
Mark Seemann. 『脳に収まるコードの書き方』. オライリー・ジャパン. 2024. 長年の経験に裏打ちされた著者なりのプログラミング哲学や著名人からの学びが、example projectを作る過程でつらつらと述べられている。ページを捲るたびにハッとすることが多い。良書 ↩
-
Mark Richards, Neal Ford. 『ソフトウェアアーキテクチャの基礎』. オライリージャパン. 2022. やりたいこと、できることに応じたソフトウェアアーキテクチャが並んでいる。アーキテクチャを考えるにあたっての指標も並んでおり、どこを目指せばよいかも考えられるし現状の診断もできるようになる ↩
-
著者調べ ↩
-
本当に 単純な場合に限る ↩

投稿されたコメントはCC BY 4.0ライセンスの下で公開されます。