概要
この記事は、君は知ってたか?スーファミのコントローラとMVCのコントローラは同じということ…を - ゲーム脳でもわかるMVC【序説】の続きです。
MVCが構造上抱える"問題点"を指摘した上で、前回MVCの雰囲気を掴んだゲーム脳の人にコントローラを棄ててアクションを起こすことを求めます。
MVCの次の構成の提案、とも言えます。
日本語で
何を言っているんだお前は、と思われたでしょうか。もう少し真面目に書くと、
- コントローラ、または他の要素が肥大化しやすい原因
- コントローラを、最近FLUXなどでも見かけるアクションに置き換えるべき理由
といった事の説明をします。この説明を通して、前回の記事で実力不足により敢えてモヤモヤした状態にしてある部分についても、解決を試みます。
その上で、MVCに代わって最近取り上げられつつあるSAM: State Action Model の概念的な紹介をします。
さらに、SAMと本質的に近い挙動を示すR言語のWebアプリフレームワークShinyの紹介もします。
ちょっと、当初想定よりもボリュームが大きくなってしまいましたね。。。
MVCでRやShinyのタグが付いているのは斬新、そしてSAMのタグはもはや付いていない…
「誰が制御するのか」問題
さて、まずは前回の記事で誤魔化して、コメントで指摘を受けた部分(と関連のある部分)から考察を始めます。
この節の結論は、コントローラが制御してもモデルが制御してもダメです。
物知りコントローラ
前回の記事ではMVCのコントローラをスーファミetc.のコントローラと思うことにしました。
しかし、世の中そうそう都合よくはできていません。
前回のコントローラの、入力処理部分のソースを再掲します。
class MazeController:
# 中略
def process_input(self, input_str):
if input_str in self.cursor_dict.keys():
self.model.process(self.cursor_dict[input_str])
return self.view.render(self.model)
return self.view.render(
self.model, message='h,j,k,lのいずれかを入力してください。')
なんということでしょう!
Controllerという名前なのに、Modelを呼び出した後で、さらにViewを呼び出し、ViewにModelを渡しているではありませんか!
これをスーファミのコントローラのアナロジーで理解しようとすると、全く意味不明ですね?
コントローラがTVにアクセスするんかーい!
これでは、スーファミ本体はあわれコントローラに乗っ取られてしまった状態です。
<イメージ画像>
ただ、このような形式でコーディングをしている理由もあります。
それは、入力による分岐処理を行っているからです。
このprocess_input()
の意図するところは、引数input_strが所定の文字列(cursor_dict.keys()
の内容)であればmodelに処理を継続させ、所定の文字列でなければmodelの更新を経由せずにviewにエラーメッセージを表示させる、ということをしたかったのでした。
入力チェックレベルの処理は、コントローラのメソッド(関数)の中で見通しよく行いたい、という気持ちの伝わるコードになっています。
実際、MVCが歴史的にはじめて生まれた頃には、コントローラの役割はこのような制御(コントロール)を含むものであったのでした。それゆえ、コントローラという名前で入力を受け取っていたというのが歴史的経緯です。
しかし、このような書き方は、しばしばコントローラの肥大化を伴います。
以下のような(アンチ)パターンが代表的です:
- 複数の入力値を受け取って場合分けが生じ、複雑になる(今回の例のように...)
- 複数のモデルを利用する場合で、モデル間の処理の制御をコントローラで行い、複雑になる
- モデルが複数の種類のエラーを返し、それに合わせてビューに返却する値を変更することで、複雑になる
困りました。これは、コントローラが物を知りすぎているパターンです。
物知りモデル(ビューと密結合)
では、コントローラは完全にビューの事を忘れてしまって、全ての入力をモデルに渡して、制御までモデルまたはビューに投げるとどうなるでしょうか?
そのような場合も、無配慮に実装をすると、やはり悪い事が起きます。
というのも、モデルに完全に制御を移してしまうと、モデルは自分自身で表示を行うビューを判別する必要が出てしまい、そもそもコードを分割する意味が無くなるからです。
前回の記事で、モデルとビューの組み合わせを自由自在に変更できたのは、コントローラがモデルとビューの結合を行っており、モデルはビューの事を知らないからでした。
形式的に分割をしたように見えても、実際にはモデルは自分の中で次に呼ぶビューを判別している…このような分割をするなら、まだ一緒に書いてしまった方がマシかもしれません。再利用性は乏しくなり、本質的な計算処理(本来モデルに書きたいもの)と画面表示処理(本来ビューに書きたいもの)が混ざってしまいます。
※今は、ある一つの事を成し遂げるための唯一の方法について考察しているのではなく、様々な実現方法がある中で保守性が少しでも高いコードを書きたいということを念頭に置いて考察をしていた事を、思い出しましょう。
不都合な真実
これまでの検討によって、解決策はなにも出ていないですが、とりあえず以下の事がわかりました。
- コントローラがモデルを通すか通さないか、およびモデルの戻り値の制御をすると、煩雑化する
- モデルがビューの呼び出しを制御すると、結合度が高くなる(モデルがビューの情報を持つことになる)
つまるところ…制御する処理を個々の要素に入れてしまうと、それによって歪が生じる、ということですね。
KISSの適用
ここで、有名な格言を思い出します。
Keep it simple stupid.
この格言に従って、バカみたいに単純なコードを目指しましょう。
複雑になる原因は、上でみたようにコントロールしようとする事にあります。
上で問題点として挙げた
- モデルの戻り値の制御(@コントローラ)
- ビューの呼び出しの制御(@モデル)
これを、どちらも行わなければよいのです。少し言い換えると、処理の流れを"行ったり来たりする形式"や"次の処理を知っている事を前提にした形式"を棄てて、"単方向に流れる形式"を目指しながらも結合度を下げるという事になります。
両方向から、単方向へ。しかし、疎結合。これが目指す方向です。
↓参考まで、現状の「両方向」のMVCのフロー図
SAMの導入〜必要なのはアクションと、ビューとモデルの接合方法〜
上で書いたとおり制御を無くして、**両方向から、単方向へ。しかし、疎結合。**というコーディングを実現します。
A:Action アクション
まず、モデルとビューの両方の振る舞いを制御するコントローラには、消えてもらいます。
モデルやビューの振る舞いを制御するのではなく、モデルを更新するための入力だけをただただ受け取る、そういうモノになってもらいます。
言ってしまえば、アクションです。アクションには、ビューの制御などの役割は一切なく、単に入力などをモデルに伝えるという意味だけを持つ存在となります。
ビューとモデルの接合
また、何かがビューを更新するために、モデルの更新を検知する必要があります。上で考えたとおり、モデルはビューの情報を持ちたくないので、モデル以外の何かとします。
これを技術的にどう実現するか…?
いくつかパターンはありますが、
- ビューの描画の元になるモデルを何かに登録しておく
- モデルは自分に更新があったことを何かに通知する
- 何かは更新されたモデルに関係のあるビューだけを更新する
というような仕組みによって、ビューとモデルの直接的な関係を切り離し、モデルがビューの事を詳しく知らなくて良い状況を作ることができます。
S:State ステート
一般には、モデルの全てがビューに影響する訳ではなく、またビューの形がモデルと一致する訳でもありません。
そこで、ビューとモデルの間に補助的に「ステート」というものを作成する、という方法によって見通しを整えることができます。
SAM爆誕
これまでに考えた事によって、MVC→MSVAとなりました。
このMSVAは、少し順番を入れ替えたりVを取ったりすることで、SAMと呼ばれています。
実際、サーバーサイドの処理に限ると、Vにあたる処理が不要になる場合があり、残りの部分が本質的であるので、
State -> Action -> Model
と見る事ができるので、その点において適切な命名と言えそうです。
(VがSの関数になっていることと、今回はあまり詳しく触れていないですがSがタイマーのような処理を行うことで、VではなくSがループの中に入るようになっています...ある意味ではVもSの一部と思えます)
これを図で示すと、以下のようになります。
これはFLUX等とも似ていますね。SAMは、実はFLUXやREDUXなどのアプローチの良いところを抽出して、さらに良い方法はないかということで考え出されたものなのです。
(このあたりについては、リンクしているinfoQの記事などを参照してください)
過去の記事のソースをSAMで書き直すとどうなるのか?は省略します
ある程度複雑なコードでなければ、SAMを用いるメリットがわかりにくいのですが、イメージを掴むために前回のコードを書き直します…
という予定で居たのですが、今回は書き直しを見送ります。
というのも、実は概念の説明としてもっと優れたShinyの例があったので、一旦Shinyのデモソースをそのまま使うことにしました。
これは、個人的にはSAMのサイトに載っているロケットの例より良いのではないかとすら思います
(ただし、SAMの重要なポイントである、Stateでの分岐とか、StateでActionを呼ぶ記述が無いのですが、それは補足に記載したSAMのページなどをご確認ください)
なお、もし要望が沢山あれば、前回のコードを書き直したものを載せるかもしれません。
ShinyによるSAMの"実践例"
さて、SAMはまだ新しい考え方で、私が不勉強な事もあり、SAMを取り入れた有名なライブラリは知りません...SAM自体は単純な関数だけで実践できる事を標榜しているので、そもそもライブラリは無くても良い、という考え方かもしれません。
(概念としては、SAMのページでMeiosisというものが紹介されています: https://github.com/foxdonut/meiosis )
ただ、そこそこ有名なもので実際にSAMに近いものを実現しているものとして、実はShinyというR言語のWebアプリフレームワークが挙げられます。Shinyは、
- Rの適当なグラフを描くだけで、それがリアクティブなオブジェクトとして配置される
- 画面でアクションを起こした場合(一般にはグラフの対象となるデータが変更される)について、アクションによって変更された値を利用しているオブジェクトだけを再描画する
という性質を持つ、とても便利な【Webアプリ】フレームワークです。(クライアントアプリではありません)
以下、リンク先にRのソース+デモがありますので、こちらを参照して実際に動くアプリを触ってみてください。
https://shiny.rstudio.com/gallery/kmeans-example.html
shinyではR言語のコードだけで、適当なhtmlの描画まで行うことができます。
一応、どれぐらい短いかを記事中でも示す為に、コードをコピーします。server.Rというサーバーサイド処理を記述するものと、ui.Rというクライアント側のコードに化けるものにファイルが分かれています。
function(input, output, session) {
# Combine the selected variables into a new data frame
selectedData <- reactive({
iris[, c(input$xcol, input$ycol)]
})
clusters <- reactive({
kmeans(selectedData(), input$clusters)
})
output$plot1 <- renderPlot({
palette(c("#E41A1C", "#377EB8", "#4DAF4A", "#984EA3",
"#FF7F00", "#FFFF33", "#A65628", "#F781BF", "#999999"))
par(mar = c(5.1, 4.1, 0, 1))
plot(selectedData(),
col = clusters()$cluster,
pch = 20, cex = 3)
points(clusters()$centers, pch = 4, cex = 4, lwd = 4)
})
}
pageWithSidebar(
headerPanel('Iris k-means clustering'),
sidebarPanel(
selectInput('xcol', 'X Variable', names(iris)),
selectInput('ycol', 'Y Variable', names(iris),
selected=names(iris)[[2]]),
numericInput('clusters', 'Cluster count', 3,
min = 1, max = 9)
),
mainPanel(
plotOutput('plot1')
)
)
あまり細かくRの文法をしらなくても、概ね理解が可能かと思います。
以下、実際にデモを触りながら読んで頂きたいのですが…
上のサンプルで、例えばドロップダウンを変更することがSAMで言うActionにあたります。このActionはほとんど自動化されて、Modelにあたる対象=「inputおよびそれを参照する"reactive"なselectedData/clusters」を変更します。
このModelの変更の後で、renderPlot内でModelを参照している部分を更新しますが、このrenderPlotが(サーバー側で行われる)Stateに対応しています。
(ui.Rを元に生成される)HTML側には、その結果を受け取って描画を更新するView相当の処理があり、このStateの結果を受け取ってブラウザでの再描画が行われます。
このような見方をすれば、実はShinyはSAMに適合するような形式であると言えます。(個々の関数等の名前こそ違うものの)
複雑な制御処理を行わないことにすれば、このように単純なデータ移送処理(モデル・ステート・ビューの間でのデータのやりとりのうち、自明なもの)はフレームワークにより自動化でき、敢えて書く必要のあるコードの量も少なくなります。
たかだか2つのファイルに、合計で40行程度のコードを書くだけで、適当な表形式のデータの任意の2つの軸を選んで任意の個数にクラスタリングをするコードを書けています。これを旧来のやり方で実装した場合、かなりコードを書く必要があるのではないでしょうか。
※Shinyスゴイと言ってるのかSAMスゴイと言っているのかワカラナイ...
Shinyは元々SAMという事を明示的に意識した文脈で生まれた訳ではなく、Reactive等の文脈で生まれたものですが、MVCの枠組みと比較して興味深い議論があります:
https://github.com/rstudio/shiny/issues/250
この最後のbborgesrさんのコメントが、まさにこの記事で述べているような事を、SAMに関する言及なしで述べています。コントローラの排除と一方向データフロー、および関心の分離(SAMではViewとStateの分離)です。
まあ正確な言葉遣いはともあれ、同じようなコーディングの"難点"にみんな興味があり、改善しようとした、という事を端的に表していますね。
まとめ
結局どういう事が言えるか…というと
- コントロールするのはやめて、コントローラはアクションにしよう
- モデルにはビューの情報を持たせないようにして、必要なビューが勝手に更新される(リアクティブ)ようにしよう
- モデルとビューの中間にステートを挟んで、タイマーなどはステートからアクションを起こさせるようにすれば、ビューは完全に制御から開放される
- SAMすごいね
- Shinyすごいね
もう少し技術的な補足など
今回の記事は、元々はMVAぐらいで止めようかというぐらいのつもりだったのですが、色々書いているうちに中途半端にするとかえって説明の見通しが悪くなり、結局SAMを説明することになってしまいました。
以下、SAMにする前に書いていたエッセンスを記しておきます。
今回のテーマでまとめたくなった背景には、序説でも触れた以下の記事があります。より詳しく理解したい場合には、以下の記事を勧めます。
https://www.infoq.com/jp/articles/no-more-mvc-frameworks
※これは日本語訳記事なので、原文を読む場合は↑のリンク先の最初にある原文へどうぞ
また、上の記事を経て、SAMは以下のようにまとめられています:
http://sam.js.org/
日本語の記事では、以下のようなものがあります:
http://izumisy-tech.hatenablog.com/entry/2017/10/14/184229
コントローラを棄ててアクションを起こすという事についての要点
コントローラが複雑な制御を伴う場合は、単純な関数合成で書きにくくなります。(関数合成にすると、一つの関数の中で複雑な分岐処理をせざるを得ない)
単純な関数合成で書きやすくするには、そもそも複雑な制御を持たない構造・設計にしてしまえばよかったのです。
infoQの記事の
従来のMVCでは、アクション(コントローラ)はモデルの更新メソッドを呼び、成功したか失敗したかでビューの更新方法が変わります。Andreが指摘するように、この方法が絶対というわけではありません。アクションは単にモデルの値を渡すだけで、モデルが更新されるかどうかを決めるものではないと考えると、同等の有効でリアクティブな方法も考えられます。
(中略)
この点を念頭に置くと、リアクティブなMVCは次のようになるでしょう。
$V = f( M.\mathrm{present}( A(\mathrm{data}) ) )$
※M:Model, A:Action, V:View, f:何かの適当な関数…物理っぽい書き方をすれば$V=V(M.\mathrm{present}(A(\mathrm{data})))$
この辺ですね。
これはinfoQの記事にも丁寧に書かれていて、リアクティブ/関数型っぽい感じで書きたい、という方向性で考えた末の"結論"となっています。
注意事項
SAMにおいて本質的に重要なのは、登場人物をどのような名前で呼ぶかということではなくて、コードの守備範囲・呼び出しの関係をどのように分割すれば、不要な処理を書かずに保守しやすいコードを書けるかということに尽きます。
SAMは新しい考え方なので、まだ取り入れられている物は少ないと思うのですが、コーディングに際してこのような事を念頭に置いておくと、きっと保守をしやすいコーディングができるに違いありません。
なお、MVC/SAMに関するフロー図については、以下のinfoQのページの物を引用しました:
https://www.infoq.com/jp/articles/no-more-mvc-frameworks