¿Por qué las interfaces requieren métodos sobre los miembros?

8

... Como esto nos obliga a crear captadores y establecedores, que en la práctica a menudo son totalmente extraños. ¿Existe alguna buena razón de diseño de lenguaje por la cual las interfaces en la mayoría de los idiomas (¿todos?) No permiten que los campos miembros ayuden a cumplir con las especificaciones de la interfaz?

En la mayoría de los idiomas existentes, los miembros reciben un trato fundamentalmente diferente de los métodos, pero ¿TIENE que ser así? En un lenguaje teórico, ¿no podríamos tener una interfaz que (a) especificara un método de cero argumentos como getter, pero aceptara un miembro legible como su implementador, y (b) especificara un método de un solo argumento como setter, pero aceptado un campo de miembro grabable como su implementador, dado un lenguaje que admite la especificación del control de acceso directo en las variables cuando se declaran? Un lenguaje prototípico que permita la herencia múltiple (para abordar el punto muy válido que algunos encuestados han hecho) sería ideal para algo como esto.

Por favor, infórmeme si ya existe algo como esto.

Ingeniero
fuente
2
Uh ... los métodos son miembros. Supongo que te refieres a "campos de miembros"?
Aaronaught
Está bien usar la terminología Java adecuada, sí, "campos de miembros". Gracias por señalar eso.
Ingeniero
1
OOP apesta, pero prueba C # si es necesario.
Trabajo

Respuestas:

3

Obviamente, en la mayoría de los idiomas existentes, los miembros reciben un trato fundamentalmente diferente de los métodos, pero ¿TIENE que ser así?

No por ninguna razón teóricamente fundamentada. Creo que las razones por las que no lo ves a menudo son prácticas:

La primera es cómo lidiar con estas cosas en tiempo de ejecución. En Java o C ++, una asignación a un miembro público de tipo primitivo foo.x = 5es fácil de manejar: cosas 5en algún registro, averiguar dónde en la memoria vive el xmiembro de la foovida y volcar el registro allí. En un lenguaje como C # donde las clases pueden encapsular las tareas de interceptación a los miembros o calcular previamente cómo evaluarán esos miembros , el compilador no tendrá a prioriconocimiento de si la implementación de una clase disponible en tiempo de ejecución utiliza esa característica. Eso significa que todas las asignaciones de miembros públicos y las evaluaciones de los miembros públicos tienen que diferirse para la implementación. * Las operaciones que involucran mucho acceso a esos miembros terminan teniendo un impacto en el rendimiento porque constantemente están haciendo llamadas de subrutina que podrían manejarse más rápido con regularidad. asignaciones en línea. Dejar la evaluación y la asignación separadas de los captadores y establecedores le da al desarrollador cierto control sobre cómo se manejan en una situación dada.

El segundo es la edad. Con la posible excepción de F #, ninguno de los idiomas de uso muy amplio en la actualidad tiene menos de una década, e incluso tan recientemente como en aquel entonces, los ciclos adicionales necesarios para hacer posible la evaluación interceptada y la asignación pueden no haber sido aceptables.

El tercero es la inercia. Creo que hasta cierto punto, todavía hay algún estigma asociado con la asignación directa a los miembros públicos, incluso si el lenguaje permite que la clase los manipule y la gente todavía piense en términos de que los captadores y los establecedores son mejores. Eso desaparecerá con el tiempo, tal como sucedió con todas las personas que tenían sus narices en el aire sobre las personas que escribieron programas en idiomas de nivel superior en lugar de asamblea.


* Hay una compensación que viene con esto: un lenguaje tiene que ir por completo con aplazar evaluaciones / asignaciones a la clase o rechazar en tiempo de ejecución si el código que usa evaluaciones / asignaciones en línea para una clase determinada intenta vincularse con una implementación que los intercepta . Personalmente, creo que esta última es una mala idea porque obliga a recompilar el código que usa una API que no ha cambiado.

