Smart UI パターンが再評価される世界

設計ナイト2020 を受けて、今どんなアーキテクチャを選ぶべきかという話をしたくなったのだ。

kichijojipm.connpass.com

お前誰よ

をやってきた人間。数百万〜数億行のデータ、月間数千万〜数十億 imp 程度を主戦場にしています。

今日の話

昂ぶって 15,000 字ぐらい書いてしまった。

DDD と PofEAA から学ぶパターン/アンチパターン

DDD も PofEAA も 2002 年出版だけど、ほとんど今でも通用する話なので、まずココを出発点とするのが良い。

利口な UI (Smart UI) アンチパターン

  • エリック・エヴァンスのドメイン駆動設計に書いてある
  • KENT WEB 時代ぐらいの PerlPHPCGI を思い出すと良いはず
    • 画面ごとの .cgi ファイルに、上の方にデータロードのコードが、下の方に UI のコードが書いてある。何なら混ざり合ってる
  • もちろん良いこともある
    • 画面ごとに分かれているので、影響が局所化される

minekoa.hatenadiary.org

これを少し改善したものが トランザクションスクリプト

f:id:onk:20201111020809p:plain

  • 分かりやすいのは ISUCON のコード
    • template とは分離された、依然として手続き的なコード
    • 各 Action 間でのコピペは引き続き横行している

PofEAA でのデータソースのアーキテクチャに関するパターン

第10章 データソースのアーキテクチャに関するパターン

テーブルデータゲートウェイ

https://bliki-ja.github.io/pofeaa/TableDataGateway/

SIにいた人なら「ダオ」のほうが通りが良いのではと思います。テーブルデーゲートウェイなんて現場で聞いたことが無い

より良いトランザクションスクリプトを目指す - enrike3のブログ

  • ほぼ Table 単位
  • すべての CRUD はこのゲートウェイを通る
  • SQL や、クエリビルダの組み立てが書かれるクラス

行データゲートウェイ

https://bliki-ja.github.io/pofeaa/RowDataGateway/

私は、トランザクションスクリプトを使用する場合に、行データゲートウェイを使用する頻度が最も高い。この場合、行データゲートウェイでデータベースアクセスコードを適切に抜き出し、別のトランザクションスクリプトで容易に再使用できるようにする。 私は、ドメインモデルを使用する場合には行データゲートウェイを使用しない。シンプルなマッピングを実行する場合には、コードレイヤを追加しなくても、アクティブレコードが同じ役割を果たす。

...

トランザクションスクリプトを行データゲートウェイとともに使用する場合、複数のスクリプトで繰り返されるビジネスロジックこそが、行データゲートウェイに必要なロジックであることがわかるだろう。ロジックを移動することによって、行データゲートウェイは段階的にアクティブレコードへと変化し、ビジネスロジックの重複を軽減する効果をもたらす。

