Clean Architecture 達人に学ぶソフトウェアの構造と設計 の読書メモ

プログラミングの仕事を始めてからもう10年近く Ruby と Rails ばかり触っていたために、 ほとんどの内容が馬の耳に念仏という感じで芯から理解できない。

まえがき・序文

アーキテクチャは最初に決定されて変更することができないものというわけではない。プログラムには変化が求められるということを認め、不完全な運用をされる可能性も否定しない。目的地ではなく旅路。

作者は50年以上プログラムを書き続けている Robert C. Martin なぜかアンクルボブと呼ばれてる。cleancoders.com の共同創業者。ソフトウェアコンサルティング・トレーニング・スキル開発。

50年前と比べるとコンピュータは小さく高性能になったけどプログラミングの構成要素(順次・選択・反復)はさほど変化してない。過去から未来に来た人も、未来から過去に行った人も、少し時間があればプログラミングできるようになるはずだ。構成要素をどのように組み立るか、というアーキテクチャは過去でも未来でも同じように使えるはずだ。

正しいソフトウェアは開発や保守のコストを最小限に減らす。「約束の地」はある。(ユダヤのカナンの地?)

第1部 イントロダクション

第1章 設計とアーキテクチャ

「設計」と「アーキテクチャ」を区別しない。下位レベルも上位レベルでも、全体の構造を示す時に使うべき。例:ソフトウェアバージョンアップのたびに、人は増えるが、コードの行数はほんの少しずつしか増えてないグラフ。 グラフが示すのは誤ったアーキテクチャは、開発をすすめるほど生産性が低下するということ。うさぎとかめみたいに、あとでアーキテクチャに置き換えるという考えでは危険。クリーンさを犠牲にしても開発速度は速くならない。

第2章 2つの価値のお話

ソフトウェアシステムは「振る舞い」「アーキテクチャ」という2つの価値を提供する。「振る舞い」は要件によって決まる価値。「アーキテクチャ」は変更しやすさ、拡張しやすさという価値。振る舞いが重視されることが多いが、アーキテクチャも同じかそれ以上に重要である。

第2部 構成要素から始めよ:プログラミングパラダイム

第3章 パラダイムの概要

  • 構造化プログラミング:goto を排除し、直接的に制御に規律を与えたもの。

  • オブジェクト指向プログラミング:関数ポインタを排除することで、間接的に制御に規律を与えたもの。

  • 関数型プログラミング:不変性によって、代入に規律を与えたもの。

いずれも、すべきでないことを制限しているということに注目しよう。

第4章 構造化プログラミング

パンチカードでプログラミングしていて、プログラマという職業が認められていない時代。この業界の専門家となろうとしていたダイクストラが、goto が不要であり、むしろ有害であると主張した。ダイクストラは10年以上も批判に晒された。ダイクストラは goto が不要なことを数学的に証明しようとしていたがそれは叶わなかった。それでも現実には goto は使われなくなった。goto の排除が、問題の分割を可能にした。分割したコードをモジュールやコンポーネントと呼ぶようになった。万有引力の法則などは数学的に証明することはできないが、科学的に実証されている。数学的に証明されていないことにも広く価値が認められていることの実例といえる。

第5章 オブジェクト指向プログラミング

かつてC言語では、データ構造の操作をプログラミングする時、ヘッダーと実装を分離することでカプセル化を実現していた。 C++が登場してからデータ構造はクラスとして書かれるようになり、ヘッダーにプライベートメンバ変数を列挙しなければならなくなった。 完全なカプセル化からは遠のいている。Java や C# ではヘッダーと実装の分離をやめた。 つまり、オブジェクト指向言語はカプセル化を重視していない。

継承について。 C言語では異なるデータ構造に対して、それぞれを操作する関数の引数を一致させることで、お互いすり替えても動作するプログラムを書くことができた。 ただし、C言語は型が重要なので、すり替えるときにはキャストが必要である。 より進んだオブジェクト指向言語は継承を使って、これと同じことを簡単に実現できる。

ポリモーフィズムについて。 C言語で標準出力や、標準入力を使う関数はポリモーフィズムを実現している。 仕組みとしては関数ポインタさえあれば、ポリモーフィズムは実現できる。 しかし、オブジェクト指向言語でのポリモーフィズムはより安全で扱いやすい。 ポリモーフィズムを実現したプログラムの優れている点は、 接合すべきものがなんであってもインターフェースさえ満たしていれば再利用可能であるということにある。 言い換えると、ポリモーフィズムは、プラグインを実現できるということである。

依存関係の逆転 ポリモーフィズムを利用しないプログラムは、 常に上位のプログラムが下位のプログラムのインターフェースを知っている必要がある。 そのために include や import などというキーワードを使って依存性を示す。 制御の流れがすなわち依存性につながる。

