Jaki jest odpowiednik interfejsu Java w Rubim?

102

Czy możemy ujawnić interfejsy w Rubim, tak jak robimy to w Javie i wymusić na modułach lub klasach Rubiego implementację metod zdefiniowanych przez interfejs.

Jednym ze sposobów jest użycie dziedziczenia i braku metody, aby osiągnąć to samo, ale czy jest dostępne inne, bardziej odpowiednie podejście?

crazycrv
źródło
1
Kliknij ten adres URL lawrencesong.net/2008/01/implement-java-interface-in-ruby
Sankar Ganesh
6
Powinieneś dwukrotnie zadać sobie pytanie, dlaczego w ogóle tego potrzebujesz. Często wystarczająca liczba interfejsów jest używana tylko do skompilowania cholernej rzeczy, co nie stanowi problemu w Ruby.
Arnis Lapsa
1
To pytanie może, ale nie musi, być traktowane jako duplikat [ W języku Ruby, co jest odpowiednikiem interfejsu w C #? ] ( StackOverflow.Com/q/3505521/#3507460 ).
Jörg W Mittag,
2
Dlaczego tego potrzebuję? Chcę zaimplementować coś, co można nazwać "wersjonowalnymi", co sprawia, że ​​dokumenty / pliki mogą być wersjonowane, ale wersjonowalne przy użyciu… Na przykład mogę uczynić to wersjonowalnym przy użyciu istniejącego oprogramowania repozytorium, takiego jak SVN lub CVS. Niezależnie od tego, jaki mechanizm opierający się wybiorę, powinien zapewnić kilka podstawowych funkcji minimum. Chcę używać interfejsu podobnego do rzeczy, aby wymusić implementację tych minimalnych funkcji przez każdą nową implementację bazowego repozytorium.
crazycrv
Sandi Metz w swojej książce POODR używa testów do dokumentowania interfejsów. Naprawdę warto przeczytać tę książkę. Od 2015 roku powiedziałbym, że odpowiedź @ aleksander-pohl jest najlepsza.
Greg Dan,

Odpowiedzi:

85

Ruby ma interfejsy jak każdy inny język.

Zauważ, że musisz uważać, aby nie pomylić koncepcji interfejsu , która jest abstrakcyjną specyfikacją obowiązków, gwarancji i protokołów jednostki z koncepcją, interfacektóra jest słowem kluczowym w programowaniu Java, C # i VB.NET Języki. W Rubim cały czas używamy tego pierwszego, ale drugiego po prostu nie ma.

Rozróżnienie tych dwóch jest bardzo ważne. Ważny jest interfejs , a nie interface. interfacePowie Ci prawie nic pożytecznego. Nic nie pokazuje tego lepiej niż interfejsy znaczników w Javie, które są interfejsami, które nie mają żadnych elementów członkowskich: wystarczy spojrzeć na java.io.Serializablei java.lang.Cloneable; te dwa interfaceoznaczają bardzo różne rzeczy, ale mają dokładnie ten sam podpis.

Tak więc, jeśli dwie interfaces, które mają różne znaczenie mają ten sam podpis, co dokładnie jest interfacenawet gwarantując ci?

Kolejny dobry przykład:

package java.util;

interface List<E> implements Collection<E>, Iterable<E> {
    void add(int index, E element)
        throws UnsupportedOperationException, ClassCastException,
            NullPointerException, IllegalArgumentException,
            IndexOutOfBoundsException;
}

Co to jest interfejs z java.util.List<E>.add?

  • że długość kolekcji się nie zmniejsza
  • że wszystkie przedmioty, które były w kolekcji wcześniej, nadal tam są
  • to elementjest w kolekcji

A który z nich faktycznie pojawia się w interface? Żaden! Nic w interfacetym nie mówi, że Addmetoda musi w ogóle dodawać , równie dobrze mogłaby usunąć element z kolekcji.

Jest to całkowicie poprawna realizacja tego interface:

class MyCollection<E> implements java.util.List<E> {
    void add(int index, E element)
        throws UnsupportedOperationException, ClassCastException,
            NullPointerException, IllegalArgumentException,
            IndexOutOfBoundsException {
        remove(element);
    }
}

Inny przykład: gdzie w java.util.Set<E>rzeczywistości jest napisane, że jest to zestaw ? Nigdzie! A dokładniej w dokumentacji. Po angielsku.

Prawie we wszystkich przypadkach interfaces, zarówno z języka Java, jak i .NET, wszystkie istotne informacje znajdują się w dokumentacji, a nie w typach. Więc jeśli typy i tak nie mówią ci nic interesującego, po co je w ogóle trzymać? Dlaczego nie trzymać się tylko dokumentacji? I to jest dokładnie to, co robi Ruby.

Zwróć uwagę, że istnieją inne języki, w których interfejs można faktycznie opisać w zrozumiały sposób. Jednak te języki zazwyczaj nie wywołują konstrukcji opisującej interfejs " interface", nazywają ją type. W języku programowania z typami zależnymi możesz na przykład wyrazić właściwości, że sortfunkcja zwraca kolekcję o tej samej długości co oryginał, że każdy element, który jest w oryginale, również znajduje się w posortowanej kolekcji i nie ma większego elementu pojawia się przed mniejszym elementem.

Krótko mówiąc: Ruby nie ma odpowiednika w Javie interface. To nie mają jednak odpowiednikiem Java interfejs i jest dokładnie taka sama jak w Javie: dokumentacja.

Podobnie jak w Javie, testy akceptacji mogą być również używane do określania interfejsów .

W szczególności w języku Ruby interfejs obiektu jest określany przez to, co może zrobić , a nie przez to , co classjest lub z czym modulesię miesza. Do każdego obiektu, który ma <<metodę, można dołączyć. Jest to bardzo przydatne w testach jednostkowych, w których można po prostu przekazać komendę Arraylub a Stringzamiast bardziej skomplikowanej Logger, nawet jeśli Arrayi Loggernie udostępniać jawnego, interfacepoza tym, że oba mają metodę nazwaną <<.

Innym przykładem jest to StringIO, który realizuje te same interfejsu a IO, a tym samym duża część interfejsu z File, ale nie dzielą każdą oprócz wspólnego przodka Object.

Jörg W Mittag
źródło
279
Chociaż dobra lektura, nie uważam odpowiedzi za pomocną. To brzmi jak rozprawa o tym, dlaczego interfacejest bezużyteczna, pomijając sens jej użycia. Byłoby łatwiej powiedzieć, że ruby ​​jest wpisywany dynamicznie, co ma inny cel i sprawia, że ​​koncepcje takie jak IOC są niepotrzebne / niepożądane. Trudna zmiana, jeśli jesteś przyzwyczajony do projektowania na podstawie umowy. Coś, na czym Railsy mogą skorzystać, co główny zespół zdał sobie sprawę, jak widać w najnowszych wersjach.
goliatone
12
Pytanie uzupełniające: jaki jest najlepszy sposób dokumentowania interfejsu w Rubim? Słowo kluczowe Java interfacemoże nie zawierać wszystkich istotnych informacji, ale zapewnia oczywiste miejsce na umieszczenie dokumentacji. Napisałem klasę w Rubim, która implementuje (wystarczającą ilość) IO, ale zrobiłem to metodą prób i błędów i nie byłem zbyt zadowolony z tego procesu. Napisałem również wiele własnych implementacji interfejsu, ale udokumentowanie, które metody są wymagane i co mają robić, aby inni członkowie mojego zespołu mogli tworzyć implementacje, okazało się wyzwaniem.
Patrick,
9
interface Konstrukt jest rzeczywiście potrzebne tylko do leczenia różnych typów jako samo w statycznie typowanych języków pojedynczego dziedziczenia (np treat LinkedHashSeti ArrayListzarówno jako Collection), ma praktycznie nic wspólnego z interfejsem jak owa odpowiedź pokazy. Ruby nie jest wpisywany statycznie, więc nie ma potrzeby konstruowania .
Esailija
16
Czytałem to jako "niektóre interfejsy nie mają sensu, dlatego interfejsy są złe. Dlaczego chcesz używać interfejsów?". Nie odpowiada na pytanie i, szczerze mówiąc, po prostu brzmi jak ktoś, kto nie rozumie, do czego służą interfejsy i jakie korzyści przynosi.
Oddman
13
Twój argument dotyczący nieważności interfejsu List poprzez przytoczenie metody, która wykonuje usuwanie w funkcji o nazwie „add”, jest klasycznym przykładem argumentu reductio ad absurdum. W szczególności możliwe jest w dowolnym języku (w tym ruby) napisanie metody, która robi coś innego niż to, czego się oczekuje. To nie jest ważny argument przeciwko "interfejsowi", to po prostu zły kod.
Justin Ohms,
58

Wypróbuj „udostępnione przykłady” rspec:

https://www.relishapp.com/rspec/rspec-core/v/3-5/docs/example-groups/shared-examples

Piszesz specyfikację swojego interfejsu, a następnie umieszczasz jedną linię w specyfikacji każdego implementującego, np.

it_behaves_like "my interface"

Kompletny przykład:

RSpec.shared_examples "a collection" do
  describe "#size" do
    it "returns number of elements" do
      collection = described_class.new([7, 2, 4])
      expect(collection.size).to eq(3)
    end
  end
end

RSpec.describe Array do
  it_behaves_like "a collection"
end

RSpec.describe Set do
  it_behaves_like "a collection"
end

Aktualizacja : Osiem lat później (2020 r.) Ruby obsługuje teraz statycznie typowane interfejsy przez sorbet. Zobacz klasy abstrakcyjne i interfejsy w dokumentacji sorbet.

Jared Beck
źródło
15
Uważam, że powinna to być akceptowana odpowiedź. W ten sposób większość słabych języków typu może zapewnić interfejsy podobne do Java. Zaakceptowany wyjaśnia, dlaczego Ruby nie ma interfejsów, a nie jak je emulować.
SystematicFrank
1
Zgadzam się, ta odpowiedź pomogła mi dużo bardziej jako programista Java, który przeszedł na Ruby, niż zaakceptowana powyżej.
Cam
Tak, ale cały interfejs polega na tym, że ma te same nazwy metod, ale to konkretne klasy muszą implementować zachowanie, które przypuszczalnie jest inne. Więc co mam testować w udostępnionym przykładzie?
Rob Wise
Ruby sprawia, że ​​wszystko jest pragmatyczne. Jeśli chciałbyś mieć udokumentowany i dobrze napisany kod, dodaj testy / specyfikacje, a to będzie rodzaj statycznej kontroli pisania.
Dmitry Polushkin
41

Czy możemy ujawnić interfejsy w Rubim, tak jak robimy to w Javie i wymusić na modułach lub klasach Rubiego implementację metod zdefiniowanych przez interfejs.

Ruby nie ma takiej funkcjonalności. W zasadzie ich nie potrzebuje, ponieważ Ruby używa tak zwanego pisania kaczego .

Istnieje kilka podejść, które możesz zastosować.

Napisz implementacje, które zgłaszają wyjątki; jeśli podklasa spróbuje użyć niezaimplementowanej metody, zakończy się to niepowodzeniem

class CollectionInterface
  def add(something)
    raise 'not implemented'
  end
end

Wraz z powyższym powinieneś napisać kod testowy, który wymusza twoje kontrakty (jaki inny post tutaj nieprawidłowo wywołuje interfejs )

Jeśli zauważysz, że piszesz metody void, jak zawsze, napisz moduł pomocniczy, który to wychwyci

module Interface
  def method(name)
    define_method(name) { |*args|
      raise "interface method #{name} not implemented"
    }
  end
end

class Collection
  extend Interface
  method :add
  method :remove
end

Teraz połącz powyższe z modułami Ruby i jesteś blisko tego, czego chcesz ...

module Interface
  def method(name)
    define_method(name) { |*args|
      raise "interface method #{name} not implemented"
    }
  end
end

module Collection
  extend Interface
  method :add
  method :remove
end

col = Collection.new # <-- fails, as it should

A potem możesz to zrobić

class MyCollection
  include Collection

  def add(thing)
    puts "Adding #{thing}"
  end
end

c1 = MyCollection.new
c1.add(1)     # <-- output 'Adding 1'
c1.remove(1)  # <-- fails with not implemented

Pozwólcie, że jeszcze raz podkreślę: jest to podstawa, ponieważ wszystko w Rubim dzieje się w czasie wykonywania; nie ma sprawdzania czasu kompilacji. Jeśli połączysz to z testowaniem, powinieneś być w stanie wykryć błędy. Co więcej, jeśli pójdziesz dalej, prawdopodobnie będziesz w stanie napisać interfejs, który sprawdza klasę po raz pierwszy, gdy tworzony jest obiekt tej klasy; Twoje testy są tak proste, jak zadzwonienie MyCollection.new... tak, przesadzone :)

