見出し画像

クリーンアーキテクチャ再入門

https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html 

1章 概要

クリーンアーキテクチャは勉強した。ある程度分かったつもりだが、実際に案件に導入するかどうか悩んでいる。そんな新米アーキテクト向けに、クリーンアーキテクチャに関する記事を書いてみた。(以下クリーンアーキテクチャをCAと呼ぶ)
まず本論に入る前に、前提事項をいくつか述べておく。

「アーキテクチャとは構造に関する戦略である」

あらゆる構造物は、内部になんらかの構造を持っている。ある構造物がどんな内部構造を持つべきか(戦略)?クリーンアーキテクチャはソフトウェアの構造に関する戦略である。

「CAは戦略であり、目的ではない」

戦略には目的とコンテキストがある。CAをコンテキスト外で使用するのはそもそも論外だし、CAが目的化したら必ず破綻する。
CAはオブジェクト指向分析設計における構造に関する戦略である。その目的は、複雑さを小さく維持することで、長期的に拡張可能な構造を作る事だ。
もしあなたのプロジェクトがオブジェクト指向分析設計をせず、長期的に拡張する予定もないなら今すぐCAを忘れよう。

「アプリケーションアーキテクチャの主目的はアプリケーションの複雑度を下げることだ」

アプリケーションは複雑だ。そのことは日々バグと戦い他人の書いたコードを追いかけている人諸君なら重々知っているだろう。なぜアプリケーションはこんなふうになってしまうのか?
それはもちろん、そのアプリケーションにアーキテクチャがないからだ。昔はあったかもしれないが、昔あった理想は忘れられ、アドホックな修正や拡張が繰り返されて無残な有様に変わり果ててしまう。

「アーキテクチャを維持するには、チームメンバーの支持が必要だ」

維持されないアーキテクチャはむしろ害だ。アドホックな拡張や修正を受け入れざるを得ないなら、CAは捨てた方がいい。
CAが維持されるには、そのチームメンバーがCAに恩恵を受けているという実感が必要だ。それがなければCAは踏みにじられるだろう。

アーキテクトは、メンバーに対してアーキテクチャを強制するだけではダメだ。メンバーが、アーキテクチャの恩恵を受けているという実感をもったとき、メンバーは積極的にそれを維持しようとするだろう。


2章 アプリケーションアーキテクチャの歴史

CAの話を始める前に軽く歴史の話をしたい。

構造化分析設計

大昔のアプリケーションには構造と呼べるものはなかった。関数もない時代、コードはGOTO文と呼ばれるものでとびまわり、そのようなコードはスパゲッティコードと呼ばれれた。
その後、関数化言語が登場した。多くの場合、アプリケーションアーキテクチャと言われて心に浮かべるのは、「構造化手法」ではないだろうか。構造化手法は、関数化プログラミング言語を利用し、その構造はDFD(データフローダイアグラム)により表現される。

https://cacoo.com/ja/blog/what-is-dfd/
https://cacoo.com/ja/blog/what-is-dfd/

ある処理があったとき、この処理を一つの関数の中に全て書く事は悪とは限らない。それが大して長い処理でないなら、それも一つの方法であると言っていい。
ただしそれがある程度長くなってくると管理不能になっていき、それを分割したいという動機が生まれる。構造化では、初めから関数を分割し、関数と関数の呼び出しの流れを設計する。

オブジェクト指向分析設計

その後、オブジェクト指向分析設計が生まれ、それに対応する「オブジェクト指向言語」が生まれる。

CAに触れるなら、ここに触れないわけにはいかない。これを読む方はどの程度オブジェクト指向に親しんでいるだろうか?オブジェクト指向プログラミング=オブジェクト指向だと思ってはいないだろうか?

「オブジェクト指向=(継承、カプセル化、ポリモーフィズム)」ではない。それはオブジェクト指向プログラミングの説明だ。

オブジェクト指向プログラミングとオブジェクト指向分析設計は別ものだ。
プログラマなら上の理解で構わないが、アーキテクトがこの理解では困る。

