【設計ナイトトーク】コンポーネント設計って何だろう?
はじめに
この記事は、2024年6月14日に開催されたテックイベント「設計ナイト2024」で筆者が登壇して話した内容をまとめたものです。設計ナイトは、「吉祥寺.pm」というテックイベントを主宰する@magnoliakさんによる個人の勉強会です。個人の勉強会なのに、虎ノ門ヒルズにあるCARTA HOLDINGSさんの素敵なオフィスのイベント会場で開催され、100名もの技術者を集客していました。すごい!
なお本記事は、発表内容のテキスト起こしではなく、当日話し切れなかったことや、他の登壇者の話を聴いて得た気づきなども盛り込んでいます。
発表スライド全体はDocswellで公開しています。
コンポーネントとは何?
コンポーネントの定義
私たちは日々の開発の営みの中で、「コンポーネント」という言葉をよく使います。ですが、コンポーネントという用語は幅広い意味で使われ、標準的かつ共通的な定義はありません。言語やフレームワークによって固有の意味づけがされている場合もあります。この先の話を進めやすくするため、まずはいくつかのコンポーネントの定義を引用して見ていきたいと思います。
まずはマーチン・ファウラー。
彼はモジュールをシステムのサブセットと定義した上で、コンポーネントはモジュールの一形態だとしています。「独立して置換できる」という特性を備えているものがコンポーネントである、と。
次にロバート・C・マーチン(通称アンクルボブ、またはボブおじさん)です。
彼はデプロイ単位だと言い切っています。JARファイルやDLLファイルが具体例となります。
続いてSWEBOK V3.0です(Software Engineering Body of Knowledge。スウェボックと読みます。たぶん)。
「独立した単位であり、明確に定義されたインターフェースと依存関係を持つ」のがコンポーネントです。また、「独立して構成・配置することができる」ともされています。(できる、というのは可能性を意味し、MUSTではないと私は捉えました)。
最後に、C4 modelを見てみましょう。
「明確に定義されたインターフェース」を持つというのはSWEBOKと同様です。背後にあるクラスのまとまりが、インターフェースの責務としての振る舞いを提供すると捉えることができます。
どのようにパッケージングしてデプロイするかは「別の独立した問題」だとし、インターフェースの定義からは切り離しています。
以上をまとめると、次のように捉えることができます。
コンポーネントは明確に定義されたインターフェースを持っていて、何らかの振る舞いを提供する単位だという点は共通しています。それに加えて、置換可能性やデプロイ可能性という特性を必須とするのか、あるいはオプションとするのかが違いだと考えます。(必須と考える場合は、必然的にコンポーネントはある程度の大きさになるでしょう)。
あくまで捉え方の話なので、どちらが正しいということではないのですが、本記事においては左側の考え方で話を進めます。
ソフトウェアのアーティファクト(成果物)の大きさについてまとめると次の図のように捉えることができます。
ユースケースなど、ユーザーにとって意味のある大きな機能性を提供する単位が、アプリケーションやサービスです。最も小さいのは、プログラミング言語の最小単位の構成要素である、クラスや関数です。それらの中間にあたるのがコンポーネントで、独立した何らかの振る舞いを提供する単位です。
なお、クラスや関数が単独で独立した振る舞いを提供する場合もあるので、その場合は「クラス(関数)=コンポーネント」となります。
C4 modelの概要図を以下に引用します。二段目のContainerが環境にデプロイされて動作するアプリケーションやサービスです。関連するアプリケーション群、サービス群によって上位概念のソフトウェアシステムが実現されます。
コンポーネント設計とは
コンポーネントとは、明確なインターフェースによって定義された振る舞いの単位であると(本記事での)定義を明らかにしました。
では、その設計はどういった手順で進めればよいのか、が本記事の主題となります。
ICONIXアプローチとロバストネス分析
次の図はICONIXアプローチの全体像を表したものです。書籍『ユースケース入門 ユーザマニュアルからプログラムを作る』は2001年に日本語訳が出版されたので、1990年代から存在する手法ということになります。
当時、オブジェクト指向分析設計の分野で著名だった3人(グラディ・ブーチ、ジェームズ・ランボー、イヴァー・ヤコブソン)を指して「スリーアミーゴ」と呼ぶのですが、ICONIXはスリーアミーゴの方法論の影響を受けています。
ICONIXアプローチにおける分析・設計では、ユースケースモデルで表現した機能要求を出発点とし、ユースケースの振る舞いを実現する静的な構造(クラス図)と動的な相互作用(シーケンス図やコミュニケーション図)を設計します。
分析モデル(ユースケースモデルやドメインモデル)から直接設計モデルを導き出すのは難しいため、分析と設計のギャップを埋める呼び設計として行うアクティビティがロバストネス分析であり、成果物としてロバストネス図を作成します。
ロバストネス図を拡大したのが次の図です。ロバストネス図は、三種類のアイコンを用いて、ユースケースの振る舞いをオブジェクト同士の相互作用に落とし込んでいきます。
境界オブジェクトはアクターとのインターフェースです。実体オブジェクトはドメインオブジェクトと考えてください。それらを結び付けるのがコントロールオブジェクトで、アプリケーションロジックにあたるものです。
ロバストネス分析の目的を確認しましょう。
まずはサニティチェックです。ユースケース記述が正しいことや、その内容とオブジェクト群との整合性が取れていることを確認します。
次に完全性チェック。ユースケースに必要な振る舞いがもれなく表現されているかを検証します。正常シナリオ、いわゆるハッピーパスだけでなく、代替シナリオや例外シナリオも含めて網羅性をチェックします。
最後にオブジェクトの識別。分析の結果得られたドメインモデルに対して、新たに発見したドメインオブジェクトを足したり、関係性を見直したりと、モデルを洗練させます。
ロバストネス分析からコンポーネント設計へ
ロバストネス「分析」というだけあって、ロバストネス図は抽象度としてはいささか高いです。最終的には動作するソースコードとしてユースケースの振る舞いを実現しますが、そこに至るにはまだまだ隔たりがあるわけです。
そこで、抽象度を下げてコンポーネントの設計を進めていく必要があります。
まず押さえておくとよいポイントは、アプリケーションの振る舞いは二種類のロジックに分けて捉えることができるということです。
ロバストネス図の実体オブジェクトが担うのが中核ロジックです。これは、ビジネスルールを表すもので、ドメインオブジェクトやドメインサービスという形で実現されます。
コントロールオブジェクトが担うのが処理フローロジックです。アプリケーションいおける処理の流れや手続きを表現するもので、アプリケーションサービスやユースケースサービスという形で実現されます。
少し解像度を上げてみましょう。次の図は、書籍『オブジェクト開発の神髄 UML2.0を使ったアジャイルモデル駆動開発のすべて』からの引用です。この本は2005年に日本語訳が出版されたスコット・アンブラーによる書籍で、良書なのですが残念ながら絶版となっています。
同書では、大学の教職員や学生が利用するアプリケーションをケーススタディとしているのですが、この図は学生の管理や履修管理などのユースケースをサポートする「学生コンポーネント」の設計図です。
(なお、このコンポーネントの粒度は、デプロイ可能性や置換可能性を重視するファウラーやアンクルボブの捉え方に近いように思います)。
この図において、学生(Student)や住所(Address)というドメインオブジェクト(DDDにおけるエンティティ、値オブジェクト)が表現する振る舞いが、中核ロジックです。
(学生)管理ファサード(Administration Facade)が実現する一連の処理手続きが処理フローロジックです。
中核ロジックとそれを統率する処理フローロジックによって、学生コンポーネントの責務であるユースケースレベルの振る舞いが実現されます。ただし、コンポーネントがそこに存在するだけでは、当然ながらユーザーに対して価値を提供することはできません。そのためには、外界との接続が必要となります。外界との接続点がポートです。
一つ目のポートは図の左側にある入力ポートで、これはユースケースの振る舞いを外部に提供するためのインターフェースです。ロリポップと呼ばれるアイコン(チュッパチャップスを想像してください)で表現されています。
二つ目のポートは、図の右側にある出力ポートです。ユースケースを実行した結果、学生や授業スケジュールといったデータをデータストアへ保存する必要があります。データの永続化という関心事は、出力ポートを経由して外部へ処理を委譲するのです。(出力ポートで使用されているアイコンは、外部から提供される能力を利用することから、ロリポップを受け入れる形状となっています)
クリーンアーキテクチャ(Clean Architecture)
先のコンポーネントの図は、ポートを使って表現されていることから、ポートアンドアダプタ(Ports and Adapters)と呼ばれるアーキテクチャスタイルを意識しているのは明らかなのですが、ここでは敢えてクリーンアーキテクチャに当てはめてみます。
最内の層(Entities)は、アンクルボブが「最重要のビジネスルール」と呼ぶ、中核ロジックが配置されます。中核ロジックは、ドメインオブジェクトやドメインサービスに割り当てられるのでした。
その外側の層(Use cases)には、アプリケーションサービスが担う処理フローロジックが配置されます。
さらにその外側の層との境界線上には、接続点であるポートが複数存在します。これらのポートと、WebやDBその他の実装技術を繋ぐ役割を担うコンポーネント(アダプター)が配置される層がInterfase adaptersです。例えば、Webとの接続ならControllerやPresenter、DBとの接続ならRepositoryやDAOといったコンポーネントが登場します。
先ほどの図を眺めてみると、登場するコンポーネントのその関係性は、アンクルボブのCA本にある図「データベースを使ったウェブベースのJavaシステムの典型的なシナリオ」に近い構成であることがわかります。
ここで注意が必要なのは、同書でも「典型的な」と書かれているとおり、重要なソフトウェア設計原則に従った「クリーンな」アーキテクチャのコンポーネント構成の一例に過ぎないということを理解しておくことです。
このことを誤解してしまうと、過ちを犯してしまうリスクがあります。クリーンアーキテクチャに対する批判の多くは、この誤解に基づくものだと考えています。
一つ目のよくある過ちは、このコンポーネント構成こそがクリーンアーキテクチャだと考えてしまうことです。クリーンアーキテクチャを学ぶ目的で、とっかかりとして倣って実装し始めるというのはアリですし、筆者も最初はそうしました。ですが、クリーンアーキテクチャの本質を理解しないままに形だけ適用してしまうと弊害が生じます。例えば、WebやDB以外の実装技術(Web APIや非同期メッセージングなど)と接続するにはどうするか、採用しているプログラミング言語やフレームワークの制約下で最適な実装方法は何か、といった応用が効かなくなってしまうのです。
鈴木まー(@suzuki_mar)さんがLTで話されたように、「自分たちのシステムにあったクリーンアーキテクチャを設計」する必要があるのです。
二つののよくある過ちは、ステレオタイプを絶対視してしまうことです。ステレオタイプとは、ControllerとかApplciationServiceとか、典型的なコンポーネントの種類を差します。
ステレオタイプを定義することで、アーキテクチャ上でのアプリケーション設計方針が理解しやすくなるメリットがある一方、思考の硬直化を招くリスクがあります。ステレオタイプで表現されたコンポーネント以外のコードを書いてはいけないと思い込む人が一定数出てきます。その結果、一つ一つのコンポーネントが肥大化し、数百行とか数千行といた神クラスが出現するのです。
詳細設計
こういった過ちを回避するには、どうしたらよいのでしょうか。
ステレオタイプを用いて表現したコンポーネント設計図は(※注 設計図といっても、UMLツールなどを使ってきれいに清書するものではありません。通常は紙やホワイトボードにフリーハンドで描いたり、脳内のキャンバスに描きます)、途中経過だと考えなければなりません。
もう一段抽象度を下げて、設計を詳細化するステップが必要です。
アプリケーションをどのように層分割するか。ステレオタイプによりどのようなコンポーネント構成を取るか。こういった方針決めは、いわゆるハイレベルな設計、あるいは(アプリケーション)アーキテクチャ設計にあたります。定めた方針に基づき、実装技術の制約など諸事情を考慮に入れた上で、具体的なプログラム構造を考えることが詳細設計なのです。(詳細設計書という名の、何の役にも立たない無駄なドキュメントを作成することが詳細設計ではありません!)。
設計の詳細化いおいてまず考えるべきことは、Divide and Conquer(分割して統治せよ)の考え方に従って、大き過ぎるものを小さく分割して適切なサイズにすることです。
例えば、アプリケーションサービスが太り過ぎているのであれば、小さなアプリケーションサービスに分割して、それらを束ねるファサードを置きます。その他、小さなヘルパーを作って処理を委譲したり、ユーティリティとして処理を切り出したり、といったことを行います。
ドメインロジックの詳細化
次に、ドメインロジックを担うコンポーネントの詳細化に焦点を当てて考えてみたいと思います。
ドメイン層の設計方法としては、マーチン・ファウラーが書籍『エンタープライズアプリケーションアーキテクチャパターン』の中でいくつかのパターンを紹介しています。
トランザクションスクリプト
テーブルモジュール
ドメインモデル
アプリケーションの特性や、開発チームのスキルや経験などを勘案し、適切なパターンを用いればよいのですが、DDDの普及もあって最近ではドメインモデルパターンが採用されることも多いかと思います。
システムが対象とする業務の複雑さに立ち向かうために、ドメインモデルが持つ表現性の高さが強力な武器となります。ドメインモデルパターンを採用する動機の一つです。
個人的には、開発者にとっての認知負荷という側面も非常に重要だと思います。ドメインモデリングによって作られた概念モデルは、ドメインエキスパートと開発者との共同作業によって生み出される、ユビキタス言語を用いて表現したモデルです。開発者は、概念モデルというレンズを通して、現場で起きているビジネスや業務を捉えます。この捉え方や認識のあり方が、開発者にとってのメンタルモデルです。
このメンタルモデルと、実際のソースコードの形状との間の距離が大きければ大きいほど、開発者の認知負荷が高まります(いわゆる課題外在性負荷と呼ばれるもの)。逆に、「この仕様(振る舞い)はソースコードのこの辺りに実装されているだろう」という開発者の期待と実際のソースコードが合致していれば、認知負荷は下がり、本来の課題に集中して取り組むことが可能となります(課題内在性負荷に対して使える脳の容量が増える)。「あー、こんなところに実装されてたか!」というサプライズは欲しくないわけです。
メンタルモデルとソースコードが合致してることが重要といっても、メンタルモデルをそっくりそのまま再現した写像を作ることが目的ではありません。そうではなく、メンタルモデルとの乖離ができるだけ少ない設計モデルを作成することが肝心なのです。手段を目的化してしまってはいけません。
最終的にソフトウェア要件を実現するソースコードは、採用するプログラミング言語やフレームワークなどの実装技術の影響を受けます。それらが課す制約条件下において、開発者にとっての余計な認知負荷を小さくし、見通しのよいコードを書くこと。これによって保守性が向上し、ひいては早く安全なフローの実現につながるわけです。
解決空間に設定を最適化する
コンポーネント設計においては、解決空間(解決領域)において設定を最適化するというステップが欠かせません。
そのテクニックの代表格がデザインパターンです。例を二つ提示しましょう。
一つ目はファーストクラスコレクションです。コードサンプルは、増田亨さんの書籍『現場で役立つシステム設計の原則 変更を楽で安全にするオブジェクト指向の実践技法』から拝借しました。
ファーストクラスコレクションとは、ドメイン固有の独自のコレクション型を指します。コードサンプルは、CustomerというドメインオブジェクトのListをラップする、Customersというファーストクラスコレクションの例です。リストの走査や集計などを行う振る舞いを、Customersのメソッドとして実装します。
問題空間を考える際のメンタルモデルには、「顧客の集合」という概念は存在しません。そんなまどろっこしい考え方をする人は、ほとんどいないでしょう。メンタルモデルには存在しない架空の概念を創作し、便宜的に導入することによって、解決空間のコードの見通しがよくなるから、このパターンを採用するのです。
二つ目の例は、Visitorパターンです。GoF(Gang of Four)の23パターンの一つですね。
筆者が若かりし時、GoFのデザインパターンを勉強して一番分からなかったのがVisitorパターンです。「Visitorをacceptするって何!?」と思ったものです。(余談ですが、このパターンを実装する際にはダブルディスパッチという実装イディオムが必要となることも、難しさの一因でした)。
でも考えてみれば分かりにくいのも当然で、問題空間のメンタルモデルには、Visitor(訪問者)なんて概念は一切出てこないわけです。
Visitorパターンは、複雑なオブジェクト構造を走査するアルゴリズムを分離することでコードの見通しがよくなるというメリットを享受できるから採用するのです。
まとめ
本記事の内容をまとめましょう。
アウトサイドインの設計
コンポーネント設計は、アウトサイドインのアプローチを取るのがよいです。外側(ユースケースの振る舞い)から入り、抽象度を段階的に下げていき、大きな振る舞いを小さな振る舞いに分割して、下位のコンポーネントにその責務として割り付けます。
まず、ユースケースレベルの大きな振る舞い、ユーザーに価値を提供する振る舞いを、大きな粒度のコンポーネントに割り当てます。
次に、ユースケースの振る舞いを、中核ロジックと処理フローロジックに分けて考えて、下位のコンポーネント(群)に責務を割り当てます。永続性のような関心事の分離も行うのでした。
そして、詳細設計としてさらに振る舞いを分割します。デザインパターンなどを活用して、小さな振る舞いを実現する小さなコンポーネントを導入します。解決空間に設計を最適化する、という考え方が役に立ちます。
アウトサイドインで、振る舞いを分割しながら設計を進めるというアプローチは、振る舞い駆動開発(BDD)やテスト駆動開発(TDD)と絡めるとまた面白いのですが、20分のトーク時間内ではさすがに語り切れないので、今回は割愛しました。
またどこかの機会で発表するか、記事としてまとめたいと思います。
まとめスライド、参考文献
補足
ドメインロジックの実現方法について
マーチン・ファウラーがエンタープライズアプリケーションパターンとして紹介したパターン(トランザクションスクリプト、テーブルモジュール、ドメインモデル)を列挙しましたが、PoEAA本も2005年に日本語訳が出版ということで、今となっては古さもあります。その後、様々な考え方やパターンが生まれています。
例えば、ドメインオブジェクトには割り当てづらい振る舞い(例:口座間の送金のように、複数のオブジェクト同士の協調で実現される手続き)はドメインサービスとして実現するのがDDD的な考え方です。そのような手続きやアルゴリズムは、OOD的な機構(クラスによる型の構造)では扱いづらいため、ロールという考え方を導入するDCIアーキテクチャという手法があります。
詳しくは、すえなみ(@a_suenami)さんが設計ナイトでお話しされたスライドを参照ください。
【朗報】焼肉食べ放題ナイトが開催されるかも!?
また、ドメインロジックを純粋関数として実装することで、コードの見通しのよさや、テストのしやすさなど、「容易を取りに行く」アプローチを、わいとん(@ytnobody)さんがお話しされていました。
より上位の設計
本記事は、アプリケーションやサービスの内部のコンポーネント設計に焦点を当てています。前提として、上位レベルで適切な設計を行い、システム自体の境界分割がうまくいってなければなりません。
このあたりは、工藤さん(@Dt3nfVOwgom3KJX)のトーク内容がとても勉強になります。
最後に宣伝
2024年7月22日(月)に、翔泳社から書籍を出します。
アーキテクティングが主題の本ですが、設計大好き人間の私ですので、第2章はソフトウェア設計について書きました。本記事でも取り上げた中核ロジックと処理フローロジックの話や、SOLID原則、パターンなどについて説明していますので参考にしてください。
この記事が気に入ったらサポートをしてみませんか?