【Qt】クリーンアーキテクチャを Python+Qt で再現してみた

どうもです、タドスケです。

ここ最近、『クリーンアーキテクチャ』を読み返しました。

現場のコード(Python+Qt=PySide6)にも取り込みたいなーと思いつつ、既存の巨大なコードにどう適用したらよいかわからなかったので、ごくシンプルなアプリを実験用に作ってみました。

目次

作ったアプリ

全てのコード(テスト含む)は以下に公開しています。

以降の説明ではコードを省略するので、必要に応じて参照してください。

main.py を実行すると、以下のウィンドウが表示されます。

名前と HP の入力欄があり、ファイルメニューからは「Save」と「Load」が実行できます。

今回はアプリ自体の出来は重要ではないので、詳しい説明は省きます。

クリーンアーキテクチャに出てくる図

今回のアプリは『クリーンアーキテクチャ』に出てくる 2つの図を参考にしています。

まずは一番有名なこれ。

これを具体的なシステムに落とし込んだ図がこちら。

図は本の中から引用しています。

クラス構成

アプリを構成するクラス図です。(Plant UML で作成しています)

クリーンアーキテクチャの図との対応関係が分かりやすいように、できるだけクラスの名前を揃えるようにしています。

「infrastructure」は、「フレームワークとドライバ」の部分に対応しています。ネット上ではそのように呼ばれていることが多かったので使うことにしました。

インターフェースをどう表現する?

Python でインターフェース(抽象基底クラス)を表現するには、abc.ABCMeta を使います。

Python documentation
abc --- 抽象基底クラス ソースコード: Lib/abc.py このモジュールは Python に PEP 3119 で概要が示された 抽象基底クラス(ABC) を定義する基盤を提供します。なぜこれが Python に付け加えられた...

しかし、元々 QObject を継承している Presenter で ABCMeta も継承させようとしたら、以下のエラーが出ました。

メッセージ内容は「metaclass が衝突しているよ!」というもの。
QObject と ABCMeta は同時に使えないようなので、今回は代わりに typing.Protocol を使うようにしました。

Python documentation
typing --- 型ヒントのサポート ソースコード: Lib/typing.py このモジュールは型ヒントの実行時サポートを提供します。 以下の関数を例に考えてみます: surface_area_of_cube 関数は、 edge_length: floa...

処理の流れ

主な処理の流れを2つ説明します。

View が操作されたとき

View が操作された時の流れはシンプルです。

単に内側の機能を順番に呼び出していけばいいだけだからです。

ファイルを読み込んだとき

ファイルを読み込んだとき(ファイルメニューから「Load」を選択したとき)の流れはもう少し複雑になります。

操作の起点は ParameterWidget(View) ですが、「名前」と「HP」の欄はファイルを読み込んで EnemyParamter を更新してからじゃないと更新できません。
EnemyParameter を更新してからの処理の流れは、それまでとは逆向きになります。

Presenter から View への通知(update_view)は、QObject のシグナルを使っています。

adapter 層を Qt(フレームワーク)に依存させることになってしまうので悩みましたが、「QtWidgets に依存していなければ OK」ということで妥協しています。

adapter 層を Qt に依存させずに、View だけを Qt 以外のフレームワークに差し替えられるのが理想ですが、View が別のフレームワークに変わった場合、ViewModel もそのままではいられない場合が多いのではないかと思います。

……かといって ViewModel の汎用性を高めようとすると、View 側に変換ロジックを持ち込むことになりかねません。それでは ViewModel を用意した意味がありません。

今回のアプリで adapter 層を用意している最大のメリットは「View を利用せずに、表示のためのロジックをテストできる」ことにあると思っています。

改善のアイデア

今回は『クリーンアーキテクチャ』で紹介されている図を再現することを重視しました。

ですが、この規模のアプリでは正直「やり過ぎ」に思えました。

以下、改善できそうなポイントを挙げてみます。

InputBoundary を廃止して、Controller に UseCase を直接持たせる

Controller → UseCaseInteractor の呼び出しは InputBoundary というインターフェースを介していますが、Controller に直接 UseCaseInteractor を持たせたほうがシンプルになるように思えました。

インターフェースを使う主なメリットは、「依存関係を逆転できること」「インターフェースの先にあるクラスを別のものに差し替えられること」だと考えています。

しかしクリーンアーキテクチャでは外側→内側に依存するのは特に問題ありません。
また、Controller をそのままに UseCaseInteractor だけを後から別のものに差し替えるようなケースは考えにくいです。

Controller を廃止して、View に直接 UseCaseInteractor を持たせる

コードを見るとわかる通り、Controller はほぼ何もしていません。
単に UseCaseInteractor のメソッドを呼んでいるだけです。

View 側の状態を管理したり、UseCaseInteractor のためにデータ形式を変換したりする必要があるなら別ですが、今回のケースでは View(ParameterWidget)に直接 UseCaseInteractor を持たせてしまったほうがシンプルになると思います。

「クリーンアーキテクチャ」は、「従うもの」ではなく「取り入れる」もの

本の中でも、クリーンアーキテクチャについて「全てこの通りにする必要はない」と断言しています。

使っている言語やフレームワークに応じてメリットのある部分だけを取り入れていくのが、うまい使い方なのではないかと思いました。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

目次