Що таке еквівалент інтерфейсу Java в Ruby?


101

Чи можемо ми розкрити інтерфейси в Ruby, як ми робимо в Java та застосувати модулі або класи Ruby для реалізації методів, визначених інтерфейсом.

Один із способів полягає у використанні успадкування та method_missing для досягнення того самого, але чи є ще якийсь більш підходящий підхід?



6
Вам слід подвійно запитати себе, навіщо вам це потрібно. Досить часто інтерфейси використовуються просто для того, щоб скласти чортову річ, яка не є проблемою в рубіні.
Арніс Лапса

1
Це питання може бути або не вважатися дублікатом [ У Ruby, що еквівалентно інтерфейсу в C #? ] ( StackOverflow.Com/q/3505521/#3507460 ).
Йорг W Міттаг

2
Навіщо мені це потрібно? Я хочу реалізувати те, що ви можете назвати "переконливим", що робить документи / файли вигідними, але переконливими, використовуючи те, що .... Бо, наприклад, я можу зробити його версійним за допомогою наявних програмних засобів сховища, таких як SVN або CVS. Який би непомірний механізм я не вибрав, він повинен забезпечувати основні мінімальні функції. Я хочу використовувати інтерфейс, як річ, щоб забезпечити реалізацію цих мінімальних функцій будь-якою новою базовою реалізацією сховища.
crazycrv

Санді Мец у своїй книзі POODR використовує тести для документування інтерфейсів. Справді варто прочитати цю книгу. Станом на 2015 рік, я б сказав, що відповідь @ aleksander-pohl найкраща.
Грег Дан

Відповіді:


85

У Ruby є інтерфейси, як і будь-яка інша мова.

Зауважте, що ви повинні бути обережними, щоб не співставити поняття Інтерфейс , який є абстрактною специфікацією обов'язків, гарантій та протоколів підрозділу з поняттям, interfaceяке є ключовим словом у програмах Java, C # та VB.NET мови. У Рубі ми постійно використовуємо перше, але останнє просто не існує.

Дуже важливо розрізняти два. Важливим є інтерфейс , а не інтерфейсinterface . У interfaceвас майже нічого корисного немає. Ніщо не демонструє це краще , ніж інтерфейси маркерів в Java, які є інтерфейси , які не мають членів на всіх: просто подивіться на java.io.Serializableі java.lang.Cloneable; ці два interfaceзначать дуже різні речі, але вони мають точно такий же підпис.

Отже, якщо два interfaces, що означають різні речі, мають один і той же підпис, що саме це вам interfaceнавіть гарантує?

Ще один хороший приклад:

package java.util;

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

Що таке інтерфейс з java.util.List<E>.add?

  • що довжина колекції не зменшується
  • що всі предмети, які були в колекції раніше, все ще є
  • що elementє в колекції

І хто з них насправді з'являється у interface? Жоден! У цьому немає нічого, interfaceщо говорить про те, що Addметод повинен взагалі додаватись , він може так само добре видалити елемент із колекції.

Це цілком коректна реалізація цього interface:

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

Інший приклад: де в java.util.Set<E>насправді йдеться про те, що це, знаєте, набір ? Нікуди! Або точніше, в документації. Англійською.

В основному у всіх випадках interfaces, як з Java, так і з .NET, вся відповідна інформація є фактично в документах, а не в типах. Тож, якщо типи все одно не розповідають тобі нічого цікавого, навіщо їх взагалі тримати? Чому б не дотримуватися лише документації? І саме цим займається Рубі.

Зауважте, що існують й інші мови, якими інтерфейс можна насправді описати змістовно. Однак ці мови, як правило, не називають конструкцію, яка описує " " інтерфейс, а " interface" type. Наприклад, у мові програмування залежно від типу ви можете висловити властивості, що sortфункція повертає колекцію тієї самої довжини, що й оригінал, що кожен елемент, що знаходиться в оригіналі, також знаходиться в відсортованій колекції і що немає більшого елемента з'являється перед меншим елементом.

Отже, коротше: у Ruby немає еквівалента Java interface. Він робить , однак, має еквівалент Java - інтерфейс , і це точно так же , як і в Java: документація.

Крім того, як і в Java, тести прийняття можуть використовуватися і для визначення інтерфейсів .

Зокрема, в Ruby інтерфейс об'єкта визначається тим, що він може робити , а не що classє, або що moduleвін змішує. До будь-якого об'єкта, який має <<метод, можна додати. Це дуже корисно в одиничних тестах, де ви можете просто Передавайте Arrayабо Stringзамість більш складних Logger, хоча Arrayі Loggerне поділяєте явні interfaceкрім того , що вони обидва мають метод , званий <<.

Інший приклад може служити StringIO, який реалізує той же інтерфейс , як IOі , отже , більша частина інтерфейсу з File, але без якого - або спільного загального предка , крім Object.


279
Хоча добре прочитане, я не вважаю відповідь такою корисною. Він читається як дисертація про те, чому interfaceмарний, пропускаючи сенс його використання. Було б простіше сказати, що рубін динамічно набирається і має інший фокус на увазі, і робить такі поняття, як МОК, непотрібними / небажаними. Це важка зміна, якщо ви звикли проектувати за контрактом. Щось може скористатися Rails, що основна команда зрозуміла, як ви бачите в останніх версіях.
goliatone

12
Наступне запитання: який найкращий спосіб документувати інтерфейс в Ruby? Ключове слово Java interfaceможе містити не всю релевантну інформацію, але воно забезпечує очевидне місце для розміщення документації. Я написав клас в Ruby, який реалізує (достатньо) IO, але я зробив це методом проб і помилок і не надто задоволений процесом. Я також написав декілька власних реалізацій інтерфейсу, але документування того, які методи потрібні та що вони повинні робити, щоб інші члени моєї команди могли створити реалізацію, виявилися проблемою.
Патрік

9
interface Конструкція дійсно необхідна тільки для лікування різних типів , як те ж саме в статично типізованих мовах одного спадкування (наприклад , лікувати LinkedHashSetі ArrayListяк в якості Collection), він не має досить багато нічого спільного з інтерфейсом , як ця відповідь показує. Рубі не набраний статично, тому немає потреби в конструкції .
Esailija

16
Я читаю це як "деякі інтерфейси не мають сенсу, тому інтерфейси погані. Навіщо ви хочете використовувати інтерфейси?". Це не відповідає на питання і, відверто кажучи, просто звучить як хтось, хто не розуміє, для чого потрібні інтерфейси, і яка їх користь.
Оддман

13
Ваш аргумент щодо недійсності інтерфейсу List, цитуючи метод, який виконує видалення у функції, що називається "додати", є класичним прикладом аргументу reductio ad absurdum. Зокрема, можна будь-якою мовою (включаючи рубін) написати метод, який робить щось інше, ніж очікується. Це не вагомий аргумент проти "інтерфейсу", це просто поганий код.
Джастін Омс

58

Спробуйте "спільні приклади" rspec:

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

Ви пишете специфікацію для свого інтерфейсу, а потім ставите один рядок у специфікації кожного реалізатора, наприклад.

it_behaves_like "my interface"

Повний приклад:

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

Оновлення : Через вісім років (2020) ruby ​​має підтримку статично типових інтерфейсів через сорбет. Див. Абстрактні класи та інтерфейси в документах сорбету.


15
Я вважаю, що це має бути прийнятою відповіддю. Це спосіб, коли більшість типів слабких мов можуть надавати Java як інтерфейси. Прийнятий пояснює, чому у Рубі немає інтерфейсів, а не як імітувати їх.
SystematicFrank

1
Я згоден, ця відповідь допомогла мені набагато більше, ніж розробник Java, який перейшов на Ruby, ніж вище прийнятий відповідь.
Cam

Так, але вся суть інтерфейсу полягає в тому, що він має однакові назви методів, але конкретні класи повинні бути тими, хто реалізує поведінку, яка, імовірно, різна. Отже, що я маю тестувати на спільному прикладі?
Роб Мудрий

Рубі робить все прагматичним. Якщо ви хочете мати задокументований і добре написаний код, додайте тести / характеристики, і це буде свого роду перевірка статичного набору тексту.
Дмитро Полушкин

41

Чи можемо ми розкрити інтерфейси в Ruby, як ми робимо в Java та застосувати модулі або класи Ruby для реалізації методів, визначених інтерфейсом.

У Ruby немає такої функціональності. В принципі, вони не потрібні їм, оскільки Рубі використовує те, що називається типом качок .

Є кілька підходів, які ви можете скористатися.

Напишіть реалізацію, яка збільшує винятки; якщо підклас намагається використати метод безреалізованого, він не вдасться

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

Поряд із вищезгаданим, ви повинні написати код тестування, який виконує ваші контракти (те, що інша публікація тут неправильно називається інтерфейсом )

Якщо ви виявляєте, що постійно пишете недійсні методи, як завжди, тоді напишіть допоміжний модуль, який це захоплює

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

Тепер поєднайте вищезгадане з модулями Ruby, і ви близькі до того, що вам хочеться ...

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

І тоді ви можете зробити

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

Дозвольте ще раз наголосити: це рудиментар, оскільки все в Ruby відбувається під час виконання; немає перевірки часу компіляції. Якщо ви поєднаєте це з тестуванням, то ви повинні мати можливість виявити помилки. Навіть далі, якщо взяти вищесказане далі, ви, ймовірно, зможете написати інтерфейс, який виконує перевірку класу при першому створенні об’єкта цього класу; що робить ваші тести такими ж простими, як дзвінки MyCollection.new... так, зверху :)


