Rubyの継承とMixin


127

Rubyでは、複数のミックスインを含めることができますが、拡張するクラスは1つだけであるため、継承よりもミックスインの方が好ましいようです。

私の質問:役立つように拡張/インクルードする必要があるコードを書いているなら、なぜそれをクラスにするのでしょうか?または別の言い方をすると、なぜそれを常にモジュールにしないのですか?

クラスが必要な理由は1つだけ考えられます。それは、クラスをインスタンス化する必要がある場合です。ただし、ActiveRecord :: Baseの場合は、直接インスタンス化することはありません。代わりに、それはモジュールではないのでしょうか?

回答:


176

私はちょうどこのトピックについて読むきちんと接地Rubyist(方法によって素晴らしい本、)。著者は私よりも説明が上手なので、引用します。


単一のルールまたは式が常に正しい設計になるわけではありません。ただし、クラス対モジュールの決定を行うときは、いくつかの考慮事項を覚えておくと便利です。

  • モジュールにはインスタンスがありません。したがって、エンティティまたはモノは一般にクラスで最もよくモデル化され、エンティティまたはモノの特性またはプロパティはモジュールでカプセル化するのが最適です。これに対応して、セクション4.1.1で述べたように、クラス名は名詞である傾向がありますが、モジュール名は形容詞であることがよくあります(StackとStacklike)。

  • クラスに含めることができるスーパークラスは1つだけですが、必要な数のモジュールを混在させることができます。継承を使用している場合は、賢明なスーパークラス/サブクラスの関係を作成することを優先してください。クラスの唯一のスーパークラスの関係を使用して、クラスにいくつかの特性セットの1つにすぎないことが判明する可能性があるものを使用しないでください。

これらのルールを1つの例にまとめると、次のことはできません。

module Vehicle 
... 
class SelfPropelling 
... 
class Truck < SelfPropelling 
  include Vehicle 
... 

むしろ、これを行う必要があります。

module SelfPropelling 
... 
class Vehicle 
  include SelfPropelling 
... 
class Truck < Vehicle 
... 

2番目のバージョンは、エンティティとプロパティをより適切にモデル化します。トラックはビークルから降りてきますが(これは理にかなっています)、SelfPropellingはビークルの特性(少なくとも、この世界のモデルで気になるものすべて)です。これは、トラックが子孫であることによってトラックに渡される特性です。または車両の特殊な形式。


1
例では、それがきちんと示されています-トラックは車両です-車両にはならないトラックはありません。
PL J

1
例はそれをきちんと示しています-IS TruckA- VehicleそれはないTruckだろうことはありませんVehicle。しかし、私はおそらくモジュールを呼び出しますSelfPropelable(:?)うーんSelfPropeled正しいですが、それはほとんど同じです:D。とにかく、私はそれを含めませんが、含まれてVehicleいないTruck車両がありSelfPropeledます。また、尋ねるのも良い指標です-存在する車両ではなく、他のものがありSelfPropeledますか -そうかもしれませんが、私は見つけるのが難しいでしょう。したがってVehicle、クラスSelfPropellingから継承できます(クラスとしては適合しませんSelfPropeled-それはより多くの役割であるため)
PL J

39

ミックスインは素晴らしいアイデアだと思いますが、名前空間の衝突という別の問題があります。考慮してください:

module A
  HELLO = "hi"
  def sayhi
    puts HELLO
  end
end

module B
  HELLO = "you stink"
  def sayhi
    puts HELLO
  end
end

class C
  include A
  include B
end

c = C.new
c.sayhi

どちらが勝ちますか?Rubyの場合、後者を使用していることがわかりmodule Bますmodule A。これで、この問題を回避するのは簡単です。すべてのmodule Aおよびmodule Bの定数とメソッドが、ありそうもない名前空間にあることを確認してください。問題は、衝突が発生してもコンパイラーが警告をまったく出さないことです。

この振る舞いはプログラマーの大規模なチームにまでは拡大しないと私は主張します-実装する人class Cがスコープ内のすべての名前を知っていると想定すべきではありません。Rubyでは、異なるタイプの定数またはメソッドをオーバーライドすることもできます。これが正しい動作と見なされるかどうかはわかりません。


2
これは賢明な注意です。C ++の多重継承の落とし穴を連想させます。
クリストンキンソン2014年

1
これに対する良い緩和策はありますか?これは、Pythonの多重継承が優れたソリューションである理由のように見えます(言語のp * ssingの一致を開始するのではなく、この特定の機能を比較するだけです)。
Marcin 2015

