Jak zaimplementować klasę abstrakcyjną w Rubim?

121

Wiem, że w rubinie nie ma pojęcia klasy abstrakcyjnej. Ale jeśli w ogóle trzeba to wdrożyć, jak się do tego zabrać? Próbowałem czegoś takiego ...

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

class B < A
  ...
  ...
end

Ale kiedy próbuję utworzyć wystąpienie B, wewnętrznie wywoła to wywołanie, A.newktóre spowoduje zgłoszenie wyjątku.

Ponadto nie można tworzyć instancji modułów, ale nie można ich również dziedziczyć. ustawienie nowej metody jako prywatnej również nie zadziała. Jakieś wskazówki?

Chirantan
źródło
1
Moduły można wmieszać, ale przypuszczam, że potrzebujesz klasycznego dziedziczenia z innego powodu?
Zach
6
Nie chodzi o to, że muszę zaimplementować klasę abstrakcyjną. Zastanawiałem się, jak to zrobić, jeśli w ogóle trzeba to zrobić. Problem programistyczny. Otóż ​​to.
Chirantan
127
raise "Doh! You are trying to write Java in Ruby".
Andrew Grimm,

Odpowiedzi:

61

Nie lubię używać klas abstrakcyjnych w Rubim (prawie zawsze jest lepszy sposób). Jeśli jednak naprawdę uważasz, że jest to najlepsza technika w tej sytuacji, możesz użyć następującego fragmentu kodu, aby bardziej zadeklarować, które metody są abstrakcyjne:

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

Zasadniczo wystarczy wywołać abstract_methodslistę metod, które są abstrakcyjne, a gdy zostaną wywołane przez instancję klasy abstrakcyjnej, NotImplementedErrorzostanie zgłoszony wyjątek.

nakajima
źródło
To bardziej przypomina interfejs, ale mam pomysł. Dzięki.
Chirantan
6
Nie brzmi to jak ważny przypadek użycia, NotImplementedErrorktóry zasadniczo oznacza „zależne od platformy, niedostępne na Twojej”. Zobacz dokumentację .
skalee
7
Zastępujesz metodę jednowierszową metaprogramowaniem, teraz musisz dołączyć element mixin i wywołać metodę. Nie sądzę, żeby to było bardziej deklaratywne.
Pascal,
1
Edytowałem tę odpowiedź, naprawiłem kilka błędów w kodzie i zaktualizowałem go, więc teraz działa. Uprościłem również nieco kod, aby użyć rozszerzenia zamiast uwzględniania, ponieważ: yehudakatz.com/2009/11/12/better-ruby-idioms
Magne
1
@ManishShrivastava: Zobacz ten kod teraz, aby uzyskać komentarz na temat znaczenia używania END tutaj w porównaniu do końca
Magne
113

Żeby zadzwonić późno, myślę, że nie ma powodu, aby powstrzymywać kogoś przed utworzeniem instancji klasy abstrakcyjnej, zwłaszcza że mogą dodawać do niej metody w locie .

Języki typu kaczego, takie jak Ruby, używają obecności / braku lub zachowania metod w czasie wykonywania, aby określić, czy powinny być wywoływane, czy nie. Dlatego twoje pytanie, w odniesieniu do metody abstrakcyjnej , ma sens

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

a to powinno dotyczyć końca historii. Jedynym powodem używania klas abstrakcyjnych w Javie jest naleganie, aby niektóre metody były „wypełniane”, podczas gdy inne zachowywały się w klasie abstrakcyjnej. W języku pisania kaczkami nacisk kładzie się na metody, a nie na klasy / typy, więc powinieneś przenieść swoje zmartwienia na ten poziom.

W swoim pytaniu zasadniczo próbujesz odtworzyć abstractsłowo kluczowe z Javy, które jest zapachem kodu do robienia Javy w Rubim.

Dan Rosenstark
źródło
3
@Christopher Perry: Jakieś powody, dlaczego?
SasQ,
10
@ChristopherPerry Nadal nie rozumiem. Dlaczego nie miałbym chcieć tej zależności, jeśli rodzic i rodzeństwo są w końcu spokrewnieni i chcę, aby ta relacja była wyraźna? Ponadto, aby skomponować obiekt jakiejś klasy wewnątrz innej klasy, musisz również znać jego definicję. Dziedziczenie jest zwykle realizowane jako kompozycja, po prostu sprawia, że ​​interfejs złożonego obiektu jest częścią interfejsu klasy, która go osadza. Potrzebujesz więc definicji osadzonego lub dziedziczonego obiektu. A może mówisz o czymś innym? Czy możesz rozwinąć więcej na ten temat?
SasQ,
2
@SasQ, nie musisz znać szczegółów implementacji klasy nadrzędnej, aby ją skomponować, potrzebujesz tylko znać jej 'API. Jeśli jednak odziedziczysz, polegasz na wdrożeniu przez rodziców. Jeśli implementacja zmieni się, kod może się nieoczekiwanie zepsuć. Więcej szczegółów tutaj
Christopher Perry,
16
Przykro nam, ale „Preferuj kompozycję zamiast dziedziczenia” nie mówi „Zawsze kompozycja użytkownika”. Chociaż generalnie należy unikać dziedziczenia, istnieje kilka przypadków użycia, w których po prostu lepiej pasują. Nie podążaj ślepo za książką.
Nowaker
1
@ Nowaker Bardzo ważny punkt. Tak często jesteśmy zaślepieni przez rzeczy, które czytamy lub słyszymy, zamiast myśleć „jakie jest pragmatyczne podejście w tym przypadku”. Rzadko jest całkowicie czarny lub biały.
Per Lundberg
44