Гаразд, але якщо ваша колекція = MyCollection реалізує метод, не визначений в інтерфейсі, це працює чудово, тому ви не можете забезпечити, щоб ваш Об'єкт мав лише визначення методів інтерфейсу.
Джоель АЗЕМАР

Це досить приголомшливо, дякую. Введення качок чудово, але іноді корисно явно повідомляти іншим розробникам, як повинен вести себе інтерфейс.
Миродіньо

10

Як усі тут казали, для рубіну не існує інтерфейсної системи. Але за допомогою самоаналізу ви можете реалізувати це самостійно досить легко. Ось простий приклад, який можна вдосконалити багатьма способами, щоб допомогти розпочати роботу:

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

Видалення одного з методів, оголошених у Person, або зміна його кількості аргументів призведе до того, що NotImplementedError.


5

Немає таких речей, як інтерфейси способом Java. Але є й інші речі, якими можна насолоджуватися в рубіні.

Якщо ви хочете реалізувати якісь типи та інтерфейс - щоб об’єкти могли перевірити, чи є у них якісь методи / повідомлення, які ви вимагаєте від них, - тоді ви можете ознайомитися з рубіконтрактами . Він визначає механізм, подібний до PyProtocols . Блог про перевірку типу в рубіні тут .

Згадані підходи не є живими проектами, хоча мета спочатку здається гарною, але здається, що більшість розробників рубінів можуть жити без суворої перевірки типу. Але гнучкість рубіну дозволяє здійснити перевірку типу.

