View FrameworkインスタンスをControllerに預けて管理してもらう現実的で雑なClean Architecture

[2019-04-11 追記]最初 Use Case に預けると書いたけど、Interactor の仕様が constructor のないものだったので Controller に変更しました。

まとめ

Web FrontendでClean Architectureを試す - Qiita を読んで違和感を覚えたので、もうちょっとざっくりでいいし React に詳細突っ込みすぎでは?という気持ちを整理しようと考えてたらたどりついた現時点での考え方。ざっくり言うと

  • MVVM でやってることの範囲をそのまま広げれば Controller まで適用可能なのでは?
  • 枯れたライブラリにもっと任せるとよいのでは?

と思った話。

もっと雑に言うとタイトルの通りで View Framework を Controller に DI しちゃえばだいたいイケんじゃね説。

※ もちろん PubSub や EventBus に置き換えてもよい。少なくとも View Framework を直接 DI してしまうと Framework への変更の通知が直接手続きになってしまいやすい。そうなると密結合となりやすく、View Framework の詳細を知らないといけなくなる。ここは注意が必要。Usecase の実行後の処理も View Framework 側にイベントで通知する形を強制するには PubSub の方が安全ではある。1

Clean Architectureってなんだっけ

The Clean Architecture - Clean Coder Blog

Clean Architecture はざっくり言うと具体的なテクノロジー、詳細を外側に置き、原則的に外から内へ依存させようという考え方。処理の流れと依存関係が逆転するところがある。テキストで無理やり有名な図を描くとこんな感じ。

+----------------------------+
| Devices                Web |
|  +----------------------+  |
|  |     Controllers      |  |
|  |   +--------------+   |  |
|  |   |  Use Cases   |   |  |
|  |   | +----------+ |   |  |
|  |   | | Entities | |   |  |
|  |   | +----------+ |   |  |
|  |   +--------------+   |  |
|  | Gateways  Presenters |  |
|  +----------------------+  |
| DB                      UI |
+----------------------------+

ではWebフロントエンドってなんだっけ

まず前提として昨今の View Framework は Presenter から Web / UI の層を担っている。というか Web フロントエンドって言ってるんだから当たり前で、そもそも中心ではなく比較的周辺の領域を扱っている。

この View / UI Framework という周辺事項から内側の Controller へは普通に依存を持つことができる。

しかし View / UI 以外の情報も扱う必要がある場合、例えば Storage や Network を扱う場合は、以下の問題にぶつかる。

  • まず Network や Storage などは View / UI コンポーネントの関心外
  • かつ Controller / Use Cases から周辺に依存しない方がよい

じゃあUseCaseを意識しつつViewFrameworkとどう付き合うのがよいのか

上の問題に対する解として例のサークルで言うところの Gateways と Use Cases の境界を曖昧にすることで、以下のように書ける気がする。例は雑に Vue.js + ES2015+ で書いてある。

☆ App.vue

<script>
import Controller from 'controller'

export default {
  created() {
    this.controller = new Controller(this)
  },
  methods: {
    handler1() {
      this.$emit('event1', val1)
    },
    handler2() {
      this.$emit('event2', val2)
    }
  }
}
</script>

「Controller に対して ViewModel 自身を DI する」ことで中心を Controller 側にずらす意味合いを表しているつもり。

考え方としては Backbone.js, Angular 1 以降に定着し、ViewModel コンポーネントも実際にやっている、「依存を片方向に限定し、反対方向はすべてイベントで伝える」ということを頼りにしつつ DI で依存関係を逆転させるというもの。

Controller のインスタンスを this の中に保存しているのは単に消えてしまわないようにするためだけで、実際には View / UI Framework から Controller の何かを操作するといったことは行わない。

☆ controller.js

import Repository   from 'repository'
import RemoteFacade from 'remote_facade'
import Interactor   from 'interactor'

class Controller {
  constructor(vm) {
    this.vm = vm
    this.initListeners()
  }

  initListeners() {
    this.vm.$on(event1, usecase1)
    this.vm.$on(event2, usecase2)
    this.vm.$on(event3, usecase3)
  }

  async usecase1(val1) {
    return await Repository.foo(val1)
  }

  async usecase2(val2) {
    return await RemoteFacade.bar(val2)
  }

