Como implementar uma classe abstrata em ruby?

121

Eu sei que não há conceito de classe abstrata em rubi. Mas se for necessário implementá-lo, como proceder? Eu tentei algo como ...

class A
  def self.new
    raise 'Doh! You are trying to write Java in Ruby!'
  end
end

class B < A
  ...
  ...
end

Mas quando tento instanciar B, ele vai chamar internamente, o A.newque gerará a exceção.

Além disso, os módulos não podem ser instanciados, mas também não podem ser herdados. tornar o novo método privado também não funcionará. Alguma dica?

Chirantan
fonte
1
Os módulos podem ser misturados, mas suponho que você precise de herança clássica por algum outro motivo?
Zach
6
Não que eu precise implementar uma classe abstrata. Eu estava pensando em como fazê-lo, se é que alguém precisa fazê-lo. Um problema de programação. É isso aí.
Chirantan
127
raise "Doh! You are trying to write Java in Ruby".
Andrew Grimm

Respostas:

61

Não gosto de usar classes abstratas em Ruby (quase sempre há uma maneira melhor). Se você realmente acha que é a melhor técnica para a situação, pode usar o seguinte snippet para ser mais declarativo sobre quais métodos são abstratos:

module Abstract
  def abstract_methods(*args)
    args.each do |name|
      class_eval(<<-END, __FILE__, __LINE__)
        def #{name}(*args)
          raise NotImplementedError.new("You must implement #{name}.")
        end
      END
      # important that this END is capitalized, since it marks the end of <<-END
    end
  end
end

require 'rubygems'
require 'rspec'

describe "abstract methods" do
  before(:each) do
    @klass = Class.new do
      extend Abstract

      abstract_methods :foo, :bar
    end
  end

  it "raises NoMethodError" do
    proc {
      @klass.new.foo
    }.should raise_error(NoMethodError)
  end

  it "can be overridden" do
    subclass = Class.new(@klass) do
      def foo
        :overridden
      end
    end

    subclass.new.foo.should == :overridden
  end
end

Basicamente, você apenas chama abstract_methodscom a lista de métodos que são abstratos e, quando eles são chamados por uma instância da classe abstrata, uma NotImplementedErrorexceção será gerada.

nakajima
fonte
Isso é mais como uma interface, na verdade, mas entendi a ideia. Obrigado.
Chirantan 8/10/09
6
Isso não soa como um caso de uso válido, o NotImplementedErrorque significa essencialmente "dependente da plataforma, não disponível no seu". Veja documentos .
Skalee
6
Você está substituindo um método de uma linha pela metaprogramação, agora precisa incluir um mixin e chamar um método. Eu não acho que isso seja mais declarativo.
187 Pascal
1
Eu editei esta resposta, corrigi alguns bugs do código e atualizei-o, então ele agora é executado. Também simplificou o código um pouco, para usar estender ao invés de incluir, devido a: yehudakatz.com/2009/11/12/better-ruby-idioms
Magne
1
@ManishShrivastava: Por favor, veja este código agora, para comentários sobre a importância de usar END aqui vs final
Magne
113

Apenas para adiar aqui tarde, acho que não há razão para impedir alguém de instanciar a classe abstrata, especialmente porque eles podem adicionar métodos rapidamente .

Linguagens de digitação de pato, como Ruby, usam a presença / ausência ou comportamento de métodos em tempo de execução para determinar se devem ser chamados ou não. Portanto, sua pergunta, como se aplica a um método abstrato , faz sentido

def get_db_name
   raise 'this method should be overriden and return the db name'
end

e isso deve ser sobre o fim da história. O único motivo para usar classes abstratas em Java é insistir que certos métodos sejam "preenchidos" enquanto outros têm seu comportamento na classe abstrata. Em uma linguagem de digitação de pato, o foco está nos métodos, não nas classes / tipos; portanto, você deve mover suas preocupações para esse nível.

Na sua pergunta, você está basicamente tentando recriar a abstractpalavra-chave do Java, que é um cheiro de código para fazer Java no Ruby.