carlosayam
źródło
Ok, ale jeśli twoja kolekcja = MyCollection implementuje metodę, która nie została zdefiniowana w interfejsie, działa to idealnie, więc nie możesz upewnić się, że obiekt ma tylko definicje metod interfejsu.
Joel AZEMAR
To jest niesamowite, dzięki. Duck typing jest w porządku, ale czasami dobrze jest wyraźnie poinformować innych programistów, jak powinien zachowywać się interfejs.
Mirodinho
10

Jak wszyscy mówili, nie ma systemu interfejsu dla Rubiego. Ale dzięki introspekcji możesz łatwo zaimplementować to samodzielnie. Oto prosty przykład, który można ulepszyć na wiele sposobów, aby ułatwić rozpoczęcie pracy:

class Object
  def interface(method_hash)
    obj = new
    method_hash.each do |k,v|
      if !obj.respond_to?(k) || !((instance_method(k).arity+1)*-1)
        raise NotImplementedError, "#{obj.class} must implement the method #{k} receiving #{v} parameters"
      end
    end
  end
end

class Person
  def work(one,two,three)
    one + two + three
  end

  def sleep
  end

  interface({:work => 3, :sleep => 0})
end

Usunięcie jednej z metod zadeklarowanych w Person lub zmiana jej liczby argumentów spowoduje wywołanie NotImplementedError.