Blrfl
fuente
Gracias, has entendido la esencia de mi pregunta y has entrado en los detalles arenosos de POR QUÉ. Tenía el presentimiento de que la velocidad sería una de las razones, como siempre ocurre cuando te diriges hacia el camino de "más dinámico". Como estamos viendo ahora con lenguajes como JS y Python, la flexibilidad a menudo supera la velocidad, por lo que la "inercia" que menciona hacia ciertas tendencias de diseño de lenguaje basadas en la optimización (lo que alguna vez fue necesario) se disuelve lentamente.
Ingeniero
F # se basa en gran medida en Objective CAML: puede ser más joven que una década, pero la mayoría (probablemente todas) de las ideas son más antiguas.
Steve314
1
Tengo curiosidad por saber por qué los dos downvoters presionaron el botón rojo.
Blrfl
@Blrfl: Mi punto de crítica sería que su respuesta se centra demasiado en los detalles de implementación, en lugar de mirarla desde el punto de vista del diseño del lenguaje. Sin embargo, no te rechacé, pero quizás otros sí.
Niels van der Rest
Scala también es relativamente joven y se usa ampliamente
nombre para mostrar
10

En la mayoría de los idiomas existentes, los miembros [campos?] Se tratan fundamentalmente de manera diferente a los métodos, pero ¿TIENE que ser así?

Si lo hace.

Los campos de miembros son datos por instancia. Los datos deben existir en algún lugar de la memoria para cada instancia; debe haber una dirección o secuencia de direcciones reservadas para ello. En Java, .NET y muchos otros lenguajes OO, estos datos van en el montón.

Los métodos son datos por clase. Son punteros a una tabla. Así es como los métodos obtienen el despacho virtual. En realidad, solo hay una instancia asignada, por clase.

Una interfaz es esencialmente un tipo especial de clase que solo contiene métodos, no datos de instancia. Eso es parte de la definición de interfaz. Si contuviera algún dato, ya no sería una interfaz, sería una clase abstracta. Ambas opciones están bien, por supuesto, dependiendo de qué tipo de patrón de diseño estés tratando de implementar. Pero si agrega datos de instancia a las interfaces, eliminaría todo lo que las convierte en interfaces.

Bien, entonces ¿por qué un método de interfaz no puede apuntar a un campo miembro? Porque no hay nada a nivel de clase a lo que señalar. Una vez más, una interfaz no es más que una tabla de métodos. El compilador solo puede generar una tabla, y cada método debe apuntar a la misma dirección de memoria. Por lo tanto, es imposible que los métodos de interfaz apunten a campos de clase, porque el campo de clase es una dirección diferente para cada instancia.

Quizás el compilador podría aceptar esto de todos modos y generar un método getter para que señale la interfaz, pero eso se convierte esencialmente en una propiedad . Si quieres saber por qué Java no tiene propiedades, bueno ... esa es una historia muy larga y tediosa en la que preferiría no entrar aquí. Pero si usted está interesado, es posible que desee echar un vistazo a otros lenguajes orientados a objetos, que hacen aplicar propiedades, tales como C # o Delphi. La sintaxis es realmente fácil:

public interface IFoo
{
    int Bar { get; }
}

public class Foo : IFoo
{
    public int Bar { get; set; }
}

Sí, eso es todo lo que hay que hacer. Importante: Estos no son campos, la "barra int" en la Fooclase es una propiedad automática , y el compilador está haciendo exactamente lo que acabo de describir anteriormente: generar automáticamente captadores y establecedores (así como el campo del miembro de respaldo).

Entonces, para recapitular la respuesta a tu pregunta:

  • Las interfaces requieren métodos porque su propósito completo no es contener ningún dato;
  • La razón por la que no existe un azúcar sintáctico para facilitar que una clase implemente una interfaz es simplemente que los diseñadores de Java no lo harán, y en este momento, la compatibilidad con versiones anteriores es un problema. Los diseñadores de idiomas hacen este tipo de elecciones; solo tendrás que lidiar con eso o cambiar.
