¿Qué es la interfaz Java equivalente en Ruby?

102

¿Podemos exponer interfaces en Ruby como lo hacemos en java y hacer cumplir los módulos o clases de Ruby para implementar los métodos definidos por interfaz?

Una forma es usar la herencia y method_missing para lograr lo mismo, pero ¿hay algún otro enfoque más apropiado disponible?

crazycrv
fuente
1
Haga clic en esta url lawrencesong.net/2008/01/implement-java-interface-in-ruby
Sankar Ganesh
6
Deberías preguntarte dos veces por qué necesitas esto. A menudo se usan suficientes interfaces solo para compilar una maldita cosa, lo cual no es un problema en ruby.
Arnis Lapsa
1
Esta pregunta puede o no considerarse un duplicado de [ En Ruby, ¿cuál es el equivalente a una interfaz en C #? ] ( StackOverflow.Com/q/3505521/#3507460 ).
Jörg W Mittag
2
¿Por qué necesito esto? Quiero implementar algo que pueda llamar "versionable" que haga que los documentos / archivos sean versionables pero versionables usando qué .... Por ejemplo, puedo hacerlo versionable usando software de repositorio existente como SVN o CVS. Cualquiera que sea el mecanismo infalible que elija, debería proporcionar algunas funciones básicas mínimas. Quiero usar una interfaz similar para hacer cumplir la implementación de estas funciones mínimas por cualquier nueva implementación de repositorio subyacente.
crazycrv
Sandi Metz en su libro POODR usa pruebas para documentar interfaces. Realmente vale la pena leer este libro. A partir de 2015, diría que la respuesta de @ aleksander-pohl es la mejor.
Greg Dan

Respuestas:

85

Ruby tiene interfaces como cualquier otro idioma.

Tenga en cuenta que debe tener cuidado de no confundir el concepto de Interfaz , que es una especificación abstracta de las responsabilidades, garantías y protocolos de una unidad, con el concepto de interfaceque es una palabra clave en la programación Java, C # y VB.NET. Idiomas En Ruby, usamos el primero todo el tiempo, pero el segundo simplemente no existe.

Es muy importante distinguir los dos. Lo importante es la interfaz , no el interface. No interfacete dice prácticamente nada útil. Nada demuestra esto mejor que las interfaces de marcador en Java, que son interfaces que no tienen miembros en absoluto: solo eche un vistazo a java.io.Serializabley java.lang.Cloneable; esos dos interfacesignifican cosas muy diferentes, pero tienen exactamente la misma firma.

Entonces, si dos interfaces que significan cosas diferentes, tienen la misma firma, ¿ qué es exactamente lo queinterface te garantiza?

Otro buen ejemplo:

package java.util;

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

¿Cuál es la interfaz de java.util.List<E>.add?

  • que la duración de la colección no disminuya
  • que todos los elementos que estaban en la colección antes todavía están allí
  • que elementesta en la coleccion

¿Y cuál de ellos aparece realmente en el interface? ¡Ninguna! No hay nada en el interfaceque diga que el Addmétodo deba agregar nada, también podría eliminar un elemento de la colección.

Esta es una implementación perfectamente válida de eso interface:

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

Otro ejemplo: ¿en java.util.Set<E>qué lugar dice realmente que es, ya sabes, un conjunto ? ¡En ninguna parte! O más precisamente, en la documentación. En inglés.

En casi todos los casos interfaces, tanto de Java como de .NET, toda la información relevante está realmente en los documentos, no en los tipos. Entonces, si los tipos no te dicen nada interesante de todos modos, ¿por qué conservarlos? ¿Por qué no ceñirse solo a la documentación? Y eso es exactamente lo que hace Ruby.

Tenga en cuenta que hay otros idiomas en los que la interfaz se puede describir de manera significativa. Sin embargo, esos lenguajes normalmente no llaman a la construcción que describe la interfaz " interface", la llaman type. En un lenguaje de programación de tipo dependiente, puede, por ejemplo, expresar las propiedades de que una sortfunción devuelve una colección de la misma longitud que el original, que cada elemento que está en el original también está en la colección ordenada y que ningún elemento más grande aparece antes de un elemento más pequeño.

Entonces, en resumen: Ruby no tiene un equivalente a Java interface. Se hace , sin embargo, tiene un equivalente a una aplicación Java de interfaz , y es exactamente el mismo que en Java: documentación.

Además, al igual que en Java, las pruebas de aceptación también se pueden utilizar para especificar interfaces .

En particular, en Ruby, la interfaz de un objeto está determinada por lo que puede hacer , no por lo classque es ni por lo que modulese mezcla. Se puede agregar cualquier objeto que tenga un <<método. Esto es muy útil en las pruebas unitarias, donde se puede simplemente pasar en una Arrayo Stringen lugar de una más complicada Logger, a pesar de que Arrayy Loggerno compartir una explícita interfaceaparte del hecho de que ambos tienen un método llamado <<.

Otro ejemplo es StringIO, que implementa la misma de interfaz como IOy por lo tanto una gran parte de la interfaz de File, pero sin compartir ninguna antepasado común además Object.

Jörg W Mittag
fuente
279
Si bien es una buena lectura, no encuentro la respuesta tan útil. Se lee como una disertación sobre por qué interfacees inútil, perdiendo el sentido de su uso. Hubiera sido más fácil decir que ruby ​​se escribe dinámicamente y que tiene un enfoque diferente en mente y hace que conceptos como IOC sean innecesarios / no deseados. Es un cambio difícil si está acostumbrado a diseñar por contrato. Algo de lo que Rails podría beneficiarse, de lo que el equipo central se dio cuenta, como puede ver en las últimas versiones.
goliatone
12
Pregunta de seguimiento: ¿cuál es la mejor manera de documentar una interfaz en Ruby? Es interfaceposible que una palabra clave de Java no proporcione toda la información relevante, pero proporciona un lugar obvio para colocar la documentación. Escribí una clase en Ruby que implementa (suficiente) IO, pero lo hice por prueba y error y no estaba muy contento con el proceso. También he escrito varias implementaciones de una interfaz propia, pero documentar qué métodos son necesarios y qué se supone que deben hacer para que otros miembros de mi equipo puedan crear implementaciones resultó ser un desafío.
Patrick
9
De hecho, la interface construcción solo es necesaria para tratar diferentes tipos como iguales en lenguajes de herencia única de tipo estático (por ejemplo, tratar LinkedHashSety ArrayListambos como a Collection), no tiene prácticamente nada que ver con Interface como muestra esta respuesta. Ruby no se escribe estáticamente, por lo que no es necesario el constructo .
Esailija
16
Leí esto como "algunas interfaces no tienen sentido, por lo tanto, las interfaces son malas. ¿Por qué querrías usar interfaces?". No responde a la pregunta y, francamente, suena como alguien que no entiende para qué sirven las interfaces y su beneficio.
Oddman
13
Su argumento sobre la invalidez de la interfaz List al citar un método que realiza una eliminación en una función llamada "agregar" es un ejemplo clásico de un argumento reductio ad absurdum. En particular, es posible en cualquier idioma (incluido ruby) escribir un método que haga algo diferente de lo esperado. Este no es un argumento válido contra la "interfaz", es solo un código incorrecto.
Justin Ohms
58

Pruebe los "ejemplos compartidos" de rspec:

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

Escribe una especificación para su interfaz y luego coloca una línea en la especificación de cada implementador, por ejemplo.

it_behaves_like "my interface"

Ejemplo completo:

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

Actualización : ocho años después (2020), ruby ​​ahora tiene soporte para interfaces de tipo estático a través de sorbete. Consulte Clases e interfaces abstractas en los documentos de sorbete.

Jared Beck
fuente
15
Creo que esta debería ser la respuesta aceptada. Esta es la forma en que la mayoría de los tipos de lenguajes débiles pueden proporcionar interfaces de tipo Java. El aceptado explica por qué Ruby no tiene interfaces, no cómo emularlas.
SystematicFrank
1
Estoy de acuerdo, esta respuesta me ayudó mucho más como desarrollador de Java a migrar a Ruby que la respuesta aceptada anteriormente.
Cam
Sí, pero el objetivo de una interfaz es que tiene los mismos nombres de método, pero las clases concretas tienen que ser las que implementen el comportamiento, que presumiblemente es diferente. Entonces, ¿qué se supone que debo probar en el ejemplo compartido?
Rob Wise
Ruby hace que todo sea pragmático. Si desea tener un código documentado y bien escrito, agregue pruebas / especificaciones y eso será una especie de verificación de escritura estática.
Dmitry Polushkin
41

¿Podemos exponer interfaces en Ruby como lo hacemos en java y hacer cumplir los módulos o clases de Ruby para implementar los métodos definidos por interfaz?

Ruby no tiene esa funcionalidad. En principio, no los necesita, ya que Ruby usa lo que se llama escritura pato .

Hay algunos enfoques que puede tomar.

Escriba implementaciones que generen excepciones; si una subclase intenta utilizar el método no implementado, fallará

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

Junto con lo anterior, debe escribir un código de prueba que haga cumplir sus contratos (qué otra publicación aquí llama incorrectamente a la interfaz )

Si se encuentra escribiendo métodos vacíos como el anterior todo el tiempo, escriba un módulo de ayuda que capture eso

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

Ahora, combine lo anterior con módulos Ruby y estará cerca de lo que desea ...

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

Y luego puedes hacer

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

Permítanme enfatizar una vez más: esto es rudimentario, ya que todo en Ruby sucede en tiempo de ejecución; no hay comprobación del tiempo de compilación. Si combina esto con las pruebas, debería poder detectar errores. Aún más, si lleva más allá lo anterior, probablemente podría escribir una interfaz que realice la verificación de la clase la primera vez que se crea un objeto de esa clase; haciendo que sus pruebas sean tan simples como llamar MyCollection.new... sí, exagerado :)

