Co jest odpowiednikiem interfejsu java w Ruby?

Czy możemy ujawnić interfejsy w Rubim tak jak to robimy w Javie i wymusić Moduły lub klasy Rubiego, aby zaimplementować metody zdefiniowane przez interfejs.

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

Author: crazycrv, 2010-12-14

9 answers

Ruby ma Interfejsy tak jak każdy inny język.

Należy pamiętać, że należy uważać, aby nie łączyć pojęcia interfejsu , który jest abstrakcyjną specyfikacją obowiązków, gwarancji i protokołów jednostki z pojęciem interface, które jest słowem kluczowym w Javie, C# i C#. VB.NET języki programowania. W Rubim używamy pierwszego przez cały czas, ale drugiego po prostu nie ma.

Bardzo ważne jest, aby wyróżnić dwa. Ważny jest interfejs , a nie interface. interface nie mówi nic użytecznego. Nic nie pokazuje tego lepiej niż marker interfaces w Javie, które są interfejsami, które w ogóle nie mają członków: wystarczy spojrzeć na java.io.Serializable oraz java.lang.Cloneable; te dwa znaczą Bardzo różne rzeczy, ale mają dokładnie ten sam podpis.

Więc jeśli dwa interfaceoznaczają różne rzeczy, mają to samo podpis, co dokładnie gwarantuje ci interface?

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ść zbioru nie zmniejsza się
  • że wszystkie przedmioty, które były wcześniej w kolekcji, nadal tam są
  • to {[11] } jest w zbiorze

I który z nich faktycznie pojawia się w interface? Żadnego! W interface nie ma nic, co by mówiło, że Add metoda musi nawet dodać w ogóle, może równie dobrze usunąć element z kolekcji.

Jest to doskonale poprawna implementacja 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> czy jest napisane, że jest to, no wiesz, zestaw ? Nigdzie! A dokładniej w dokumentacji. Po Angielsku.

W prawie wszystkich przypadkach interfaces, zarówno z Javy jak i. NET, wszystkie istotne informacje są właściwie w dokumentach, nie w typach. Więc, jeśli typy i tak nie mówią nic ciekawego, po co je w ogóle trzymać? Dlaczego nie trzymać się tylko dokumentacji? I to właśnie robi Ruby.

Zauważ, że istnieją inne języki, w których interfejs może być właściwie opisany w znaczący sposób. Jednak języki te zazwyczaj nie wywołują konstrukcji opisującej interfejs "interface", nazywają to type. W zależności od typu język programowania, można na przykład wyrazić właściwości, że funkcja sort zwraca kolekcję o tej samej długości co oryginał, że każdy element, który jest w oryginale, jest również w sortowanej kolekcji i że żaden większy element nie pojawia się przed mniejszym elementem.

W skrócie: Ruby nie ma odpowiednika Javy interface. W przeciwieństwie do Javy, interfejs Java jest podobny do interfejsu Java, który jest identyczny z interfejsem Java.: dokumentacja.

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

W szczególności, w Rubim, interfejs obiektu jest określony przez to, co może zrobić , a nie to, co class jest, lub w co module miesza się. Do każdego obiektu posiadającego metodę << można dołączyć. Jest to bardzo przydatne w testach jednostkowych, gdzie można po prostu przejść w Array lub String zamiast bardziej skomplikowanego Logger, nawet chociaż Array i Logger nie mają jednoznacznego interface, poza tym, że obie mają metodę o nazwie <<.

Innym przykładem jest StringIO, który implementuje ten sam interfejs Jak IO, a więc dużą część interfejsu z File, ale bez wspólnego przodka poza Object.

 86
Author: Jörg W Mittag,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-05-20 17:34:21

Wypróbuj "wspólne przykłady" rspec:

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

Piszesz spec dla swojego interfejsu, a następnie umieszczasz jedną linię w specyfikacji każdego implementatora, 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
 37
Author: Jared Beck,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-01-10 16:32:17

Czy możemy wystawiać interfejsy w Rubim, tak jak to robimy w Javie i wymuszać Moduły lub klasy Rubiego, aby zaimplementować metody zdefiniowane przez interfejs.

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

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

Pisać implementacje, które wywołują wyjątki; jeśli podklasa spróbuje użyć metody nie zaimplementowanej, będzie fail

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 znajdziesz się pisząc metody void jak zawsze, to napisz moduł pomocniczy, który przechwytuje to

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

And then you can do

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

Pozwolę sobie jeszcze raz podkreślić: jest to elementarny, jak 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 weźmiesz powyższe pod uwagę, prawdopodobnie będziesz w stanie napisać interfejs, który sprawdza klasę po raz pierwszy, gdy obiekt tej klasy jest tworzony; co sprawia, że testy są tak proste, jak wywołanie MyCollection.new... tak, na topie:)

 25
