みんなの「作ってみた」

Vue.js + TypescriptでClean Architectureな🍆アプリを書いてみた

2019/03/02

uu4k
uu4k

経緯

これまでのエンジニア人生の中の大半が他人のコードを修正だったが、修正作業は修正自体よりもその修正の影響範囲の調査に時間がかかることが多く、かつそこでの影響調査漏れでバグを起こすということが多々あった。

影響範囲の調査にかかる理由はだいたいモジュールが分割されてない、分割されていても依存関係が整理されておらず結局全てのモジュールを見ないといけない、というものであった。

ということでモジュールを分割しつつ依存関係を整理した設計の経験を積んでみようと思い、実際にやってみた。

Clean Architectureとは

実際に本を読んでもらうと一番いいですが、@nrslibさんが具体的なコードでまとめてくださっているのでそちらを参考にしていただくとよいかと思います。

実践クリーンアーキテクチャ │ nrslib

なぜClean Architectureなのか

  • 流行りなので
  • 本読んだ限りだとモジュールの依存関係が整理されていて修正・機能拡張を容易にできそうだったので

作ったの

なす生える🍆

uu4k/aubergines-grow-up

説明

アイコン画像になすを生やすアプリ。

ベースとなるアイコン(baseIcon)と生やす形状(shape)、生やす量(rate)を指定してなすの生えたアイコンを作ることができる。

主なユースケースは以下の3つで、設計でもこの3つのユースケースを中心に行った。

  • ベースアイコンをアップロードする
  • なすを生やす形状を選択する
  • なすを生やす量を選択する

ディレクトリ構成

vue関連のディレクトリは省略してます。

.
├── controllers
├── entities
├── presenters
├── repositories
│   └── store
├── usecases
│   ├── base-icon-upload # ベースアイコンをアップロードする
│   ├── rate-select # なすを生やす量を選択する
│   └── shape-select # なすを生やす形状を選択する
└── valueobjects

はじめはユースケースごとにディレクトリを分ける形で構成していたのだが、ユースケース間で共通するrepositoryとかentityが収まり悪いのと、Clean Architectureの図の各領域との関連付けが取りづらいと思ったので、とりあえずは図の各要素に合わせてディレクトリ構成を分けることにした。(gatewaysはrepositoriesに変えてるが)

ただ、この構成の場合、アプリの規模が大きくなるとentityが多くなりentity間の関連が追いづらくなるので、別途ドメインの集約関係をもとにディレクトリ分ける必要が出てくると思われる。(集約に応じてリポジトリも整理必要になるはず)

クラス図

値オブジェクトとかは力尽きたので省略。

クソ汚い図で申し訳ないが、パッケージ(モジュール)の矢印の出入りで依存関係を確認してみる。

まずClean Architectureの図の中心のentitiesのパッケージでは入ってくる矢印はあるが、外に出ていく矢印がないことから、usecaseやpresenterなどの他のモジュールからの参照はされるが、他のモジュールへの参照を行っていないことがわかる。

次に、usecaseについても、外に出る矢印はentities宛のみとなっており、Clean Architectureの図の通りの依存関係が維持できていることがわかる。

usecaseからpresenterやrepositoryを使う場合はインタフェースを用いることで依存関係を逆転して、依存の方向性を整えている。

作る前に勘違いしていたこと

InteractorとInputBoundaryは別物

Clean Architectureの典型的なシナリオの図で、InteractorとInputBoundaryが別の名前で出てきていたため、別の役割をするものと考えていたが、実際にはユースケースのインタフェースがInputBoundaryで、その実装がInteratorとなる。(図をちゃんと見ればわかるとか言わない)

PresenterとOutputBoundaryは別物

上と同じで名前が別なので別の役割を持つものかと思ってましたが、実際にはPresenterのインタフェースがOutputBoundaryで、その実装がPresenterとなる。

usecaseごとにpresenterが必要

ユースケースごとにOutputBoundary(Presenterのインタフェース)は必要になるが、その実装自体は一つにまとめてしまっても問題ない(と思う)。

今回の設計では表示に関わるコンポーネントごとにPresenterを実装する方針をとったので、1つのPresenterで複数のOutputBoundaryを実装するようになっている。

実装する上で迷ったこと

presenterで生成されるViewModelをビューに渡すには?

最初の構想ではpresenteはvueコンポーネントとして実装するつもりだったが、ユースケースにvueコンポーネントをDIする方法が思いつかなかったので、presntern->vuex->vueコンポーネントという形でvuex経由でviewModelをビューに渡すことにした。

ただ、こうするとvuexには画面表示と同期取るためのviewModelとアプリケーションの状態を管理するStateの別の役割を持つデータが混在してしまう。とりあえずはstoreのmoduleを別にすることで対応したが、可能なら管理方法自体を別にしたかった。

同じインタフェースが多すぎる

repositoryやpresenterのインタフェースはユースケース内で持つことになるが、割とユースケースごとで必要になる機能がかぶるので似たようなインタフェースが多くできてしまう。

かといって、このあたり共通化するとユースケース間で依存関係ができてしまうためあまりやりたくない。

ということでなんかモヤモヤするけどインタフェースいっぱい作った。

実装してみた感想

本当にこれでいいのか?

Clean Architectureに限った話ではないが、設計にはこれといった正解が明確にないので常にこれで良いのかと考えないといけないのが辛くもあり楽しいところでもあった。

間違っているかもという不安を払拭するためには、正解が選択できない(正解などない)前提でいつでもリファクタリングできるようにテストコードを必ず書く文化が必要だなーとも思った。

FWによってpresenterの実装の仕方は変わりそう

今回の場合、presenter->vuex->viewという形でpresenterとviewを連携させているが、MVCなFWとかだとpresenterで生成したviewModelをcontrollerまで渡さないといけない場合もありそう。

そうなった場合、presenterを使うusecaseがviewModelを参照することになり依存の方向性が逆になるが、usecaseでviewModelをいじるとかしなければそこまで影響ないかなーと思ってる。