結合度よりも機能的凝集度の観点でアプリケーション設計をするべきと考える理由


はじめに

Qiitaとは関係ないのですが、以前noteに「疎結合は本当に良いことなのか?」という記事を書いたのですが、ちょうどそれに関連して業務コードにおいて設計に迷うことがあったので、凝集度をテーマに考えをまとめていました。

結論からいうと、凝集度の中でも機能的凝集度を高めることにプライオリティを置くことで、その選択が結果的に結合度の塩梅を取ってくれるのではないかと考えています。

予めご留意頂きたいのですが、この記事は現時点での私の解釈であり、普遍的な正しさを追求したものではなく、将来考えが変わるかもしれません。またWebサービスの設計に限った話であり、ソフトウェア設計全般に関わることでもありません。

それでは前置きはここまでにして、本題に入っていきましょう。

凝集度と結合度は対立する?

今回私が考えていたテーマは、サーバーサイドアプリケーション設計における結合度、凝集度の塩梅とその決め方です。

ソフトウェア設計において、Low coupling(疎結合)とHigh cohesion(高凝集)はセットにして良いシグナルであると一般に捉えられています。しかし私はかねてから疎結合それ自体がビジネス的価値に直結するわけではないようにも感じていました。

そして今回様々な資料を当たってみましたが、仮説を唱えるにあたり役立ったのは以下の2つの記事です。(厳密に言うと「TDD再考」の記事において、「Test-induced design damage」が紹介されています)

TDD再考 (8) – 凝集性(cohesion)とは何なのか?
Test-induced design damage

2つ目の記事はRails生みの親であるDHHが書いたものですが、DHHといえばかつて「TDD is dead. Long live testing」(TDDは死んだ、テスティングよ栄えろ)という記事を出して論争を呼んだことでも知られています。そして「Test-induced design damage」もまた同じ文脈で書かれたものです。

これはOOPにおける主流派であろう、関心の分離を図りつつテスタビリティを高める手法に対して一石を投じた内容です。またこの時期には「Is TDD Dead?」というテーマで、DHHはTDDの祖であるケント・ベック、そしてケント・ベックに近い立場でありソフトウェア設計で高名なマーティン・ファウラーと対談しています。

その対談のなかで一つ興味深い発言をDHHは残しており、その模様は動画に収められています。(23分20秒辺り)

I think often time to get really low coupling, you do damage cohesion

Low coupling(疎結合)とHigh cohesion(高凝集)はしばしば対立するという意見です。しかし多くのプログラマはこれに首を傾げると思いますし、実際にYouTubeのコメント欄でも疑問を投げかける内容が見られます。なぜなら疎結合と高凝集はそれぞれセットで考えられており、それに逆行する内容だからです。

これについて「TDD再考」の記事では以下のように解釈が書かれています。

DHH氏の言う凝集性は論理的凝集ではなく「機能的凝集(Functional Cohesion)」のことを指しているのだと思われる。

これに加えて、DHHがどのような文脈でTDD批判を投げかけたのかを考えると、確かにある場面では密結合が機能的凝集につながることに合点がいきました。

論理的凝集ではなく機能的凝集で考える

この前後のDHHの主張を見てみると、DHHはソフトウェア設計全般というよりもWebシステムに強い関心を持っているように見受けられます。

例えば「Test-induced design damage」を読むと、ヘキサゴナルアーキテクチャを導入することでテスタビリティを高めることを狙った手法を、デザインを毀損しているとして痛烈に批判しています。

これはどのようなものかというと、Clean Architectureのような関心の分離を狙ったアーキテクチャの場合、Presenter層が永続化層からデータを取得するまで次のような経路を辿ります。

Presenter → UseCase → Gateway → Driver

しかしDHHはこれを「不必要なIndirection(間接層)を設けている」と言います。Railsを使っている人ならすぐにイメージがつくと思いますが、以下のような経路で十分だということでしょう。

Controller → Model

テストを書きやすくするために「10行で済むコードを60行にしているが、10行のほうがずっと分かりやすい」という立場を表明しており、他にも対談の中で「同じ要件を達成するのであればコードが少ない方が良いデザインである」と発言しています。

しかしマーティン・ファウラーは「ヘキサゴナルアーキテクチャは外部環境と切り離すのが本質的な価値なので批判はお門違いだ」と話しています。一方でDHHはRailsをコマンドラインのアプリケーションにしようとすることはバカげているし、両者をスワップアウトすることに意味はないとしています。つまりDHHが指摘するケースはソフトウェア設計そのものよりも、Webシステムに限定しているのでしょう。

この部分について「TDD再考」記事にもある通り、私はDHHが言う良いデザインとは「機能的凝集度が高い」状態だと解釈しました。

それはどういうことなのか具体的なケースを考えてみましょう。

例えば一律の権限を持った管理者が、単純なフォーム入力によってマスターデータをRDBに永続化するケースを想定します。(そして一般ユーザーがそれらのマスターデータを編集することはありません)

RailsのModelはActiveRecordと密結合することにより、永続化層とドメイン層、さらにはデータのバリデーションも担っていますが、上記のユースケースにおいてそれらが一体化することは高い機能的凝集につながります。なぜならCRUD操作をするルートは1つのみ、またデータをRDBに永続化することは不変であるので、技術的な文脈以外でそれらを剥がす意味はないからです。

また対象のControllerが担う責務がデータを表示・編集することだけであれば、間接層を増やしても技術的な意味で凝集しているのみで、機能的な凝集度はむしろ下がってしまいます。

おそらくこの状況がDHHの言う「疎結合はしばしば凝集性を毀損する」ことなのでしょう。

