herança de rubi vs mixins

127

No Ruby, como você pode incluir vários mixins, mas estender apenas uma classe, parece que os mixins seriam preferidos à herança.

Minha pergunta: se você está escrevendo um código que deve ser estendido / incluído para ser útil, por que você o tornaria uma classe? Ou, dito de outra maneira, por que você nunca o transformava em um módulo?

Só consigo pensar em uma razão pela qual você deseja uma aula, e é se precisar instanciar a aula. No caso de ActiveRecord :: Base, no entanto, você nunca a instancia diretamente. Portanto, não deveria ter sido um módulo?

Brad Cupit
fonte

Respostas:

176

Eu apenas li sobre esse assunto em O Rubyist bem fundamentada (grande livro, por sinal). O autor faz um trabalho melhor de explicar do que eu faria, então vou citá-lo:


Nenhuma regra ou fórmula única sempre resulta no design certo. Mas é útil ter em mente algumas considerações ao tomar decisões de classe versus módulo:

  • Módulos não têm instâncias. Segue-se que entidades ou coisas geralmente são melhor modeladas em classes, e características ou propriedades de entidades ou coisas são melhor encapsuladas em módulos. Da mesma forma, como observado na seção 4.1.1, os nomes de classes tendem a ser substantivos, enquanto os nomes de módulos são frequentemente adjetivos (Stack versus Stacklike).

  • Uma classe pode ter apenas uma superclasse, mas pode misturar quantos módulos desejar. Se você estiver usando herança, dê prioridade à criação de um relacionamento sensato de superclasse / subclasse. Não use o único e único relacionamento de superclasse de uma classe para dotar a classe do que pode vir a ser apenas um dos vários conjuntos de características.

Resumindo essas regras em um exemplo, eis o que você não deve fazer:

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

Em vez disso, você deve fazer o seguinte:

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

A segunda versão modela as entidades e propriedades com muito mais cuidado. O caminhão desce do veículo (o que faz sentido), enquanto a autopropulsão é uma característica dos veículos (pelo menos, todos aqueles com quem nos preocupamos neste modelo do mundo) - uma característica que é repassada aos caminhões em virtude de o caminhão ser descendente, ou forma especializada, de veículo.

Andy Gaskell
fonte
1
O exemplo mostra perfeitamente - o caminhão é um veículo - não há caminhão que não seria um veículo.
PL J
1
O exemplo mostra perfeitamente - TruckIS A Vehicle- não há Truckque não seria a Vehicle. No entanto, eu chamaria o módulo talvez SelfPropelable(:?) Hmm SelfPropeledparece certo, mas é quase o mesmo: D. De qualquer forma, eu não o incluiria, Vehiclemas sim Truck- como existem veículos que não são SelfPropeled. Também é uma boa indicação a perguntar - existem outras coisas, NÃO veículos que SÃO SelfPropeled? - Bem, talvez, mas seria mais difícil de encontrar. Assim, Vehiclepoderia herdar da classe SelfPropelling (como classe, não caberia como SelfPropeled- pois isso é mais importante) #
PL PL
39

Eu acho que mixins são uma ótima idéia, mas há outro problema aqui que ninguém mencionou: colisões de namespace. Considerar:

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

Qual deles vence? No Ruby, verifica-se o último module B, porque você o incluiu depois module A. Agora, é fácil de evitar este problema: certifique-se de todos module Ae module B's constantes e métodos estão em namespaces improváveis. O problema é que o compilador não avisa quando ocorrem colisões.

Argumento que esse comportamento não se ajusta a grandes equipes de programadores - você não deve assumir que a pessoa que implementa class Cconhece todos os nomes no escopo. O Ruby até permite que você substitua uma constante ou método de um tipo diferente . Eu não tenho certeza de que poderia nunca ser considerado um comportamento correto.