  async usecase3(val3) {
    return await Interactor.run(val3)
  }
}

Controller 側は ViewModel インスタンスの特定のイベントを処理できるようにしつつ、関連する Repository や RemoteFacade, Interactor などもここで初期化している。

大事なのは Repository や RemoteFacade, Usecase Interactor は外側の技術的な詳細そのものではないということ。あくまで Gateway 的なものですよという言い訳と言ってもよいかもしれない。少なくともここを見ると「何と何を使って何をしようとしているのかは分かる」くらいの粒度に収めたいというのが前提にある。

各 use case はメソッド一つで処理できるかもしれないし、中身が複雑になったら class に分けてもよい。Operator とか Command とか Service とかそんなやつ。Clean Architecture 的には Interactor.

今回改めて考えてみたこと

上で意識したのは

  • View / UI Framework のコンポーネントは2それだけで複雑であり、その初期化に関して必要以上に情報を持ち込まない方がよい
  • View / UI Framework は「entry point ではあるが全体から見ると周辺事項」であり、「全体を見る人は別にいた方」が扱いやすそう
    • 全体には当然 View / UI 以外も入る
  • Repository や RemoteFacade の詳細は気にしたくないので書かない。その向こうは依存の方向が内→外になる可能性もあるが、気にしない
    • テスタビリティの確保なら stub out とか方法はある
    • 必要ならこれらを Factory にして具象 → 抽象の依存の方向を実現することはできる
  • Repository と Factory の関係は Usecase Interactor にも適用できるかも
    • 直接インスタンスを作るのではなく Factory 経由で全部 constructor DI にするとか

こうしておけば例えば WebRTC などが増えた際には素直に WebRTC を中心に追加できる。3

いずれにせよ、いかに Clean かということよりも、

  1. View / UI Framework の中だけで頑張りすぎないこと
  2. いかに独立してテストしやすくするかということ
  3. そのうえで関心外の情報が一気に全部見えないようにすること
  4. 中心的に見なければいけないのは Controller / Use Cases であり、そこを見ると関連をたどりやすいこと

を考えている。

本来の Clean Architecture はフレームワーク非依存な考え方だが、現実的にはなんらかのフレームワークが考え方の起点となっていることは多い。

実際に Web フロントエンドはなんらかの View / UI Framework を採用することはほぼ前提と言ってよく、これらのフレームワークでは DOM や Event といった最も外側に位置する最も詳細なテクノロジーから一段階距離を置く手法が取られている。そしてこれと同じことは Storage や Remote Facade 系のライブラリにも言える。

つまりそれぞれの領域において概ね成熟したライブラリに任せていることは同じであり、必要以上に Clean な依存の方向にこだわりすぎてコード量が増えて特定のところで一気に依存解決する複雑さが見えてしまうくらいなら、まずは思い切ってざっくり View(UI) / Storage / Network くらいに分けてそれらをコントロールする人を中心に置く方が全体の見通しは楽になるし、個々のライブラリの機能を生かすことでテスタビリティは十分確保できるので、こんなもんでいいんじゃないの?ということを考えている。

逆に言うとこの考え方では cookie も localStorage も Fetch API もブラウザネイティブのものは極力直接使わないことを前提にしている。

で、「これはフレームワークから距離を置いた方がよい」と考えた場合には、グッと Clean Architecture に寄せてコード量を増やしてゴリゴリ書けばよい。

TDD も Clean Architecture も考え方を参考にしつつ、現実には増えるコード量とコスト感との兼ね合いの部分は必ず出てくる。その際に

  • どこが中心か
  • どこは任せてよいか
    • 外側の詳細に直接タッチしていないか
  • 特定のフレームワークの中だけで頑張ろうとしすぎていないか

くらいだけを気にしておけば、「そこそこClean」な状態は維持できるんじゃないかなーという感じのことを最近は考えている。

参考

  1. Vueの場合、空っぽのVueインスタンスをEventBus ( EventHub ) として使う方法も公式に紹介されてたりする。https://vuejs.org/v2/guide/migration.html#dispatch-and-broadcast-replaced 

  2. 以前よりシンプルになっているとは言え 

  3. もちろんその役割を表す一段抽象化してものだが 

More