Якщо ви хочете розширити об'єкти або класи (те саме, що в рубіні) певним чином, або дещо мати рубіновий спосіб багатократного успадкування, скористайтеся механізмом includeабо extend. За допомогою includeвас можна включити в об’єкт методи іншого класу або модуля. Зextend ви можете додати поведінку в класі, так що його екземпляри матимуть додані методи. Хоча це було дуже коротке пояснення.

На мою думку, найкращий спосіб вирішити необхідність інтерфейсу Java - це зрозуміти модель об'єкта ruby ​​(див., Наприклад, лекції Дейва Томаса ). Можливо, ви забудете про інтерфейси Java. Або у вас є винятковий додаток у вашому графіку.


Ці лекції Дейва Томаса стоять за платною стіною.
Purplejacket

5

Як свідчить багато відповідей, у Ruby немає ніякого способу змусити клас реалізувати певний метод шляхом успадкування від класу, включаючи модуль чи щось подібне. Причиною тому, мабуть, є поширеність TDD у спільноті Ruby, що є іншим способом визначення інтерфейсу - тести не лише визначають підписи методів, але й поведінку. Таким чином, якщо ви хочете реалізувати інший клас, який реалізує деякий вже визначений інтерфейс, ви повинні переконатися, що всі тести проходять.