1
@bazzそれは素晴らしいことですが、ほとんどの言語での構成は面倒です。また、主にアヒル型の言語に関連しています。また、奇妙な状態にならないことも保証されません。
Marcin、2016年

古い投稿ですが、まだ検索で判明しています。答えは部分的に正しくありません。Rubyが定数を混同するためではなく、Rubyが定数を近い方から遠い方に解決するため、C#sayhi出力B::HELLOはそうです。したがって、でHELLO参照されるBと、常にに解決されB::HELLOます。これは、クラスCが独自に定義した場合でも当てはまりC::HELLOます。
Laas、

13

私の見解:モジュールは動作を共有するためのものであり、クラスはオブジェクト間の関係をモデル化するためのものです。技術的には、すべてをオブジェクトのインスタンスにして、必要な動作のセットを取得したいモジュールに混在させることができますが、これは貧弱で無計画で、かなり読みにくいデザインになります。


2
これは直接的な方法で質問に答えます。継承は、プロジェクトを読みやすくする特定の組織構造を適用します。
エメリー

10

あなたの質問への答えは、主に文脈に依存しています。pubbの観察を蒸留して、選択は主に検討中のドメインによって駆動されます。

そして、はい、ActiveRecordは、サブクラスによって拡張されるのではなく、組み込まれている必要があります。別のORM-データマッパー -は正確にそれを実現します!


4

私はAndy Gaskellの答えがとても好きです-はい、それを追加したいだけです。ActiveRecordは継承を使用するのではなく、モデル/クラスに動作(主に永続化)を追加するモジュールを含める必要があります。ActiveRecordは単に間違ったパラダイムを使用しています。

同じ理由で、MongoMapperよりもMongoIdが非常に気に入っています。問題のドメインで意味のあるものをモデル化する方法として継承を使用する機会が開発者に残るためです。

Railsコミュニティのだれも、「Rubyの継承」を本来の方法で使用することはほとんどありません。動作を追加するだけでなく、クラス階層を定義するためでもありません。


1

私がミックスインを理解する最良の方法は、仮想クラスとしてです。ミックスインは、クラスまたはモジュールの祖先チェーンに挿入された「仮想クラス」です。

「include」を使用してモジュールを渡すと、継承元のクラスの直前の祖先チェーンにモジュールが追加されます。

class Parent
end 

module M
end

class Child < Parent
  include M
end

Child.ancestors
 => [Child, M, Parent, Object ...

Rubyのすべてのオブジェクトにもシングルトンクラスがあります。このシングルトンクラスに追加されたメソッドは、オブジェクトで直接呼び出すことができるため、「クラス」メソッドとして機能します。オブジェクトで "extend"を使用してオブジェクトをモジュールに渡すと、モジュールのメソッドがオブジェクトのシングルトンクラスに追加されます。

module M
  def m
    puts 'm'
  end
end

class Test
end

Test.extend M
Test.m

シングルトンクラスにアクセスするには、singleton_classメソッドを使用します。

Test.singleton_class.ancestors
 => [#<Class:Test>, M, #<Class:Object>, ...

Rubyは、クラス/モジュールに混合されるときに、モジュールにいくつかのフックを提供します。includedRubyが提供するフックメソッドで、モジュールをモジュールまたはクラスに含めると呼び出されます。含まれているように、extended拡張のためのフックが関連付けられています。モジュールが別のモジュールまたはクラスによって拡張されたときに呼び出されます。

module M
  def self.included(target)
    puts "included into #{target}"
  end

  def self.extended(target)
    puts "extended into #{target}"
  end
end

class MyClass
  include M
end

class MyClass2
  extend M
end

これにより、開発者が使用できる興味深いパターンが作成されます。

module M
  def self.included(target)
    target.send(:include, InstanceMethods)
    target.extend ClassMethods
    target.class_eval do
      a_class_method
    end
  end

  module InstanceMethods
    def an_instance_method
    end
  end

  module ClassMethods
    def a_class_method
      puts "a_class_method called"
    end
  end
end

class MyClass
  include M
  # a_class_method called
end

ご覧のとおり、この単一のモジュールはインスタンスメソッド、「クラス」メソッドを追加し、ターゲットクラスに直接作用しています(この場合はa_class_method()を呼び出しています)。

ActiveSupport :: Concernはこのパターンをカプセル化します。ActiveSupport :: Concernを使用するように書き換えられた同じモジュールを次に示します。

module M
  extend ActiveSupport::Concern

  included do
    a_class_method
  end

  def an_instance_method
  end

  module ClassMethods
    def a_class_method
      puts "a_class_method called"
    end
  end
end

-1

今、templateデザインパターンについて考えています。それはモジュールでは正しくないと思います。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.