Comment implémenter une classe abstraite en ruby?

121

Je sais qu'il n'y a pas de concept de classe abstraite en rubis. Mais s'il faut le mettre en œuvre, comment s'y prendre? J'ai essayé quelque chose comme ...

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

class B < A
  ...
  ...
end

Mais quand j'essaye d'instancier B, il appelle en interne A.newce qui va lever l'exception.

De plus, les modules ne peuvent pas être instanciés mais ils ne peuvent pas non plus être hérités. rendre la nouvelle méthode privée ne fonctionnera pas non plus. Des pointeurs?

Chirantan
la source
1
Les modules peuvent être mélangés, mais je suppose que vous avez besoin de l'héritage classique pour une autre raison?
Zach le
6
Ce n'est pas que j'ai besoin d'implémenter une classe abstraite. Je me demandais comment le faire, si du tout il fallait le faire. Un problème de programmation. C'est tout.
Chirantan le
127
raise "Doh! You are trying to write Java in Ruby".
Andrew Grimm

Réponses:

61

Je n'aime pas utiliser des classes abstraites dans Ruby (il y a presque toujours une meilleure façon). Si vous pensez vraiment que c'est la meilleure technique pour la situation, vous pouvez utiliser l'extrait de code suivant pour être plus déclaratif sur les méthodes abstraites:

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

Fondamentalement, vous appelez simplement abstract_methodsavec la liste des méthodes abstraites, et lorsqu'elles sont appelées par une instance de la classe abstraite, une NotImplementedErrorexception sera déclenchée .

Nakajima
la source
Cela ressemble plus à une interface en fait, mais je comprends l'idée. Merci.
Chirantan
6
Cela ne semble pas être un cas d'utilisation valide NotImplementedErrorqui signifie essentiellement "dépendant de la plate-forme, non disponible sur le vôtre". Voir la documentation .
skalee
7
Vous remplacez une méthode d'une ligne par une méta-programmation, vous devez maintenant inclure un mixin et appeler une méthode. Je ne pense pas que ce soit plus déclaratif du tout.
Pascal
1
J'ai édité cette réponse, corrige quelques bogues dans le code et l'ai mis à jour, donc il fonctionne maintenant. Également simplifié un peu le code, pour utiliser extend au lieu d'inclure, en raison de: yehudakatz.com/2009/11/12/better-ruby-idioms
Magne
1
@ManishShrivastava: S'il vous plaît voir ce code maintenant, pour le commentaire sur l'importance d'utiliser END ici vs end
Magne
113

Juste pour marquer tard ici, je pense qu'il n'y a aucune raison d'empêcher quelqu'un d'instancier la classe abstraite, surtout parce qu'ils peuvent y ajouter des méthodes à la volée .

Les langages de typage Duck, comme Ruby, utilisent la présence / absence ou le comportement des méthodes au moment de l'exécution pour déterminer si elles doivent être appelées ou non. Par conséquent, votre question, telle qu'elle s'applique à une méthode abstraite , a du sens

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

et cela devrait être la fin de l'histoire. La seule raison d'utiliser des classes abstraites en Java est d'insister pour que certaines méthodes soient "remplies" tandis que d'autres ont leur comportement dans la classe abstraite. Dans un langage de typage canard, l'accent est mis sur les méthodes, pas sur les classes / types, vous devriez donc déplacer vos inquiétudes à ce niveau.

Dans votre question, vous essayez essentiellement de recréer le abstractmot - clé à partir de Java, qui est une odeur de code pour faire Java dans Ruby.

Dan Rosenstark
la source
3
@Christopher Perry: Des raisons pour lesquelles?
SasQ
10
@ChristopherPerry Je ne comprends toujours pas. Pourquoi ne voudrais-je pas cette dépendance si le parent et le frère sont liés après tout et que je veux que cette relation soit explicite? De plus, pour composer un objet d'une classe dans une autre classe, vous devez également connaître sa définition. L'héritage est généralement implémenté en tant que composition, il fait simplement que l'interface de l'objet composé fasse partie de l'interface de la classe qui l'incorpore. Vous avez donc néanmoins besoin de la définition de l'objet incorporé ou hérité. Ou peut-être que vous parlez d'autre chose? Pouvez-vous nous en dire plus?
SasQ
2
@SasQ, Vous n'avez pas besoin de connaître les détails d'implémentation de la classe parente pour la composer, vous avez seulement besoin de connaître son 'API. Cependant, si vous héritez, vous dépendez de l'implémentation des parents. Si l'implémentation change, votre code pourrait se rompre de manière inattendue. Plus de détails ici
Christopher Perry
16
Désolé, mais "Favoriser la composition par rapport à l'héritage" ne dit pas "Toujours la composition de l'utilisateur". Bien qu'en général l'héritage doive être évité, il existe quelques cas d'utilisation où ils s'adaptent mieux. Ne suivez pas aveuglément le livre.
Nowaker
1
@Nowaker Point très important. Si souvent, nous avons tendance à être aveuglés par des choses que nous lisons ou entendons, au lieu de penser «quelle est l'approche pragmatique dans ce cas». C'est rarement complètement noir ou blanc.
Per Lundberg
44