Dan Rosenstark
fonte
3
@ Christopher Perry: Alguma razão para isso?
SasQ 03/09
10
@ChristopherPerry Eu ainda não entendi. Por que não quero essa dependência se o pai e o irmão estão relacionados, afinal, e quero que essa relação seja explícita? Além disso, para compor um objeto de alguma classe dentro de outra classe, você também precisa conhecer sua definição. A herança geralmente é implementada como composição, apenas faz com que a interface do objeto composto faça parte da interface da classe que o incorpora. Portanto, você precisa da definição do objeto incorporado ou herdado. Ou talvez você esteja falando de outra coisa? Você pode elaborar um pouco mais sobre isso?
SasQ 03/09
2
@SasQ, você não precisa conhecer os detalhes de implementação da classe pai para compor, apenas a API '. No entanto, se você herdar, depende da implementação dos pais. Se a implementação mudar, seu código poderá ser quebrado de maneiras inesperadas. Mais detalhes aqui
Christopher Perry
16
Desculpe, mas "Favorecer a composição sobre a herança" não diz "Sempre composição do usuário". Embora, em geral, a herança deva ser evitada, há alguns casos de uso em que eles se encaixam melhor. Não siga cegamente o livro.
Nowaker
1
@Nowaker Ponto muito importante. Muitas vezes, tendemos a ser surpreendidos por coisas que lemos ou ouvimos, em vez de pensar "qual é a abordagem pragmática neste caso". Raramente é completamente preto ou branco.
Per Lundberg
44

Tente o seguinte:

class A
  def initialize
    raise 'Doh! You are trying to instantiate an abstract class!'
  end
end

class B < A
  def initialize
  end
end
Andrew Peters
fonte
38
Se você quiser usar super in #initializede B, você pode simplesmente aumentar o que for inicializado em A # if self.class == A.
Mk12
17
class A
  private_class_method :new
end

class B < A
  public_class_method :new
end
bluehavana
fonte
7
Além disso, pode-se usar o gancho herdado da classe pai para tornar o método construtor automaticamente visível em todas as subclasses: def A.inherited (subclasse); subclass.instance_eval {public_class_method: new}; fim
t6d 26/02
1
Muito bom t6d. Como comentário, verifique se está documentado, pois esse é um comportamento surpreendente (viola menos surpresa).
#
16

para qualquer pessoa no mundo do Rails, a implementação de um modelo ActiveRecord como uma classe abstrata é feita com esta declaração no arquivo de modelo:

self.abstract_class = true
Fred Willmore
fonte
12

Meu 2 ¢: opto por uma mixagem DSL simples e leve:

module Abstract
  extend ActiveSupport::Concern

  included do

    # Interface for declaratively indicating that one or more methods are to be
    # treated as abstract methods, only to be implemented in child classes.
    #
    # Arguments:
    # - methods (Symbol or Array) list of method names to be treated as
    #   abstract base methods
    #
    def self.abstract_methods(*methods)
      methods.each do |method_name|

        define_method method_name do
          raise NotImplementedError, 'This is an abstract base method. Implement in your subclass.'
        end

      end
    end

  end

end

# Usage:
class AbstractBaseWidget
  include Abstract
  abstract_methods :widgetify
end

class SpecialWidget < AbstractBaseWidget
end

SpecialWidget.new.widgetify # <= raises NotImplementedError

E, é claro, adicionar outro erro para inicializar a classe base seria trivial nesse caso.

Anthony Navarre
fonte
1
EDIT: Para a boa medida, uma vez que esta abordagem utiliza define_method, pode querer certificar-se backtrace permanece intacta, por exemplo: err = NotImplementedError.new(message); err.set_backtrace caller()YMMV
Anthony Navarra
Acho essa abordagem bastante elegante. Obrigado por sua contribuição para esta pergunta.
precisa saber é o seguinte
12

Nos últimos 6 anos e meio de programação do Ruby, eu não precisei de uma aula abstrata nenhuma vez.

Se você pensa que precisa de uma aula abstrata, está pensando demais em uma linguagem que as forneça / exija, não no Ruby.