もしあなたがアーキテクトとしてCAをきちんと学びたいなら、その前にオブジェクト指向分析設計を学んでほしい。ちなみにドメイン駆動デザイン(DDD)は、オブジェクト指向分析設計の子孫であるので、今ならDDDを学ぶことを薦める。DDDを学べばオブジェクト指向分析設計もある程度理解できるだろう。

オブジェクト指向分析設計の共通言語はUMLだ。オブジェクト指向分析設計をするなら、少なくとも全開発者がUMLを読めないといけない。
UMLを書く道具にはいろいろあるが、今ならPlantUMLをおススメする。MarkdownにPlantUMLを埋め込める環境を準備したい。
自分が表現したいアーキテクチャを、UMLでさらっと書いて説明できるのがアーキテクトだ。

構造化とオブジェクト指向の終わらない闘争

この歴史から問題が生じる。現在の日本のソフトウェア業界は、構造化でコードを書いてきた人の割合が殆どだ。そういう人々は、トランザクションスクリプトという開発スタイルをする。これは名前を変えただけの構造化分析設計だ。
彼らからオブジェクト指向分析設計に対する理解を得るのは並大抵ではない。ここに「歴史的に最大のギャップがある」ということは覚悟してほしい。このギャップを乗り越えられなければCAは必ず崩壊する。その覚悟を開発者と共有してほしい。


3章 クリーンアーキテクチャの概要と戦略

おそらくこれを読む人たちがCAと聞いて想像するのはこの絵ではないだろうか。いくつかの円で区切られていて、内側にEntityがありその外側にUseCaseがあり、そして依存の矢印が内側に向いている。
右下に怪しげな継承や依存が書かれた絵がある。そんな絵だ。

一体この絵は何を言っているのだろうか?まるで魔法陣のようにも見えるこの絵を見ていると「この絵を実現したい!」と思えてくるのではないだろうか。それは分かるが、それだけでCAを採用するのはやめた方がいい。

「CAは、アジャイル開発とオブジェクト指向分析設計を前提としている。」

何度も言うが、もしあなたのプロジェクトがウォーターフォール開発か、もしくはオブジェクト指向分析設計などしないというなら、今すぐCAを捨てるべきだ。

さて、ではCAを採用するとどんないいことがあるんだろうか?

CAの何がいいのか?

CAに似たようなレイヤードアーキテクチャやオニオンアーキテクチャは世にいくらでもある。そのなかでなぜCAなのか?

「CAはアジャイル開発に向いている」

CAは将来の変更を容易にするための設計という理念である。これはアジャイル開発にマッチしている。なぜそうなのかというと、レイヤー間の疎結合性を維持するためのアーキテクチャだからだ。CAが言うように設計しているなら、Domain、Usecase、Interfaceそれぞれのモジュールは疎結合であるはずだ。

「CAは制約が少ない」

CAが言っていることは実はそんなに多くない。我々がオブジェクト指向分析設計するとき

  1. オブジェクト指向分析 > ドメインモデル(Entity)

  2. オブジェクト指向設計 > ユースケースモデル(UseCase)

  3. オブジェクト指向プログラミング > 実装(Interface)

こんな手順で開発するだろう。設計が分析に依存し、実装が設計に依存するのは当然であろう。設計に依存する分析や、実装に依存する設計はおそらくない。
だから、手順に従ってコードを書いて行けば自然にそうなるということを言っているに過ぎない。
「まずドメインモデルを書き、つぎにユースケースを書き、最後にインターフェースを書くこと。」CAはこれしか要求していない。
もしあなたがInterfaceから書き始めた時、CAはあなたの敵になるだろう。

「言語非依存」

CAの制約は多くないので、どのような言語でもCAを選択することができ、CAの言葉で書いてあるコードなら知らない言語でも理解しやすい。PythonはDjangoルールで、RubyはRailsルールでというようにそれぞれが独自ルールで書かれているよりもいいとは思わないか?

CAはなぜ難しいのか?

逆に、なぜCAが難しいのか考えてみる。