しかしポリモーフィズムを利用するプログラムはこの依存性を逆転できる。 具体的には上位のプログラムがインターフェースを公開する。 すると下位のプログラムは上位のプログラムの任意の関数を呼び出すことができる。 このとき、制御の流れは上位→下位であるにも関わらず依存性は下位→上位となっているのである。

プログラマは依存性のアーキテクチャを自在に変更できるということである。 たとえばUI、ビジネスルール、データベースの三者があるとき、 ビジネスルールが何者にも依存しないアーキテクチャを作る事ができる。 これが実現すればビジネスルールを変更せずにUIを自由に差し替えるということが可能になる。 より発展させれば、ビジネスルールはそれ単体のライブラリとして独立した開発、デプロイが可能になる。

オブジェクト指向の本質は、依存性の向きを自由自在に変化させること。

補足:ポリモーフィズムというのは元は生物学の言葉で、日本語では多態性などという。 多態性とは同じ生物が違う性質を持っていることを指す。例としては人間の血液型の違いがある。 これはコンピューターサイエンスの文脈では、 同じようなインターフェースを持っている関数はすり替え可能であり、 そのすり替えによってコードの再利用が可能であるということを指して言う。 たとえば、コピーという関数は、読み出しと書き出しの2つの関数ペアが揃っていればいかなる対象もコピー可能である。 このように実装されたコピー関数は、ポリモーフィズムを実現しているなどと言う。

第6章 関数型プログラミング

関数型のプログラミング言語では変数は一度決定されると決して変化しない。 変数が変化しないということは、競合やデッドロックといった問題は起こり得ない。 このことが並列処理に適しているかもしれない。 しかし、その代償として他のプログラミング言語よりも多くリソースを消費することになるだろう。

可変性の分離 一般的な方法として、可変コンポーネントと、不変コンポーネントにわけるという方法がある。 これらを並列処理する場合、競合を避けるためにトランザクショナルメモリを使用するのが一般的。

イベントソーシング 銀行口座の取引を実現するプログラムは素直に考えてみよう。 現在の預金額を記録して、競合が発生しないようにロックを取りながら預金額を変更する。 さて、このような方法とは別に、口座に対するすべての取引履歴のみを記録し、現在の預金額は記録しないという方法がある。 現在の預金額を知るには、わざわざすべての取引を集計しなければならない。 しかし、現在の計算機資源があれば、このような実装でもさほど大きな問題は発生しない。 これが、イベントソーシングという考え方である。イベントソーシングをしながら効率よく計算するには、 たとえば、今日の0時の時点で集計して預金額を記録する。 そうすれば、0時の預金額と0時以降の取引を足し合わせるだけで現在の預金額が計算できる。 このような方針で作られたアプリケーションはCRUDのCRのみを実装すればよく、 すべての値は不変であるため完全な関数型プログラミングが可能である。

まとめ 時代とともに3つのプログラミングパラダイムが生まれた。 いずれも実現できるものを拡張することはなく、 機能を制限することで良い性質を与え、扱いやすくするものであった。

第3部 設計の原則

SOLID原則はクラスに対して適用するルール。モジュールレベルの開発に用いる。

  • 変更に強い

  • 理解しやすい

  • 再利用しやすい

40年以上前から洗練されてきた原則なので価値あるものとなっているはずだ。

  • 単一責任の原則(single responsibility principle)

  • オープン・クローズドの原則(open-closed principle)

  • リスコフの置換原則(liskov substitution principle)

  • インターフェース分離の原則(interface segregation principle)

  • 依存関係逆転の原則(dependency inversion principle)

第7章 単一責任の原則

「モジュールはただ一つのアクターに対して責務を負うべき」という主張。 アクターというのはある指向を持ったユーザーやステークホルダーをひとまとめにしたもの。

単一責任の原則を満たしていない例:想定外の重複 給与システムにおける従業員クラス Employee を考えよう。 このクラスが3つのメソッド calculatePay, reportHours, save を持っている。 実は、それぞれのメソッドを利用する部署が違っている。 言い換えると、このクラスは3つのアクターに対して責任を持っており、単一責任の原則に反する。 たまたま3つのメソッドで同じアルゴリズムで労働時間を計算していたために、 このクラスに実装が集められたのだが、これによって1つのアクターが望んだ変更が他2つのアクターにも影響を及ぼしてしまう。 また、それぞれのアクターのために異なる変更が加えられた時、コンフリクトが発生する。コンフリクト解消の手間は明らかだろう。

解決策 Employee クラスは単にデータ構造(に責任を負う)だけのクラスに置き換える。 そして、3つのアクターのための操作はそれぞれ別のクラスに分割する。 このことが扱いにくく感じるなら Employee のための facade を用意してもよい。

別の案としては、Employee に最も重要なアクターに対する操作だけを残し、 他の機能は他のアクターのためのクラスに委譲する。 (これのほうがオブジェクト指向言語では、自然な気がする)

