標高+1m

Don't be rational

クラスの使い時

TL;DR

だいたいみんな知ってることだと思います。On Lispの「いつオブジェクトを使うのか」に言いたいことだいたい書いてあります。

背景

前にサーバーサイドMVCって記事で、クラスはせっかくモジュラーな関数をそうじゃなくするから基本的に悪だってちろっと書いた。

でも最近Carrotの型チェッカをCLOS(のサブセット)を使って書き直した時に、主観的なコードのきれいさが劇的に向上したという経験をしていたり、PILOTの人と最近のPHPOOP*1の話をしたりしてて、クラス、もしくは代数的データ型の使い時について、少なくとも自分が則るガイドラインを作りたいと思った。

この記事では、クラスを作るスタイルと、既存のデータ構造と関数を使うスタイルを対立させて考えます。オブジェクト指向と関数型の話ではなくて、例えばRubyでArrayとかHashをそのまま使うスタイルは後者です。関数型の人なら、前者は代数的データ型をどんどん定義していくスタイル、後者はだいたいシノニムで済ませちゃうスタイルと思ってくれればいいです。*2

あと、僕はいつも、でかいハッシュテーブルみたいに明らかにパフォーマンスに影響があるオブジェクト以外は、作って読んで捨てるという風に、実装がミュータブルだとしてもイミュータブルなデータ構造っぽく使うので以下の話もそういう前提があると思ってください。

クラスを作らないということ

上記の記事を書いたときは、特にClojureに傾倒していて、Stuart SierraのThinking in DataとかRich Hickeyのプレゼンとかで説明されてるみたいに、プログラム中の全てのデータを既存のデータ構造(基本型, map, vector, set, sequence, etc)で表現するということをしていた。

この主張の根幹には、昔から知られている格言、"It is better to have 100 functions operate on one data structure than 10 functions on 10 data structures."*3という考えがある。ここで言われている100個の関数がそれのために定義された1つのデータ構造っていうのはLispのリストのことなのだけど、要するになんでもリストで表現すれば100個の関数がそのまま使えるんだから、わざわざ新しくデータ構造(例: クラス)を作ってそのための関数(例: メソッド)を作ったりなんかしなくていいじゃんということ。

利点 - ポータブル

既存のデータ構造でなんでも表現するスタイルは、特にWeb APIみたいにデータをワイヤを越えてやりとりする環境と相性がいいと思う。クライアント<->サーバ<->DB と頻繁に行き来するデータは結局JSONなりなんなりにシリアライズされるわけなので、ステートレスなサーバ側のプログラムでは、わざわざ自分で定義したクラスのインスタンスに落とし込まなくても、JSONをパースしてArrayやらHashやらをそののまま使った方が楽だし、実際そうしてる人も多いと思う。

ワイヤを通ってきたデータ構造は基本的に読むだけだから、状態の管理なんて必要ないって言うのと、 基本的にユーザ定義の型はワイヤに乗らないので、いちいちオブジェクト作ってたら、クラス定義をサーバとクライアントに重複して書かなきゃいけなくなるというのが大きいと思う。*4

欠点 - 識別が面倒

既存のデータ構造をそのまま使った場合、クラス情報がないのでそのデータが何を表現しているのか の管理を自前でやらなきゃいけない。Webの例だと、

  • サーバならどのエンドポイントにどんなデータが来て、どんなデータを返すのかはわかっているし、エンドポイントごとの実行寿命が短い
  • クライアント側でも実行のパスはだいたい決まっていて短い(イベント->状態の更新->描画->ストップ)からどんなデータをとったりやったりするかは簡単に追える

というのがあって、これをやらなきゃいけないことは少ないんだけど、

  • ゲーム: モンスターとかキャラクターみたいにヒエラルキーがあるデータ
  • 言語処理系: ASTみたいにいろんな形のデータが一つのデータ構造に詰まっているデータ(ポリモーフィックなデータ構造)

こういうのを処理するときにmapをそのまま使ってたりすると、結局自分でタグをつけてやってcaseで場合分けしなきゃいけなくなる場合が多い。

クラス、というか適切な型を持ったデータの利点

一方、データに適切なクラス情報がくっついていれば、言語処理系の、クラスによってディスパッチする手続きを選択する機能が使える。ポリモーフィズムってやつです。Cat#talkHuman#talkがあったとして、x talkがどっちを呼ぶかはxのクラスによって決定されるっていうあれです。

つまり、データの種類によって場合分けをしたい時はクラスを作った方が便利ということ。ちょー当たり前ですね。

クラスを作るのが普通になってるけどそうしない方がいい時

上記の場合以外の時。

特にサーバーサイドMVC。特にコントローラはクラスなんか書いてるからファットになるんじゃないの。サーバーサイドMVCでも書いたけど、コントローラはRailsとかCodeIgniterみたいなクラスを使って書くやつじゃなくてSinatraとかCompojureとかExpressみたいに手続きをそれぞれ独立させた方がいい。だいたいコントローラはステートレスなんだから状態を管理する必要ないじゃんて話。

モデルはやりたければクラス作ってもいいと思うけど、結局なんだかんだ言ってDBとの通信が主になるし、あんまり旨味はないと思う。さっきの記事で書いたみたいにWrite系とRead系の名前空間を分けてコントローラではWrite、ビューではReadしか使わないみたいなアクセス制限をかけるとMVCが破綻しづらくていいと思う。

結論とCLOS

今の結論はクラスはディスパッチャとして便利だけどそれ以上ではないってとこ。

そうするとクラスは階層構造だけ、オブジェクトはデータとクラス名だけ持ってればよくて、メソッドは必ずしもオブジェクトにくっついてなくていい。というかくっついてない方がいい。くっついているとselfの参照のせいでクラスからそのままひっぺがせなくなるから = クラスにくっついたメソッドはモジュラーでない。

CLOSではメソッドはクラスではなく総称関数に属していて、selfなんてなくて引数としてオブジェクトをとるからメソッドをどこに置いたっていい。

ということで締めの言葉です。

CLOS最高〜

*1:最近のPHPすごいんすよ。interfaceあるし、abstractあるし、traitあるし、型チェックつきのタイプヒントまであるんすよ。

*2:この記事ではポリモーフィックなデータ型ならADT,違うならシノニムっていうほとんど自明なことを言ってます

*3:意訳: 10個のデータ構造に10個ずつ関数が定義されているより、1つのデータ構造を扱う関数が100個定義されている方が良い

*4:Meteorではサーバとクライアントで同じコードが走るからEJSONていうJSONの亜種を使ってクラスをワイヤにのせてて、これは面白いと思う。