carlosayam
fuente
Bien, pero si su Colección = Mi colección implementa un método no definido en la Interfaz, esto funciona perfectamente, por lo que no puede asegurarse de que su Objeto solo tenga las definiciones de los métodos de la Interfaz.
Joel AZEMAR
Esto es bastante impresionante, gracias. Duck typing está bien, pero a veces es bueno comunicar explícitamente a otros desarrolladores cómo debe comportarse una interfaz.
Mirodinho
10

Como todos dijeron aquí, no existe un sistema de interfaz para ruby. Pero a través de la introspección, puede implementarlo usted mismo con bastante facilidad. Aquí hay un ejemplo simple que se puede mejorar de muchas maneras para ayudarlo a comenzar:

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

Eliminar uno de los métodos declarados en Person o cambiar su número de argumentos generará un NotImplementedError.

fotanus
fuente
5

No existen las interfaces a la manera de Java. Pero hay otras cosas que puedes disfrutar en ruby.

Si desea implementar algún tipo de tipos e interfaz, de modo que los objetos se puedan verificar si tienen algunos métodos / mensajes que necesita de ellos, puede echar un vistazo a rubycontracts . Define un mecanismo similar a los PyProtocols . Un blog sobre verificación de tipos en ruby ​​está aquí .

Los enfoques mencionados no son proyectos vivos, aunque el objetivo parece ser bueno al principio, parece que la mayoría de los desarrolladores de ruby ​​pueden vivir sin una estricta verificación de tipos. Pero la flexibilidad de ruby ​​permite implementar la verificación de tipos.

