¿Cómo implementar una clase abstracta en ruby?

121

Sé que no existe el concepto de clase abstracta en ruby. Pero si es necesario implementarlo, ¿cómo hacerlo? Intenté algo como ...

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

class B < A
  ...
  ...
end

Pero cuando intento crear una instancia de B, va a llamar internamente A.newcuál va a generar la excepción.

Además, los módulos no se pueden instanciar, pero tampoco se pueden heredar. hacer que el nuevo método sea privado tampoco funcionará. ¿Algún consejo?

Chirantan
fuente
1
Los módulos se pueden mezclar, pero supongo que necesita la herencia clásica por alguna otra razón.
Zach
6
No es que necesite implementar una clase abstracta. Me preguntaba cómo hacerlo, si es que es necesario hacerlo. Un problema de programación. Eso es.
Chirantan
127
raise "Doh! You are trying to write Java in Ruby".
Andrew Grimm

Respuestas:

61

No me gusta usar clases abstractas en Ruby (casi siempre hay una forma mejor). Sin embargo, si realmente cree que es la mejor técnica para la situación, puede usar el siguiente fragmento para ser más explícito sobre qué métodos son abstractos:

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

Básicamente, solo llama abstract_methodscon la lista de métodos que son abstractos, y cuando son llamados por una instancia de la clase abstracta, se NotImplementedErrorgenerará una excepción.

nakajima
fuente
En realidad, esto es más como una interfaz, pero entiendo la idea. Gracias.
Chirantan
6
Esto no suena como un caso de uso válido de lo NotImplementedErrorque esencialmente significa "dependiente de la plataforma, no disponible en la suya". Ver documentos .
skalee
7
Está reemplazando un método de una línea con metaprogramación, ahora necesita incluir un mixin y llamar a un método. No creo que esto sea más declarativo.
Pascal
1
Edité esta respuesta, corrigió algunos errores del código y lo actualicé, por lo que ahora se ejecuta. También se simplificó un poco el código, para usar extender en lugar de incluir, debido a: yehudakatz.com/2009/11/12/better-ruby-idioms
Magne
1
@ManishShrivastava: Vea este código ahora, para el comentario sobre la importancia de usar END aquí vs end
Magne
113

Solo para intervenir tarde aquí, creo que no hay razón para evitar que alguien cree una instancia de la clase abstracta, especialmente porque pueden agregarle métodos sobre la marcha .

Los lenguajes de tipificación de pato, como Ruby, utilizan la presencia / ausencia o el comportamiento de métodos en tiempo de ejecución para determinar si deben llamarse o no. Por lo tanto, su pregunta, ya que se aplica a un método abstracto , tiene sentido.

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

y eso debería ser el final de la historia. La única razón para usar clases abstractas en Java es insistir en que ciertos métodos se "completen" mientras que otros tienen su comportamiento en la clase abstracta. En un lenguaje de tipeo de pato, el enfoque está en los métodos, no en las clases / tipos, por lo que debe mover sus preocupaciones a ese nivel.

En su pregunta, básicamente está tratando de recrear la abstractpalabra clave de Java, que es un olor a código para hacer Java en Ruby.

Dan Rosenstark
fuente
3
@Christopher Perry: ¿Alguna razón?
SasQ
10
@ChristopherPerry Todavía no lo entiendo. ¿Por qué no debería querer esta dependencia si el padre y el hermano están relacionados después de todo y quiero que esta relación sea explícita? Además, para componer un objeto de alguna clase dentro de otra clase, también necesita conocer su definición. La herencia generalmente se implementa como composición, simplemente hace que la interfaz del objeto compuesto sea parte de la interfaz de la clase que lo integra. No obstante, necesita la definición del objeto incrustado o heredado. ¿O quizás estás hablando de otra cosa? ¿Puedes dar más detalles sobre eso?
SasQ
2
@SasQ, no necesita conocer los detalles de implementación de la clase principal para componerla, solo necesita conocer su API. Sin embargo, si hereda, dependerá de la implementación de los padres. Si la implementación cambia, su código podría romperse de formas inesperadas. Más detalles aquí
Christopher Perry
16
Lo sentimos, pero "Favorecer la composición sobre la herencia" no dice "Siempre la composición del usuario". Aunque en general se debe evitar la herencia, hay un par de casos de uso en los que simplemente encajan mejor. No sigas ciegamente el libro.
Nowaker
1
@Nowaker Punto muy importante. Muy a menudo tendemos a quedarnos sorprendidos por lo que leemos o escuchamos, en lugar de pensar "cuál es el enfoque pragmático en este caso". Rara vez es completamente negro o blanco.
Per Lundberg
44