Author: carlosayam,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-07-16 07:09:20

Jak wszyscy tutaj mówili, nie ma żadnego systemu interfejsu dla Rubiego. Ale poprzez introspekcję możesz ją całkiem łatwo wdrożyć. Oto prosty przykład, który można poprawić na wiele sposobów, aby pomóc Ci zacząć:

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 przez osobę lub zmiana jej liczby argumentów spowoduje powstanie NotImplementedError.

 9
Author: fotanus,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-10-16 14:40:35

Nie ma czegoś takiego jak interfejsy w Javie. Ale są inne rzeczy, które możesz cieszyć się w ruby.

Jeśli chcesz zaimplementować jakieś typy i interfejsy-aby obiekty mogły być sprawdzane, czy mają jakieś metody/wiadomości, których od nich wymagasz-możesz rzucić okiem narubycontracts . Definiuje mechanizm podobny do PyProtocols . Blog o sprawdzaniu typu w Rubim jest tutaj .

Wspomniane podejście nie jest żywe projekty, chociaż cel wydaje się być miły na początku, wydaje się, że większość programistów ruby może żyć bez ścisłego sprawdzania typu. Ale elastyczność ruby pozwala zaimplementować sprawdzanie typu.

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

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

 4
Author: fifigyuri,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-11-25 08:16:28

Jak wiele odpowiedzi wskazuje, nie ma sposobu, aby zmusić klasę do zaimplementowania 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, co jest innym sposobem definiowania interfejsu - testy określają nie tylko sygnatury metod, ale także zachowanie. Tak więc jeśli chcesz zaimplementować inną klasę, która implementuje jakiś już zdefiniowany interfejs, masz aby upewnić się, że wszystkie testy przechodzą.

Zazwyczaj testy są definiowane w izolacji za pomocą mocks i stubs. Ale są też narzędzia takie jak Bogus , umożliwiające 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 testing, który implementuje testowanie kontraktów.

 4
Author: Aleksander Pohl,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-05-15 20:20:19

Wszystkie przykłady tutaj są interesujące, ale brakuje walidacji umowy interfejsu, to znaczy, jeśli chcesz, aby twój obiekt zaimplementował definicję wszystkich metod interfejsu, a tylko tych, których nie możesz. proponuję więc szybki prosty przykład (można poprawić na pewno), aby upewnić się, że masz dokładnie to, czego oczekujesz za pośrednictwem interfejsu (umowy).

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 [email protected]_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

Wtedy można napisać obiekt z co najmniej Kontrakt interfejsu:

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

Możesz wywołać obiekt bezpiecznie przez interfejs, 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

I równie dobrze możesz zapewnić, że Twój obiekt zaimplementuje wszystkie definicje metod interfejsu

class BadFooImplementation
end

#  > FooInterface.new(BadFooImplementation.new)
# => NotImplementedError: BadFooImplementation must implement the method foo
 3
Author: Joel AZEMAR,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-05-28 15:04:36

Zdałem sobie sprawę, że używam wzorca "nie zaimplementowany błąd" zbyt dużo do kontroli bezpieczeństwa obiektów, które chciałem określonego zachowania. Skończyło się na napisaniu klejnotu, który w zasadzie pozwala na korzystanie z interfejsu takiego jak ten:

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 metody . Działa od wersji 0.2.0. Bardziej szczegółowy przykład na https://github.com/bluegod/rint

 0
Author: BLuEGoD,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-10-07 15:10:43

Rozszerzyłem trochę o odpowiedź carlosayama na moje dodatkowe potrzeby. Dodaje to kilka dodatkowych wymuszeń i opcji do klasy interfejsu: required_variable i optional_variable, która obsługuje wartość domyślną.

Nie jestem pewien, czy chciałbyś używać tego meta programowania z czymś zbyt dużym.

Jak stwierdziły inne odpowiedzi, najlepiej napisać testy, które prawidłowo wymuszają to, czego szukasz, zwłaszcza gdy chcesz zacząć wymuszać parametry i zwracać wartości.

Caveat ta metoda powoduje błąd tylko przy wywołaniu kodu. Testy nadal będą wymagane do prawidłowego wykonania przed uruchomieniem.

Przykład Kodu

Interfejs.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 podanego wzoru, z którego korzystam. W ten sposób wszystkie podklasy dziedziczą bibliotekę singleton podczas implementacji 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 moje potrzeby wymaga to, aby Klasa implementująca "interfejs" go podklasowała.

class MyPlugin < Plugin

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

  def self.run
    puts 'Do Stuff™'
  end
end
 0
Author: CTS_AE,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-12-27 19:13:49