Como outros sugeriram, um mixin é mais apropriado para coisas que deveriam ser interfaces (como Java as define), e repensar seu design é mais apropriado para coisas que "precisam" de classes abstratas de outras linguagens como C ++.

Atualização 2019: não preciso de aulas abstratas em Ruby em 16 anos e meio de uso. Tudo o que todas as pessoas comentando sobre a minha resposta estão dizendo é abordado aprendendo Ruby e usando as ferramentas apropriadas, como módulos (que até oferecem implementações comuns). Existem pessoas nas equipes que eu gerenciei que criaram classes com implementação básica que falham (como uma classe abstrata), mas essas são principalmente um desperdício de codificação, porque NoMethodErrorproduziriam exatamente o mesmo resultado que AbstractClassErrorna produção.

Austin Ziegler
fonte
24
Você também não precisa / de uma classe abstrata em Java. É uma maneira de documentar que é uma classe base e não deve ser instanciada por pessoas que estendem sua classe.
Fijiaaron 25/11/2010
3
Na IMO, poucas linguagens devem restringir suas noções de programação orientada a objetos. O que é apropriado em uma determinada situação não deve depender do idioma, a menos que haja um motivo relacionado ao desempenho (ou algo mais atraente).
thekingoftruth
10
@ fijiaaron: Se você pensa assim, definitivamente não entende o que são classes básicas abstratas. Eles não são muito para "documentar" que uma classe não deve ser instanciada (é um efeito colateral de ser abstrata). É mais sobre declarar uma interface comum para várias classes derivadas, o que garante que ela será implementada (caso contrário, a classe derivada permanecerá abstrata também). Seu objetivo é apoiar o Princípio de Substituição de Liskov para classes para as quais a instanciação não faz muito sentido.
SasQ 03/09/13
1
(continuação) É claro que é possível criar apenas várias classes com alguns métodos e propriedades comuns, mas o compilador / intérprete não saberá que essas classes estão relacionadas de alguma forma. Os nomes dos métodos e propriedades podem ser os mesmos em cada uma dessas classes, mas somente isso ainda não significa que eles representam a mesma funcionalidade (a correspondência de nomes pode ser acidental). A única maneira de informar o compilador sobre essa relação é usar uma classe base, mas nem sempre faz sentido que as instâncias dessa classe base existam.
SasQ
4

Pessoalmente, levanto NotImplementedError em métodos de classes abstratas. Mas você pode deixar de fora o método 'novo', pelos motivos mencionados.

Zack
fonte
Mas então como impedir que isso seja instanciado?
Chirantan 04/02/09
Pessoalmente, estou apenas começando o Ruby, mas no Python, as subclasses com métodos declarados __init ___ () não chamam automaticamente os métodos __init __ () de suas superclasses. Eu esperava que houvesse um conceito semelhante em Ruby, mas como eu disse, estou apenas começando.
Zack
No ruby ​​parent, os initializemétodos não são chamados automaticamente, a menos que sejam explicitamente chamados com super.
Mk12
4

Se você deseja ir com uma classe instável, no seu método A.new, verifique se self == A antes de gerar o erro.

Mas, na verdade, um módulo parece mais com o que você deseja aqui - por exemplo, Enumerable é o tipo de coisa que pode ser uma classe abstrata em outros idiomas. Tecnicamente, você não pode subclassificá-los, mas chamar include SomeModuleatinge aproximadamente o mesmo objetivo. Existe algum motivo para isso não funcionar para você?

Mandril
fonte
4

Qual o objetivo que você está tentando servir com uma classe abstrata? Provavelmente existe uma maneira melhor de fazê-lo em rubi, mas você não forneceu detalhes.

Meu ponteiro é esse; use um mixin não herança.

jshen
fonte
De fato. Misturar um módulo seria equivalente a usar uma AbstractClass: wiki.c2.com/?AbstractClass PS: Vamos chamá-los de módulos e não de mixins, pois os módulos são o que são e misturá-los é o que você faz com eles.
Magne
3

Outra resposta:

