※この記事は、まとめ記事「多人数によるRailsアプリケーション開発」の1項目にする予定です。
要旨
Railsアプリケーションでの可読性、引き継ぎや改修のしやすさについてあれこれ考えているのですが、1つには「コードの共通化」に問題があると感じています。
これから作るRailsアプリケーションでは、次のガイドラインを設けたい。
- コードの共通化のために親クラスやモジュールを書くのは禁止。
- コードを共通化するときは、サービスクラスを呼び出す形にする。
- いきなりコードを共通化せずに、立ち止まってよく考える。重複を放置することも選択肢に入れる。
コードの共通化の害
継承の害についてはいろんな人がよい記事を書いていますので、「継承 アンチパターン」とか「inheritance antipattern」でググってください。
次の動画もどうぞ。
私は、次の記事からヒントを得ています。「下手な抽象化よりも重複のほうがまし」というものです。
私が体験した範囲では次のようなことがありました(犯人は私であったりします)。
- 先人がよかれと思って作った共通コードを誰も再利用しない。
- 先人がよかれと思って作った共通コードの出来の悪さに苦しめられる。
- 共通コードに次々にifやオプションが生えていき、奇怪なコードになる(上記の動画やThe Wrong Abstractionを参照)。
コードを共通化したいときは
サービスクラスを使う
次のように別のクラスに似たような機能を見つけて、コードを共通化したくなったとします。
class Dog
def save
# 長大な似たようなコード
end
end
class Cat
def save
# 長大な似たようなコード
end
end
親クラスやモジュールを作るよりも、まずサービスクラスを呼び出す形にすることを考えます。
class Dog
def save
serive = AnimalSaver.new(self)
service.perform
end
end
class Cat
def save
serive = AnimalSaver.new(self)
service.perform
end
end
クラスメソッドを呼び出す形にしてもよいです。
class Dog
def save
AnimalSaver.perform(self)
end
end
コピペして修正する
その次にChickenクラスを加えるとします。DogやCatでやることとは少しでも違うことがあれば、AnimalSaverにifやオプションを生やすよりも、コピペして新しいクラスを修正することを検討します。
class Chicken
def save
serive = ChickenSaver.new(self)
service.perform
end
end
放置する
重複を放置することで可読性や引き継ぎ、改修の容易さが上回るなら、共通化より優先するべきです。
次の例でNamableモジュールが作りたくなったりしたら、こだわり過ぎです(というか、DRY原則について勘違いがある気がします)。
class Client
def full_name
"#{family_name} #{given_name}"
end
end
class Customer
def full_name
"#{family_name} #{given_name}"
end
end
継承やMix-inを使ってもいい場合
次の場合は、継承やMix-inを使ってもよいです。
- 継承やMix-inを前提としたライブラリを使うとき。Railsのコントローラやモデル、ActiveModel::Modelなど。
- app/helpers 下でのテンプレート用のメソッド。
- どうしても継承やMix-inでないとうまく書けないもの。
- クラスのコードが長大化したのでモジュールに分けるのは、まあ、ありかなあと思うけどどうでしょう。
実際にあった例
ここは思い付いたら追加します。
不要なインターフェイス
このシステムを引き継いだときにはAnimalはCatしかいませんでしたし、これから増える見込みもありません。実装の予定がない設計は混乱の元です。
module AnimalInterface
def say
raise NotImplementedError
end
end
class Cat
include AnimalInterface
def say
puts "meow"
end
end
クラス内モジュールの使い回し
これは自分で何度かやってしまいました、すいません。わかりにく過ぎるので、こういうの書いてはいけません。たぶんクラスの組み立てからやり直すべき。
class CatUploader
# かぶってるからモジュールにしてしまえ!
module Common
def s3_key
"#{Rails.env}/cats/#{cat.id}"
end
end
include Common
end
class CatDownloader
include CatUploader::Common
end
思い付いたこと
継承は、言語の入門書には必ず載っているので「使わなければならない機能」のようなイメージがあります。
現実の継承が前提にしているのは、まず設計をして(ドキュメントを作って)、それから開発に取り掛かる、というプロジェクトなんじゃないだろうか。サクッと作って後からガンガン直す、というプロジェクトには不向きなのではと思います。
参考文献:
『Effective Java 第2版』 ジョシュア・ブロック、2001年、P80-90