Aaronaught
fuente
Esta es una buena respuesta, en particular, encontré la información sobre la búsqueda de funciones informativa. Gracias.
Ingeniero
9

Esto es el resultado de la ocultación de información, es decir, esos miembros no deberían ser visibles como campos sino como propiedades que pueden o no almacenarse / almacenarse en caché

Un ejemplo es el código hash que cambia cuando el objeto cambia, por lo que es mejor calcularlo cuando se solicita y no cada vez que cambia el objeto.

Además, la mayoría de los lenguajes son de herencia única + implementos múltiples y permitir que los campos formen parte de interfaces lo coloca en un territorio de herencia múltiple (lo único que falta es la implementación predeterminada de funciones), que es un campo minado de casos extremos para acertar

monstruo de trinquete
fuente
los campos de implementación múltiple son el problema aquí.
DeadMG
Debo decir, pensando más en esto, realmente no estoy de acuerdo con la herencia múltiple. Porque lo que realmente estoy preguntando es si es posible una representación de los miembros de datos en las interfaces, no si puedes usar miembros de datos para construir interfaces (que es esencialmente una clase abstracta de C ++). Hay una diferencia clave allí.
Ingeniero
Realmente no hay problema con MI; un lenguaje solo tendría que resistirse a implementar dos interfaces con tipos en conflicto para el mismo miembro.
Blrfl
6

Las variables miembro son la implementación de la clase, no la interfaz. Es posible que desee cambiar la implementación, por lo que no se debe permitir que otras clases hagan referencia a esa implementación directamente.

Considere una clase con los siguientes métodos: sintaxis de C ++, pero eso no es importante ...

class example
{
  public:
    virtual void Set_Mode (int p_mode);
    virtual int  Mode () const;
};

Parece bastante obvio que habrá una variable miembro llamada mode o m_modesimilar que almacena directamente ese valor de modo, haciendo más o menos lo que una "propiedad" haría en algunos idiomas, pero eso no es necesariamente cierto. Hay muchas formas diferentes de manejar esto. Por ejemplo, el método Set_Mode podría ...

  1. Identifique el modo usando una declaración de cambio.
  2. Instanciar una subclase de algunas clases internas, dependiendo de ese valor de modo.
  3. Almacene un puntero a esa instancia como una variable miembro.

Una vez hecho esto, muchos otros métodos de la clase de ejemplo podrían llamar métodos apropiados de esa clase interna a través del puntero, obteniendo un comportamiento específico del modo sin necesidad de verificar cuál es el modo actual.

El punto aquí es menos que hay más de una forma posible de implementar este tipo de cosas, y mucho más que en algún momento puede cambiar de opinión. Tal vez comenzaste con una variable simple, pero todas las declaraciones de cambio para identificar el modo en cada método están teniendo problemas. O tal vez comenzaste con el puntero de instancia, pero resultó ser demasiado pesado para tu situación.

Resulta que cosas como esta pueden suceder para los datos de cualquier miembro. Es posible que necesite cambiar las unidades en las que se almacena un valor de millas a kilómetros, o puede encontrar que su enumeración de identificación única del caso ya no puede identificar de manera única todos los casos sin considerar alguna información adicional, o lo que sea.

Esto también puede suceder con los métodos: algunos métodos son exclusivamente para uso interno, dependen de la implementación y deberían ser privados. Pero muchos métodos son parte de la interfaz y no será necesario cambiarles el nombre o lo que sea solo porque la implementación interna de la clase ha sido reemplazada.

De todos modos, si su implementación está expuesta, otro código inevitablemente comenzará a depender de él, y se le bloqueará para mantener esa implementación. Si su implementación está oculta de otro código, esa situación no puede suceder.

El bloqueo del acceso a los detalles de implementación se denomina "ocultación de datos".