fotanus
źródło
5

W Javie nie ma takich rzeczy jak interfejsy. Ale są też inne rzeczy, którymi możesz się cieszyć w rubinie.

Jeśli chcesz zaimplementować jakieś typy i interfejsy - aby obiekty można było sprawdzić, czy mają jakieś metody / komunikaty, których od nich wymagasz - możesz wtedy przyjrzeć się rubycontractom . Definiuje mechanizm podobny do PyProtocols . Blog o sprawdzaniu typów w Rubim jest tutaj .

Wspomniane podejścia nie są żywymi projektami, chociaż początkowo cel wydaje się fajny, wydaje się, że większość programistów Ruby może żyć bez ścisłego sprawdzania typu. Ale elastyczność ruby ​​umożliwia zaimplementowanie sprawdzania typu.

Jeśli chcesz rozszerzyć obiekty lub klasy (to samo w Ruby) o określone zachowania lub w pewnym sensie mieć Ruby sposób wielokrotnego dziedziczenia, użyj mechanizmu includelub extend. Za pomocą includemożesz dołączyć do obiektu metody z innej klasy lub modułu. Za pomocą extendmożesz dodać zachowanie do klasy, dzięki czemu jej instancje będą miały dodane metody. To było jednak bardzo krótkie wyjaśnienie.

