実況中継シリーズ - 複雑なJavaScriptアプリケーションに立ち向かうためのアーキテクチャ
先日行われた builderscon tokyo 2017 にて、「複雑なJavaScriptアプリケーションに立ち向かうためのアーキテクチャ」という発表をしてきました。この記事では、そのプレゼンテーションの再現を行います。
続きを読む実況中継シリーズ「Vue.jsで学ぶMVVM 非同期処理 その光と闇」 Aパート
2017年3月4日に、大阪にてYAPC::Kansaiというカンファレンスが開催されました。弊社からもわたしがスピーカーとして参加しており、「Vue.jsで学ぶMVVM 非同期処理 その光と闇」という発表をしてまいりました。とても参考になる発表が非常に多く、Perlコミュニティの力を改めて感じました。また、懇親会などでも、たくさんのハッカーと議論や情報交換ができ、大変有意義でした。カンファレンスは議論や情報交換してこそですよね!
というわけで、恒例のプレゼン再現ブログ「実況中継シリーズ」を行います。長くなりすぎるので前半と後半に分けます。本日は前半のみの公開です。
続きを読むModel View Whatever + Rx のアンチパターン
GUIアーキテクチャパターンとしてModel View Whateverを採用した際に、Rxのストリームをプレゼンテーション層からモデル層まで一気通貫でつなげてしまうのはアンチパターンである、という話をします。
前提
GUIアーキテクチャパターンにおける Model View Whatever パターン、とくにMVVMに近いパターンを前提とします。いわゆるサーバサイドの「web系MVC」は前提としません。
Model View WhateverパターンとPDS
そもそもGUIアプリケーションでModel View Whateverというアーキテクチャパターンを採用する理由として、PDSの実現があります。このあたりの話は詳しくは実況中継シリーズ Vue.jsで実現するMVVMパターン Fluxアーキテクチャとの距離 - Re.Ra.Ku アドベントカレンダー day 13 - Re.Ra.Ku tech blogを参照してください。
そして、リッチクライアントにおいてモデルはステートフルです。状態管理というのは非常に複雑なものです。この複雑なものを、GUIプラットフォームの都合に振り回されるプレゼンテーション層からひっぺがして、モデル層で独立に設計、管理できると、コードの見通しもよくなるしテストもしやすくて嬉しいね、というのが、リッチクライアントにおいてM V Whateverアーキテクチャパターンを採用する動機のひとつです。
これを徹底すると、モデル層とプレゼンテーション層のコミュニケーションは、以下のようになっているべきです。(参考:MVVMのModelにまつわる誤解)
- プレゼンテーション層 -> モデル層のコミュニケーションは、voidなメソッドの呼び出しと、属性の取得のみ行われる
- モデル層 -> プレゼンテーション層のコミュニケーションはイベントの発行のみ行われる(MVPの場合Presenterで定義されるインターフェイスの呼び出しのみ行われる)
モデル層とプレゼンテーション層のコミュニケーションを上記のように整理、制限を持たせることによって、モデル層はプレゼンテーション層に依存しないで済むようになる、というのがそもそもMVWのアイデアです。
具体的な例
たとえば、API通信を伴う操作を例に取りましょう。ある画面で、ボタンをタップするとAPIから情報を取ってきて、それを画面に表示するとしましょう。
この場合、例えば一気通貫に
rx.tap .flatMap { // API通信を伴う操作。失敗時にはonErrorが呼ばれる } .subscribe { onNext: { // APIの結果から表示を操作 } onError: { // エラーの型を見て分岐 } }
という感じで書くのではなく、
//////////////// // in presentation //////////////// rx.tap .subscribe { onNext: { service.invokeNyan(param) } } someModel.xxEvent.subscribe { onNext: { //someModelの状態に応じてViewを描画 } } errorModel.nyanErrrorOccured.subscribe{ onNext: { //nyanErrorに応じたViewの描画 } } errorModel.wanErrrorOccured.subscribe{ onNext: { //wanErrorに応じたViewの描画 } } /////////////////////// // in model /////////////////////// // service func invokeNyan(param) { validate(param).flatMap {validatedParam => return api.dispatch(validatedParam) }.subscribe { onNext: { // データ・モデルやドメイン・モデルを操作。 // MVVMなら、モデルの状態変化の結果として、イベントがdispatchされる // MVPならPresenterのインターフェイスを叩く } onError: { // エラー種別に応じてエラーモデルやデータ・モデルを操作 // MVVMなら、モデルの状態変化の結果として、イベントがdispatchされる // MVPならPresenterのインターフェイスを叩く } } } }
こういうふうに書いたほうが良い、ということです(擬似言語によるコードです)。
まず、前者の場合(そもそもtapがonErrorに入らないとかそういう問題もあるんだけど話がそれるので置いておきます)、まず「エラーの型を見て分岐」というロジックがプレゼンテーション層に漏れ出していますが、後者の場合、「このユースケースではAPIからnot found
エラーが返ってきてもエラーとせず単に空白表示にするが、あのユースケースの場合APIからnot found
エラーが返ってきた場合はエラーとして扱う」みたいな「ユースケースに応じたロジック」をモデル層に隠蔽することができます。(ユースケース依存のロジックなので、この分岐ロジックはアプリケーションサービス的な層に書かれることになるでしょう)
また、前者の場合、「このAPIを叩いた結果によってあのデータ・モデルの状態がこのように変化する」というようなロジックがこれまたプレゼンテーション層に漏れ出してしまう一方、後者の場合「このAPIを叩いて、成功すればこのデータ・モデルがこう変化し、not found
の場合はあのデータ・モデルがこう変化し、想定していないエラーが怒ったらエラー・モデルが変化し」という「アプリケーションの論理的な共同」をモデル層に閉じ込めることができています。
これは、ストリームの発生元がtapイベントであろうとAPI呼び出しであろうと同じことです。モデル層の奥深くや、プレゼンテーション層で発生したイベントを、PDSの境目を超えて一気通貫で扱おうとすると、PDSを成り立たせている「プレゼンテーション層とモデル層のコミュニケーションのルール」から外れてしまい、PDSが崩壊します。
別の例として、UIのイベントとはストリームを繋がなかったけど、非同期でAPIを叩いた結果をObservableで返したものを、そのまま返り値としてプレゼンテーション層まで返して、プレゼンテーション層でsubscribeしてしまう、というのもまた、アンチパターンであると言えるでしょう。あくまで、プレゼンテーション層からはvoidなメソッドを叩いて、APIから返ってきた非同期な値はモデル層内でsubscribeしてハンドルし、データ・モデルやドメイン・モデルに変化を起こす(その変化をプレゼンテーション層で知るためにはモデル層がイベントを発行する)、という形にするべきでしょう。
まとめ
まとめもなにも、「GUIアーキテクチャパターンとしてModel View Whateverを採用した際に、Rxのストリームをプレゼンテーション層からモデル層まで一気通貫でつなげてしまうのはアンチパターンである」が結論なのですが、もっと具体的なところまで話をしてしまうと、Model View WhateverパターンにおいてRxのストリームは以下のように使うのがよいお作法なのではないでしょうか。
- プレゼンテーション層においては、UIイベントのストリームを論理的な意味のあるイベントのストリームへと変換する(たとえばrx.tapイベントのストリームをまとめて「ダブルタップされた」というイベントのストリームに変換するなど)のみに留める
- モデル層においては、Rxが「アプリケーションの論理的な挙動」をうまくモデリングできる部分に自由に使う
- モデル層からプレゼンテーション層へのイベント通知のためにモデルにPublishSubjectやBehaviorSubjectな属性を持たせるのはアリでしょう
Re.Ra.Ku アドベントカレンダー 2016 まとめ
今年もアドベントカレンダーの季節が終わりました。
今年は弊社もアドベントカレンダーに参加しよう、と決めてから、少ないメンバーで担当を回しながら書いてきましたので、せっかくなのでリンクをまとめておきたいと思います。興味のあるタイトルがあれば、ぜひ読んで下さい。
来年はもっとたくさんのメンバーでアドベントカレンダーが実施できるよう、会社もプロダクトも成長させていきたいと思っております。
どうぞよろしく!
労働の理由と労働の環境について - Re.Ra.Ku アドベントカレンダー day 25
リラクアドベントカレンダーの最終日です。丸山です。せっかくなんで、最後はポエムで締めましょう!
わたしは現在リラクでマネージメント職として働いています。自分でもコードを書いているので、かっこよく言えば一応「プレイングマネージャ」という感じでしょうか。ただ、弊社は優秀なプログラマが集まっており、なおかつ今は人数も多くないため、彼らをマネージメントするにあたって困ることはほとんどありません。どちらかというとプロダクトに関わるステークスホルダー間の調整のほうが消耗することが多い、という感じの日々で、人的な部分で悩むことが少なく、本当にメンバーには感謝しています。
さて、「プロダクトに関わるステークホルダー間の調整に消耗する」と書いていますが、これはどちらかと言うと自分の問題で、単にわたしに調整力のようなものが欠けている、というのが問題だと感じています。苦手なことをするのは本当に大変です。今回は、「そんな苦手なことをやってまでなぜこの会社で働くのか」ということについて書いてみたいと思います。
夢のないことを言うようですが、わたしが働く第一の理由は、金です。家族が楽しく生きていくのには金がかかるし、息子の教育にだってお金がかかります。そのためにわたしは労働し、お金を稼いでいます。
でも、それだけでは心のどこかがどんどんすり減っていきます。わたしはわがままで強欲なので、せっかく働くならば以下の楽しみも同時に摂取したいと思ってしまいます。
- 自分の能力が誰かを幸福にしていると信じられる
- 技術的にチャレンジングな環境で働ける
以前自分のブログでも書いたことがありますが、労働というのは基本的に世界への働きかけです。例えば農業は自然を人間にとって都合の良いように改変することにほかなりません。そのように、どんな労働だって多かれ少なかれ「世界を変えている」と思います。そして、どんな労働も、少しは世界を変えている以上、「無垢」ではあり得ないと思っています。世界を変化させることは、だれかを傷つけることとコインの両面のようなものであると思います。だから、だれかの特定の仕事を「あれは不幸を生み出す仕事だ」と断罪することはとてもむずかしいことだとわたしは思います。だれにとって何が不幸であるのか、というのは一面から判断できることではないでしょう。
でも、自分の仕事については、自分で選択することができます。
「仕事だからしょうがないんだよ」と思い、「自分の技術で不幸な人間をたくさん生み出すのはしょうがない」と思いながら仕事をするのか、「自分の技術でこのサービスを作ることで、総量としては幸せが増えるはずだ」と信じながら仕事をするのか、そこには大きな違いがわたしにとってはあります(そこに大きな違いがないひともいると思うし、それはそれでいいと思ってます。あくまで自分の話をしています)。
消耗する仕事も日々のなかにはあるけれど、それでも自分の労働によって生み出すものは世界の幸福の総量を増やせるものであろうと信じて日々労働をすることができることは、わたしにとっては幸福なことです。こう思えるのは、会社が目指す方向と自分の目指す方向が大体一致している、というのが大きな理由ですね。
技術的にも同じような話で、いくら自分の目指す方向と会社の目指す方向が一緒でも、プログラマとしてつまらない仕事しかないならばわたしはそこで働きたくありません。幸い、弊社のプログラマメンバーはみんな私よりもずっと優秀な技術者で、彼らと働くことはわたしにとってかなり大きな刺激になっています。
わたしは、自分の働く理由が「正解」だとは思っていません。というか、働く理由に正しさなんてないと思っています。どんな理由でもいい。第一、わたしの働く理由の第一は「金」ですし。ただ、自分の働く理由と向き合ったときに、その理由にふさわしい場所でふさわしい労働をできているならば、それは幸福のひとつの形であろう、と思っています。
めちゃめちゃスピリチュアルで薄っぺらいような話をしてしまいましたが、せっかくの年末年始、自分が働く理由と向き合って見ました。
みなさんが働く理由はなんですか? 正解なんてないと思いますが、みなさんの働く理由と働く環境がマッチした、幸福な来年になりますように! たとえばとにかく楽に働きたいというひとはとにかく楽な労働ができますように! 労働を一切したくないひとは労働しなくてもよくなれますように(めっちゃ努力が必要そう……)! 良いお年を!!!!
型安全なサンタクロース クリスマス前にコンパイルエラー - Re.Ra.Ku アドベントカレンダー day 24
アドベントカレンダー24日め。本日はクリスマス・イブです! サンタクロースのみなさんは無事にプレゼントを用意できましたでしょうか?
ところで、クリスマスといえば、となりのヤングジャンプなどで連載されている「ブラックナイトパレード」、面白いですね。この作品で初めて知ったのですが、クリスマスは良い子には赤いサンタがやってきてプレゼントをくれる一方、悪い子のところには黒いサンタがやってきて石炭をくれるという文化があるそうですね(なぜ石炭なんだ……)。
今日はせっかくのクリスマス・イブなので、これをソフトウェアとしてScalaでモデリングしてみましょう!
まずは型を定義しよう
さて、まずはサンタクロース、プレゼント、子供の型をそれぞれ定義します。
trait SantaClaus trait Child trait Gift
サンタクロースには赤いサンタと黒いサンタがおり、子供には良い子と悪い子がいます。また、プレゼントにはおもちゃと石炭を用意しておきましょう。
trait SantaClaus class RedSantaClaus extends SantaClaus { override def toString: String = "赤いサンタクロース" } class BlackSantaClaus extends SantaClaus { override def toString: String = "黒いサンタクロース" } trait Gift class Toy extends Gift { override def toString: String = "おもちゃ" } class Coal extends Gift { override def toString: String = "石炭" } trait Child class GoodChild extends Child { override def toString: String = "良い子" } class BadChild extends Child { override def toString: String = "悪い子" }
それぞれがモデリングできました。今回はアプリケーションのエントリポイントに、giveGiftというメソッドを生やして、サンタが子供にプレゼントを上げるという行為をモデリングしてみましょう。
object Xmas { def main(args: Array[String]):Unit = println(giveGift(new RedSantaClaus, new GoodChild, new Toy)) def giveGift(s: SantaClaus, c: Child, g: Gift) = { s"${s}が${c}に${g}をあげたよ!" } }
このプログラムを走らせると、「赤いサンタクロースが良い子におもちゃをあげたよ!」が出力されますね。よかった、たかしくんもにっこりです。
しかし、よく考えてみましょう。このプログラムは本当にクリスマスをきちんとモデリングできているでしょうか。もしもmain
メソッドを以下のように書き換えたら?
object Xmas { def main(args: Array[String]):Unit = println(giveGift(new RedSantaClaus, new GoodChild, new Coal)) // !!!! def giveGift(s: SantaClaus, c: Child, g: Gift) = { s"${s}が${c}に${g}をあげたよ!" } }
「赤いサンタクロースが良い子に石炭をあげたよ!」と出力されました。これではせっかく良い子に過ごしていたたかしくんが可愛そうです。このような事故をなんとかして防ぐことはできないでしょうか……。
このような悲しい事故を防ぐためには、giveGift
の引数の型の組み合わせを制限してあげればよさそうです。Scalaではそのようなときに型クラスを利用することで型に制限をつけることができます。
scalaでの型クラスについては手前味噌ですがScala の implicit parameter は型クラスの一種とはどういうことなのかを参照してください。
まずは制約のためにTypeClassedXmas
という型クラスを定義し、「赤いサンタと良い子とおもちゃ」の組み合わせ、「黒いサンタと悪い子と石炭」の組み合わせをそれぞれ型インスタンスにしましょう。
object Xmas { trait TypeClassedXmas[S <: SantaClaus, C <: Child , G <: Gift] implicit object GoodChildXmas extends TypeClassedXmas[RedSantaClaus, GoodChild, Toy] implicit object BadChildXmas extends TypeClassedXmas[BlackSantaClaus, BadChild, Coal] ... }
そして、giveGift
メソッドは型パラメータを取りimplicit parameterでこの型クラスを受け取るようにします。
def giveGift[S <: SantaClaus, C <: Child, G <: Gift](s: S, c: C, g: G)(implicit ev: TypeClassedXmas[S, C, G]): String = { s"${s}が${c}に${g}をあげたよ!" }
さて、これでgiveGift
の引数は「赤いサンタと良い子とおもちゃ」の組み合わせか「黒いサンタと悪い子と石炭」の組み合わせ以外を受け取ったときにコンパイルエラーとなるようになりました!
object Xmas { trait TypeClassedXmas[S <: SantaClaus, C <: Child , G <: Gift] implicit object GoodChildXmas extends TypeClassedXmas[RedSantaClaus, GoodChild, Toy] implicit object BadChildXmas extends TypeClassedXmas[BlackSantaClaus, BadChild, Coal] def main(args: Array[String]):Unit = { println(giveGift(new RedSantaClaus, new GoodChild, new Toy)) println(giveGift(new BlackSantaClaus, new BadChild, new Coal)) // println(giveGift(new BlackSantaClaus, new GoodChild, new Coal)) -> コンパイルエラー!!! } def giveGift[S <: SantaClaus, C <: Child, G <: Gift](s: S, c: C, g: G)(implicit ev: TypeClassedXmas[S, C, G]): String = { s"${s}が${c}に${p}をあげたよ!" } }
これであわてんぼうの黒いサンタも赤いサンタも、間違えたプレゼントを間違えた子供にプレゼントしようと思ったら、クリスマス実行前にコンパイルエラーになるようになりましたね!
今年もすべての子どもたちが幸せになれるクリスマスだと良いですね。それでは、We wish you a merry Xmas and a happy New Year!