2019-01-28

サーバサイドViewModelのボキャブラリをクライアントサイドに寄せながら整理してみる

サーバサイドで ViewModel と呼ばれるもののボキャブラリがあまり整理されていないように見えるので、あれこれ調べたり書いたりしていた。そこでちょっと見えてきたものを挙げていきつつ、最終的には近年概念が大幅に整理されたクライアントサイドの ViewModel に寄せていくことで、荒れがちな View 由来のコード周りに秩序を与えられないか考えたメモ。

雑にパターン出し

サーバサイドの ViewModel という概念は以下のパターンで適用できそう。

  1. 変数の binding を担うもの
    • Hanami::Viewlaravel-view-models に相当
    • Rails では locals 縛りとかにしなければ不要なもの
    • View の直前でかつ Model としての複雑さはあまり考慮しなくてよいのでいわゆる Presenter と呼びたい
  2. 変数のセットまでの処理が複雑なもの
    • Fat Controllerの原因の一つを取り除く
    • たぶんこれは本来やったらダメなもの(正しく View 側の設計ができていない)
  3. state の複雑なコンポーネントを扱うもの

このうち 3 がクライアントサイドの ViewModel に近く、最も ViewModel っぽく機能しそう。これまで自分は View Objects という名前が全然しっくり来なくて馴染めなかったんだけど ViewModel と呼ばれるとよいかも。1

ちなみに単なるデータ構造を ViewModel というパターンもあるにはあるようだが、個人的にはそれは Model の Presentation (serialize)に相当すると思っているので、除外している。

サーバサイドViewModelはクライアントサイドのコンポーネントに対応できるのか?

Cells を例にとると、定義側は

cells/foo_cell.rb

class FooCell < Cell::ViewModel
  def bar
  end
end

cells/foo/bar.erb

<div class="cell">
</div>

みたいな感じになる。

これを呼び出す側が

@cell(:foo).(:bar)

こんな感じ。これで、呼び出すメソッドに応じて View も中の処理も切り替えることができる。

ここで疑問を覚えるのが、メソッド単位で View が変わるだけでは Component として複雑な状態を保持するには機能が足りないのではないか、ということである。

クライアントサイドの ViewModel Component は nest させることができ、悪名高き「propsバケツリレー」を使って state を受け渡す形を守ることで個々の Component のシンプルさを維持しつつ、複雑、大規模な機能、システムまでスケールすることを目指している。

サーバサイド ViewModel にクライアント ViewModel のその考え方を援用できるのだろうか。

RubyのCellsはnestできるしlayoutもある

trailblazer/cells: View components for Ruby and Rails.

Ruby の Cells については nest もできるし、layout もあり、またメソッドを state と(内部では)呼んでいるので、概ねクライアントサイドの ViewModel の考え方を援用できそうに見える。

例えば状態を3つ持つ Cell を作り、以下のように layout も用意してあげれば

layout

<div class="layout">
 <ul>
   <%= yield %>
 </ul>
</div>

cell の view

<li><%= complex_logic_results %></li>

最終的には

<div class="layout">
 <ul>
   <li></li>
   <li></li>
   <li></li>
 </ul>
</div>

こういう形のタブ構造などを簡単に実現できる。

このような形にできることが分かれば、Component の役割とどのような State を持つのかをモデリングすることで自ずと Cell の形が決まるし、形状、表現に変更が入ってもロジックやデータ構造にそこまでのダメージがないようにすることもできそうである。

Laravelのtorann/cellsの場合

ドキュメントにはそういう話はないので中のコードを読んだり実際に試してみると、

  • Layout の機能は CellBaseController にはないが、blade テンプレートの置き場所と名前付けにルールを設けることで layout のようなものは可能(layout の利用は @extends)
  • @cell の nest は @include の nest と同義なので特に問題ない

ということで やれば できそう。

cf. CellsはLayoutを持っているか - あーありがち(2019-01-27)

課題はロジックの分離とテスト

ViewModel として見た時にこれまで意識して来なかった課題が見えてくる。それは

  • ロジックを処理するメソッドと View を render するメソッドの区別がない

ことである。

例えば Vue.js を例に取ると、ViewModel コンポーネントは以下のような構造になっている。

{
  data:       <-- binding 用の data
  extends:    <-- 継承元
  components: <-- View で custom element として利用する component
  computed:   <-- メソッドベースの binding 用の data
  methods:    <-- event handler
}

このうち、 だいたい computed に判定用のメソッドを置く感じになる。2ということは computed をテストすると View のことを気にしなくてもロジックはテストできる。

しかし Cells の場合はメソッドと property しかなく、state / action はメソッドであり、これらを call すると render するのが基本である。これだとうっかりロジックを呼んだらそれが View に露出してしまう。幸い Ruby の Cells はまだテストも考慮されているが、torann/cells にはそれもない。

よくあるのは外から呼べないようにロジックの部分を private とかにしてしまう方法だが、それをやると今度はテストコードでテストできなくなる。

ではどうするか。

案1 - ロジックの結果を変数に放り込む

実際に torann/cells で書いてみた感じだと、property / attribute の方に「ロジックの結果を保持」するのがやりやすそうな気がした。すべてを state / data として扱って、初期化時にその前処理を終わらせる。template 側にロジックを書く際にも attribute はそのまま参照できるし、それっぽい名前の attribute であれば自然に読める。