Steve314
fuente
"Código para una interfaz, no una implementación" ... Correcto. A veces sabemos estas cosas de forma latente, pero se necesita a alguien para razonarlas como lo ha hecho, para volver a solidificar los conceptos.
Ingeniero
OK, espera un minuto. Después de pensarlo más, vea mi edición de la pregunta.
Ingeniero
Es un poco difícil saber cuál fue su edición; por alguna razón, las diferencias para algunas de las ediciones anteriores no se muestran. Sin embargo, lo que creo que dice es "¿por qué una propiedad no puede tener una variable miembro como implementación predeterminada?" Una propiedad se ve (interfaz) como una variable miembro, pero se implementa a través de métodos getter / setter. Y en mi opinión, no hay ninguna razón por la cual, por defecto, esos métodos getter / setter no deberían implementar una lectura / escritura en una variable miembro, o por qué la llamada getter y setter no deberían optimizarse cuando sea seguro, permitiendo la herencia, por separado compilación etc.
Steve314
@Nick - Entonces sí, siempre que sea posible decir "el captador y / o definidor de esta propiedad debería ser puro" o " no deberían ser los predeterminados", creo que es una buena idea: eliminar parte del desorden para un Caso común con propiedades. No sé si ya hay un lenguaje que lo haga, no he usado muchas propiedades incluso en los idiomas donde existen, pero me sorprendería si esto no es compatible.
Steve314
3

Como otros han señalado, una interfaz describe lo que hace una clase, no cómo lo hace. Es esencialmente un contrato que las clases heredadas deben cumplir.

Lo que usted describe es ligeramente diferente: una clase que proporciona alguna implementación pero quizás no toda, como una forma de reutilización. Estos se conocen generalmente como clases abstractas y se admiten en varios lenguajes (por ejemplo, C ++ y Java). Estas clases no pueden ser instanciadas por derecho propio, solo pueden ser heredadas por clases concretas (o más abstractas).

Nuevamente, como ya se señaló, las clases abstractas tienden a ser de mayor valor en los lenguajes que admiten herencia múltiple, ya que pueden usarse para proporcionar piezas de funcionalidad adicional a una clase más grande. Sin embargo, esto a menudo se considera un mal estilo, ya que generalmente una relación de agregación ("has-a") sería más apropiada

Steve Mallam
fuente
Buena respuesta. Tiendo a favorecer la composición casi por completo en el tipo de tareas que realizo en estos días. Personalmente, no creo en el tratamiento separado de las clases abstractas y las interfaces en ciertos idiomas, como han dicho otros en este foro, aunque hay razones para esto, generalmente estas razones se deben a limitaciones lingüísticas fundamentales (como se menciona en mi comentario en la respuesta de freak de trinquete).
Ingeniero
1
Tiendo a estar de acuerdo con tu comentario sobre: ​​resumen / interfaces. Cuando miro hacia atrás al antiguo código donde usaba muchas clases abstractas, es claro para mí con 10-15 años más de experiencia que estaba usando características del lenguaje para codificar el mal diseño
Steve Mallam
1

Por favor, infórmeme si ya existe algo como esto.

Las propiedades Objective-C proporcionan este tipo de funcionalidad con sus descriptores de acceso sintetizados. Una propiedad de Objective-C es esencialmente una promesa de que una clase proporciona accesores, típicamente de la forma foopara el getter y setFoo:para el setter. La declaración de propiedad especifica ciertos atributos de los accesores relacionados con la gestión de la memoria y los comportamientos de subprocesamiento. También hay una @synthesizedirectiva que le dice al compilador que genere accesores e incluso la variable de instancia correspondiente, por lo que no tiene que molestarse en escribir getters y setters o declarar un ivar para cada propiedad. También hay algo de azúcar sintáctico que hace que el acceso a la propiedad parezca acceder a los campos de una estructura, pero en realidad resulta en llamar a los accesores de la propiedad.

El resultado de todo eso es que puedo declarar una clase con propiedades:

@interface MyClass : NSObject
{}
@property (assign) int foo;
@property (copy) NSString *bar;
@end