補足:責任という言葉は responsibility の訳であり、そこには応答可能性というニュアンスが含まれている。 つまり、単一責任の原則とは、クラスや関数はだたひとつの要求や要件に対して応答する機能を持つべきだということを指している。 言い換えると、複数の要求や要件を満たすような機能を持っているクラスはわかりにくく、壊れやすいということである。

第8章 オープン・クローズドの原則

「ソフトウェアは既存の成果物を変更しないように拡張できるようにするべき」という主張。

部品Aが部品Bを参照している時、 部品Aを修正すると部品Bに影響を与える。 しかしその逆は絶対に影響を与えない。安全な修正ができる。

プログラムが部品の組み合わせである以上影響を与えることは避けられないが、 特定の部品が他の部品から依存を外して、影響を受けないように保護することはできる。 こうして保護するべき対象は、ビジネスルールを含む部品。

依存関係のないものをトップレベルの部品とすると、 トップレベルの部品に依存している部品を第2層の部品と言い換えれる。 これを続けていくと、部品が階層関係になっていることがわかる。 アーキテクトはこの階層関係を適切に定めることでオープンクローズドの原則を守ることができる。

第9章 リスコフの置換原則

S型のオブジェクト o1 のそれぞれに、対応するT型のオブジェクト o2 が存在する。 T型を使って定義されたプログラムに対して、o2 の代わりに o1 を使ってもプログラムの振る舞いが変わらないとする。 このとき S は T の派生型である。

License を継承した PersonalLicense と BuisinessLicense があるときこれらは置き換えることが出来る。 リスコフの置換原則に従っている。

長方形クラス Rectangle を継承したクラス Square は、リスコフの置換原則に違反している。 なぜなら、Rectangle が幅と高さを自由に変更できるのに対して、Square は幅と高さを同時に変更する必要があるため。 Rectangle だと思っているクラスに Square を渡すと、期待した振る舞いができないことがある。 これはリスコフの置換原則に違反している。

リスコフの置換原則はインターフェースにも使える。

(タクシー会社の例)

(オブジェクトが互換性を持つようにしよう、ということかな?)

第10章 インターフェース分離の原則

必要としないモジュールに依存するべきでない。 (java の例でなく ruby の例で考えてみる)

class Calculator
  def initialize(user)
  end

  def total()
    ..
  end

  def analyze()
    ..
  end
end

###########################################

require "calculator"

class User
  def total_comment
    Calculator.new(self).total(@comments)
  end
end

###########################################

require "calculator"

class AdminUser
  def analyze_comment
    Calculator.new(self).analyze(@comments)
  end
end

こんな感じで存在していた時に User が必要としている Calculator の情報は total だけなのに analyze も参照可能になっているというのが変かもしれない。 total と analyze が独立した機能ならこれは分解するべきだと言っているのかもしれない。

class Totalizer
  def total()
    ..
  end
end

###########################################

class Analyzer
  def analyze()
    ..
  end
end

###########################################

require "totalizer"

class User
  def total_comment
    Totalizer.new(self).total(@comments)
  end
end

###########################################

require "analyzer"

class AdminUser
  def analyze_comment
    Analyzer.new(self).analyze(@comments)
  end
end

なるほどこれであれば Calculator にあった2つの責任が分解されて余分な依存性が解消されている。 これがインターフェース分離の原則かもしれない。

この簡単な例では強く必要だとは感じないが、より多機能なモジュールを考えた時には複雑さを分解できるというメリットはあるかもしれない。 ただそれは単一責任の原則と重なっているのではという気もする。 ruby だとインターフェースというものがないからどうしてもそうなってしまうのかも。 無理やりインターフェース作ったらもう少しわかりやすくなるかもしれないけど、 わざわざ require を書く必要がないので依存性はそもそも発生しない。

第11章 依存性逆転の法則

変化しやすいクラスやモジュールに対して依存関係を作らないようにしよう。 依存するのはインターフェースだけにする。

Application が ServiceFactory.makeService() を呼出して Service クラスのインスタンスを得る。 ServiceFactory, Service ともにインターフェース。 実際には具象クラスが動く。たとえば ConcreteServiceFactory が ConcreteService を作る。 このとき

  • 上位コンポーネント Application, ServiceFactory, Service

  • 下位コンポーネント ConcreteServiceFactory, ConcreteService

下位コンポーネントはすり替えが可能。

ちょっとよくわからんかったので後でみなおす。

第4部 コンポーネントの原則

第12章 コンポーネント

デプロイの単位のことをコンポーネントと呼ぶことにする。 java なら jar で ruby なら gem という感じ。

