クライアントはいつまでサーバーを意識するべきか ~設計議論の過程~

この記事は、クライアント・サーバー間でのデータ受け渡しに伴い、クライアントがどのレイヤーまでサーバーを意識した設計にするか、という話をしたときににじみ出たお気持ちの記録。

多少読みにくいが、リアルに会話するとこんな感じになる。

揉めがちな話題

サーバー・クライアント間でデータの受け渡しを行うとき、単純にRESTAPIからJSONを渡すのではなくて、RPCのサーバーからProtocol Bufferを経由して送受信する場合、受け取ったRPCのオブジェクトを、なんとかして束になった状態から、細かく切ってクライアントのデータクラスに変換する必要がある

その際、以下のテーマで議論が勃発する

  • 極力クライアントはサーバーのことを意識したくないが、それはどのレイヤーまでなのか
  • サーバーにリクエストを投げるに当たってある程度の量のデータを加工してあげる必要があるが、どのレイヤーでそれをやるのか
  • サーバーから受け取ったデータを、クライアント用のデータとして扱い出すのはいつなのか

この話の前提条件

僕が出くわした事案(スマホゲームのクライアントサイド開発時)では、以下のような前提条件があった。

リクエスト時のモデルの取り扱い

基本的にRPCでリクエストを投げる際、様々なモデルを操作することになり、個別にロジックを持たせると「お前は何をしているんだ」「結果得たものは」というのが見えなくなる。

  • サーバーにリクエストを投げるに当たってある程度の量のデータを加工してあげる必要があるが、どのレイヤーでそれをやるのか

という問題。

なので、Facadeパターンが採用され、「このメソッドを経由すれば諸々一括で処理をして、リクエスト投げたりクライアントで完結する計算処理を踏めるぞ」という状態だった。普通web系だとservicesとして作るやつ。語彙が散らばるので、以下サービス層と呼ぶ。

通信処理+データ変換クラスの存在

上記のサービス層があっても、クライアントから通信するのは別のクラスに切り出さないともちろんダメ。クライアント側のモデルオブジェクトをRPCのオブジェクトに変換する、ないしレスポンスで受け取ったデータをクライアント側のデータクラスに詰めて、サービス層にコールバックで返す、みたいな処理もあるとして、それは通信処理+データ変換をしてくれるエージェント的なクラスがあった。

ということで、データの流れとしては、

View -> Controller -> Service(背後に複数のModel) -> 通信処理+ データ変換(Protocol Buffer RPC) -> Server

的な感じ。

ぶつかった課題

先ほどの話題のうち、

  • 極力クライアントはサーバーのことを意識したくないが、それはどのレイヤーまでなのか

が当初の議題だった。

もっと言うと、通信処理+データ変換クラスからコールバックで返ってくる値はどこまでそのままの形で伝播すべきなのか、という話だった。

通信処理+データ変換クラスで、ほぼほぼデータの加工は済んでいて、サービス層はそのデータをもとに、クライアントの持つ状態を更新する感じの役割を担ってる。そして、データ自体はControllerやViewに横流しするべく、通信処理+データ変換クラス, サービス層の間で受け渡される値はほぼ同一だった。

でも、コールバックが同じ型っぽくなるの、気持ち悪いし、Controller的なレイヤーまでいらない値を含めて返ってくるのはよろしくないので、なんとかしたいなという話を持ち出した。

通信処理+データ変換とサービス層(サーバーに2番目に近いクラス)はどう棲み分けるべきか

まず根本的な問題はこれがはっきりしていなかった点だ。

直感的には、

  • 通信処理+データ変換クラス = サーバーを意識する
  • サービス層 = クライアントのみを意識する

というのが、出口がはっきりしてて良い。

そもそも何でデータ変換が通信処理と一緒に書かれてるのか?

当初、通信処理+データ変換クラスは、データ変換を担わず、純粋に通信だけを担うのが一番だが、このクラスができた当時、サービス層がまだなかったのと、普段クライアントの情報しか扱わないデータクラスにrpcを意識させるのもな、というので、データ変換っぽい処理は通信処理と一緒のクラスに書かれることとなった。

そして今になって、データ変換の処理も含めて、サービス層にもたせてやり、通信処理クラスは通信に徹してあげたほうがスッキリするよね、リファクタしようという話になった。

ホワイトボードにControllerとサービス層と、通信クラスが書いてあって、ワイワイだの悶々だの、していた。Fatなサービス層になるけど、まあサービス層って総じてFatだよね、しょうがないか。受け取ったrpcレスポンスをそのままサービス層に渡してしまったほうがよさそうという話でひと段落した。

再審議

コードを書き出して、サービス層にデータ変換の処理を移そうとして一瞬でわかったのが、通信処理クラスもぬけの殻みたいになるということだった。

各エンドポイントごとにメソッドがあるけど、データを特にいじらないならコールバック経由でサービス層に横流しするだけの、流しそうめんの竹みたいなクラスになることがわかった。

また、レスポンスをデータクラスに変換するのと同様に、リクエスト時の変換は誰が担うのだろう、これも切り出したらいよいよコールバックしかない、クラスにするほどでもないものができるぞ、となった。

なので、これはさすがに存在意義が揺らぐので再審議にかけることになった。

結果どうなったか

純粋な通信処理クラスを作る -> やっぱ通信処理+データ変換クラスのままで

もともと通信処理+データ変換のデータ変換処理を、サービス層に寄せたいというのは、

  • サーバーから受け取ったデータを、クライアント用のデータとして扱い出すのはいつなのか

というのをはっきりさせたかったから。これは通信処理+データ変換の処理を担うクラスで現行通り行ってもギリギリ担保されるので、そうしましょう、とした。

サービス層はサーバー(からのProtocol Bufferオブジェクト)を(ほぼ)意識しない

サービス層にはコールバックの結果、Protocol bufferのオブジェクトは一切返らないこととなる。若干違和感があるが、根本のrequest/responseの処理は各通信処理の親クラスに実装されてるので、変換処理があってもよしとしようという感じだった。

結果、

  • 極力クライアントがサーバーのことを意識したくないが、それはどのレイヤーまでなのか

は、完全に通信処理+データ変換のクラスに閉じた形になるべき、という結論に至った。

Controller等はどんな値を受け取るか

それに合わせて、各レイヤーのコールバックの引数もちゃんと別個にして、明らかにControllerまで行かせたくない、どでかいモデルオブジェクトとかは、サービス層から返さないようにしようね、という話になった。

右往左往した結果、落ち着きどころが見えたので、includeする関係図を描いてみるかー、昨今と未来はこうだぞ、未来は明るいな、という話をして、昨今と未来の差分を矢印で書くなどしていた。

設計思想

教訓

  • サーバー・クライアント間のデータ受け渡しは議論が勃発しがちだけど、早々にサーバーのことをクライアントに意識させずに済む設計が万事よさそう。
  • データ変換だけを担うConverterクラスを作るべきか、という議論もあったけど、それはこう、エンドポイントごとに管理するのも辛いし、Networkクラスと合わせて二重管理になりますね、というので、そのアイデアはオジャンとなった。二重管理は徹底して避けるのが良さそう。
  • たとえ議論してても誰かしらがコードをその場でリファクタしてみて、あー実際の風景はこうなりますね、というのを話すべきだった。ホワイトボードとPCの画面を並べてみないと、設計の話は手戻りが多くなるので注意しないとな、という想い。