y luego puedo escribir código que parece que accede a ivars directamente, pero de hecho usa métodos de acceso:

MyClass *myObject = [[MyClass alloc] init];  // create myObject

// setters
myObject.foo = 127;                 // same as [myObject setFoo:127];
myObject.bar = @"Hello, world!";    // same as [myObject setBar:@"Hello, world!"];

// getters
int i = myObject.foo                // same as [myObject foo];
NSString *s = myObject.bar          // same as [myObject bar];

Ya se ha dicho que los accesores le permiten separar la interfaz de la implementación, y la razón por la que desea hacerlo es preservar su capacidad de cambiar la implementación en el futuro sin afectar el código del cliente. El inconveniente es que debes ser lo suficientemente disciplinado para escribir y usar los accesores solo para mantener abiertas tus opciones futuras. Objective-C hace que generar y usar accesores sea tan fácil como usar ivars directamente. De hecho, debido a que se encargan de gran parte del trabajo de gestión de la memoria, el uso de las propiedades es en realidad mucho más fácil que acceder a los ivars directamente. Y cuando llegue el momento en que desee cambiar su implementación, siempre puede proporcionar sus propios accesores en lugar de dejar que el compilador los genere por usted.

Caleb
fuente
1

¿Por qué las interfaces requieren métodos sobre los miembros?

... Como esto nos obliga a crear captadores y establecedores, que en la práctica a menudo son totalmente extraños. ¿Hay alguna buena razón de diseño de lenguaje por la cual las interfaces en la mayoría de los idiomas (¿todos?) No permiten a los miembros ayudar a cumplir con las especificaciones de la interfaz?

¿"La mayoría" de los idiomas? ¿Qué idiomas has investigado? En lenguajes dinámicos no hay interfaces. Solo hay tres o cuatro lenguajes OO estáticamente tipados con alguna popularidad: Java, C #, Objective-C y C ++. C ++ no tiene interfaces; en su lugar, usamos clases abstractas, y una clase abstracta puede contener un miembro de datos público. C # y Objective-C son compatibles con las propiedades, por lo que no hay necesidad de getters y setters en esos lenguajes. Eso deja solo Java. Presumiblemente, la única razón por la que Java no admite propiedades es porque ha sido difícil introducirlas de una manera compatible con versiones anteriores.

Kevin Cline
fuente
Los miembros debían referirse a "campos de miembros" en lugar de "funciones de miembros" a los que me refiero simplemente como métodos. No estoy seguro de qué idioma (si lo hay) tomé esta convención. Entonces, dado lo que había escrito, veo su respuesta, pero supongo que un comentario debajo de mi pregunta para aclarar habría sido más constructivo.
Ingeniero
Re C ++, touché. Sin embargo, en realidad hay interfaces en lenguajes dinámicos, JS y Python son dos que inmediatamente vienen a la mente. Si un defensor de los lenguajes de tipo estático llamaría a estas "interfaces", es otra discusión en la que no veo ningún punto de entrar. Personalmente, muestro poca preferencia por cualquiera de los lados de esa cerca, utilizando como hago una amplia gama de idiomas para diferentes propósitos.
Ingeniero
@ Nick: JS y Python tienen herencia, pero no diría que tienen interfaces; Uno puede llamar a cualquier método en cualquier objeto.
Kevin Cline
1

Algunas de las respuestas mencionan 'ocultación de información' y 'detalles de implementación' como la razón por la cual los métodos de interfaz son preferibles a los campos. Creo que la razón principal es más simple que eso.

Las interfaces son una característica del lenguaje que le permite cambiar el comportamiento haciendo que múltiples clases implementen una interfaz. El comportamiento de una clase se define por sus métodos. Los campos, por otro lado, solo reflejan el estado de un objeto.