「依存性に対して制約がある」

CAはモジュール間の依存性というわけのわからないものを制約する。
依存性の制御は、疎結合の設計における主要なテーマだ。モジュール間、もしくはレイヤー間を双方向依存にしないこと、依存の方向をあらかじめ決定しておくことで、レイヤー間を疎に保つことができる。そのような技術をDependency Inversion(依存性の逆転)といい、Dependency Inversionを実現する実装の一つにDependency Injection(依存性の注入)がある。
この種の知識がなければ、依存性に関する制約は面倒なだけだろう。

「原理主義になりがち」

これもまた、CAの難しさの要因だと思う。CAの絵を見て「こうでなければならない」と勘違いしたとき、CAは失敗するだろう。アーキテクチャの目的を忘れてはならない。

「構造化と対立している」

構造化に慣れている人は、前からコードを書いていく。まずfrontendを作り、次にcontrollerを書き、そしてusecaseを書き、最後にdbを呼び出すadapterを実装する。こういう人に、処理から独立した「ドメインモデル(抽象モデル)」を先に考えろ、というのは至難の業だ。
開発者の多くがこのような具象思考タイプであるなら、オブジェクト指向分析設計もCAも諦めた方がいい。
ドメインモデル、ユースケースは抽象を理解したプログラマで固めることができるなら、interface層を彼らに任せるのは良い戦略であろう。

具象思考とは、ようするに実現したい画面という具象がまずあり、それを実現するための文字列や数字という具象を操作するコードを書く、という思考のこと。抽象思考とは、そういう個別の文字列や数字を離れて、例えばUserやCompanyのような抽象概念でものごとを考え、コードを書くこと。
抽象思考の人は、Aさん(User)を返すというインターフェースを書く。
具象思考の人は、AさんのUserIdと、AさんのGenderと、Aさんの住所を返しますというインターフェースを書く。具象思考の人が書くコードは、引数が異常に多い。それはとりもなおさず密結合であるということだが・・・

CAの戦略

つぎに、CAがどういう戦略を採用しているかを考えてみたい。

戦略1:開発手順とソフトウェアの構造を一致させよ

最近よく聞くこととして、ソフトウェア構造と、組織構造を一致させようという話があるが、それに似ている。(逆コンウェイの法則)
ソフトウェアの構造には、組織構造だけでなく開発手順も反映される。もしオブジェクト指向分析設計で開発するのなら、その手順と一致した構造を選択するのは自然である。

戦略2:名前を与えて役割を一元化せよ

こういうものをControllerと呼ぶよ、とかこういうものがUseCaseと呼ぶよというような役割を定義するのは結構難しい。そいういうものをあらかじめ用意しておくことで、設計方針を立てやすい。これはアーキテクチャ一般の議論であり、CAの特徴とは言えないが、とにもかくにもアーキテクチャは必要だ。そしてそれがドメイン非依存、言語非依存の一般性の高いアーキテクチャならなお素晴らしいではないか。

戦略3:疎結合に設計して分業せよ

データベースが得意なプログラマは、Adapterを担当するだろう。ドメイン分析が得意なプログラマはドメインモデルを担当するだろう。重要なのは、Adapterのインターフェース(Repository)を設計するのはドメインモデル担当の役割であるということ。
また、UseCaseを書くときに、Adapterの詳細は知る必要がなく、Repositoryインターフェースだけ知っていればいいということ。これが疎結合であり、分業である。
もしRepositoryインターフェースがなかったら、Usecaseに合わせたAdapterを書くことになるだろう。そしてこのUsecaseとAdapterは密結合となり、分業可能性は失われる。レイヤー分割は有名無実となり、CAは崩壊する。