Essaye ça:

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
la source
38
Si vous voulez pouvoir utiliser super in #initializede B, vous pouvez en fait simplement lever n'importe quoi dans A # initialize if self.class == A.
mk12
17
class A
  private_class_method :new
end

class B < A
  public_class_method :new
end
bluehavana
la source
7
De plus, on pourrait utiliser le hook hérité de la classe parent pour rendre la méthode du constructeur automatiquement visible dans toutes les sous-classes: def A.inherited (subclass); subclass.instance_eval {public_class_method: new}; end
t6d
1
Très beau t6d. En guise de commentaire, assurez-vous simplement qu'il est documenté, car c'est un comportement surprenant (viole la moindre surprise).
bluehavana
16

pour quiconque dans le monde des rails, l'implémentation d'un modèle ActiveRecord en tant que classe abstraite se fait avec cette déclaration dans le fichier de modèle:

self.abstract_class = true
Fred Willmore
la source
12

Mon 2 ¢: j'opte pour un mixin DSL simple et léger:

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

Et, bien sûr, ajouter une autre erreur pour initialiser la classe de base serait trivial dans ce cas.

Anthony Navarre
la source
1
EDIT: Pour faire bonne mesure, puisque cette approche utilise define_method, on pourrait vouloir s'assurer que la trace arrière reste intacte, par exemple: err = NotImplementedError.new(message); err.set_backtrace caller()YMMV
Anthony Navarre
Je trouve cette approche assez élégante. Merci pour votre contribution à cette question.
wes.hysell
12

Au cours des 6 1/2 dernières années de programmation de Ruby, je n'ai pas eu besoin d' une classe abstraite une seule fois.

Si vous pensez avoir besoin d'une classe abstraite, vous pensez trop dans un langage qui les fournit / les exige, pas dans Ruby en tant que tel.

Comme d'autres l'ont suggéré, un mixin est plus approprié pour les choses qui sont supposées être des interfaces (comme Java les définit), et repenser votre conception est plus appropriée pour les choses qui "nécessitent" des classes abstraites d'autres langages comme C ++.

Mise à jour 2019: je n'ai pas eu besoin de classes abstraites dans Ruby depuis 16 ans et demi d'utilisation. Tout ce que disent toutes les personnes qui commentent ma réponse est abordé en apprenant réellement Ruby et en utilisant les outils appropriés, comme des modules (qui vous donnent même des implémentations courantes). Il y a des gens dans les équipes que j'ai dirigées qui ont créé des classes dont l'implémentation de base échoue (comme une classe abstraite), mais ce sont surtout un gaspillage de codage car NoMethodErrorcela produirait exactement le même résultat qu'une AbstractClassErroren production.

Austin Ziegler
la source
24
Vous n'avez pas / besoin / d'une classe abstraite en Java non plus. C'est un moyen de documenter qu'il s'agit d'une classe de base et ne doit pas être instanciée pour les personnes qui étendent votre classe.
fijiaaron
3
OMI, peu de langages devraient restreindre vos notions de programmation orientée objet. Ce qui est approprié dans une situation donnée ne devrait pas dépendre de la langue, à moins qu'il y ait une raison liée aux performances (ou quelque chose de plus convaincant).
thekingoftruth
10
@fijiaaron: Si vous pensez que oui, alors vous ne comprenez certainement pas ce que sont les classes de base abstraites. Il ne s'agit pas de "documenter" qu'une classe ne devrait pas être instanciée (c'est plutôt un effet secondaire du fait qu'elle soit abstraite). Il s'agit plus de déclarer une interface commune pour un tas de classes dérivées, qui garantit alors qu'elle sera implémentée (sinon, la classe dérivée restera également abstraite). Son objectif est de soutenir le principe de substitution de Liskov pour les classes pour lesquelles l'instanciation n'a pas beaucoup de sens.
SasQ
1
(suite) Bien sûr, on ne peut créer que plusieurs classes avec des méthodes et des propriétés communes, mais le compilateur / interpréteur ne saura alors pas que ces classes sont liées de quelque manière que ce soit. Les noms des méthodes et des propriétés peuvent être les mêmes dans chacune de ces classes, mais celui-ci à lui seul ne signifie pas encore qu'ils représentent la même fonctionnalité (la correspondance de nom pourrait être simplement accidentelle). La seule façon d'indiquer au compilateur cette relation est d'utiliser une classe de base, mais cela n'a pas toujours de sens que les instances de cette classe de base elle-même existent.
SasQ
4