Spróbuj tego:

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
źródło
38
Jeśli chcesz mieć możliwość używania super w #initializeB, możesz po prostu podnieść cokolwiek w inicjalizacji A # if self.class == A.
mk12
17
class A
  private_class_method :new
end

class B < A
  public_class_method :new
end
bluehavana
źródło
7
Dodatkowo można by użyć odziedziczonego punktu zaczepienia klasy nadrzędnej, aby metoda konstruktora była automatycznie widoczna we wszystkich podklasach: def A.inherited (subclass); subclass.instance_eval {public_class_method: new}; koniec
t6d
1
Bardzo ładny t6d. Jako komentarz, po prostu upewnij się, że jest to udokumentowane, ponieważ jest to zaskakujące zachowanie (narusza najmniejszą niespodziankę).
bluehavana
16

dla każdego w świecie railsów implementacja modelu ActiveRecord jako klasy abstrakcyjnej jest wykonywana z następującą deklaracją w pliku modelu:

self.abstract_class = true
Fred Willmore
źródło
12

Moje 2 ¢: wybieram prostą, lekką mieszankę DSL:

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

I oczywiście dodanie kolejnego błędu przy inicjalizacji klasy bazowej byłoby w tym przypadku trywialne.

Anthony Navarre
źródło
1
EDYCJA: Na wszelki wypadek, ponieważ to podejście wykorzystuje metodę define_method, można chcieć upewnić się, że ślad pozostanie nienaruszony, np .: err = NotImplementedError.new(message); err.set_backtrace caller()YMMV
Anthony Navarre
Uważam, że to podejście jest dość eleganckie. Dziękuję za wkład w to pytanie.
wes.hysell
12

Przez ostatnie półtora roku programowania w Rubim ani razu nie potrzebowałem klasy abstrakcyjnej.

Jeśli myślisz, że potrzebujesz klasy abstrakcyjnej, myślisz za dużo w języku, który ich dostarcza / wymaga, a nie w języku Ruby jako takim.

Jak sugerowali inni, mixin jest bardziej odpowiedni dla rzeczy, które mają być interfejsami (tak jak definiuje je Java), a przemyślenie projektu jest bardziej odpowiednie dla rzeczy, które „potrzebują” abstrakcyjnych klas z innych języków, takich jak C ++.

Aktualizacja 2019: Nie potrzebowałem klas abstrakcyjnych w Rubim od 16 i pół roku użytkowania. Wszystko, co mówią wszyscy ludzie komentujący moją odpowiedź, jest omówione przez faktyczną naukę języka Ruby i użycie odpowiednich narzędzi, takich jak moduły (które nawet zapewniają powszechne implementacje). W zespołach, którymi zarządzałem, są ludzie, którzy stworzyli klasy, których podstawowa implementacja kończy się niepowodzeniem (jak klasa abstrakcyjna), ale są to głównie marnowanie kodu, ponieważ NoMethodErrordałyby dokładnie taki sam wynik jak AbstractClassErrorw produkcji.

Austin Ziegler
źródło
24
Nie potrzebujesz / nie potrzebujesz / abstrakcyjnej klasy w Javie. Jest to sposób na udokumentowanie, że jest to klasa bazowa i nie należy jej tworzyć dla osób rozszerzających klasę.
fijiaaron
3
IMO, kilka języków powinno ograniczać twoje wyobrażenia o programowaniu obiektowym. To, co jest odpowiednie w danej sytuacji, nie powinno zależeć od języka, chyba że istnieje powód związany z wydajnością (lub coś bardziej przekonującego).
thekingoftruth
10
@fijiaaron: Jeśli tak myślisz, to na pewno nie rozumiesz, czym są abstrakcyjne klasy bazowe. Nie są zbytnio „dokumentujące”, że klasa nie powinna być tworzona (jest to raczej efekt uboczny abstrakcyjności). Chodzi bardziej o określenie wspólnego interfejsu dla kilku klas pochodnych, co gwarantuje, że zostanie on zaimplementowany (jeśli nie, klasa pochodna również pozostanie abstrakcyjna). Jego celem jest wspieranie zasady podstawiania Liskova dla klas, dla których tworzenie instancji nie ma większego sensu.
SasQ
1
(cd.) Oczywiście można utworzyć tylko kilka klas z pewnymi typowymi metodami i właściwościami, ale kompilator / interpreter nie będzie wtedy wiedział, że te klasy są w jakikolwiek sposób powiązane. Nazwy metod i właściwości mogą być takie same w każdej z tych klas, ale ta sama nie oznacza jeszcze, że reprezentują tę samą funkcjonalność (korespondencja nazw może być po prostu przypadkowa). Jedynym sposobem poinformowania kompilatora o tej relacji jest użycie klasy bazowej, ale nie zawsze ma sens, aby istniały wystąpienia samej klasy bazowej.
SasQ,
4

Osobiście zgłaszam NotImplementedError w metodach klas abstrakcyjnych. Ale możesz zechcieć wykluczyć to z „nowej” metody z powodów, o których wspomniałeś.

Zack
źródło
Ale w takim razie jak powstrzymać jego tworzenie?
Chirantan
Osobiście dopiero zaczynam pracę z Rubim, ale w Pythonie podklasy z zadeklarowanymi metodami __init ___ () nie wywołują automatycznie metod __init __ () swoich nadklas. Mam nadzieję, że w Rubim będzie podobna koncepcja, ale tak jak powiedziałem, dopiero zaczynam.
Zack
W Ruby initializemetody rodzicielskie nie są wywoływane automatycznie, chyba że zostaną jawnie wywołane z super.
mk12
4

Jeśli chcesz przejść z klasą niemożliwą do zainstalowania, w swojej metodzie A.new sprawdź, czy self == A przed wyrzuceniem błędu.

Ale tak naprawdę moduł wydaje się bardziej podobny do tego, co chcesz tutaj - na przykład Enumerable jest czymś, co może być klasą abstrakcyjną w innych językach. Technicznie nie można ich podklasy, ale dzwonienie include SomeModulepozwala osiągnąć mniej więcej ten sam cel. Czy jest jakiś powód, dla którego to nie zadziała?

Głaskanie pod brodę
źródło
4

Czemu starasz się służyć klasie abstrakcyjnej? Prawdopodobnie jest lepszy sposób na zrobienie tego w rubinie, ale nie podałeś żadnych szczegółów.

Mój wskaźnik jest taki; użyj mieszanki, a nie dziedziczenia.

jshen
źródło
W rzeczy samej. Mieszanie modułu byłoby równoznaczne z użyciem klasy AbstractClass: wiki.c2.com/?AbstractClass PS: Nazwijmy je modułami, a nie mixinami, ponieważ moduły są tym, czym są, a ich mieszanie jest tym, co z nimi robisz.
Magne
3

Inna odpowiedź:

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

Opiera się to na normalnym #method_missing do zgłaszania niezaimplementowanych metod, ale zapobiega implementacji klas abstrakcyjnych (nawet jeśli mają metodę inicjalizacji)

class A
  include Abstract
end
class B < A
end

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

Tak jak powiedzieli inni plakaty, prawdopodobnie powinieneś używać mieszanki, a nie klasy abstrakcyjnej.

rampion
źródło
3

Zrobiłem to w ten sposób, więc na nowo definiuje nową klasę potomną, aby znaleźć nową klasę nieabstrakcyjną. Nadal nie widzę praktycznego zastosowania klas abstrakcyjnych w języku 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
źródło
3

Jest też ten mały abstract_typeklejnot, który pozwala deklarować abstrakcyjne klasy i moduły w dyskretny sposób.

Przykład (z pliku 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
シ リ ル
źródło
1

Nie ma nic złego w Twoim podejściu. Zgłoszenie błędu podczas inicjalizacji wydaje się być w porządku, o ile wszystkie podklasy oczywiście zastępują inicjalizację. Ale nie chcesz tak definiować siebie. Nowego. Oto, co bym zrobił.

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

Innym podejściem byłoby umieszczenie całej tej funkcjonalności w module, którego, jak wspomniałeś, nigdy nie można zainicjować. Następnie włącz moduł do swoich klas, zamiast dziedziczyć po innej klasie. Jednak to zepsułoby rzeczy takie jak super.

To zależy od tego, jak chcesz to zorganizować. Chociaż moduły wydają się czystszym rozwiązaniem problemu „Jak napisać coś, co jest przeznaczone do użycia przez inne klasy”

Alex Wayne
źródło
Ja też nie chciałbym tego robić. Dzieci nie mogą więc nazywać „super”.
Austin Ziegler
0

Chociaż to nie wygląda jak Ruby, możesz to zrobić:

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

    puts 'initialized'
  end
end

class B < A
end

Wyniki:

>> A.new
  (rib):2:in `main'
  (rib):2:in `new'
  (rib):3:in `initialize'
RuntimeError: abstract class
>> B.new
initialized
=> #<B:0x00007f80620d8358>
>>
lulalala
źródło