2009年2月5日木曜日

[37signals] クラス 対 モジュール

(原文: Models vs. Modules)

アプリケーションが生き物のように成長していくにつれて、機能というものは汚れた斜面を転げ落ちる雪だるまのように膨れあがる。開発を進める際のちょっとした気遣いを忘れさえしなければ、これは良い事(実際、この雪だるまというやつは人の手であちらこちらへと方向を変えられるのを喜ぶものだからね)なのだが、時として膨張をストップし・考え直し・若干のリファクタリングを行う必要が出てくる。当社の製品全般に関してもそうだった(それこそ何度もね)

つい最近のことなんだが、人物のアバターと企業のロゴに関わるBasecamp のコードについてクリーンナップおよび最適化に取り組んでいた。この進化する雪だるま(少なくともアバター機能に関してはそんな感じだった)は以下のような経緯で大きくなった
  • 当初、人物のアバターは無かった
  • 後日、その機能を追加した。Person クラスに avatar?・avatar_path・attach_avatar といったようなメソッドを追加する事でアバター機能を提示するというものだった。最終的に7つ程の追加メソッドがPerson クラスに追加された。
  • その後、David が来て、こういった新しいメソッドを Person クラスから抜き出してモジュールにまとめた。彼は実にいい仕事してくれた。というのも、本来の Person クラス定義を Avatar 機能で汚さずに済むようになったし、Avatar 関連のコードも分離出来たからだ。
こういった経緯で今日までやってきた。メソッドはモジュールに移し、機能が必要になった時はベースクラスにモジュールをインクルードするだけで済むようにするというパターンに従って来たので、コードは今でも実にクリーンかつ包括的だ。

とは言うものの、このアプローチには2つ問題がある。第1に、クラスに多くのモジュールをインクルード(我々のうちにもそうしているのが数人いる)すると、個々のメソッドがどのモジュールで定義されているかを調べるのが困難になり、あげくの果てにはどのメソッドが定義済なのかを調べることすら困難になる。メソッド定義部分を探す際に混乱を招くし(さらに悪い事に)同一名称のメソッドを二重あるいはそれ以上に重複定義出来てしまうので、これはよろしくない。複数モジュールをインクルードする際に、同一名称のメソッドがあっても警告は出ないので、あるメソッドを同一名称を持つ他のメソッドで上書きすることでバグが発生、といったことが容易く起こる。名前の衝突を避ける為に、メソッドの名前空間を重複(Avatar モジュールのメソッド名には avatar_ と接頭辞を付けるといったような)しなければならなくなるというハメに陥るのだ。

(誤解無きよう。モジュールは素晴らしい。私たちは山のようにモジュールを使っているし、愛着もある。ここでは、モジュールにも弱点はあるのだということを知る手助けになれば、という観点から論じている)

モジュールによるアプローチでは更に厄介な事がある。違う種類のアバター(個人のアバターに対する、グループのアバターとか企業のアバターが例としてあげられる)が出てきたとしたらどうする? Ruby ではモジュールからサブクラスを派生させる事は出来ないので別の手(モジュールにモジュールをインクルードするといったような)を捜し出さねばならなくなるのがオチだ。それに、異なるアバターをそれぞれ異なるエンティティ(実体)として参照する手段が存在しないので、代替手段として、アバターを取り扱うコントローラはアバターが所属するエンティティを取り扱う形にする必要がある。

こういった問題について打つ手はあるのか? 結局のところ、アバターは人に関する属性定義表である Person クラスの一部分であることを免れ得ない。どうにかしてアバター関連のコードを分離することが出来ないか?

出来る。Avatarを独自のクラスにし(モジュールのインクルードやクラス継承によってでなく)ActiveRecordのアグリゲーションによってPersonクラスに取り込む。例を挙げよう。
class Avatar
attr_reader :master

def initialize(master)
@master = master
end

def exists?
master.has_avatar?
end

def create(file)
master.update_attribute :has_avatar, true
# thumbnail and install the given avatar
end

def destroy
master.update_attribute :has_avatar, false
# delete avatar file
end

# etc.
end

class Person < ActiveRecord::Base
# ...

def avatar
@avatar ||= Avatar.new(self)
end
end
こうすることで得られる利点は何か? 私見ではあるがいくつか挙げてみよう:
  • "person.avatar.exists?" や "person.avatar.create(io)" といった書き方が出来るようになる
  • 異なる種類のアバターをサポートするために Avatar クラスを継承・拡張出来る
  • Person クラスから独立した形で Avatar クラスのテストが行える
  • Avatar クラスのメソッドが Person クラスのメソッドを隠蔽することについて気にかけずに済む
  • "person.avatar.destroy" という記述において、"destroy" メソッドの定義がどこにあるかがより明確になる
さらに言うならば、avatar そのものについても person から分離・移転することが可能だ。
class AvatarsController < ApplicationController
before_filter :find_avatar

def create
@avatar.create(params[:avatar])
end

def destroy
@avatar.destroy
end

protected

def find_avatar
person = Person.find(params[:id])
@avatar = person.avatar
end
end
モジュールの利用を止めてこのパターンを絶対に使えと勧めるつもりはない。上で述べたように、モジュールは実に手軽であり、私達もあいかわらず広範にわたって利用している。だがもし諸君がある特定の事例においてモジュールの持つ制限事項にぶち当たっていると気付いた場合は、問題になっているモジュールをクラスに抜き出すと、事が実に有利に運ぶということもあるのだ。


訳者コメント:
model == class として訳出しています。文脈から見て model == data model であり、結局のところ class Person(models.Model): なのだから意味は通ると思われましたし、「ベースモデル」「派生モデル」で通じるのか?という疑問もありまして。

Ruby のモジュールに関しては、インスタンス化しなくても使えるメソッドという点から、Python の import みたいなもんかという程度の理解しかしていなかったのですが、こういったクラス継承におけるセマンティクスとファンクションの不整合を解決する際に有効なのだなと、Ruby素人である私は認識しました。

ちなみに私は「アバター」という表記に抵抗を感じる者の一人であります。「アバタールだろ!」派であります。MOOGはいまだに「ムーグ」と読んでおり、年がばれるというものです。今回は変節してアバターとしましたが、これではバカタール師に顔向けが出来ません。イエスの事を知らぬと言ったペテロの悔悟を身にしみて感じる私でありましたw

0 件のコメント:

コメントを投稿