どうもです、タドスケです。
ここ最近、『クリーンアーキテクチャ』を読み返しました。
現場のコード(Python+Qt=PySide6)にも取り込みたいなーと思いつつ、既存の巨大なコードにどう適用したらよいかわからなかったので、ごくシンプルなアプリを実験用に作ってみました。
作ったアプリ
全てのコード(テスト含む)は以下に公開しています。
以降の説明ではコードを省略するので、必要に応じて参照してください。
main.py を実行すると、以下のウィンドウが表示されます。
![](https://tadosuke.com/wp-content/uploads/2025/01/app_image.png)
名前と HP の入力欄があり、ファイルメニューからは「Save」と「Load」が実行できます。
クリーンアーキテクチャに出てくる図
今回のアプリは『クリーンアーキテクチャ』に出てくる 2つの図を参考にしています。
まずは一番有名なこれ。
![](https://tadosuke.com/wp-content/uploads/2025/01/IMG_7634-300x201.jpeg)
これを具体的なシステムに落とし込んだ図がこちら。
![](https://tadosuke.com/wp-content/uploads/2025/01/IMG_7635-300x213.jpeg)
クラス構成
アプリを構成するクラス図です。(Plant UML で作成しています)
![](https://tadosuke.com/wp-content/uploads/2025/01/class_diagram.png)
クリーンアーキテクチャの図との対応関係が分かりやすいように、できるだけクラスの名前を揃えるようにしています。
インターフェースをどう表現する?
Python でインターフェース(抽象基底クラス)を表現するには、abc.ABCMeta を使います。
![](https://docs.python.org/3/_static/og-image.png)
しかし、元々 QObject を継承している Presenter で ABCMeta も継承させようとしたら、以下のエラーが出ました。
![](https://tadosuke.com/wp-content/uploads/2025/01/error.png)
メッセージ内容は「metaclass が衝突しているよ!」というもの。
QObject と ABCMeta は同時に使えないようなので、今回は代わりに typing.Protocol を使うようにしました。
![](https://docs.python.org/3/_static/og-image.png)
処理の流れ
主な処理の流れを2つ説明します。
View が操作されたとき
![](https://tadosuke.com/wp-content/uploads/2025/01/sequence_view_changed.png)
View が操作された時の流れはシンプルです。
単に内側の機能を順番に呼び出していけばいいだけだからです。
ファイルを読み込んだとき
![](https://tadosuke.com/wp-content/uploads/2025/01/sequence_loaded.png)
ファイルを読み込んだとき(ファイルメニューから「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 を持たせてしまったほうがシンプルになると思います。
「クリーンアーキテクチャ」は、「従うもの」ではなく「取り入れる」もの
本の中でも、クリーンアーキテクチャについて「全てこの通りにする必要はない」と断言しています。
使っている言語やフレームワークに応じてメリットのある部分だけを取り入れていくのが、うまい使い方なのではないかと思いました。
コメント