Dan Barowy
fonte
2
Esta é uma sábia palavra de cautela. Remanescente das múltiplas armadilhas da herança em C ++.
precisa saber é o seguinte
1
Existe alguma boa mitigação para isso? Isso parece um motivo pelo qual a herança múltipla do Python é uma solução superior (sem tentar iniciar uma correspondência p * ssing de linguagem; apenas comparando esse recurso específico).
Marcin
1
@bazz Isso é ótimo e tudo, mas a composição na maioria dos idiomas é complicada. Também é principalmente relevante em idiomas tipificados por patos. Também não garante que você não tenha estados estranhos.
Marcin
Post antigo, eu sei, mas ainda aparece em pesquisas. A resposta é parcialmente incorreta - C#sayhigera B::HELLOnão porque o Ruby mistura as constantes, mas porque o ruby ​​resolve as constantes do mais próximo para o mais distante -, então as HELLOreferências mencionadas em Bsempre resolveriam B::HELLO. Isso vale mesmo se a classe C definiu a sua C::HELLOtambém.
Laas
13

Minha opinião: os módulos são para compartilhar comportamentos, enquanto as classes são para modelar relacionamentos entre objetos. Tecnicamente, você pode transformar tudo em uma instância do Object e misturar os módulos em que deseja obter o conjunto desejado de comportamentos, mas isso seria um design pobre, aleatório e bastante ilegível.

Mandril
fonte
2
Isso responde à pergunta de maneira direta: a herança impõe uma estrutura organizacional específica que pode tornar seu projeto mais legível.
esmeril
10

A resposta para sua pergunta é amplamente contextual. Destilando a observação do pubb, a escolha é impulsionada principalmente pelo domínio em consideração.

E sim, o ActiveRecord deveria ter sido incluído em vez de estendido por uma subclasse. Outro ORM - datamapper - consegue exatamente isso!

nareshb
fonte
4

Eu gosto muito da resposta de Andy Gaskell - só queria acrescentar que sim, o ActiveRecord não deve usar herança, mas incluir um módulo para adicionar o comportamento (principalmente persistência) a um modelo / classe. O ActiveRecord está simplesmente usando o paradigma errado.

Pelo mesmo motivo, eu gosto muito do MongoId sobre o MongoMapper, porque deixa ao desenvolvedor a chance de usar a herança como uma maneira de modelar algo significativo no domínio do problema.

É triste que praticamente ninguém na comunidade Rails esteja usando "herança Ruby" da maneira que deveria ser usada - para definir hierarquias de classes, não apenas para adicionar comportamento.

Tilo
fonte
1

A melhor maneira de entender mixins é como classes virtuais. Mixins são "classes virtuais" que foram injetadas na cadeia ancestral de uma classe ou módulo.

Quando usamos "include" e passamos a ele um módulo, ele adiciona o módulo à cadeia ancestral logo antes da classe da qual estamos herdando:

class Parent
end 

module M
end

class Child < Parent
  include M
end

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

Todo objeto no Ruby também tem uma classe singleton. Os métodos adicionados a essa classe singleton podem ser chamados diretamente no objeto e, portanto, agem como métodos de "classe". Quando usamos "estender" em um objeto e passamos ao objeto um módulo, estamos adicionando os métodos do módulo à classe singleton do objeto:

module M
  def m
    puts 'm'
  end
end

class Test
end

Test.extend M
Test.m

Podemos acessar a classe singleton com o método singleton_class:

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

O Ruby fornece alguns ganchos para os módulos quando eles estão sendo misturados em classes / módulos. includedé um método de gancho fornecido pelo Ruby que é chamado sempre que você inclui um módulo em algum módulo ou classe. Assim como incluído, há um extendedgancho associado para extensão. Será chamado quando um módulo for estendido por outro módulo ou classe.

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

Isso cria um padrão interessante que os desenvolvedores poderiam usar:

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

Como você pode ver, este módulo único está adicionando métodos de instância, métodos "class" e agindo diretamente na classe de destino (chamando a_class_method () neste caso).

ActiveSupport :: Concern encapsula esse padrão. Aqui está o mesmo módulo reescrito para usar o 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
Donato
fonte
-1

No momento, estou pensando no templatepadrão de design. Simplesmente não pareceria certo com um módulo.

Geo
fonte