Prueba esto:

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
fuente
38
Si desea poder usar super in #initializede B, en realidad puede generar lo que sea en A # initialize if self.class == A.
mk12
17
class A
  private_class_method :new
end

class B < A
  public_class_method :new
end
bluehavana
fuente
7
Además, se podría usar el gancho heredado de la clase padre para hacer que el método constructor sea automáticamente visible en todas las subclases: def A.inherited (subclase); subclass.instance_eval {public_class_method: new}; fin
t6d
1
Muy bonito t6d. Como comentario, solo asegúrese de que esté documentado, ya que es un comportamiento sorprendente (viola la menor sorpresa).
bluehavana
16

para cualquiera en el mundo de los rieles, la implementación de un modelo ActiveRecord como una clase abstracta se realiza con esta declaración en el archivo del modelo:

self.abstract_class = true
Fred Willmore
fuente
12

Mi 2 ¢: opto por una mezcla DSL simple y liviana:

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

Y, por supuesto, agregar otro error para inicializar la clase base sería trivial en este caso.

Anthony Navarre
fuente
1
EDITAR: Por si acaso, dado que este enfoque usa define_method, es posible que desee asegurarse de que el backtrace permanezca intacto, por ejemplo: err = NotImplementedError.new(message); err.set_backtrace caller()YMMV
Anthony Navarre
Encuentro este enfoque bastante elegante. Gracias por su contribución a esta pregunta.
wes.hysell
12

En los últimos 6 años y medio de programar Ruby, no he necesitado una clase abstracta ni una sola vez.

Si está pensando que necesita una clase abstracta, está pensando demasiado en un lenguaje que las proporciona / requiere, no en Ruby como tal.

Como han sugerido otros, un mixin es más apropiado para cosas que se supone que son interfaces (como las define Java), y repensar su diseño es más apropiado para cosas que "necesitan" clases abstractas de otros lenguajes como C ++.

Actualización 2019: No he necesitado clases abstractas en Ruby en 16 años y medio de uso. Todo lo que dicen todas las personas que comentan en mi respuesta se aborda aprendiendo realmente Ruby y usando las herramientas apropiadas, como módulos (que incluso le brindan implementaciones comunes). Hay personas en los equipos que he administrado que han creado clases que tienen una implementación base que falla (como una clase abstracta), pero en su mayoría son un desperdicio de codificación porque NoMethodErrorproducirían exactamente el mismo resultado que una AbstractClassErroren producción.

Austin Ziegler
fuente
24
Tampoco necesitas / necesitas / una clase abstracta en Java. Es una forma de documentar que es una clase base y no debe crearse una instancia para las personas que extienden su clase.
fijiaaron
3
En mi opinión, pocos lenguajes deberían restringir sus nociones de programación orientada a objetos. Lo que es apropiado en una situación dada no debería depender del idioma, a menos que haya una razón relacionada con el desempeño (o algo más convincente).
thekingoftruth
10
@fijiaaron: Si crees que sí, definitivamente no entiendes qué son las clases base abstractas. No son mucho para "documentar" que una clase no debería ser instanciada (es más bien un efecto secundario de que sea abstracta). Se trata más de establecer una interfaz común para un grupo de clases derivadas, que luego garantiza que se implementará (de lo contrario, la clase derivada también seguirá siendo abstracta). Su objetivo es respaldar el principio de sustitución de Liskov para las clases para las que la instanciación no tiene mucho sentido.
SasQ
1
(cont.) Por supuesto, uno puede crear solo varias clases con algunos métodos y propiedades comunes en ellas, pero el compilador / intérprete no sabrá entonces que estas clases están relacionadas de alguna manera. Los nombres de los métodos y propiedades pueden ser los mismos en cada una de estas clases, pero esta sola no significa todavía que representen la misma funcionalidad (la correspondencia de nombres podría ser simplemente accidental). La única manera de decirle al compilador sobre esta relación es usar una clase base, pero no siempre tiene sentido que existan las instancias de esta clase base.
SasQ
4

