Help us understand the problem. What is going on with this article?

Rails: 継承とMix-inは使わないこと〜コードの共通化について

※この記事は、まとめ記事「多人数による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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした