Что эквивалентно интерфейсу Java в Ruby?

105

Можем ли мы предоставить интерфейсы в Ruby, как мы это делаем в java, и заставить модули или классы Ruby реализовать методы, определенные интерфейсом.

Один из способов - использовать наследование и method_missing для достижения того же, но есть ли другой более подходящий подход?

crazycrv
источник
1
Щелкните этот URL-адрес lawrencesong.net/2008/01/implement-java-interface-in-ruby
Санкар Ганеш
6
Вы должны дважды спросить себя, зачем вам это вообще нужно. Часто интерфейсы используются просто для того, чтобы скомпилировать чертову штуку, что в ruby ​​не проблема.
Арнис Лапса,
1
Этот вопрос можно рассматривать как дубликат [ В Ruby, что эквивалентно интерфейсу в C #? ] ( StackOverflow.Com/q/3505521/#3507460 ).
Jörg W Mittag
2
Зачем мне это нужно? Я хочу реализовать что-то, что вы можете назвать «версионируемым», что делает документы / файлы версионируемыми, но версионируемыми с помощью чего… Например, я могу сделать его версионным с помощью существующих программ репозитория, таких как SVN или CVS. Какой бы базовый механизм я ни выбрал, он должен обеспечивать некоторые базовые минимальные функции. Я хочу использовать интерфейс как вещь, чтобы обеспечить реализацию этих минимальных функций любой новой реализацией базового репозитория.
crazycrv
Санди Метц в своей книге POODR использует тесты для документирования интерфейсов. Книгу действительно стоит прочитать. По состоянию на 2015 год я бы сказал, что ответ @ aleksander-pohl является лучшим.
Грег Дэн

Ответы:

85

У Ruby есть интерфейсы, как и у любого другого языка.

Обратите внимание, что вы должны быть осторожны, чтобы не объединить концепцию интерфейса , которая является абстрактной спецификацией обязанностей, гарантий и протоколов модуля, с концепцией, interfaceкоторая является ключевым словом в программировании на Java, C # и VB.NET. языков. В Ruby мы все время используем первое, а второго просто не существует.

Это очень важно различать. Важен интерфейс , а не файл 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.

Йорг В. Миттаг
источник
282
Хотя я хорошо прочитал, я не считаю ответ полезным. Она читается как диссертация о том, почему interfaceона бесполезна, без смысла ее использования. Было бы проще сказать, что Ruby динамически типизирован и имеет другой фокус и делает такие концепции, как IOC, ненужными / нежелательными. Это тяжелый сдвиг, если вы привыкли к дизайну по контракту. Кое-что может принести пользу Rails, что основная команда осознала, как вы можете видеть в последних версиях.
goliatone 07
12
Последующий вопрос: как лучше всего задокументировать интерфейс на Ruby? Ключевое слово Java interfaceможет не предоставлять всю необходимую информацию, но дает очевидное место для размещения документации. Я написал класс на Ruby, который реализует (достаточное количество) ввода-вывода, но я сделал это методом проб и ошибок и был не очень доволен процессом. Я также написал несколько реализаций собственного интерфейса, но документирование того, какие методы требуются и что они должны делать, чтобы другие члены моей команды могли создавать реализации, оказалось сложной задачей.
Патрик
9
Эта interface конструкция действительно нужна только для обработки разных типов как одного и того же в статически типизированных языках с одним наследованием (например, рассматривать LinkedHashSetи ArrayListоба как a Collection), она практически не имеет ничего общего с интерфейсом, как показывает этот ответ. Ruby не имеет статической типизации, поэтому в конструкции нет необходимости .
Esailija
16
Я прочитал это как «некоторые интерфейсы не имеют смысла, следовательно, интерфейсы плохие. Почему вы хотите использовать интерфейсы?». Он не отвечает на вопрос и, откровенно говоря, просто звучит так, будто кто-то не понимает, для чего нужны интерфейсы и от чего они приносят пользу.
Oddman
13
Ваш аргумент о недействительности интерфейса List, цитирующий метод, который выполняет удаление в функции с именем «add», является классическим примером аргумента reductio ad absurdum. В частности, на любом языке (включая Ruby) можно написать метод, который делает что-то отличное от ожидаемого. Это не веский аргумент против «интерфейса», это просто плохой код.
Джастин
61

Попробуйте "общие примеры" 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 ​​теперь поддерживает статически типизированные интерфейсы через sorbet. См. Абстрактные классы и интерфейсы в документации по сорбету.

Джаред Бек
источник
17
Я считаю, что это должен быть принятый ответ. Именно так большинство слабых типов языков могут предоставлять интерфейсы, подобные Java. Принятый объясняет, почему у Ruby нет интерфейсов, а не то, как их эмулировать.
SystematicFrank
1
Я согласен, этот ответ помог мне как разработчику Java, переходящему на Ruby, гораздо больше, чем принятый выше ответ.
Cam
Да, но весь смысл интерфейса в том, что он имеет одинаковые имена методов, но конкретные классы должны быть теми, которые реализуют поведение, которое, по-видимому, отличается. Итак, что я должен тестировать в общем примере?
Роб Уайз
Ruby делает все прагматичным. Если вы хотите иметь документированный и хорошо написанный код, добавьте тесты / спецификации, и это будет своего рода проверкой статической типизации.
Дмитрий Полушкин
42

Можем ли мы предоставить интерфейсы в Ruby, как мы это делаем в java, и заставить модули или классы Ruby реализовать методы, определенные интерфейсом.

Ruby не имеет такой функциональности. В принципе, они ему не нужны, поскольку Ruby использует так называемую утиную типизацию .

Есть несколько подходов, которые вы можете использовать.

Напишите реализации, вызывающие исключения; если подкласс пытается использовать нереализованный метод, он потерпит неудачу

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

Наряду с вышеизложенным, вы должны написать тестовый код, который обеспечивает выполнение ваших контрактов (какой другой пост здесь неправильно называет Интерфейс )

Если вы все время пишете методы void, как указано выше, напишите вспомогательный модуль, который фиксирует это

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... да, сверх меры :)

Карлосаям
источник
Хорошо, но если ваш Collection = MyCollection реализует метод, не определенный в интерфейсе, это работает отлично, поэтому вы не можете гарантировать, что ваш объект имеет только определения методов интерфейса.
Джоэл АЗЕМАР
Это здорово, спасибо. Утиная печать - это хорошо, но иногда полезно явно сообщить другим разработчикам, как должен вести себя интерфейс.
Mirodinho
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 не существует таких вещей, как интерфейсы. Но есть и другие вещи, которыми вы можете наслаждаться в рубине.

Если вы хотите реализовать какие-то типы и интерфейс - чтобы можно было проверить объекты, есть ли у них какие-то методы / сообщения, которые вам требуются, - вы можете взглянуть на rubycontracts . Он определяет механизм, аналогичный PyProtocols . Блог о проверке типов в Ruby находится здесь .

Упомянутые подходы не являются живыми проектами, хотя цель поначалу кажется хорошей, кажется, что большинство разработчиков Ruby могут жить без строгой проверки типов. Но гибкость ruby ​​позволяет реализовать проверку типов.

Если вы хотите расширить объекты или классы (то же самое в ruby) определенным поведением или иметь какой-то рубиновый способ множественного наследования, используйте механизм includeили extend. С includeего помощью вы можете включать в объект методы из другого класса или модуля. С участиемextend вы можете добавить поведение к классу, чтобы его экземпляры имели добавленные методы. Однако это было очень короткое объяснение.

Я считаю, что лучший способ решить потребность в интерфейсе Java - это понять объектную модель Ruby (например, см. Лекции Дэйва Томаса ). Возможно, вы забудете об интерфейсах Java. Или у вас есть исключительное приложение в вашем расписании.

фифигюри
источник
Те лекции Дэйва Томаса находятся за платным доступом.
Purplejacket
5

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

Обычно тесты определяются изолированно с использованием имитаторов и заглушек. Но есть также такие инструменты, как Bogus , позволяющие определять тесты контрактов. Такие тесты не только определяют поведение «основного» класса, но также проверяют, существуют ли заглушенные методы в взаимодействующих классах.

Если вас действительно интересуют интерфейсы в 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

И вы также можете убедиться, что ваш объект реализует все ваши определения методов интерфейса

class BadFooImplementation
end

#  > FooInterface.new(BadFooImplementation.new)
# => NotImplementedError: BadFooImplementation must implement the method foo
Джоэл АЗЕМАР
источник
2

Я немного расширил ответ Карлосаяма для моих дополнительных потребностей. Это добавляет пару дополнительных принудительных мер и параметров к классу Interface: required_variableи optional_variableкоторый поддерживает значение по умолчанию.

Я не уверен, что вы захотите использовать это метапрограммирование с чем-то слишком большим.

Как указывалось в других ответах, вам лучше всего писать тесты, которые правильно обеспечивают соблюдение того, что вы ищете, особенно если вы хотите начать принудительное соблюдение параметров и возвращаемых значений.

Обратите внимание: этот метод вызывает ошибку только при вызове кода. Перед запуском все равно потребуются тесты для надлежащего применения.

Пример кода

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
CTS_AE
источник
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 возникнет соответствующая ошибка. Вы даже можете определить тип констант, назначив тип в интерфейсе. Если nil, разрешен любой тип.

Метод «реализует» должен вызываться в последней строке класса, потому что это позиция кода, где уже проверены реализованные выше методы.

Подробнее на: https://github.com/magynhard/class_interface

Magynhard
источник
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

BLuEGoD
источник