昔のプログラムはプログラマが、プログラムをメモリ上のどこに配置するのかを決定していた。 「2000番地に以下のプログラムをロードする」みたいな感じ。 そして、ライブラリもソースコードごと一緒にコンパイルしていた。 ライブラリが増えるとコンパイル実行時間も膨れ上がってしまう。 そこでライブラリは個別にコンパイルして、そのバイナリをメモリ上にロードするようにした。 しかしライブラリをロードする番地もプログラマが管理するし、 ライブラリの配置アドレスはコンパイルしたら変えられない。それは大変。

そこで再配置可能性(relocatability)という概念がうまれた。 コンパイラが出力するバイナリに手を加えて、開始アドレスを指定できるようにした。 さらにコンパイラに手を加えて、再配置可能なバイナリのメタデータに関数名を出せるようにした。 プログラムからライブラリ関数が呼ばれた時に、その関数名を 外部参照 として出力する。 あとは外部参照がどこにあるのかを指示すれば良い。これが リンク と呼ばれる機能のようだ。

リンクのおかげでプログラムは小さなセグメントに分割できるようになった。 しかしプログラムが巨大化するのに対して、ハードディスクの読み取りは低速だったため 外部参照の解決のための読み込みがボトルネックになってきた。

そこでロードとリンクを別のフェーズにわけることになった。 これによってハードディスクの読み取り回数が減り遅いのはリンク処理だけになった。 リンクの処理は リンカ とよばれるアプリケーションになった。

C言語が使われるくらいになったころ、10万行を超えるようなプログラムが作られ始めた。 C言語ではコンパイル結果を .o ファイルとして出力する。 それらはリンカによってつなぎ合わされてリロケータブルな実行ファイルを作る。 モジュール一個ずつのコンパイルはそれほど時間はかからないが すべてのモジュールをコンパイルするのはやはり低速だった。 リンカを動かすのも低速で一時間以上かかることもあった。

その後、メモリもCPUもハードディスクもすべてが大幅に性能向上していった。 ある程度のプログラムなら、ロードとリンクをわけずとも高速に動くようになった。

第13章 コンポーネントの凝集性

再利用・リリース等価の原則

再利用の単位とリリースの単位は等価になる。 コンポーネントを再利用するためにも、コンポーネントにはリリース番号をつけるべき。 そして、リリースによってどのような変化が起きたのかを示すリリースノートもつけるべき。 コンポーネントには一貫するテーマや目的があり、まとめてリリース可能でなければならない。 よくわからない。

閉鎖性共通の原則

同じ理由、同じタイミングで変更されるクラスをコンポーネントにまとめるべき。 変更の理由やタイミングが異なるクラスは別のコンポーネントにするべき。 これは単一責任の原則をコンポーネント向けに言い換えたもの。

全再利用の原則

コンポーネントのユーザーに対して、実際には使わないものへの依存を強要してはいけない。 結合していないクラスをコンポーネントにまとめるべきではないということも主張している。 これはインターフェース分離の原則を一般化したもの。 どちらも「不要なものには依存すべきでない」といっている。

これらの3原則はお互い相反するところがあり、同時に徹底することはできない。 現在の目的に沿って、どの原則を重視するかを変化させていくのがよい。

第14章 コンポーネントの結合

非循環依存関係の原則

コンポーネントの依存グラフに循環があってはいけない。というルール。

他の開発者が修正したことによって自分の修正したプログラムが動作しなくなるということがある。 これを避けるため 週次ビルド という手法が生まれた。 週次ビルドでは、最初の4日間は自由に開発をして、最後の1日でマージするという方法。 ただ、開発者が増えて複雑になると限界を迎える。

これを解決するためには開発環境をリリース可能なコンポーネントに分割する。 そうすることで修正がコンポーネント単位になり、影響範囲が明確になる。 また、依存コンポーネントへの追従が遅れても全体には影響しない。 ただしここで循環依存関係があるとこの作戦がうまく行かない。

循環依存の解消方法は2つ。依存関係逆転の法則を使う。もしくは、中間コンポーネントを作る。

トップダウンの設計

コンポーネントはトップダウンで設計することはできない。 コンポーネントが何かの機能を率直に表現することはほとんどなくて、 ビルド可能性や保守性を表すマップでしかないことも多い。 なので最初に設計するのではなくて、出来上がったものの依存性を分析した結果、コンポーネントを分けていく感じになる。

安定依存の原則

辞書的には「安定している」とは「簡単に動かせないこと」である。 これをソフトウェアにも当てはめてみよう。 多数のコンポーネントから依存されているようなコンポーネントは、とても変更しにくい。 これは辞書的には「安定している」と言ってよいかもしれない。

独立コンポーネントとは、他のコンポーネントに依存していないコンポーネントのこと。 逆に、従属コンポーネントとは、他のコンポーネントに依存しているコンポーネントのこと。

