clean architectureで設計してrxjsを使った話
TRANSCRIPT
kondei
twitter: @_kondei
Clean Architectureで設計して RxJSを使った話
何に使ったの?
ニコニコ生放送の、動画広告
…を配信するための、運営ツール
(ユーザーには見えない)
素のTypeScriptで実装するか…
▂▅▇█▓▒░(’ω’)░▒▓█▇▅▂ うわああああああ
要件が複雑
• 機能が多い
• 13個の細かい機能
• 3つのAPIを叩く
• 状態が多い
• 非同期処理が多く、イベント発火の時系列が複雑
• ツールの有効無効の切り替え
• カウントダウン表示 & 終了時に色んな処理
• ツールをクリックしたら10秒おきに動画広告状態取得開始
• 色んな表示を10秒おきに取る動画広告状態に同期
• …
• 非同期処理のリッチなライブラリを使う
• アーキテクチャは何でもいいが、「プレゼンテーションとドメインの分離」原則を適用する
• データの流れと依存関係を単方向にする
• Fluxや、ちゃんとしたMVCのように
• 可能なかぎりReactiveに書く
• Interactiveに操作しだすと、どこから変更されるのか分からない
こうしないと書ける気がしない
何を採用しよう?
RxJS 採用
• jQueryと協調する
• TypeScriptですぐに使えるFRPライブラリ
• IE8対応してる
• 社内で使用実績があった(nicocas)
• (のちのち書いたFRPライブラリ比較)
• http://qiita.com/kondei/items/17e5d4867a0652911e52
クリーンアーキテクチャ 採用
• 厳密に適用すれば、依存関係は一方向になる
• DDDを適用したほうが機能拡張に強いはず
• サーバサイドはDDDだし…
http://blog.8thlight.com/uncle-bob/2012/08/13/the-clean-architecture.html
設計
Controller Gateway Presenter
Domain Model
Usecase
View (html)
API
<I> Gateway
<I> Controller
External Interface
Interface Adapters
Application Business Rules
Enterprise Business Rules
Controller Gateway Presenter
Domain Model
Usecase
View (html)
API
<I> Gateway
<I> Controller
External Interface
Interface Adapters
Application Business Rules
Enterprise Business Rules
入力イベントをObservableとして公開
APIレスポンスをObservableとして公開
Controller Gateway Presenter
Domain Model
Usecase
View (html)
API
<I> Gateway
<I> Controller
External Interface
Interface Adapters
Application Business Rules
Enterprise Business Rules
入力イベントをObservableとして公開
APIレスポンスをObservableとして公開
Domain Modelや入出力を利用して ユースケースのドメインロジックを実現し、
Observableとして公開
Controller Gateway Presenter
Domain Model
Usecase
View (html)
API
<I> Gateway
<I> Controller
External Interface
Interface Adapters
Application Business Rules
Enterprise Business Rules
入力イベントをObservableとして公開
APIレスポンスをObservableとして公開
Domain Modelや入出力を利用して ユースケースのドメインロジックを実現し、
Observableとして公開
UsecaseやControllerの Observableを利用して
プレゼンテーションロジックを実現
Presenter は何をしてるの
1. Controller や Usecase らの Observable を入力
2. ストリーム(イベント)を操作・合成
3. Subscribe 内で DOM 操作
表示要素が、ユースケースにReactする
どういう流れでストリーム構築するの
1. TypeScriptだったので、全てconstructorで構築
要するに、ストリームを1度だけ宣言することが重要
2. 後述のInitializerで、依存関係の無い順に初期化
こんなInitializerをHTMLから呼んだ
export class Initializer { constructor(private apiUrl: string, private $hogeElement: JQuery) { // 入力 var controller = new Controller($hogeElement); var apiClient = new ApiClient(apiUrl); // ユースケース var usecase1 = new Usecase1(controller, apiClient); var usecase2 = new Usecase2(controller, apiClient); var usecase3 = new Usecase3(controller); // 出力 var presenter1 = new Presenter1(controller, usecase1, …); var presenter2 = new Presenter2(controller, usecase1, …); } }
実装後しばらくして気づいた
MVI アーキテクチャに似てた
Model
Intent View
入力: Modelからのデータイベント 責務: プレゼンテーションロジック出力: Modelのレンダリングや、生のユーザー入力イベント
入力: Intentからのユーザーインタラクションイベント 責務: ビジネスロジック 出力: データイベント
入力: Viewからの生のユーザー入力イベント 出力: Modelフレンドリーなユーザーインタラクションイベント(Observable)
MVI とは
• 完全リアクティブなMVC
• 提唱者によるフレームワークがある
• staltz/Cycle.js
• RxJSを活用している
• Flux/Reactで使ってるEvent emitterは単機能すぎるという意見らしい
http://futurice.com/blog/reactive-mvc-and-the-virtual-dom/
クリーンアーキテクチャ 感想
今回の自分の設計では…
• RxJS とうまく協調できた
• MVIに近いものになり、その方が単純化できただろう
• 設計力不足でドメインモデル貧血症になった
• フロントエンドでは、動画広告というドメインモデルがフラグ1個と同一性判定しか持たなかった
• 動画広告案件のユビキタス言語も決めたが、ほぼサーバ側でしか使わなかった
• この規模で依存関係の逆転は過度の抽象化だった
心残り
jQueryでDOM操作した部分がInteractive感ある
• MVVM のようなデータバインドでReactiveにできそう
• http://webrxjs.org/ というRxJSのMVVMフレームワークもある
• Cycle.jsでJSXで宣言的にDOM操作する例あり
• https://github.com/ivan-kleshnin/cyclejs-examples
RxJS だけの話
RxJS 気をつけたこと
• なるべくストリームの操作で済ませる
• Subscribe を書く場所を絞る(今回は基本的にPresenter)
• Subscribe 内で Subscribe しない
• 副作用はSubscribeに書き、Doは極力避ける
• ストリームの途中で副作用を書くと、分岐したら何の原因で副作用が発生するかわからなくなる
RxJS 気をつけたこと
• Subjectをなるべく使わない。使える用途は、
• テスト用
• 非Observable → Observableのラッパ作るとき
• 何らかの事情でObservableを拡張したいとき
• (とはいえpublish/multicast等が似たようなことしているので必要ないかも)
• 子ストリームの結果を親へ流したくて、副作用でデータを受け付けて流すストリームを提供したいとき
RxJS 気をつけたこと
• OnNextを避ける
• 明示的に書くと手続き型になるため
• 手続き型に書いてるなら使ってもいいかも(ただしその場合はクラス内に隠蔽し、外に公開するならasObservableをかませる)
RxJS 気をつけたこと
• Observable を公開するときはHot変換しておく
• Coldのまま公開して分岐すると、Subscribe のたびにそのObservable内の処理が走る
RxJS 気をつけたこと
• 知見の共有!
• 使い手が少ないとレビュー依頼にも困る
• slackの #rx 活用
• 社内ConfluenceにRxの知見まとめページを作った
• 他、Qiitaに記事を上げたりなど
RxJS ハマったこと
• 現象
• APIリクエストを含むストリームを関数で定義して公開したら、Subscribeの回数分APIリクエストが走った
• 理由
• Subscribeのたびに関数が呼ばれ、ストリームが構築され、APIリクエストも走った
• 対策
• ストリームはコンストラクタで定義してローカルメンバ変数に代入しておき、それを返すだけの関数を提供すべし
RxJS ハマったこと
• 現象
• Cold Observableを Hot変換してrepeat()したら、Cold Observable の終了後に Call stack overflow
• 理由
• 終了した Cold が Completed を流し、repeat()が再Subscribeする、を無限に再帰
• 対策
• Cold ObservableをHot変換したらrepeat()しない
• Rx の仕様かも?
Special Thanks: wilfrem さん、toRisouP さん
RxJS ハマったこと
• 現象
• just(jQuery.val()) を返す関数を作ったら、undefined が流れてきた
• 理由
• just でストリーム作るタイミングで jQuery.val()がundefined だった
• 対策
• defer(() => just(JQuery.val())) のように、遅延評価
RxJS ハマったこと
• 現象
• 定期的な fromPromise を起点にした処理が、IEでは2度目のリクエスト以降動作しなかった
• 理由
• IE は Ajax の GET をキャッシュするせいで、2度目以降はストリームが発火しない
• 対策
• cache: false 追加
• http://qiita.com/kondei/items/99648a07418793d5f50b
RxJS ハマったこと
• 現象
• IE8でエラー
• 理由
• IE8の予約語に「do」があり、do()と重複
• 対策
• RxJSのGitHubドキュメント見て、別名メソッドに変える
• do → tap
• catch → catchException
RxJS 使って何が良かった?
• 複雑な処理を、他の複数の処理の起点にできた
• Observableの公開・合成 最高に便利!
• 素早く機能追加できた
• ストリームに関数1個挟むだけだったり
• 時間の概念を含む実装が楽だった
• 定期的リクエスト
• 複数の終了条件を持つカウントダウンも簡単に
private countdownStream(remain: number) { Rx.Observable .timer(0, this.countdownSpan) .map(r => { return remain - r + 1; }).takeWhile(r => { // 0秒で終了 return r >= -1; }).takeUntil(動画広告状態が非再生中); }
RxJS 使わなかったら?
• 独自イベントの乱舞
• コールバック地獄
総括
• RxJS と Clean Architecture を良く協調させれた
• RxJS を全面的に使う際のアーキテクチャは、大抵はMVIの方が単純で良い
• RxJS は罠が多いので知見を共有すべし
リンク
• クリーンアーキテクチャ 初出記事
• http://blog.8thlight.com/uncle-bob/2012/08/13/the-clean-architecture.html
• クリーンアーキテクチャについて書いた記事
• http://qiita.com/kondei/items/41c28674c1bfd4156186
• RxJS について書いた記事
• http://qiita.com/kondei/items/99648a07418793d5f50b
• http://qiita.com/kondei/items/17e5d4867a0652911e52