Si desea extender objetos o clases (lo mismo en ruby) mediante ciertos comportamientos o tener la forma ruby ​​de la herencia múltiple, use el mecanismo includeo extend. Con includepuede incluir métodos de otra clase o módulo en un objeto. Con extendpuede agregar comportamiento a una clase, para que sus instancias tengan los métodos agregados. Sin embargo, esa fue una explicación muy corta.

En mi opinión, la mejor manera de resolver la necesidad de la interfaz Java es comprender el modelo de objetos ruby ​​(consulte las conferencias de Dave Thomas, por ejemplo). Probablemente se olvide de las interfaces Java. O tiene una aplicación excepcional en su agenda.

Fifigyuri
fuente
Esas conferencias de Dave Thomas están detrás de un muro de pago.
Purplejacket
5

Como indican muchas respuestas, Ruby no tiene forma de obligar a una clase a implementar un método específico, heredando de una clase, incluido un módulo o algo similar. La razón de esto es probablemente la prevalencia de TDD en la comunidad Ruby, que es una forma diferente de definir la interfaz: las pruebas no solo especifican las firmas de los métodos, sino también el comportamiento. Por lo tanto, si desea implementar una clase diferente, que implemente alguna interfaz ya definida, debe asegurarse de que todas las pruebas pasen.