依存しているコンポーネントの数から指標 I を作ってみよう。 fi = ファンイン:依存入力数 fo = ファンアウト:依存出力数 I = fo / (fi + fo) とする。このとき I はある不安定さの指標になる。 ファンインが大きいほど0 に近づき、ファンアウトが大きいほど1に近づく。

安定依存の原則はコンポーネントの依存関係を順にたどっていくと I の値は減少していくべきである。という主張。

※純粋な木構造でできたコンポーネントを考えてみよう。 根コンポーネントは、どの入力にも依存せず、出力は他のエッジにつながっているから I=1 (不安定)である。 葉コンポーネントを考えると、それは入力に依存しており出力は他に依存しないから I=0 (安定)である。

実行可能なコードが一切含まれてないコンポーネントもある。そのような抽象コンポーネントは、安定度が高い。 ただし ruby のような動的言語では抽象コンポーネントは存在しない。

安定度・抽象度等価の原則

コンポーネントの抽象度はその安定度と同程度でなければならない。 安定度が高いことで拡張性を損なってはならないという考え方。

抽象度の指標 A を作ってみよう。 Nc = コンポーネント内のクラスの総数 Na = コンポーネント内の抽象クラスとインターフェースの総数 A = Na / Nc A は抽象クラスが多いほど 1 に近づき、抽象クラスがなければ 0 になる。

安定度と抽象度の2軸をとるグラフを書いてみる。 プロットされるべきでない場所は (I,A) = (0,0) や (1,1) の近辺である。

(0, 0) は安定していて、抽象クラスが一つもないようなコンポーネントを指す。 出力が他のコンポーネントで多用されているのに拡張性がない状態なので、これは望ましくない。 データベーススキーマやユーティリティクラスはこの領域になりがちである。 あまり変動しないなら問題はないが、変更が苦痛となる事が多いため、苦痛ゾーンなどと呼ぶ。

(1, 1) は不安定な上に、抽象クラスばかりで構成されたコンポーネントを指す。 出力が一切他のコンポーネントで利用されていないのに、拡張性が充実している状態なので、これは望ましくない。 実行不可能な上に利用されていない無駄なコードなので、無駄ゾーンなどと呼ぶ。

上記のゾーンから遠い (1, 0) や (0, 1) を結ぶ直線が望ましい状態と言える。 この直線を主系列(main sequence)と呼ぶことにする。 主系列からの距離を D とすると、コンポーネントの D が 0 に近いほど望ましい状態と言える。 リリースごとに D の値を計算して、分析すればコンポーネントが望ましくない状態へ向かっていることを観測できるかもしれない。

第5部 アーキテクチャ

アーキテクチャの主な目的はシステムのライフサイクルをサポートすることである。 優れたアーキテクチャは、システムを容易に開発・デプロイ・運用・保守できる。

  • 開発はチームが大きくなると厳しくなる。アーキテクチャは方向性を定める。

  • デプロイはソフトウェアが大きくなると手順が増えてしまい複雑化することがある。良いアーキテクチャはデプロイも容易にする。

  • 運用に関してアーキテクチャが貢献できることは少ないが、開発者から運用化が透けて見えるようなアーキテクチャは開発保守にも貢献する。

  • 保守で最も難しいのは、機能の追加や修正に対する最適な場所や戦略を見つける「洞窟探検」である。これを助けるのがアーキテクチャである。

柔軟性こそがソフトウェアがソフトウェアたる所以であり「振る舞いの価値」と「構造の価値」では「構造の価値」に重きを置きたい。 柔軟性を保つためには、重要ではない詳細をそのままにし続けること。 ここで言う「詳細」とはIOデバイスやデータベース、通信プロトコル、フレームワーク、サービスの提供形態(web application or native application?)などである。 「詳細」の逆は、ビジネスルールである。ビジネスルールは柔軟性をもたせる必要がない。

かつてパンチカードだった自体に、パンチカード専用のプログラムをハードコーディングしてしまったため、 磁気テープが生まれてからそれらのプログラムをすべて書き直すことになった。このことからデバイス非依存にするという方針が生まれ IOデバイスは抽象化されるようになった。そして今日ではOSを通じて操作するのでIOデバイスがなんであるかを気にせずプログラミングできる。 これがソフトウェアの詳細を決定しないということの実例。

第16章 独立性

これまでに書いたことをサポートするようなアーキテクチャを実現するために、コンポーネントのバランスを取るのは難しい。 現実にはすべてのユースケースを把握することは出来ないし、運用上の制約、チームの構造、デプロイの要件は、わからない。 そして、それらは、わかっていたとしてもシステムのライフサイクルに応じて、変化していく。