Moim zdaniem najlepszym sposobem rozwiązania problemu interfejsu Java jest zrozumienie modelu obiektowego ruby ​​(patrz na przykład wykłady Dave'a Thomasa ). Zapewne zapomnisz o interfejsach Java. Lub masz wyjątkową aplikację w swoim harmonogramie.

fifigyuri
źródło
Te wykłady Dave'a Thomasa są za paywallem.
Purplejacket
5

Jak wskazuje wiele odpowiedzi, w Rubim nie ma sposobu, aby zmusić klasę do implementacji określonej metody poprzez dziedziczenie z klasy, w tym modułu lub czegokolwiek podobnego. Powodem tego jest prawdopodobnie rozpowszechnienie TDD w społeczności Ruby, czyli innego sposobu definiowania interfejsu - testy określają nie tylko sygnatury metod, ale także zachowanie. Dlatego jeśli chcesz zaimplementować inną klasę, która implementuje jakiś już zdefiniowany interfejs, musisz upewnić się, że wszystkie testy przejdą.

Zazwyczaj testy są definiowane w izolacji przy użyciu prób i kodów pośredniczących. Ale są też narzędzia takie jak Bogus , pozwalające na definiowanie testów kontraktowych. Takie testy nie tylko definiują zachowanie klasy „podstawowej”, ale także sprawdzają, czy metody stubbed istnieją w klasach współpracujących.

Jeśli naprawdę interesują cię interfejsy w Rubim, polecam użycie frameworka testowego, który implementuje testowanie kontraktowe.

Aleksander Pohl
źródło
3

Wszystkie przykłady tutaj są interesujące, ale brakuje walidacji kontraktu Interface, mam na myśli, że jeśli chcesz, aby twój obiekt zaimplementował wszystkie definicje metod interfejsu, a tylko te nie możesz. Proponuję więc szybki, prosty przykład (z pewnością można go ulepszyć), aby upewnić się, że masz dokładnie to, czego oczekujesz, dzięki interfejsowi (kontrakt).

rozważ swój interfejs z takimi zdefiniowanymi metodami

class FooInterface
  class NotDefinedMethod < StandardError; end
  REQUIRED_METHODS = %i(foo).freeze
  def initialize(object)
    @object = object
    ensure_method_are_defined!
  end
  def method_missing(method, *args, &block)
    ensure_asking_for_defined_method!(method)
    @object.public_send(method, *args, &block)
  end
  private
  def ensure_method_are_defined!
    REQUIRED_METHODS.each do |method|
      if !@object.respond_to?(method)
        raise NotImplementedError, "#{@object.class} must implement the method #{method}"
      end
    end
  end
  def ensure_asking_for_defined_method!(method)
    unless REQUIRED_METHODS.include?(method)
      raise NotDefinedMethod, "#{method} doesn't belong to Interface definition"
    end
  end
end

Następnie możesz napisać obiekt z przynajmniej kontraktem Interface:

class FooImplementation
  def foo
    puts('foo')
  end
  def bar
    puts('bar')
  end
end

Możesz bezpiecznie wywołać swój obiekt za pośrednictwem interfejsu, aby upewnić się, że jesteś dokładnie tym, co definiuje interfejs

#  > FooInterface.new(FooImplementation.new).foo
# => foo

#  > FooInterface.new(FooImplementation.new).bar
# => FooInterface::NotDefinedMethod: bar doesn't belong to Interface definition

Możesz również upewnić się, że obiekt implementuje wszystkie definicje metod interfejsu

class BadFooImplementation
end

#  > FooInterface.new(BadFooImplementation.new)
# => NotImplementedError: BadFooImplementation must implement the method foo
Joel AZEMAR
źródło
2

Rozszerzyłem nieco odpowiedź Carlosayama na moje dodatkowe potrzeby. Dodaje to kilka dodatkowych wymuszeń i opcji do klasy Interface: required_variablei optional_variablektóra obsługuje wartość domyślną.

Nie jestem pewien, czy chciałbyś użyć tego metaprogramowania z czymś zbyt dużym.

Jak stwierdziły inne odpowiedzi, najlepiej jest pisać testy, które odpowiednio egzekwują to, czego szukasz, zwłaszcza gdy chcesz zacząć wymuszać parametry i zwracać wartości.

Z zastrzeżeniem, że ta metoda zgłasza błąd tylko podczas wywoływania kodu. Testy będą nadal wymagane do prawidłowego egzekwowania prawa przed uruchomieniem.

Przykład kodu

interface.rb

module Interface
  def method(name)
    define_method(name) do
      raise "Interface method #{name} not implemented"
    end
  end

  def required_variable(name)
    define_method(name) do
      sub_class_var = instance_variable_get("@#{name}")
      throw "@#{name} must be defined" unless sub_class_var
      sub_class_var
    end
  end

  def optional_variable(name, default)
    define_method(name) do
      instance_variable_get("@#{name}") || default
    end
  end
end

plugin.rb

Użyłem biblioteki singleton dla danego wzorca, którego używam. W ten sposób wszystkie podklasy dziedziczą pojedynczą bibliotekę podczas implementowania tego „interfejsu”.

require 'singleton'

class Plugin
  include Singleton

  class << self
    extend Interface

    required_variable(:name)
    required_variable(:description)
    optional_variable(:safe, false)
    optional_variable(:dependencies, [])

    method :run
  end
end

my_plugin.rb

Dla moich potrzeb wymaga to, aby klasa implementująca "interface" była jej podklasą.

class MyPlugin < Plugin

  @name = 'My Plugin'
  @description = 'I am a plugin'
  @safe = true

  def self.run
    puts 'Do Stuff™'
  end
end
CTS_AE
źródło
2

Sam Ruby nie ma dokładnego odpowiednika interfejsów w Javie.

Ponieważ jednak taki interfejs może być czasami bardzo przydatny, sam opracowałem perełkę dla Rubiego, która emuluje interfejsy Java w bardzo prosty sposób.

To się nazywa class_interface.

Działa po prostu. Najpierw zainstaluj klejnot przez gem install class_interfacelub dodaj go do swojego Gemfile i rund bundle install.

Definiowanie interfejsu:

require 'class_interface'

class IExample
  MIN_AGE = Integer
  DEFAULT_ENV = String
  SOME_CONSTANT = nil

  def self.some_static_method
  end

  def some_instance_method
  end
end

Implementacja tego interfejsu:

class MyImplementation
  MIN_AGE = 21
  DEFAULT_ENV = 'dev' 
  SOME_CONSTANT = 'some_value'

  def specific_method
    puts "very specific"
  end

  def self.some_static_method
    puts "static method is implemented!"
  end

  def some_instance_method
    # implementation
  end

  def self.another_methods
    # implementation
  end

  implements IExample
end

Jeśli nie zaimplementujesz określonej stałej lub metody lub numer parametru nie pasuje, odpowiedni błąd zostanie zgłoszony przed wykonaniem programu Ruby. Możesz nawet określić typ stałych, przypisując typ w interfejsie. Jeśli nil, dozwolony jest dowolny typ.

Metoda „implements” musi zostać wywołana w ostatnim wierszu klasy, ponieważ jest to pozycja kodu, w której zaimplementowane powyżej metody są już sprawdzane.

Więcej na: https://github.com/magynhard/class_interface

magynhard
źródło
0

Zdałem sobie sprawę, że zbyt często używam wzorca „Nie zaimplementowano błędu” do sprawdzania bezpieczeństwa obiektów, dla których chciałem mieć określone zachowanie. Skończyło się na napisaniu klejnotu, który w zasadzie pozwala na użycie takiego interfejsu:

require 'playable' 

class Instrument 
  implements Playable
end

Instrument.new #will throw: Interface::Error::NotImplementedError: Expected Instrument to implement play for interface Playable

Nie sprawdza argumentów metod . Działa od wersji 0.2.0. Bardziej szczegółowy przykład na https://github.com/bluegod/rint

BLUEGoD
źródło