Por lo general, las pruebas se definen de forma aislada mediante simulaciones y códigos auxiliares. Pero también hay herramientas como Bogus , que permiten definir pruebas de contrato. Tales pruebas no solo definen el comportamiento de la clase "primaria", sino que también verifican que los métodos stubped existen en las clases cooperantes.

Si está realmente preocupado por las interfaces en Ruby, le recomendaría usar un marco de prueba que implemente las pruebas de contrato.

Aleksander Pohl
fuente
3

Todos los ejemplos aquí son interesantes, pero falta la validación del contrato de interfaz, quiero decir, si desea que su objeto implemente toda la definición de métodos de interfaz y solo estos, no puede. Así que te propongo un ejemplo rápido y sencillo (seguro que se puede mejorar) para asegurarte de que tienes exactamente lo que esperas tener a través de tu interfaz (el contrato).

considere su interfaz con los métodos definidos como ese

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

Entonces puede escribir un objeto con al menos el contrato de interfaz:

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

Puede llamar a su Objeto de forma segura a través de su Interfaz para asegurarse de que es exactamente lo que define la Interfaz

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

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

Y también puede asegurarse de que su objeto implemente toda la definición de sus métodos de interfaz

class BadFooImplementation
end

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

He extendido un poco la respuesta de carlosayam para mis necesidades adicionales. Esto agrega un par de implementaciones y opciones adicionales a la clase Interfaz: required_variabley optional_variableque admite un valor predeterminado.

No estoy seguro de que desee utilizar esta metaprogramación con algo demasiado grande.

Como han indicado otras respuestas, es mejor que escriba pruebas que hagan cumplir adecuadamente lo que está buscando, especialmente una vez que desee comenzar a aplicar parámetros y valores de retorno.

Advertencia: este método solo arroja un error al llamar al código. Las pruebas seguirían siendo necesarias para una aplicación adecuada antes del tiempo de ejecución.

Ejemplo de código

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

Usé la biblioteca singleton para el patrón dado que estoy utilizando. De esta forma, cualquier subclases hereda la biblioteca singleton al implementar esta "interfaz".

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

Para mis necesidades, esto requiere que la clase que implementa la "interfaz" la subclasifique.

class MyPlugin < Plugin

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

  def self.run
    puts 'Do Stuff™'
  end
end
CTS_AE
fuente
2

Ruby en sí no tiene un equivalente exacto a las interfaces en Java.

Sin embargo, dado que dicha interfaz a veces puede ser muy útil, yo mismo desarrollé una joya para Ruby, que emula las interfaces de Java de una manera muy simple.

Se llama class_interface.

Funciona de forma bastante sencilla. Primero instale la gema gem install class_interfaceo agréguela a su Gemfile y rund bundle install.

Definición de una interfaz:

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

Implementando esa interfaz:

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

Si no implementa una determinada constante o método o el número de parámetro no coincide, se generará un error correspondiente antes de que se ejecute el programa Ruby. Incluso puede determinar el tipo de constantes asignando un tipo en la interfaz. Si es nulo, se permite cualquier tipo.

El método "implements" debe ser llamado en la última línea de una clase, porque esa es la posición del código donde los métodos implementados arriba ya están marcados.

Más en: https://github.com/magynhard/class_interface

magynhard
fuente
0

Me di cuenta de que estaba usando demasiado el patrón "Error no implementado" para las comprobaciones de seguridad de objetos que deseaban un comportamiento específico. Terminé escribiendo una joya que básicamente permite usar una interfaz como esta:

require 'playable' 

class Instrument 
  implements Playable
end

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

No comprueba los argumentos del método . Lo hace a partir de la versión 0.2.0. Ejemplo más detallado en https://github.com/bluegod/rint

AZUL
fuente