職能型かfeature型かの分業スタイルの議論が良くあるが、feature型が分業を否定しているというのは誤解だと思う。疎結合なモジュールを分業して担当するのは推奨すべきであり、密結合なモジュールを分業する事に対して否定しているに過ぎないと思う。
つまりfeature型とは、疎結合なモジュール内部の基本設計~詳細設計~実装~テストを一貫して担当することであって、画面~Controller~UseCase~Adapterを一貫して担当することではない。
SPAで結合されたフロントエンドとバックエンドは疎結合だから分業していい。というより、疎結合なAPI定義をしなければならない。
分業する際は、その境界定義(インターフェース)が疎結合である事を意識しよう。

戦略4:アジャイル開発の基本を守れ

CAは長期的な拡張・修正を容易にするアーキテクチャであり、言い換えればリファクタリングを当然すべきことだと考えている。密結合な関数群の一部を修正したとき、その修正の影響は関数群全体に波及するが、疎結合に結合された関数の一つに対する変更は、変更した関数の外には波及しないはずだ。
CAを採用するならそのようにインターフェース定義されていなければならない。そして当然そのインターフェースは単体テストによって常に証明され続けていなければならない。

CAを採用するならリファクタは前提であり、かつ単体テストの実装とCI(継続的インテグレーション)もまた前提だ。単体テストもCIも計画にないプロジェクトにCAを導入するのは時期尚早だ。
まずアジャイル開発の基本に立ち戻ろう。


4章 導入事例

某社社内システムのDX事例

言語はJavaでSpring Bootを使っている。フルスクラッチではなく、某社がもつテンプレートを使用して開発をしている現場である。
開発スタイルはScrumなアジャイル開発であり、DevOpsからもう一歩進んだBizDevOpsを採用している。

controllerはこれらの外部にまとめられており、infrastructureのなかにはない。controllerはfrontendと密結合だという設計思想であり、CAなモジュール群からあえて外されている。
とはいえ、これがCAアーキテクチャであることはパッケージ構成から伝わるのではないかと思う。
私がこれを見て特徴的だと思うのは、usecaseにインターフェースだけを格納していることだ。インターフェース(usecase)とその実装(application)を層として分離しており、一般的なCAより一階層多い印象をもった。

なぜこうしているかというと、UseCaseを非常に重視する開発スタイルだからだ。UseCase駆動開発と言ってもいいかもしれない。そのように、開発スタイルに合わせてレイヤーも変わってくるというのは面白いと思う。

ただし、UseCase駆動がいきすぎるとドメインモデルを軽視しがちだ。このプロジェクトでもそういうケースが所々見られたため、リファクタの対象となった。ドメイン中心のCAを採用するならドメイン駆動でなければならない。

また、本プロジェクトはマイクロサービスを目指しているものの、現時点ではマイクロサービスではなく、モジュラモノリスを採用している。「マイクロサービスはまだ早いけど、モノリシックにはしたくないんだよなあ」というアーキテクト諸君の参考になると思う。

ただし、モジュラモノリスではマイクロサービスのメリットを享受できないという主張もある。
https://www.infoq.com/jp/news/2014/10/microservices-shared-libraries/
システムの中には完全に分離できる複数のサービスが混じっているケースが多い。こうしたものをマイクロサービスに分離することに躊躇する必要はない。恐れずにマイクロサービスに分離すればいいと個人的には思う。

ちなみに、依存関係のチェックのために、checkstyle-inport-controlを利用している。人間の目だけで依存関係をチェックするのは大変なので、もしJavaでCAを採用するなら、是非検討してほしい。


5章 最後に

JJUG CC 2021 Springにて「クリーンアーキテクチャは難しいのか」というタイトルで登壇した。ご興味のある方はどうぞ。

CAはアジャイル開発スタイルで、オブジェクト指向分析設計であるならば、シンプルで、制約が少なく、汎用性の高いアーキテクチャだ。私は適用できるプロジェクトは多いと考えているが、実現は難しいという声もよく聞く。

そんなプロジェクトを預かるアーキテクトたちが、もっと気軽にCAを採用できるようになると嬉しい。

とはいえ、この世界は常に進歩しており、CAがその時の最適解かどうかは定かではない。いろんなアーキテクチャを貪欲に知っていく作業に終わりはない。

この記事が気に入ったらサポートをしてみませんか?