システムが満たすべき要件を、ビジネスルールと、それ以外のものに分ける。 ビジネスルールの中にも、ドメインに密接に結びついているものもあれば、ごく一般的なものもある。 たとえば口座の利率計算はそのドメインに密接に結びついているが、入力欄のバリデーションはごく一般的なものである。 これらは異なる理由で変更されるため、独立して取り扱うことができるようにする。 このようにして、システムは、水平に切り離されたレイヤーに分割する。 システムの水平レイヤーをさらに垂直に分割していく。

第17章 バウンダリー:境界線を引く

ソフトウェアの要素を分離し、お互いのことがわからないように線を引く。 すぐ行うこともあるし、判断を保留することもある。

境界線を引くのが早すぎたために起きた悲しい事例がある。 GUIサーバー、ミドルウェアサーバー、データベースサーバーの3つが連携するサービスを開発した。 開発はちょっとしたことをするのにも3つのサーバ間の通信がひつようであり手間がかかった。 しかし販売したときは1台のサーバーに詰め込んでいた。

ソフトウェアアーキテクチャに境界線を引くためには、システムをコンポーネントに分割する。 そして、いくつかをコアとし、それ以外をプラグインとする。 プラグインコンポーネントは、コアコンポーネントに依存するようにする。

第18章 境界の解剖学

コンポーネント間に境界を引くのは、コンパイルの影響範囲とかデプロイのやり直しを減らすため。 コンポーネントの境界を飛び越える方法はいくつかある。 モノリスでは単にメソッドを呼び出しで良い。 他には、スレッド実行とその結果の引き渡し。プロセスのフォーク。マイクロサービスの通信など。

第19章 方針とレベル

アーキテクチャの技術は方針を慎重に分離し、再編成するところにも見られる。 「レベル」は入力と出力からの距離。 ソースコードの依存性は、データフローではなく、レベルと結びつけるべき。 入力を暗号化して出力するプログラムなら reader と write というインターフェースに分けることでレベルと結びついた依存性になる。

第20章 ビジネスルール

ビジネスルールとはお金を生み出したり、節約したりするルールや手続きのこと。 システムの有無によらず業務に欠かせないビジネスルールを最重要ビジネスルールという。 そのビジネスルールが要求するデータを最重要ビジネスデータと呼ぶ。

エンティティは最重要ビジネスデータへのアクセス手段を提供し、 最重要ビジネスルールをメソッドとして持つようなオブジェクトのこと。 DB,UI,フレームワークなどの概念から切り離して考えるほうが良い。

最重要ビジネスルール以外のビジネスルールに目を向けてみよう。 自動化されたシステムを使用する方法を表現したビジネスルールをユースケースと呼ぶ。 たとえば下記の例は、新規ローンを始めるユースケースである。

  1. 氏名を受け取り、バリデーションする。

  2. 与信スコアを取得する。

  3. 与信スコアが 500 未満ならローンの申込みは失敗にする。

  4. それ以外なら顧客を作成してローンの支払見積もりを起動する。

最重要ビジネスルールを、どのような順番で適用していくかを制御している。 ユースケースは、エンティティよりも入出力に近いのでユースケースが上位レベルである。 UIを制限せずユースケースを実装するには、リクエストとレスポンスというインターフェースを定める。 このとき、リクエストやレスポンスの属性に、エンティティをもたせてはならない。 (※rails 的ではない)

Ivar jacobson がエンティティ、ユースケースを命名した。「オブジェクト指向ソフトウェア工学」という本に書いてある。

第21章 叫ぶアーキテクチャ

よいアーキテクチャはその構造物の利用方法や目的が見た目から伝わってくる。 優れたアーキテクチャはユースケースを中心にしている。 フレームワークやツールは保留したまま話をすすめることができる。 テストさえもフレームワークに依存しないように作ることは出来る。

第22章 クリーンアーキテクチャ

前進のアーキテクチャ3つほど見ていくと、どれも「関心事の分離」というのが目的になっている。 そして、ソフトウェアをレイヤーに分けることでこの分離を実現している。 少なくともビジネスルールのレイヤーと、ユーザーやシステムとのインターフェースとなるレイヤーを持つ。

  • フレームワーク非依存

  • テスト可能

  • UI非依存

  • DB非依存

  • 外部エージェント非依存

よく引用されるクリーンアーキテクチャの同心円図。 中心に行くほどレベルの高いレイヤーとなり、依存性が整理されている。

エンティティとユースケースはこれまでに説明した通り。 次のレイヤーであるインターフェースアダプターは、 ユースケースをウェブやデータベースに都合の良い形にデータ変換する。 最も外側の円はフレームワークやツールなどでありほとんどコードは書かない。

図の右下には、レイヤーの境界線を超えるときの例が書かれている。 入力の受け取りはコントローラで受け取るが、 ユースケースによって定められたインターフェースに従わなければならない。 同じように、出力するときはプレゼンターで出力するが、 ユースケースによって定められたインターフェースに従わなければならない。 境界線を超えてデータを渡すときは単に関数の引数でも良い。 ハッシュマップを作ったり、専用のオブジェクトを作ったりしても良い。 このとき、内側のレイヤーにとって都合の良い形に整形して渡すようにする。 なぜなら外側の知識が漏れるとそれは依存性になってしまうため。