「コンストラクタ頑張りすぎ問題」のようなにおいはするが、基本的にサーバサイドの ViewModel は副作用を扱う必要がないのでわざわざ階層を深くしなくても、メソッドの名前空間が render 処理に予約されているのであればメソッド以外を利用する、という考え方は一応、理には適っている。ような気がする。

案2 - 名前空間を独自に切る

例えば簡単な例だと

  • ロジックを扱うメソッドを _ で始める

というような形。

torann/cells の内部的にも $__env という名前で「頑張っている部分はある」し、_ や __ で始まるものに特別な意味を持たせるのは LL 界隈ではポピュラーな考え方だ。prefix を付けてよいというルールにすれば _ だけに限る必要もない。

案3 - ロジックをdelegateする新しいModelを作る

Ruby の Cells の場合は Model を一つ与えてそいつで delegate しつつ render するのが基本スタイルなので、その Model にロジックも Collection も全部押し付けることで ViewModel の名前空間で困らないようにすることはできる。

その場合、

app/cells/models/

みたいな階層を適当に掘ってみるのもアリなのではないだろうか。こうすれば間違いなくロジックはテストしやすくなる。

torann/cells の場合は Model object ではなく attributes array を受け取る設計なのでやや勝手が異なる。恐らく array の中に Model を押し込むことで似たようなことはできると思う。案2 とのハイブリッドのような形で、そこに Model を押し込んだことが分かる名前のルールがあれば可能そうだ。

とは言えコンポーネントの概念だけで全部解決することはなさそう

目的の明確なアプリを作るのであれば、できるだけ分かりやすくシンプルにすればよいと考えることができるし、「ユーザーは何らかの操作をしてくれる」という前提で補足的な説明は全部ポップアップなどに寄せてしまうということもできる。

しかしいわゆる Web サイトの場合は「提示しなければいけないデータ」の他に「事前に促しておくべき注意事項」などもあるはずで、アプリを構築する際に扱いやすい ViewModel の考え方に寄せるだけではやや解決の難しいものも出てきそうではある。

その場合は state / action という言葉にこだわらず View を提供しやすいという柔軟性に頼るとか、どうしても複数の Model の情報を渡したいという要求にどう応えるかを考えるなど、やはりフレームワークの提供してくれない解決策を探る必要はあるだろう。

  1. という気になるんだから自分はどこまで名前重要信者なんだろうか。 

  2. methods に置く場合もあるが、原則的に methods には副作用のあるものを置くはず。 

About

例によって個人のなんちゃらです

Recent Posts

Categories

Tool 日々 Web Biz Net Apple MS ことば News Unix howto Food PHP Movie Edu Community Book Security Text TV Perl Ruby Music Pdoc 生き方 RDoc ViewCVS CVS Rsync Disk Mail FreeBSD Cygwin PDF Photo Zebedee Debian OSX Comic Cron Sysadmin Font Analog iCal Sunbird DNS Linux Wiki Emacs Thunderbird Sitecopy Terminal Drawing tDiary AppleScript Life Money Omni PukiWiki Xen XREA Zsh Screen CASL Firefox Fink zsh haXe Ecmascript PATH_INFO SQLite PEAR Lighttpd FastCGI Subversion au prototype.js jsUnit Apache Trac Template Java Rhino Mochikit Feed Bloglines CSS del.icio.us SBS qwikWeb gettext Ajax JSDoc Rails HTML CHM EPWING NDTP EB IE CLI ck ThinkPad Toy WSH RFC readline rlwrap ImageMagick epeg Frenzy sysprep Ubuntu MeCab DTP ERD DBMS eclipse Eclipse Awk RD Diigo XAMPP RubyGems PHPDoc iCab DOM YAML Camino Geekmonkey w3m Scheme Gauche Lisp JSAN Google VMware DSL SLAX Safari Markdown Textile IRC Jabber Fastladder MacPorts LLSpirit CPAN Mozilla Twitter OpenFL Rswatch ITS NTP GUI Pragger Yapra XML Mobile Git Study JSON VirtualBox Samba Pear Growl Mercurial Rack Capistrano Rake Win RSS Mechanize Sitemaps Android JavaScript Python RTM OOo iPod Yahoo Unicode Github iTunes God SBM friendfeed Friendfeed HokuUn Sinatra TDD Test Project Evernote iPad Geohash Location Map Search Simplenote Image WebKit RSpec Phone CSV WiMAX USB Chrome RubyKaigi RubyKaigi2011 Space CoffeeScript Nokogiri Hpricot Rubygems jQuery Node GTD CI UX Design VCS Kanazawa.rb Kindle Amazon Agile Vagrant Chef Windows Composer Dotenv PaaS Itamae SaaS Docker Swagger Grape WebAPI Microservices OmniAuth HTTP 分析基盤 CDN Terraform IaaS HCL Webpack Vue.js BigQuery Middleman CMS AWS PNG Laravel Selenium OAuth OpenAPI GitHub UML GCP TypeScript SQL Hanami Document SVG AsciiDoc Pandoc DocBook Develop Jekyll macOS Node.js Vite Heroku Transformer AI Data Cloud Wasm