Por supuesto, los campos o propiedades de una clase pueden ser parte del comportamiento, pero no son la parte importante del contrato definido por la interfaz; son solo un resultado. Es por eso que, por ejemplo, C # le permite definir propiedades en una interfaz, y en Java usted define un captador en la interfaz. Aquí es donde entra en juego un poco de ocultación de información, pero no es el punto principal.

Como dije antes, las interfaces le permiten cambiar el comportamiento, pero echemos un vistazo a la parte de intercambio. Una interfaz le permite cambiar la implementación subyacente creando una nueva clase de implementación. Por lo tanto, tener una interfaz que consta de campos no tiene mucho sentido, porque no se puede cambiar mucho sobre la implementación de los campos.

La única razón por la que desea crear una interfaz de solo campo es cuando quiere decir que alguna clase es un cierto tipo de objeto. He enfatizado la parte 'es a', porque deberías usar herencia en lugar de interfaces.


Nota al margen: Sé que la composición que prefería sobre la herencia y la composición generalmente solo se puede lograr mediante el uso de interfaces, pero usar interfaces solo para 'etiquetar' una clase con algunas propiedades es igual de malo. La herencia es excelente cuando se usa apropiadamente, y siempre y cuando cumpla con el Principio de Responsabilidad Única, no debería tener ningún problema.

Quiero decir, nunca he trabajado con una biblioteca de control de IU, por ejemplo, que no usa herencia. La única área donde las interfaces entran en juego aquí es en el aprovisionamiento de datos, por ejemplo, ITableDataAdapterpara vincular datos a un control de tabla. Este es exactamente el tipo de cosa que las interfaces están destinadas a tener un comportamiento intercambiable .

Niels van der Rest
fuente
0

En Java, la razón es que las interfaces solo permiten especificar métodos .

Por lo tanto, si desea algo con un campo en el contrato de interfaz, debe hacerlo como método getter y setter.

También vea esta pregunta: ¿ Cuándo se justifican los captadores y establecedores?

Comunidad
fuente
1
Gracias por responder, en realidad fue su respuesta sobre ese mismo tema lo que me hizo hacer esta pregunta :) Pero la pregunta sigue siendo, "¿POR QUÉ- solo permitieron métodos?" El objetivo de las interfaces no es especificar métodos, eso es lógica cíclica. El objetivo de las interfaces es garantizar que se cumpla un grado básico de funcionalidad. Los miembros pueden formar parte de esa funcionalidad. Entonces, ¿por qué no permitirles participar en el cumplimiento de la interfaz?
Ingeniero
Yo no sé. Tal vez si le preguntas a Gosling o Steele directamente?
-1

El objetivo de una interfaz es especificar las funciones que debe implementar para afirmar que admite esta interfaz.

Scott C Wilson
fuente
2
Esto no responde la pregunta. La pregunta es ¿POR QUÉ lo hicieron de esa manera? ¿Alguna vez has considerado no escribir getters y setters para cada variable y aún así poder cumplir con una interfaz? ¡Eso ahorraría tiempo!
Ingeniero
Sigues preguntando sobre captadores y establecedores, y no tengo idea de por qué. Mire una interfaz simple: download.oracle.com/javase/6/docs/api/java/util/… ¿Observe cómo no hay captadores y establecedores?
Scott C Wilson
1
¿Qué prueba eso? ¿Ese ejemplo demuestra que nunca necesitarás acceso simple / directo a los miembros? No. El punto es que es una tontería tener miembros de envoltura en captadores / establecedores como parte de la interfaz, si el lenguaje en sí mismo puede lidiar con eso automáticamente.
Ingeniero
Hay cientos de otros ejemplos como este. ¿Me puede dar un ejemplo de la situación de la que está hablando?
Scott C Wilson
-1

Los miembros no están permitidos en las interfaces porque esos lenguajes tendrían que lidiar con el mal horrendo que es, la herencia múltiple.

DeadMG
fuente
2
No estoy seguro de seguir. ¿Por qué permitir que las variables miembro en una interfaz requieran o impliquen soporte para herencia múltiple?
Steve Mallam