第23章 プレゼンターと Humble Object

Humble Object パターンはモジュールを2つに分ける。 テストが難しいモジュールを Humble(控えめ)と、そうでないモジュールである。 たとえばGUIはテストが難しいので humble な View と Presenter に分ける。 View はできるだけ GUI の表示を行うシンプルなものとする。 Presenter はデータを加工したりフォーマットしたりする。

他の例としてはユースケースとデータベースの境界、データベースゲートウェイがある。 データベースのテストは難しいので humble な Gateway とそれ以外の各ユースケースに分ける。 Gateway は SQL を呼び出してデータを取得・更新・削除することだけを行う。 ユースケースは Gateway を使ってビジネスルールを表現する。 データベースの操作を引き受ける ORM は Gateway のような Humble Object とも言える。 アプリケーションが他のサービスと通信する場合も Humble Object が役に立つ。

第24章 部分的な境界

コンポーネントの間にしっかりした境界を作ると、それなりのコストがかかる。 インターフェースを明らかにして、リリースバージョンをつけて依存性を管理する。 これが過剰だと感じられる場合は、最後のステップを省略する手もある。

つまり、インターフェースの設定などは済ませるが、コンポーネントの分割だけせず1つのコンポーネントにまとめる。 これによってバージョン番号をつけたりリリースを管理する負担が軽くなる。

Strategy パターンはその実例と言える。 (※たとえばレコメンド機能を作るときアルゴリズムA,アルゴリズムBがあるとする。 このとき原始的な実装ならレコメンドモジュールの中に詰め込むことになる。 しかし Strategy パターンではレコメンド機能のアルゴリズムをプラグイン化して取替可能にする。 レコメンドストラテジインタフェースを満たすならどれでも呼び出し可能というポリモーフィックにする。 そして、アルゴリズムA、アルゴリズムBはそれぞれレコメンドストラテジインタフェースを満たすモジュールとして別々に実装する。 これによってレコメンド機能とアルゴリズムはコンポーネント境界を作ることができるが、 個別にコンパイル可能にして、バージョンを切ってリリースしたりはしないだろう。)

Facade パターンも当てはまる。 Facade に従属する各 Service は Facade を通じてしか呼び出すことができない。 Facade を参照する Client は Service に依存せず Facade に依存しているので、そこがある種の境界となる。

デメリットとしては、拡張するうちに別の依存性が生まれてしまい破綻しやすいことがある。 お互いが同じコンポーネントにあるために、依存性を追加することはたやすい。 その結果、境界が役割を果たせなくなってしまう。

第25章 レイヤーと境界

Hunt the Wumpus というゲームが有る。これはテキストベースのアドベンチャーゲームである。 これのアーキテクチャを考えていくと、簡素なゲームであっても抽象化出来る部分はいくらでも見つかることがわかる。 しかしオーバーエンジニアリングは悪質であるということもよく言われる。 しかしアーキテクチャや境界が無かったとしたら、拡張性に乏しく、あとから境界を追加するコストは大きなものとなるだろう。 そのためアーキテクトは常に未来を考えながら、システムの進化に合わせて境界が必要となりそうな部分を見つける必要がある。 境界を作るコストと作らないで放置したままシステムを拡張するコストを比較して、 境界を作ることに天秤が傾いた時に動き出すのが理想。

第26章 メインコンポーネント

メインコンポーネントはシステムのエントリポイントである。 メインコンポーネントは開発用、テスト用、本番用、国別、権限別、顧客別などのファミリを用意してもよい。

第27章 サービス:あらゆる存在

(マイクロ)サービスのメリットは、サービス同士が強く分離されていることだと言われる。 サービスはお互いが異なるプロセスで実行されているので変数にアクセスされたりしない。 インターフェースが明確に定義される。

というのがよく言われることだが、実際にはそれほどメリットでもない。 サービスが共有しているデータで強く結びついてしまうので強く分離しているとは言い難い。 サービスのインターフェースが明確に定義されるとは言っても関数で定義した方が明確である。

もう一つのサービスのメリットは、それ専属のチームがサービスを運用することにあると言われる。 それらはスケーラブルだと言われる。デプロイを独立して行えると言われる。 しかし、スケーラブルにする選択肢はマイクロサービスだけではない。 加えて、一つのサービスがアップデートする時、他のサービスは互換性に気をつけなければならないし、デプロイの独立性もない。

失敗例:マイクロサービスによってタクシー配車サービスを実現しているとしよう。 話を簡単にするため、タクシー会社の検索、手配、UI、の三つに分かれているとする。 このサービスがペットの子猫を購入した時にタクシーで届けるという機能を追加した時、 全てのマイクロサービスに改修を加えて同時にデプロイしなければならない。