Personalmente planteo NotImplementedError en métodos de clases abstractas. Pero es posible que desee dejarlo fuera del método "nuevo", por las razones que mencionó.

Zack
fuente
Pero entonces, ¿cómo evitar que se cree una instancia?
Chirantan
Personalmente, recién estoy comenzando con Ruby, pero en Python, las subclases con métodos declarados __init ___ () no llaman automáticamente a los métodos __init __ () de sus superclases. Espero que haya un concepto similar en Ruby, pero como dije, recién estoy comenzando.
Zack
En ruby ​​parent, los initializemétodos no se llaman automáticamente a menos que se llamen explícitamente con super.
mk12
4

Si desea ir con una clase no desinstalable, en su método A.new, verifique si self == A antes de lanzar el error.

Pero en realidad, un módulo se parece más a lo que quiere aquí; por ejemplo, Enumerable es el tipo de cosa que podría ser una clase abstracta en otros lenguajes. Técnicamente, no puede subclasificarlos, pero llamar include SomeModulelogra aproximadamente el mismo objetivo. ¿Hay alguna razón por la que esto no funcione para usted?

Arrojar
fuente
4

¿Qué propósito estás tratando de cumplir con una clase abstracta? Probablemente haya una mejor manera de hacerlo en ruby, pero no dio ningún detalle.

Mi puntero es este; utilizar un mixin no herencia.

jshen
fuente
En efecto. Mezclar un módulo sería equivalente a usar un AbstractClass: wiki.c2.com/?AbstractClass PD: llamémoslos módulos y no mixins, ya que módulos es lo que son y mezclarlos es lo que se hace con ellos.
Magne
3

Otra respuesta:

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

Esto se basa en el #method_missing normal para informar métodos no implementados, pero evita que se implementen clases abstractas (incluso si tienen un método de inicialización)

class A
  include Abstract
end
class B < A
end

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

Como han dicho los otros carteles, probablemente debería usar una mezcla, en lugar de una clase abstracta.

rampion
fuente
3

Lo hice de esta manera, por lo que redefine lo nuevo en la clase secundaria para encontrar una nueva en la clase no abstracta. Todavía no veo nada práctico en el uso de clases abstractas en 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
ZeusElDios Verdadero
fuente
3

También está esta pequeña abstract_typejoya, que permite declarar clases y módulos abstractos de forma discreta.

Ejemplo (del archivo 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
シ リ ル
fuente
1

No hay nada de malo en tu enfoque. Generar un error en la inicialización parece estar bien, siempre y cuando todas sus subclases anulen la inicialización, por supuesto. Pero no quieres definirte a ti mismo. Nuevo así. Esto es lo que haría.

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

Otro enfoque sería poner toda esa funcionalidad en un módulo, que, como mencionaste, nunca se podrá implementar. Luego incluya el módulo en sus clases en lugar de heredarlo de otra clase. Sin embargo, esto rompería cosas como super.

Entonces depende de cómo quieras estructurarlo. Aunque los módulos parecen una solución más limpia para resolver el problema de "¿Cómo escribo algunas cosas que están diseñadas para que las utilicen otras clases?"

Alex Wayne
fuente
Yo tampoco querría hacer eso. Entonces, los niños no pueden llamar "super".
Austin Ziegler
0

Aunque esto no se siente como Ruby, puedes hacer esto:

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

    puts 'initialized'
  end
end

class B < A
end

Los resultados:

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