Come implementare una classe astratta in ruby?


121

So che non esiste il concetto di classe astratta in Ruby. Ma se è necessario implementarlo, come farlo? Ho provato qualcosa come ...

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

class B < A
  ...
  ...
end

Ma quando provo a istanziare B, internamente chiamerà A.newche solleverà l'eccezione.

Inoltre, i moduli non possono essere istanziati ma anche loro non possono essere ereditati. anche rendere privato il nuovo metodo non funzionerà. Qualche suggerimento?


1
I moduli possono essere mescolati, ma suppongo che tu abbia bisogno dell'eredità classica per qualche altro motivo?
Zach

6
Non è che ho bisogno di implementare una classe astratta. Mi stavo chiedendo come farlo, se è necessario farlo. Un problema di programmazione. Questo è tutto.
Chirantan

127
raise "Doh! You are trying to write Java in Ruby".
Andrew Grimm

Risposte:


61

Non mi piace usare classi astratte in Ruby (c'è quasi sempre un modo migliore). Se pensi davvero che sia la tecnica migliore per la situazione, puoi usare il seguente frammento per essere più dichiarativo su quali metodi sono astratti:

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

Fondamentalmente, si chiama semplicemente abstract_methodscon l'elenco dei metodi astratti e quando vengono chiamati da un'istanza della classe astratta, NotImplementedErrorverrà sollevata un'eccezione.


Questa è più come un'interfaccia in realtà, ma ho l'idea. Grazie.
Chirantan

6
Questo non sembra un caso d'uso valido NotImplementedErrorche significa essenzialmente "dipendente dalla piattaforma, non disponibile sulla tua". Vedi i documenti .
skalee

7
Stai sostituendo un metodo a una riga con una meta-programmazione, ora devi includere un mixin e chiamare un metodo. Non credo che questo sia affatto più dichiarativo.
Pascal

1
Ho modificato questa risposta, correggendo alcuni bug del codice e aggiornato, quindi ora funziona. Ha anche semplificato un po 'il codice, per usare estendere invece di includere, a causa di: yehudakatz.com/2009/11/12/better-ruby-idioms
Magne

1
@ManishShrivastava: per favore guarda questo codice ora, per il commento sull'importanza di usare END qui vs end
Magne

113

Solo per intervenire in ritardo qui, penso che non ci sia motivo per impedire a qualcuno di istanziare la classe astratta, soprattutto perché possono aggiungere metodi al volo .

I linguaggi di battitura a papera, come Ruby, utilizzano la presenza / assenza o il comportamento dei metodi in fase di esecuzione per determinare se devono essere chiamati o meno. Quindi la tua domanda, in quanto si applica a un metodo astratto , ha senso

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

e quella dovrebbe essere la fine della storia. L'unico motivo per utilizzare classi astratte in Java è insistere sul fatto che alcuni metodi vengono "riempiti" mentre altri hanno il loro comportamento nella classe astratta. In un linguaggio di battitura a papera, l'attenzione è sui metodi, non sulle classi / tipi, quindi dovresti spostare le tue preoccupazioni a quel livello.

Nella tua domanda, stai fondamentalmente cercando di ricreare la abstractparola chiave da Java, che è un odore di codice per fare Java in Ruby.


3
@Christopher Perry: Qualche motivo?
SasQ

10
@ChristopherPerry Continuo a non capirlo. Perché non dovrei volere questa dipendenza se il genitore e il fratello sono imparentati dopo tutto e voglio che questa relazione sia esplicita? Inoltre, per comporre un oggetto di una classe all'interno di un'altra classe è necessario conoscerne anche la definizione. L'ereditarietà è solitamente implementata come composizione, rende semplicemente l'interfaccia dell'oggetto composto una parte dell'interfaccia della classe che lo incorpora. Quindi è comunque necessaria la definizione dell'oggetto incorporato o ereditato. O forse stai parlando di qualcos'altro? Puoi approfondire un po 'di più su questo?
SasQ

2
@SasQ, non è necessario conoscere i dettagli di implementazione della classe genitore per comporla, è sufficiente conoscere la sua 'API. Tuttavia, se erediti fai affidamento sull'implementazione dei genitori. Se l'implementazione cambia, il codice potrebbe interrompersi in modi imprevisti. Maggiori dettagli qui
Christopher Perry

16
Spiacenti, ma "Preferisci composizione rispetto all'ereditarietà" non dice "Sempre composizione utente". Sebbene in generale l'ereditarietà debba essere evitata, ci sono un paio di casi d'uso in cui si adattano meglio. Non seguire ciecamente il libro.
Nowaker

1
@Nowaker Punto molto importante. Così spesso tendiamo a essere accecati da cose che leggiamo o sentiamo, invece di pensare "qual è l'approccio pragmatico in questo caso". Raramente è completamente bianco o nero.
Per Lundberg

44

Prova questo:

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

class B < A
  def initialize
  end
end

38
Se vuoi essere in grado di usare super in #initializedi B, puoi effettivamente sollevare qualsiasi cosa in A # inizializza if self.class == A.
mk12

17
class A
  private_class_method :new
end

class B < A
  public_class_method :new
end

7
Inoltre si potrebbe usare l'hook ereditato della classe genitore per rendere automaticamente visibile il metodo del costruttore in tutte le sottoclassi: def A.inherited (sottoclasse); subclass.instance_eval {public_class_method: new}; fine
t6d

1
T6d molto bello. Come commento, assicurati che sia documentato, poiché si tratta di un comportamento sorprendente (viola la sorpresa minore).
bluehavana

16

per chiunque nel mondo rails, l'implementazione di un modello ActiveRecord come classe astratta viene eseguita con questa dichiarazione nel file del modello:

self.abstract_class = true

12

Il mio 2 ¢: opto per un mixin DSL semplice e leggero:

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, ovviamente, l'aggiunta di un altro errore per l'inizializzazione della classe base sarebbe banale in questo caso.


1
EDIT: Per buona misura, poiché questo approccio utilizza define_method, potresti voler assicurarti che il backtrace rimanga intatto, ad esempio: err = NotImplementedError.new(message); err.set_backtrace caller()YMMV
Anthony Navarre

Trovo che questo approccio sia piuttosto elegante. Grazie per il tuo contributo a questa domanda.
wes.hysell

12

Negli ultimi 6 anni e mezzo di programmazione in Ruby, non ho avuto bisogno di una lezione astratta nemmeno una volta.

Se stai pensando di aver bisogno di una classe astratta, stai pensando troppo in un linguaggio che li fornisce / li richiede, non in Ruby in quanto tale.

Come altri hanno suggerito, un mixin è più appropriato per cose che dovrebbero essere interfacce (come le definisce Java), e ripensare il tuo design è più appropriato per cose che "necessitano" di classi astratte da altri linguaggi come C ++.

Aggiornamento 2019: non ho bisogno di lezioni astratte in Ruby in 16 anni e mezzo di utilizzo. Tutto ciò che dicono tutte le persone che commentano la mia risposta viene affrontato imparando effettivamente Ruby e utilizzando gli strumenti appropriati, come i moduli (che forniscono anche implementazioni comuni). Ci sono persone nei team che ho gestito che hanno creato classi che hanno un'implementazione di base che fallisce (come una classe astratta), ma questi sono principalmente uno spreco di codifica perché NoMethodErrorprodurrebbe lo stesso identico risultato di una AbstractClassErrorin produzione.


24
Neanche tu / hai bisogno / di una classe astratta in Java. È un modo per documentare che si tratta di una classe base e non dovrebbe essere istanziata per le persone che estendono la tua classe.
fijiaaron

3
IMO, pochi linguaggi dovrebbero restringere le tue nozioni di programmazione orientata agli oggetti. Ciò che è appropriato in una determinata situazione non dovrebbe dipendere dalla lingua, a meno che non ci sia un motivo relativo alle prestazioni (o qualcosa di più convincente).
thekingoftruth

10
@fijiaaron: Se la pensi così, allora sicuramente non capisci cosa siano le classi base astratte. Non sono molto per "documentare" che una classe non dovrebbe essere istanziata (è piuttosto un effetto collaterale del fatto che sia astratta). Si tratta più di stabilire un'interfaccia comune per un gruppo di classi derivate, che quindi garantisce che sarà implementata (in caso contrario, anche la classe derivata rimarrà astratta). Il suo obiettivo è supportare il principio di sostituzione di Liskov per le classi per le quali l'istanza non ha molto senso.
SasQ

1
(segue) Ovviamente si possono creare solo diverse classi con alcuni metodi e proprietà comuni, ma il compilatore / interprete non saprà allora che queste classi sono in qualche modo correlate. I nomi dei metodi e delle proprietà possono essere gli stessi in ciascuna di queste classi, ma questo da solo non significa ancora che rappresentano la stessa funzionalità (la corrispondenza del nome potrebbe essere solo accidentale). L'unico modo per informare il compilatore di questa relazione è usare una classe base, ma non ha sempre senso che esistano le istanze di questa classe base stessa.
SasQ


4

Personalmente sollevo NotImplementedError nei metodi delle classi astratte. Ma potresti lasciarlo fuori dal "nuovo" metodo, per i motivi che hai menzionato.


Ma allora come impedire che venga istanziato?
Chirantan

Personalmente sto appena iniziando con Ruby, ma in Python, le sottoclassi con metodi dichiarati __init ___ () non chiamano automaticamente i metodi __init __ () delle loro superclassi. Spero che ci sia un concetto simile in Ruby, ma come ho detto ho appena iniziato.
Zack

Nei initializemetodi di ruby parent non vengono chiamati automaticamente a meno che non siano chiamati esplicitamente con super.
mk12

4

Se vuoi andare con una classe non istanziabile, nel tuo metodo A.new, controlla se self == A prima di lanciare l'errore.

Ma in realtà, un modulo sembra più come quello che vuoi qui - per esempio, Enumerable è il tipo di cosa che potrebbe essere una classe astratta in altri linguaggi. Tecnicamente non puoi sottoclassarli, ma la chiamata include SomeModuleraggiunge più o meno lo stesso obiettivo. C'è qualche motivo per cui questo non funzionerà per te?


4

Che scopo stai cercando di servire con una classe astratta? Probabilmente c'è un modo migliore per farlo in ruby, ma non hai fornito alcun dettaglio.

Il mio suggerimento è questo; utilizzare un mixin non ereditarietà.


Infatti. Mescolare un modulo equivarrebbe a usare una AbstractClass: wiki.c2.com/?AbstractClass PS: chiamiamoli moduli e non mixin, poiché i moduli sono ciò che sono e mescolarli è ciò che fai con loro.
Magne

3

Un'altra risposta:

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

Si basa sul normale #method_missing per segnalare metodi non implementati, ma impedisce l'implementazione di classi astratte (anche se hanno un metodo di inizializzazione)

class A
  include Abstract
end
class B < A
end

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

Come hanno detto gli altri poster, probabilmente dovresti usare un mixin, piuttosto che una classe astratta.


3

L'ho fatto in questo modo, quindi ridefinisce la nuova classe figlio per trovare una nuova classe non astratta. Non vedo ancora alcuna pratica nell'utilizzo di classi astratte in 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

3

C'è anche questo piccolo abstract_type gioiello, che consente di dichiarare classi e moduli astratti in modo discreto.

Esempio (dal file 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

1

Niente di sbagliato nel tuo approccio. Sollevare un errore in initialize sembra corretto, a patto che tutte le tue sottoclassi sovrascrivano l'inizializzazione, ovviamente. Ma non vuoi definire il sé. Nuovo in questo modo. Ecco cosa farei.

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

Un altro approccio sarebbe mettere tutte quelle funzionalità in un modulo, che come hai detto non può mai essere istanziato. Quindi includi il modulo nelle tue classi piuttosto che ereditare da un'altra classe. Tuttavia, questo romperebbe cose come super.

Quindi dipende da come vuoi strutturarlo. Sebbene i moduli sembrino una soluzione più pulita per risolvere il problema di "Come scrivo qualcosa che è degno di essere utilizzato da altre classi"


Non vorrei nemmeno farlo. I bambini non possono chiamare "super", quindi.
Austin Ziegler


0

Anche se non sembra Ruby, puoi farlo:

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

    puts 'initialized'
  end
end

class B < A
end

I risultati:

>> A.new
  (rib):2:in `main'
  (rib):2:in `new'
  (rib):3:in `initialize'
RuntimeError: abstract class
>> B.new
initialized
=> #<B:0x00007f80620d8358>
>>
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.