Personnellement, je lève NotImplementedError dans les méthodes de classes abstraites. Mais vous voudrez peut-être la laisser en dehors de la «nouvelle» méthode, pour les raisons que vous avez mentionnées.

Zack
la source
Mais alors comment l'empêcher d'être instancié?
Chirantan le
Personnellement, je ne fais que commencer avec Ruby, mais en Python, les sous-classes avec les méthodes __init ___ () déclarées n'appellent pas automatiquement les méthodes __init __ () de leurs superclasses. J'espère qu'il y aurait un concept similaire dans Ruby, mais comme je l'ai dit, je ne fais que commencer.
Zack le
Dans ruby, les initializeméthodes des parents ne sont pas appelées automatiquement à moins qu'elles ne soient explicitement appelées avec super.
mk12 du
4

Si vous voulez utiliser une classe non instable, dans votre méthode A.new, vérifiez si self == A avant de lancer l'erreur.

Mais en réalité, un module ressemble plus à ce que vous voulez ici - par exemple, Enumerable est le genre de chose qui pourrait être une classe abstraite dans d'autres langages. Vous ne pouvez pas techniquement les sous-classer, mais appeler include SomeModuleatteint à peu près le même objectif. Y a-t-il une raison pour laquelle cela ne fonctionnera pas pour vous?

Mandrin
la source
4

Quel but essayez-vous de servir avec une classe abstraite? Il y a probablement une meilleure façon de le faire en rubis, mais vous n'avez donné aucun détail.

Mon pointeur est le suivant; utiliser un mixin pas un héritage.

jshen
la source
En effet. Mélanger un module équivaudrait à utiliser une classe abstraite: wiki.c2.com/?AbstractClass PS: Appelons-les modules et non mixins, puisque les modules sont ce qu'ils sont et les mélanger est ce que vous en faites.
Magne
3

Une autre réponse:

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

Cela s'appuie sur le #method_missing normal pour signaler les méthodes non implémentées, mais empêche les classes abstraites d'être implémentées (même si elles ont une méthode d'initialisation)

class A
  include Abstract
end
class B < A
end

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

Comme les autres affiches l'ont dit, vous devriez probablement utiliser un mixin plutôt qu'une classe abstraite.

rampion
la source
3

Je l'ai fait de cette façon, donc cela redéfinit la nouvelle classe enfant pour trouver une nouvelle classe non abstraite. Je ne vois toujours pas de pratique à utiliser des classes abstraites dans 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
ZeusLeVraiDieu
la source
3

Il y a aussi ce petit abstract_typebijou, qui permet de déclarer des classes et des modules abstraits de manière discrète.

Exemple (à partir du fichier 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
シ リ ル
la source
1

Rien de mal avec votre approche. Générer une erreur lors de l'initialisation semble correct, tant que toutes vos sous-classes écrasent bien sûr initialize. Mais vous ne voulez pas définir self.new comme ça. Voici ce que je ferais.

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

Une autre approche consisterait à mettre toutes ces fonctionnalités dans un module, qui, comme vous l'avez mentionné, ne peut jamais être instauré. Incluez ensuite le module dans vos classes plutôt que d'hériter d'une autre classe. Cependant, cela casserait des choses comme super.

Cela dépend donc de la manière dont vous souhaitez le structurer. Bien que les modules semblent être une solution plus propre pour résoudre le problème de "Comment écrire des trucs daignés pour d'autres classes à utiliser"

Alex Wayne
la source
Je ne voudrais pas non plus faire ça. Les enfants ne peuvent donc pas appeler "super".
Austin Ziegler le
0

Bien que cela ne ressemble pas à Ruby, vous pouvez le faire:

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

    puts 'initialized'
  end
end

class B < A
end

Les resultats:

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