Зазвичай тести визначають ізольовано, використовуючи макети та заглушки. Але є також такі інструменти, як « Богус» , що дозволяють визначити контрактні тести. Такі тести не лише визначають поведінку "первинного" класу, але і перевіряють, чи існують затруднені методи у співпрацюючих класах.

Якщо ви справді переймаєтесь інтерфейсами в Ruby, я б рекомендував використовувати тестувальну основу, яка реалізує контрактне тестування.


3

Усі приклади тут цікаві, але відсутня перевірка контракту на інтерфейс, я маю на увазі, якщо ви хочете, щоб ваш об’єкт реалізував усі визначення методів інтерфейсу, і лише цей, який ви не можете. Тому я пропоную вам короткий простий приклад (можна точно покращити), щоб переконатися, що у вас є саме те, що ви розраховуєте мати через свій інтерфейс (контракт).

розгляньте свій інтерфейс із визначеними методами

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

Тоді ви можете написати об'єкт хоча б з договором інтерфейсу:

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

Ви можете безпечно зателефонувати за своїм Об’єктом через свій Інтерфейс, щоб переконатися, що ви точно визначаєте Інтерфейс

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

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

Ви також можете забезпечити, щоб ваш Object реалізував усі ваші визначення методів інтерфейсу

class BadFooImplementation
end

#  > FooInterface.new(BadFooImplementation.new)
# => NotImplementedError: BadFooImplementation must implement the method foo

2

Я трохи продовжив відповідь карлосаяма для моїх додаткових потреб. Це додає пару додаткових застосувань та параметрів до класу інтерфейсів: required_variableі optional_variableякий підтримує значення за замовчуванням.

Я не впевнений, що ви хочете використовувати це мета-програмування із чимось занадто великим.

Як було зазначено в інших відповідях, вам найкраще писати тести, які належним чином виконують те, що ви шукаєте, особливо після того, як ви хочете почати застосовувати параметри та повертати значення.

Caveat цей метод видає помилку лише при виклику коду. Випробування все ще потрібні для належного виконання до виконання.

Приклад коду

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

Я використовував бібліотеку синглів для заданої схеми, яку я використовую. Таким чином, будь-які підкласи успадковують бібліотеку синглів при реалізації цього "інтерфейсу".

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

Для моїх потреб це вимагає, щоб клас, що реалізує "інтерфейс", підкласи.

class MyPlugin < Plugin

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

  def self.run
    puts 'Do Stuff™'
  end
end

2

Сам Ruby не має точного еквіваленту інтерфейсам на Java.

Однак, оскільки такий інтерфейс іноді може бути дуже корисним, я сам розробив дорогоцінний камінь для Ruby, який імітує інтерфейси Java дуже просто.

Це називається class_interface.

Це працює досить просто. Спочатку встановіть дорогоцінний камінь, gem install class_interfaceабо додайте його у свій Gemfile та rund bundle install.

Визначення інтерфейсу:

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

Реалізація цього інтерфейсу:

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

Якщо ви не реалізуєте певну константу або метод або номер параметра не збігається, перед виконанням програми Ruby буде викликана відповідна помилка. Ви навіть можете визначити тип констант, призначивши тип в інтерфейсі. Якщо нуль, будь-який тип дозволений.

Метод "Implements" повинен бути викликаний в останньому рядку класу, оскільки це положення коду, де вже перевірені реалізовані методи.

Детальніше за посиланням: https://github.com/magynhard/class_interface


0

Я зрозумів, що я занадто сильно використовую шаблон "Не реалізована помилка" для перевірки безпеки об'єктів, яким я хотів конкретної поведінки. Написав дорогоцінний камінь, який в основному дозволяє використовувати такий інтерфейс:

require 'playable' 

class Instrument 
  implements Playable
end

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

Він не перевіряє аргументи методу . Це робиться за версією 0.2.0. Більш детальний приклад на https://github.com/bluegod/rint

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.