Pythonによるクリーンアーキテクチャ:ステップバイステップの用例
著者:Leonardo Giordani - 14/11/2016 Updated on Sep 24, 2021
2015年、私は友人の Roberto Clatti から、Robert Martin が言うところの Clean Architecture というコンセプトを紹介されました。よく知られているボブおじさんは、カンファレンスでこのコンセプトについてよく話していますし、非常に興味深い記事も書いています。彼が "Clean Architecture "と呼ぶものは、ソフトウェアシステムの構造化の方法であり、異なるレイヤーとそこでのアクターの役割についての一連の考察(厳密なルールというよりも)です。
(訳注:Uncle Bob は Rober Martin のニックネーム)
彼がすでに説明していることをここで繰り返すことはしませんので、これらのコンセプトの探求を始めるためにチェックすることができるいくつかのリソースを紹介します。
Hakka Labs: Robert "Uncle Bob" Martin - Architecture: Hakka Labsのロバート・マーティン氏のビデオです。
https://www.youtube.com/watch?v=HhNIttd87xs
クリーンなシステムの一日
ここでは、クリーン・アーキテクチャで設計された(非常にシンプルな)システムを紹介します。このセクションの目的は、
関心の分離(SoC:Separation of Concerns)や制御の反転(IoC: Inversion of Control)といった、システム設計において最も重要な概念に慣れることです。このシステムでのデータの流れを説明しますが、詳細は意図的に省略し、全体的なアイデアに集中できるようにし、実装についてはあまり気にしないようにします。
データの流れ
この本の残りの部分では、部屋を借りるシステムを提供するシンプルなWebアプリケーションの一部を一緒に設計します。ここでは、"Rent-o-Matic "アプリケーション(Day of the Tentacleに登場する "Sludge-O-Matic™"にインスパイアされたもの)が https://www.rentomatic.com で動作していて、ユーザーが利用可能な部屋を見たいと思っているとします。ユーザーはブラウザを開いてアドレスを入力し、メニューやボタンをクリックすると、当社がレンタルしているすべての部屋のリストが表示されたページにたどり着きます。 このURLが /rooms?status=available だとします。ユーザーのブラウザがこのURLにアクセスすると、HTTPリクエストがシステムに到達し、HTTP接続を待つコンポーネントがあります。このコンポーネントを Webフレームワーク(Web Framework) と呼びましょう。
Webフレームワークの目的は、HTTPリクエストを理解し、レスポンスを提供するために必要なデータを取得することです。この単純なケースでは、リクエストには2つの重要な部分があります。すなわち、エンドポイント自体(/rooms)と、1つのクエリストリングパラメータ(status=available)です。エンドポイントはシステムのコマンドのようなもので、ユーザーがエンドポイントにアクセスすると、特定のサービスが要求されたことをシステムに通知します(このケースでは、レンタル可能なすべての部屋のリスト)。
https://gyazo.com/0178760c1ea9135e047eb6a3d13fd976
Webフレームワーク
Web フレームワークが動作する領域は、HTTP プロトコルの領域です。したがって、Web フレームワークがリクエストをデコードしたときには、関連する情報を処理する別のコンポーネントに渡す必要があります。この別のコンポーネントはユースケースと呼ばれ、ビジネスロジックを実装するため、クリーンシステム全体の中で最も重要なコンポーネントとなります。
https://gyazo.com/17b53437b9fddc622c5b4d336578dae6
ビジネスロジック
ビジネスロジックは、システム設計において重要な概念です。あなたがシステムを作るのは、世の中に役立ちそうな、あるいは少なくとも市場性がありそうな知識を持っているからです。その知識とは、結局のところ、データを処理する方法であり、他の人が持っていないようなデータを抽出したり提示したりする方法です。例えば、検索エンジンがクエリに含まれる用語に関連するすべてのウェブページを検索したり、ソーシャルネットワークがフォローしている人の投稿を特定のアルゴリズムに従ってソートして表示したり、旅行会社が2つの場所の間の旅行に最適なオプションを見つけたり......。これらはすべて、ビジネスロジックの良い例です。
ビジネスロジック
ビジネスロジックとは、具体的なアルゴリズムやプロセス、データを変換してサービスを提供する方法のことです。システムの中で最も重要な部分です。
ユースケースは、ビジネスロジック全体の非常に特定の部分を実装します。このケースでは、ステータスというパラメータの値が指定された部屋を検索するユースケースがあります。これは、ユースケースが、私たちの会社で管理されているすべての部屋を抽出し、利用可能なものだけを表示するようにフィルタリングしなければならないことを意味します。
なぜWebフレームワークではできないのでしょうか?良いシステムアーキテクチャの主な目的は、関心を分離すること、つまり異なる責任やドメインを分離することです。ウェブフレームワークは、HTTPプロトコルを処理するためのもので、システムの特定の部分に関係するプログラマーによって維持されていますが、そこにビジネスロジックを追加すると、2つの全く異なる分野が混ざり合ってしまいます。
関心の分離
システムの異なる部分は、プロセスの異なる部分を管理する必要があります。システムの2つの別々の部分が同じデータやプロセスの同じ部分で動作する場合、それらは必ず結合されます。結合は避けられませんが、2つのコンポーネント間の結合が高ければ高いほど、一方を変更しても他方に影響を与えることが難しくなります。
これから説明するように、レイヤーを分離することで、より少ない労力でシステムを維持することができ、単一のパーツをよりテストしやすく、簡単に交換することができます。
ここで説明している例では、ユースケースは、利用可能な状態にあるすべての部屋をデータソースから抽出して取得する必要があります。これがビジネスロジックで、この場合は非常に簡単で、おそらく属性の値に対する単純なフィルタリングで構成されているでしょう。しかし、そうとは限りません。より高度なビジネスロジックの例としては、レコメンデーションシステムに基づいた注文が挙げられますが、この場合、ユースケースはデータソースだけでなく、より多くのコンポーネントと接続する必要があります。
そこで、ユースケースが処理したい情報をどこかに保存します。これをコンポーネントストレージシステムと呼びましょう。多くの人は、すでにデータベースを思い浮かべているでしょう。おそらくリレーショナルなものですが、これは可能なデータソースの1つに過ぎません。ストレージシステムで表現される抽象度は、ユースケースがアクセスでき、データを提供できるものなら何でもソースになります。それは、ファイルかもしれないし、データベース(リレーショナルかどうかに関わらず)かもしれないし、ネットワークエンドポイントかもしれないし、リモートセンサーかもしれません。
抽象化(Abstraction)
システムを設計する際には、抽象度、つまりビルディングブロックの観点から考えることが最も重要です。コンポーネントは、そのコンポーネントの具体的な実装に関わらず、システムの中で役割を持っています。抽象度が高ければ高いほど、コンポーネントの詳細は少なくなります。抽象度の高い設計では、現実的な問題を考慮していないことは明らかです。そのため、抽象度の高い設計を、特定のソリューションや技術を用いて実装する必要があります。
わかりやすくするために、ここではPostgresのようなリレーショナルデータベースを例に挙げています。これは多くの読者にとって馴染みのあるものだと思いますが、より一般的なケースを念頭に置いてください。
https://gyazo.com/e53860bae6ba5b79cbab766fe5a2b820
ストレージ
ユースケースとストレージシステムの関係は?明らかに、ユースケースに特定のシステムへの呼び出しをハードコードすると(例:SQLを使用する)、2つのコンポーネントは強結合になりますが、これはシステム設計で避けたいことです。結合されたコンポーネントは独立したものではなく、密接につながっており、片方で発生した変更は、もう片方での変更を余儀なくされます(逆もまた然り)。これは、コンポーネントのテストが難しくなることを意味します。一方のコンポーネントは他方のコンポーネントなしでは生きていけませんし、2つ目のコンポーネントがデータベースのような複雑なシステムである場合、これは開発を著しく遅らせることになります。
例えば、ユースケースがpsycopgのようなPostgreSQLにアクセスするための特定のPythonライブラリを直接呼び出すとします。これはユースケースとその特定のソースを結びつけることになり、データベースを変更するとそのコードも変更されることになります。ユースケースにはビジネスロジックが含まれており、それは1つのデータベースシステムから他のデータベースシステムに移行しても変更されないからです。ビジネスロジックを含まないシステムの部分は、実装の詳細のように扱われるべきです。
実装詳細
特定のソリューションや技術が設計全体の中心ではない場合、それは詳細(detail)と呼ばれます。この言葉は、より中心的な部分よりも大きいかもしれない対象の本質的な複雑さを意味しません。
リレーショナルデータベースは、HTTPエンドポイントよりも何百倍も高機能で複雑であり、オブジェクトのリストを注文するよりも複雑ですが、アプリケーションの中核はユースケースであり、データの保存方法やアクセス方法ではありません。通常、実装の詳細は主にパフォーマンスやユーザビリティに関連しており、コア部分は純粋なビジネスロジックを実装しています。
強い結合を避けるにはどうすればよいのでしょうか?ここではその方法を簡単に説明し、適切な実装方法については、この本の後のセクションで、まさにこの例を実装するときに紹介します。
制御の逆転(Inversion of Control)は2つのフェーズで起こります。まず、呼び出されたオブジェクト(この例ではデータベース)は、標準的なインターフェイスでラップされます。これは、ターゲットのすべての実装で共有される機能性のセットであり、各インターフェースは、機能性をラップされた実装の特定の言語1の呼び出しに変換します。 制御の逆転(IoC: Inversion of Control)
システムのコンポーネント間の強い結合を避けるために用いられる技術で、特定のインターフェイスを公開するようにコンポーネントをラップします。そのインターフェイスを期待するコンポーネントは、特定の実装の詳細を知らずにそれらに接続することができ、結果として特定の実装ではなくインターフェイスに強く結合されることになります。
電気製品は、特定の電源プラグではなく、仕様(サイズ、極数など)に合わせて作られた電源プラグに接続できるように設計されています。英国でテレビを購入すると、英国用プラグ(BS 1363)が付属しているはずです。それがない場合は、電子機器を外国のソケットに接続するためのアダプターが必要です。今回のケースでは、ユースケース(テレビ)を、共通のインターフェイスに合わせて設計されていないデータベース(電源システム)に接続する必要があります。
今回の例では、ユースケースは指定されたステータスを持つすべての部屋を抽出する必要があるため、データベースのラッパーは list_rooms_with_status と呼ぶ単一のエントリーポイントを提供する必要があります。
https://gyazo.com/aef20950366b53e99a0008b3e78c54db
ストレージ・インタフェース
制御の逆転の第二段階では、呼び出し側(ユースケース)を変更し、特定の実装への呼び出しをハードコーディングしないようにします。これは、両者を再び結合することになるからです。ユースケースは、コンストラクタのパラメータとして入力オブジェクトを受け取り、生成時にアダプタの具象インスタンスを受け取ります。これを実装するための具体的な手法は、使用するプログラミング言語に大きく依存します。Pythonにはインターフェイスのための明示的な構文はありませんので、渡すオブジェクトが必要なメソッドを実装していると仮定します。
https://gyazo.com/da794e323fb4f1e09c4649f9ac04aaa8
ストレージインターフェースの制御の逆転
これで、ユースケースはアダプタに接続され、インターフェイスを知ることができました。そして、利用可能なステータスを渡してエントリポイント list_rooms_with_status を呼び出すことができます。アダプタはストレージシステムの詳細を知っているので、 メソッドコールとパラメータを、要求されたデータを抽出する特定のコール(またはコール群)に変換し、 さらにユースケースが期待する形式に変換します。例えば、部屋を表す辞書のPythonリストを返すような場合です。
https://gyazo.com/f1dfb3fb24f8ceb21bbcf25705974795
ビジネスロジックは、ストレージからデータを抽出
この時点で、ユースケースは必要に応じて残りのビジネスロジックを適用し、その結果をウェブフレームワークに返す必要があります。
https://gyazo.com/cc8e744a257bd4f84673fbdee1b510d6
ビジネスロジックが処理したデータをWebフレームワークに返す
Web フレームワークは、ユースケースから受け取ったデータを HTTP レスポンスに変換します。このケースでは、Webサイトのユーザーが明示的にアクセスするエンドポイントを考えているので、WebフレームワークはレスポンスのボディにHTMLページを返します。しかし、これが内部のエンドポイントで、例えばフロントエンドの非同期のJavaScriptコードによって呼び出される場合、レスポンスのボディはおそらく単なるJSON構造になるでしょう。
https://gyazo.com/4ccedf6e21eb60a22dc82870c9d30922
Webフレームワークは、データをHTTPレスポンスで返す
レイヤード・アーキテクチャの利点
ご覧のように、このプロセスの各ステージは明確に分かれており、その間には多くのデータ変換が行われています。共通のデータフォーマットを使用することは、コンピュータシステムのコンポーネント間の独立性(ルースカップリング)を実現する方法の一つです。
プログラマーにとってのルースカップリングの意味を理解するために、最後の絵を見てみましょう。前の段落では、ユーザーインターフェイスにWebフレームワーク、データソースにリレーショナルデータベースを使用したシステムの例を挙げましたが、もしフロントエンド部分がコマンドラインインターフェイスだったらどうなるでしょうか?また、リレーショナル・データベースの代わりに、別のタイプのデータ・ソース、例えばテキスト・ファイルのセットがあったらどうなるでしょうか?
https://gyazo.com/656732c1752d4a970241b788ffa72231
WebフレームワークをCLIに置き換える
https://gyazo.com/974d8b66894b09150cc08124b4837677
データベースをより簡単なファイルベースのストレージに置き換える
ご覧のように、どちらの変更もいくつかのコンポーネントを交換する必要があります。ウェブページではなくコマンドラインを管理するためには、別のコードが必要になるからです。しかし、システムの外形は変わりませんし、データの流れ方も変わりません。私たちは、ユーザーインターフェース(Webフレームワーク、コマンドラインインターフェース)とデータソース(リレーショナルデータベース、テキストファイル)は、実装の詳細であって、中核部分ではないシステムを作りました。
しかし、レイヤード・アーキテクチャの最大の利点は、テストのしやすさにあります。コンポーネントを明確に分離することで、各コンポーネントが受け取るべきデータと生成すべきデータが明確になり、理想的には単一のコンポーネントを切り離してテストすることができます。今回追加したWebフレームワークのコンポーネントを、他のアーキテクチャを忘れてちょっと考えてみましょう。図のように、理想的にはテスターをその入出力に接続することができます。
https://gyazo.com/414e6a335366ba522f873c5fe064e5ea
Web層を分離してテストする
https://gyazo.com/38f43167c129e61930ec3b9024445f80
Web層テストの詳細設定
Webフレームワークは、特定のターゲットと特定のクエリ文字列を持つ HTTP リクエスト(1) を受け取り、特定のパラメータを渡してユースケースのメソッド (2) を呼び出す必要があることがわかっています。ユースケースがデータ (3) を返すと、Web フレームワークはそれを HTTP レスポンス (4) に変換しなければなりません。これはテストなので、偽のユースケースを用意することができます。つまり、ビジネスロジックを実際に実装することなく、ユースケースが行うことを模倣したオブジェクトです。そして、Webフレームワークが正しいパラメータでメソッド2を呼び出し、HTTPレスポンス4に正しいデータが適切な形式で含まれていることをテストします。これらのことは、システムの他の部分を一切介さずに行われます。
Clean Architectures in Python:書籍の紹介
この入門書はお役に立ちましたでしょうか。ここまで読んでいただいたのは、The Digital Cat Booksでオンラインで読むことができる書籍「Clean Architectures in Python」の第1章です。この本は、LeanpubでPDFや電子書籍として販売されています。
https://gyazo.com/1aae0030e7049864782c1d4575ae3f57
本書の概要は次の通りです。
第2章では、このソフトウェア・アーキテクチャの背後にあるコンポーネントとその考え方について簡単に説明しています。
第3章ではクリーンアーキテクチャの具体的な例を紹介しています。
第4章ではその上にWebアプリケーションを追加して例を拡張しています。
第5章では、エラー管理と、前の章で開発したPythonコードの改良について説明します。
第6章と第7章では、前章で作成したWebサービスにさまざまなデータベースシステムを接続する方法を説明します。
第8章では、本番環境でアプリケーションを実行する方法を示して例題を締めくくります。