(PofEAA より

アクティブレコード

https://bliki-ja.github.io/pofeaa/ActiveRecord/

データマッパー

https://bliki-ja.github.io/pofeaa/DataMapper/

  • レイヤー化しようと思うとデータマッパーパターンになりがち
    • だけど、密結合してエイッてやるともっと楽だよというのが Rails が示した道 (後述
  • 各レイヤー間を疎結合にしようとすると DTO による詰め直しが必要になる

この辺りは一昔前 (2000年代前半) に SSH (Struts, Spring, Hibernate) アーキテクチャが流行っていた頃を思い出す。Hibernate は二次キャッシュで DB アクセスを隠蔽して、例えば

# 概念コード
my $entry_1 = EntryRepository->find_all_by_author_id(author_id => 1)->[0]
my $entry_2 = EntryRepository->find_by_id(id => 2)

で読み込む $entry_1, $entry_2 はメモリ上も同じインスタンスであるべきという考え方で、片方に変更を加えると (同じインスタンスであるので) もう片方にも影響する。

  • 同じデータなんだから 1 つなのは当然だし、レースコンディションも絶対に発生しない
  • DB からロード済みだと SQL は発行しない (Identity Map を持つ)、というのをデータマッパー上でできるので、うまく使うとパフォーマンスが向上する
    • 我々の道具で言うと Apollo Client のキャッシュに似たイメージ

PofEAA や DDD で語られたパターンの詳しい話は texta.fm で id:t-wadaid:Yasaichi が話しているのでぜひ聞いてください!

anchor.fm

話はトランザクションスクリプトに戻る

  • Smart UI パターンよりモデル化していきやすいので、何らかのコードをトランザクションスクリプトに抜き出すのは推奨されている
  • どういうレイヤー分けを行うかは正解がない

トランザクションスクリプトをどこに置くかは、レイヤをどのように体系化するかによって異なる。

...

トランザクションスクリプトを複数のクラスに体系化する方法は2つある。 最も一般的な方法は、複数のトランザクションスクリプトを1つのクラスに入れ、各クラスが関連するトランザクションスクリプトの対象エリアを定義する方法である。この方法は最も簡単で一般的な手法である。 もう1つの方法は、トランザクションスクリプトごとに独自のクラスを持たせ、「コマンドパターン」を使うというものだ。

(PofEAA より

ドメインロジックを構築する方法 3 種類

ドメインロジックを構築する方法は以下の 3 つが PofEAA に書かれている。(第2章 ドメインロジックの構築)

それぞれのコストはこう図示される

f:id:onk:20201110231849p:plain

ドメインで扱う概念の中には、1つの機能や処理が単体で存在していて、もの(オブジェクト)として扱うのが不自然なものもある。そうしたものは、サービスという形でユビキタス言語に組み込む。サービスは基本的に状態をもたない(stateless)。

[ 技術講座 ] Domain-Driven Designのエッセンス 第2回|オブジェクトの広場

 

ドメインにおける重要なプロセスや変換処理が、エンティティや値オブジェクトの自然な責務でない場合は、その操作は、サービスとして宣言される独立したインターフェイスとしてモデルに追加すること。モデルの言語を用いてインターフェイスを定義し、操作名が必ずユビキタス言語の一部になるようにすること。サービスには状態を持たせないこと。

...

このコードを見てもらうと、"なんだ ドメインモデルを入出力にとる関数じゃないか" と思うでしょう。そのとおりです。

混乱しがちなサービスという概念について - かとじゅんの技術日誌

ここまでのまとめ

  • もっともコピペが横行していて、その分労働集約的に並列作業できる、他には影響を与えずに改修できるのが Smart UI パターン
  • データロードと Template を分離するトランザクションスクリプト
    • まずは ISUCON コードを思い浮かべると良い
    • 各 Action の中を設計しようと思うと、無限の可能性が広がっている。実装のレールは (PofEAA には) 無い
  • データと振る舞いを 1 箇所に集めようとしているのが「ドメイン

はい。まだ 1/3 ぐらいだよ!

Rails によって発見された、密結合で速く走れるソフトウェア

和田:Ruby on Rails自身は疎結合の設計に対してNoを言っている。密結合にすることによって疎結合な設計以上の開発スピードが生まれる。少なくともスタートアップ企業にとってスピードは本当にクリティカルな力なので、もし密結合の状態でも速く走れるソフトウェアの構造があるのであれば、それはゆっくり安定して継続的に歩いていく疎結合のソフトウェア設計より強いということをRailsはある程度証明していたわけですね。そしていま、その構造のまま大きくなるとすごく大変になるということも証明している。

マニアが潰したテスト駆動開発〜『健全なビジネスの継続的成長のためには健全なコードが必要だ』対談 (5) | by Takeshi Kakeda | 時を超えたプログラミングの道

DHH がどのように密結合を作り上げていったのか、については以下のスライドが詳しい。

speakerdeck.com

  • RESTful ルーティングと ActiveRecord パターンによって、URL で表されるリソースから DB 上のテーブルまでが密結合する構造を作った
  • ActiveRecord パターンとその Validations/Callbacks によって、ビジネスロジックとその組み立て処理を全て Model に書けるようにした

この密結合は、間違いなく最速の設計技法である。じゃないとあんなにスタートアップ界隈で採用されなかったし、十分にワークすることは歴史が証明している。

RailsMVC が標準となっていった歴史は最近だとここでも語られている。

この頃に同時に起こったのがRuby on Railsに代表されるLLの躍進とそれに伴うテンプレートエンジンの簡素化です。

これによりASP.NETJSFは所謂Web界隈と呼ばれるようなコンシューマよりへの拡大はもちろん、主戦場であるエンタープライズ領域すらLL言語にフロントエンド系を中心に浸食されていきました。

またこれらのFWは細かい理由は知りませんが結果的にコンポーネント指向ではなく、シンプルなMVC Model 2を採用しておりテンプレートエンジンはループや条件分岐、変数をバインディングしたりレイアウトを作れる程度の簡素なものでイベントドリブンなどは採用さていません。一部、ClickやWicketなんかは採用していましたが、まあ普及していませんし?

WASMとRustはVue.js/React.jsを打倒するのか? - JSへの侵略の歴史

僕の感覚としても、シンプルに Rails をベースラインにしていると幸せ。

このコードを見てもらうと、"なんだ ドメインモデルを入出力にとる関数じゃないか" と思うでしょう。そのとおりです。最初はその程度の認識でよいと思いますが、ここで一点だけいいたいのは、乱用は禁止ということです。

...

従属するエンティティや値オブジェクトがないということで早期あきらめてしまい、なんでもかんでもドメインサービスにするというのもの違うのです。後者の場合は、振る舞いがあるべきドメインモデルから振る舞いを奪うことになるので、ドメインモデル貧血症の温床になる可能性があるのです。

混乱しがちなサービスという概念について - かとじゅんの技術日誌

素朴な密結合 MVC では限界がある

Rails は最速ではあるが、「その構造のまま大きくなるとすごく大変になる」フレームワークでもある。

素朴な MVC では限界があるというのを皆が発表している。

Rails は少人数スタートアップで小〜中規模 *1 なアプリケーションを作るために最適化されたフレームワークなので、ギャップはある。

ただ僕の体感としては、複雑なのはごく一部 (10%程度) で、ほとんどの要件はシンプルに扱える。

複雑さをベースにしたアーキテクチャは不当に難しい。シンプルな要件のときに大仰に見せたくないので、複雑なところは例外的に見えるようなアーキテクチャであると良い。

本当に複雑なものと、複雑ではあるが工夫で対処できるもの

ActiveRecord を前提として、解決方法はいくつも語られてきた

また、「アプリケーションサービス」はよく導入される(が、間違えやすい)

トランザクションスクリプトの延長にあるこの Service クラス (コマンドパターン) で戦う方法がよく採られている。

間違えないように Operation に持って行く=Trailblazer

そもそものはじまりは、作者のNick Sutterer氏がRailsMVC抽象レイヤーのあり方に疑問を持ったこと。Railsの手軽さを認める一方、ModelやControllerの肥大カオス化により、のちの保守性が下がることを問題視されたそうです(Nickさんの本意訳)。

TrailBlazer概要まとめてみた - Qiita

概要は Rails のアーキテクチャ設計を考える - Qiita を読むと掴みやすいかもしれない。

Trailblazer: Operation Overview

  • Model の validation に当たるものを contract に持っていく
  • Operation の中身は本質的に手続き的なものなので step で処理する
    • このときに step の連続である=state を持っている、という点も扱いやすさに繋がっている
    • クラスメソッドではなく、インスタンスを作って状態を持つと楽になる、というのが PofEAA でも語られている。

この手法のメリットは、スクリプトインスタンスを実行時にオブジェクトとして扱える点であるが、トランザクションスクリプトを使ってドメインロジックを体系化するようなシステムでは、このメリットを活かす必要性はほとんどない。もちろん多くの言語では、クラスを完全に無視してグローバル関数だけを使うこともできる。しかし、新たなオブジェクトをインスタンス化することでスレッドの問題が解決できる場合もある。データの分離が簡単になるからだ。

(PofEAA より

間違えないように Interactor に持って行く=Hanami(=Clean Architecture)

Hanami is based on two principles: Clean Architecture and Monolith First.

Architecture: Overview | Hanami Guides

Architecture: Interactors | Hanami Guides

Overview の次に Interactors の章が置かれる程度には Interactor が主軸なフレームワーク

Interactor も Operation と同じく initializecall のみを持つクラスである。Iterator の考え方は HanamiはRubyの救世主(メシア)となるか、愚かな星と散るのか を読むと良いかな。

イマイチ流行らなかった rectify

https://github.com/andypike/rectify

  • FormObject, Command パターンがあるのはもう前提として、僕は書き味の話をしたい!
  • Controller が Controller に必要な本質だけを追究できる世界

Controller に必要な本質は、これも PofEAA に書かれている。

ドメインロジックの扱いで最も難しいのは、人々が思っているように、何がドメインロジックで何が他のロジックかを見極めることだろう。私が好きな非公式のテストは、Webアプリケーションにコマンドラインインタフェースを追加するときのように、まったく異なるレイヤをアプリケーションに追加することを想像するというものである。この追加を行うときに機能を複製する必要があれば、それはドメインロジックがプレゼンテーションの中にはみ出していることを示している。

(PofEAA より

Web アプリケーションとしてのインタフェース (View 以外) が Controller の役目で、「同機能の CUI コマンドを作るときに重複が無い」という基準で考えると良い。

rectify を使ったときの Controller は

def create
  @form = RegistrationForm.from_params(params)

  RegisterAccount.call(@form) do
    on(:ok)      { redirect_to dashboard_path }
    on(:invalid) { render :new }
    on(:already_registered) { redirect_to login_path }
  end
end

と、ビジネスロジックで起きたイベントを on で捕捉して、redirect や render を行う。これは CUI にするときにまったく重複しないまさしく Controller の役目だし、見た目も美しい。

とまぁ僕は好きなんだけど、ガッツリ実務で使ったわけではないので見た目の美しさにのみ囚われている可能性はある。

いずれも Controller or Action と 1:1 対応する新しい層 という考え方

本当に複雑なもの

以上、だいたいのことは工夫で解決できる程度の複雑性、と置いた。じゃあ本当に複雑なものは何かというと、SoR だろうと思う。

ただ僕の目には SoE=モード2 で十分なものがほとんどに見えている。

すべて SoE 的手法でもいけるんじゃないか

https://speakerdeck.com/naoya/system-of-record-to-system-of-engagement#25

バランスを欠いていたと id:naoya は振り返っているが、p20 の表にあるようなシステム領域はあまり普段の開発=イテレーションを回しながら不確実性に対処していく中には出てこないので、だいたい SoE と捉えていくのが Web アプリケーション開発の実情じゃないかな。

今求められているアーキテクチャ

SoE の戦場はクライアント側に移っている。

  • ビジネスにおいて、(ネイティブ or Web SPA) クライアントの UX が必須な時代なので、 API 開発の手綱もサーバからクライアントに移していくような流れが生まれています
  • GraphQL も BFF も、クライアント側で制御していこうという発想
    • 新しい概念「クエリ」を入れたのが GraphQL。クライアントはともかく、サーバは新規開発になる
    • クライアントごとの中間レイヤーを作ろうというのが BFF。途中からでも導入しやすい

The NEXT of REST - onk.ninja

【エンジニアブログ】ダイニーのエンジニアリング3カ条|dinii(ダイニー)公式|note では Hasura を用いると GraphQL の query 側はほとんど開発が要らないという話をしている。

「フロントエンド領域」を再定義する - Speaker Deck でもフロントエンジニアの領域がサーバ側に広がっている。 GraphQL や BFF をフロントエンジニアが扱うことで、ブラウザがバックエンドからデータを取得する、データソースのアーキテクチャはフロントエンジニアのものになった。

Builderscon Tokyo 2019 で「ビジネスの構造を扱うアーキテクチャとユーザとの接点を扱うアーキテクチャ」というタイトルで登壇してきました #builderscon - assertInstanceOf('Engineer', $a_suenami) では

プレゼンテーション優位な技術駆動アーキテクチャを選択する場合は、むしろ立派な実装パターンであると言える。

 

「DDDのスマートUIアンチパターン」もSoEの目的に合わせて使えばデザインパターンになる

という話をしている。

まさに Smart UI パターンは再評価されるポイントに来ている。クライアントが、表示の都合でクエリを都度書く世界になった。

  • フロントエンドではコンポーネントの作り方が確立されている
  • データマッパーは Apollo Client のキャッシュが上手いことやってくれる

ので、Smart UI の痛みも少なく実装することができる。

そしてバックエンドは、SoE では ActiveRecord で表現できないほど複雑な「ドメイン」は無いと位置づけて良く、サーバサイドの仕事は GraphQL の schema を提供したら loader を考えるだけの仕事になっている、というのがイマココです。

(誤解がありそうなのでちょっとだけ言っておくと、GraphQL の schema を考える仕事は RESTful API の schema を考える仕事とほとんど変わんない感覚です。「DB を露出している」と捉えるのは筋違いかな)

Mutation 側は?

Mutation はアプリケーションサービスと非常に親和性が高く、コマンドパターンの Service クラスを無思考で作ってしまいがち。

ただ今までの話であったように、安易に Service に全てを押し込めるとドメインモデル貧血症に陥ります。ActiveRecord の機能をちゃんと使って、適切に validation, callback を使った上で、それでも複雑だから Service クラスが必要になったというのが歴史です。まずはオブジェクト指向的に育てることを忘れないでください。

シンプルな Muation はシンプルに扱える。もしナイーブに実装し過ぎたとしても、改善方法が確立されているので大丈夫。

なんか言及する隙が無かったものあれこれ

*1:複雑さは色んな指標があると思うけど、雑な一つの基準として、100 テーブル以内ぐらいだと考えると良いと思う