module Abstract
  def self.append_features(klass)
    # access an object's copy of its class's methods & such
    metaclass = lambda { |obj| class << obj; self ; end }

    metaclass[klass].instance_eval do
      old_new = instance_method(:new)
      undef_method :new

      define_method(:inherited) do |subklass|
        metaclass[subklass].instance_eval do
          define_method(:new, old_new)
        end
      end
    end
  end
end

Isso depende do #method_missing normal para relatar métodos não implementados, mas evita a implementação de classes abstratas (mesmo se elas tiverem um método de inicialização)

class A
  include Abstract
end
class B < A
end

B.new #=> #<B:0x24ea0>
A.new # raises #<NoMethodError: undefined method `new' for A:Class>

Como os outros pôsteres disseram, você provavelmente deveria estar usando um mixin, em vez de uma aula abstrata.

rampion
fonte
3

Eu fiz dessa maneira, por isso redefine o novo na classe filho para encontrar um novo na classe não abstrata. Ainda não vejo nenhuma prática no uso de classes abstratas em ruby.

puts 'test inheritance'
module Abstract
  def new
    throw 'abstract!'
  end
  def inherited(child)
    @abstract = true
    puts 'inherited'
    non_abstract_parent = self.superclass;
    while non_abstract_parent.instance_eval {@abstract}
      non_abstract_parent = non_abstract_parent.superclass
    end
    puts "Non abstract superclass is #{non_abstract_parent}"
    (class << child;self;end).instance_eval do
      define_method :new, non_abstract_parent.method('new')
      # # Or this can be done in this style:
      # define_method :new do |*args,&block|
        # non_abstract_parent.method('new').unbind.bind(self).call(*args,&block)
      # end
    end
  end
end

class AbstractParent
  extend Abstract
  def initialize
    puts 'parent initializer'
  end
end

class Child < AbstractParent
  def initialize
    puts 'child initializer'
    super
  end
end

# AbstractParent.new
puts Child.new

class AbstractChild < AbstractParent
  extend Abstract
end

class Child2 < AbstractChild

end
puts Child2.new
ZeusTheTrueGod
fonte
3

Há também essa pequena abstract_typejóia, que permite declarar classes e módulos abstratos de maneira discreta.

Exemplo (do arquivo README.md ):

class Foo
  include AbstractType

  # Declare abstract instance method
  abstract_method :bar

  # Declare abstract singleton method
  abstract_singleton_method :baz
end

Foo.new  # raises NotImplementedError: Foo is an abstract type
Foo.baz  # raises NotImplementedError: Foo.baz is not implemented

# Subclassing to allow instantiation
class Baz < Foo; end

object = Baz.new
object.bar  # raises NotImplementedError: Baz#bar is not implemented
シ リ ル
fonte
1

Nada de errado com sua abordagem. Gerar um erro na inicialização parece bom, desde que todas as suas subclasses substituam a inicialização, é claro. Mas você não quer se definir.novo assim. Aqui está o que eu faria.

class A
  class AbstractClassInstiationError < RuntimeError; end
  def initialize
    raise AbstractClassInstiationError, "Cannot instantiate this class directly, etc..."
  end
end

Outra abordagem seria colocar toda essa funcionalidade em um módulo, que, como você mencionou, nunca pode ser instanciado. Em seguida, inclua o módulo em suas classes em vez de herdar de outra classe. No entanto, isso quebraria coisas como super.

Portanto, depende de como você deseja estruturá-lo. Embora os módulos pareçam uma solução mais limpa para solucionar o problema de "Como eu escrevo algumas coisas que são dignas para outras classes usarem"

Alex Wayne
fonte
Também não gostaria de fazer isso. As crianças não podem chamar de "super", então.
21760 Austin Ziegler
0

Embora isso não pareça Ruby, você pode fazer o seguinte:

class A
  def initialize
    raise 'abstract class' if self.instance_of?(A)

    puts 'initialized'
  end
end

class B < A
end

Os resultados:

>> A.new
  (rib):2:in `main'
  (rib):2:in `new'
  (rib):3:in `initialize'
RuntimeError: abstract class
>> B.new
initialized
=> #<B:0x00007f80620d8358>
>>
lulalala
fonte