しかし私が思うに、高度な機能要件を達成しなければならない状況では、密結合が凝集度を下げるシーンはむしろ多く、凝集度の低下によってもたらされるメンテナンス性や再利用性が失われてるシーンを経験してきました。

機能的凝集が失われるシーン

ではどのような時に機能的凝集が損なわれるのかというと、例えばModelのバリデーションやコールバックにアプリケーションのロジックが入り込んだ場合を考えてみましょう。

「一般ユーザーにかけるバリデーションと管理者にかけるバリデーションで分岐する」といった要件をModelで達成しようとすると、フラグ等を通じて制御することになります。例として以下のようなコードを見ていきましょう。

post.rb
class Post
  attr_accessor :admin_mode

  validate :validate_fields
  after_create :reflect_changes

  def validate_fields
    return if admin_mode
    errors.add(:content) if content.blank?
  end

  def reflect_changes
    return if admin_mode
    user.update(post_count: user.post_count + 1)
  end
end

Modelの責務がドメインと永続化層だけでなく、アプリケーションロジックまで含んでおり、このような制御が増えるほど将来に渡って多様化するであろうユースケースに対応しきれなくなるリスクを孕んでいます。これはMVCにおいて一貫した制御を提供するレイヤーがModelにしかないため起きるのでしょう。

前の章で挙げた、管理者のみが操作するといった単一のユースケースであれば密結合の利点を享受できていたものが、複数のユースケースが発生すると雲行きが怪しくなります。

この場合では永続化層のロジックと、アプリケーションのロジックを明確にし、バリデーションやコールバックを制御するためのレイヤー(例えばServiceなど)を設けること、すなわち結合を緩やかにすることで、機能的な凝集度を高められます。

モジュールが適切に分割されていないと、コードリーディングの観点でもアプリケーションのロジックと永続化のロジック、ドメインのロジックがそれぞれ絡み合っていると判別が難しくなります。またデータを永続化しなければアクセスできない機能が増えればそれだけ再利用性が低下します。

テスタビリティの観点から見ても低い凝集度は痛みを伴います。例えばユースケースが一つだけならば、Request SpecでDBにデータを永続化したものを適切に取り出せているかをチェックすれば十分だったものが、ユースケースや内部の制御構造が複雑になれば、Integration Testだけでは担保が難しく、レイヤー間をモックしながらUnit Testをするインセンティブが高まるからです。

とはいえ一つ補足しておきたいのは、これはRails自身の問題ではなく設計をするプログラマの判断によるものです。

例えばスタートアップにおいて開発速度を優先し、アプリケーションロジックと業務ロジックを混同させてもユーザーに機能を提供するという判断を下すのもプログラマの判断ですし、それよりもスケーラビリティを意識して間接層を増やすという選択をするのも肯定されるべきです。

しかし一方でRails自身がMVCというレールを提供しており、それに囚われて適切な判断ができないということは避けるべきでしょう。

業務上での難しさ

さて結合度を下げれば自ずと凝集度を高めるという考えでいると、Presenter層・Usecase層・Repository層というようにレイヤーを設けることに傾倒しがちですが、実のところ機能的凝集度を下げている可能性があります。これについて「TDD再考」の記事で紹介されているStack Exchangeの回答が示唆に富んでいます。

論理的凝集は機能的な特徴よりも技術的な特徴でグルーピングを行うという意味であまり望ましくないと言える。… 例えば、データアクセスを行うモジュールをグルーピングすれば、それは論理的凝集を実現したことになる。… しかし、実際にモジュールの境界を決めるのは技術ドメインではなくビジネスドメインなので、論理的凝集はここで問題となる。論理的凝集を実現することによって、結果的に機能的凝集を失う事になる。

しかしこれを業務シーンにおいて実践し続けるのは難しいことです。なぜなら画一されたルールがない中で、機能的凝集度を意識しながらその都度適切な判断をしなければならないからです。

そしてこれを助けるためのプラクティスの一つがTDDかもしれません。ケント・ベックが言うようにTDDはプログラマにプレッシャーを与えることで、コードに自信を与えるキッカケを作ってくれます。

実際にコードを書く前にテストを書こうとした際に、明らかにPass Throughしかしていない、あるいは制御構造が複雑でテストが書きづらいといったシグナルをもとに、現在修正を加えようとしているモジュールの適切な粒度を判断することができます。

DHHはかつて「TDD is dead」としてTDDを痛烈に批判していましたが、おそらくそれは時代背景としてTDDの手段が目的化していた狂信的な人がいたことが影響したのだと思います。しかし少なくともDHHの記事に書かれているTDD批判は、無意味に間接層を作ることを想定した藁人形論法に近い部分があります。

(とはいえ私自身の話をすると、実のところTDDは苦手で、どこかテストに取り憑かれたような感覚がして良い設計がしづらくなるという経験があります)

またある程度割り切ってしまって、ミクロの機能的凝集度に傾倒せず、アプリケーション全体を見た上で尤もらしい設計をルール化するのも良いでしょう。少なくともチーム開発する上で戸惑う場面は減るはずです。

ケント・ベックが「良い設計が思い浮かばない時には散歩をするのも良い手段」と言うのが象徴的なように、何か一つのプラクティスやアーキテクチャが銀の弾丸になるわけではないので、その時々でビジネスにおいて適切な判断をするのが望まれます。

最後に

ここ数年、関心の分離を図った疎結合なアプリケーション設計に価値を置いて考えていましたが、必ずしも疎結合であることが業務的価値に直結しないのではないかという迷いから、かつての「TDD is dead」にまつわる議論を再考しました。

結果的に論理的凝集ではなく「機能的凝集」を軸に整理していくと、自分が抱えていた漠然とした課題感がクリアになったので、今回記事として起こすことにしました。