このことから、マイクロサービスは全ての動作に影響を与える新機能の追加に弱いと言える。 コンポーネントで構成していたらそこまで大変ではなくデプロイ独立性も保てたはず。 コンポーネント的アーキテクチャをマイクロサービスに適用すれば同じことを実現できるはず。

まとめると、マイクロサービスはスケーラビリティや開発の利便性をあげるが、それはアーキテクチャで重要な要素ではない。 システムの境界と、境界をこえる依存性を定義するのがアーキテクチャ。

第28章 テスト境界

テストは全てに依存している。 アーキテクチャの円の一番内側にある。

共通コンポーネントを変更すると、大量のテストが壊れることがある。 これは fragile tests problem 呼ばれる。 fragile tests があると、プログラマは変更を嫌がるようになる。 ナビゲーションの構成を変えるとかそんなこと。

テストを容易にするというのが大事。 「変化しやすいものに依存しない」ということ。 GUIは変化しやすいので、それに依存しないようにテストを書くべき。

第29章 クリーン組み込みアーキテクチャ

ソフトウェアは長寿だが、ハードウェアの進化に伴いファームウェアは時代遅れになる。

ハードウェアに依存する部分はファームウェアになってしまう。 なるべくファームウェアを作らないようにしよう。 ファームウェアを作ってしまう原因は「とりあえず動かす」ことを重視していて長寿にさせることを考えてないから。

C言語とかでは露骨にファームウェアになりやすい。 型の振る舞いがプロセッサのアーキテクチャに依存していたりする。

ソフトウェアと、ファームウェアの境界を引くことは難しい。 だからハードウェア抽象化レイヤー(Hardware Abstraction Layer)というのを作って明確化する。 HAL の API はソフトウェアに応じて調整して良い。

OS のシステムコールもそうかもしれない。OS抽象化レイヤーが必要かもしれない。

第6部 defail(詳細/些細)

第30章 データベースは detail

どんなデータベースを使うかはアーキテクチャにとってはさほど重要でない。 リレーショナルデータベースは普及しており、とても堅牢で数学的に妥当だが、 アプリケーションは、取り扱うデータがリレーショナルデータだということを知る必要はないし、知らないで使える方が良い。

ハードディスクがいらなくなって全てが RAM に変わったなら、RDB のような形式でデータを保存する必要はなくなるかもしれない。 そういうことがあり得るのでデータベースは detail なのだ。

第31章 ウェブは detail

ブラウザとウェブアプリの間でできることは広い。 それはUNIXとデスクトップアプリの間でできることと共通化するのは無理。 ただし、UIとアプリケーションの間の境界なら抽象化できる。 ビジネスロジックはWebでもスタンドアロンのアプリでもスマホアプリでも変わらないはず。

第32章 フレームワークは detail

フレームワークの作者は、自分の身の回りの問題を解決するためにフレームワークを作っている。 だから、フレームワークの利用者がどんな問題を報告したとしても、それが改善されない可能性はある。

フレームワークの作者は、自分がフレームワークを自由に変化させられるので、 ビジネスロジックとフレームワークが結合することに抵抗はない。むしろ、結合させようとする。

フレームワークに依存するリスクは、拡張性のなさ、将来の不安、乗り換えにくさ。 フレームワークに依存せず使っていくのが良い方策。

C++ の STL などは避けられないフレームワーク。 Java だって標準ライブラリを使わずにはいられない。 ただ、そういう物を使うという決断をしたことを認識すること。 離れられないということを自覚すること。

第33章 動画販売サイトの事例

第34章 書き残したこと

別の人が書いたのかな?

controller / service / repository という3つの層に分けるアーキテクチャがある。 これは昔からあるやつで、単純なアプリケーションを作るには良い。 ただしこれは大きなアプリケーションには適さないし、 何よりビジネスロジックに関して言及するところがない。

java のパッケージングシステムを使って、 名前をつけることで機能単位にコードを分けることもできる。 これはビジネスロジックについても多少言及できる。

他にはポートとアダプター、ヘキサゴナルアーキテクチャ、BCEという手法もある。 これらに共通するのはコードを、インフラとドメインの2つに分類することにある。 内側のドメインではユビキタス言語になるような名前づけをすることが推奨される。

パッケージシステムやレイヤーの手法によって依存性を制限しようとしても、依存性を破ってしまうことがある。 それを避けるために静的解析ツールやCIで違反を見つけることもできるが、コンパイラで発見できるようにした方が良い。

java で public を安易に使うのはやめるべき。パッケージの利点が失われる。 最近の java なら public と published を区別しているので、外部から呼び出せる機能をさらに制限できる。

第7部 付録

略!