Comprender la "programación a una interfaz"

30

Me he encontrado con el término "programar para una interfaz en lugar de una implementación", y creo que entiendo lo que significa. Pero quiero asegurarme de entender sus beneficios y sus posibles implementaciones.

"Programación en una interfaz" significa que, cuando sea posible, uno debe referirse a un nivel más abstracto de una clase (una interfaz, clase abstracta o, a veces, una superclase de algún tipo), en lugar de referirse a una implementación concreta.

Un ejemplo común en Java es usar:

List myList = new ArrayList();en lugar de ArrayList myList = new ArrayList();.

Tengo dos preguntas con respecto a esto:

  1. Quiero asegurarme de comprender los principales beneficios de este enfoque. Creo que los beneficios son principalmente flexibilidad. Declarar un objeto como una referencia de más alto nivel, en lugar de una implementación concreta, permite una mayor flexibilidad y mantenimiento durante todo el ciclo de desarrollo y en todo el código. ¿Es esto correcto? ¿Es la flexibilidad el principal beneficio?

  2. ¿Hay más formas de 'programar en una interfaz'? ¿O es "declarar una variable como una interfaz en lugar de una implementación concreta" la única implementación de este concepto?

No estoy hablando de la interfaz de construcción Java . Estoy hablando del principio OO "programar en una interfaz, no en una implementación". En este principio, la "interfaz" mundial se refiere a cualquier "supertipo" de una clase : una interfaz, una clase abstracta o una superclase simple que es más abstracta y menos concreta que sus subclases más concretas.

Aviv Cohn
fuente
posible duplicado de ¿Por qué son útiles las interfaces?
mosquito
1
Esta respuesta le brinda un ejemplo fácil de entender programmers.stackexchange.com/a/314084/61852
Tulains Córdova

Respuestas:

46

"Programación en una interfaz" significa que, cuando sea posible, uno debe referirse a un nivel más abstracto de una clase (una interfaz, clase abstracta o, a veces, una superclase de algún tipo), en lugar de referirse a una implementación concreta.

Esto no es correcto . O al menos, no es del todo correcto.

El punto más importante proviene de una perspectiva de diseño de programa. Aquí, "programar en una interfaz" significa enfocar su diseño en lo que está haciendo el código, no en cómo lo hace. Esta es una distinción vital que empuja su diseño hacia la corrección y la flexibilidad.

La idea principal es que los dominios cambian mucho más lentamente que el software. Digamos que tiene un software para realizar un seguimiento de su lista de compras. En los años 80, este software funcionaría contra una línea de comando y algunos archivos planos en disquete. Entonces tienes una interfaz de usuario. Entonces, tal vez ponga la lista en la base de datos. Más tarde, tal vez se trasladó a la nube o los teléfonos móviles o la integración de Facebook.

Si diseñó su código específicamente en torno a la implementación (disquetes y líneas de comando) no estaría preparado para los cambios. Si diseñó su código alrededor de la interfaz (manipulando una lista de compras), entonces la implementación es libre de cambiar.

Telastyn
fuente
Gracias por la respuesta. A juzgar por lo que escribiste, creo que entiendo lo que significa "programar en una interfaz" y cuáles son sus beneficios. Pero tengo una pregunta: el ejemplo concreto más común para este concepto es el siguiente: al crear una referencia a un objeto, haga que el tipo de referencia sea el tipo de interfaz que este objeto implementa (o la superclase que hereda este objeto), en lugar de hacer el tipo de referencia El tipo de objeto. (Aka, en List myList = new ArrayList()lugar de ArrayList myList = new ArrayList(). (La pregunta está en el siguiente comentario)
Aviv Cohn
Mi pregunta es: ¿Pueden darme más ejemplos de lugares en el código del mundo real donde se lleva a cabo el principio de "programación a una interfaz"? ¿Aparte del ejemplo común que describí en el último comentario?
Aviv Cohn
66
NO . List/ ArrayListNo es lo que estoy hablando en absoluto. Es más como proporcionar un Employeeobjeto en lugar del conjunto de tablas vinculadas que usa para almacenar un registro de Empleado. O proporcionar una interfaz para iterar a través de las canciones, y no preocuparse si esas canciones se mezclan, o en un CD, o se transmiten desde Internet. Son solo una secuencia de canciones.
Telastyn
1
La existencia del 'botón turbo': en.wikipedia.org/wiki/Turbo_button es un ejemplo real del mundo real de la analogía de su disquete.
JimmyJames
1
@AvivCohn Sugeriría SpringFramework como un buen ejemplo. Todo en Spring se puede mejorar o personalizar haciendo su propia impedancia de sus interfaces y el comportamiento principal de Springs, y las características seguirán funcionando según lo esperado ... O en caso de personalizaciones, como esperaba. La programación en una interfaz también es la mejor estrategia para diseñar marcos de integración . Nuevamente Spring lo hace con su integración de resortes. En tiempo de diseño, la programación en una interfaz es lo que hacemos con UML. O es lo que haría. Abstraerme de cómo funciona y enfocarme en qué hacer.
Laiv
19

Mi comprensión de "programar en una interfaz" es diferente de lo que sugieren la pregunta u otras respuestas. Lo que no quiere decir que mi comprensión sea correcta, o que las cosas en las otras respuestas no sean buenas ideas, solo que no son lo que pienso cuando escucho ese término.

La programación en una interfaz significa que cuando se le presenta alguna interfaz de programación (ya sea una biblioteca de clases, un conjunto de funciones, un protocolo de red o cualquier otra cosa), debe usar solo las cosas garantizadas por la interfaz. Es posible que tenga conocimiento sobre la implementación subyacente (puede que la haya escrito), pero nunca debe usar ese conocimiento.

Por ejemplo, supongamos que la API le presenta un valor opaco que es un "identificador" de algo interno. Su conocimiento podría decirle que este identificador es realmente un puntero, y podría desreferenciarlo y acceder a algún valor, lo que podría permitirle realizar fácilmente alguna tarea que desee hacer. Pero la interfaz no le brinda esa opción; es su conocimiento de la implementación particular que lo hace.

El problema con esto es que crea un fuerte acoplamiento entre su código y la implementación, exactamente lo que se suponía que la interfaz debía evitar. Dependiendo de la política, puede significar que la implementación ya no se puede cambiar, porque eso rompería su código, o que su código es muy frágil y sigue rompiendo en cada actualización o cambio de la implementación subyacente.

Un gran ejemplo de esto son los programas escritos para Windows. WinAPI es una interfaz, pero muchas personas usaron trucos que funcionaron debido a la implementación particular en, digamos Windows 95. Estos trucos quizás hicieron que sus programas fueran más rápidos o les permitieron hacer cosas en menos código de lo que hubiera sido necesario. Pero estos trucos también significaron que el programa colapsaría en Windows 2000, porque la API se implementó de manera diferente allí. Si el programa fuera lo suficientemente importante, Microsoft podría continuar y agregar algún truco a su implementación para que el programa continúe funcionando, pero el costo de esto es una mayor complejidad (con todos los problemas posteriores) del código de Windows. También hace la vida más difícil para la gente de Wine, porque también intentan implementar WinAPI, pero solo pueden consultar la documentación para saber cómo hacerlo,

Sebastian Redl
fuente
Ese es un buen punto, y lo escucho mucho en ciertos contextos.
Telastyn
Te entiendo. Entonces, déjame ver si puedo adaptar lo que estás diciendo a la programación general: digamos que tengo una clase (clase A) que utiliza la funcionalidad de la clase abstracta B. Las clases C y D heredan la clase B: proporcionan una implementación concreta de lo que Se dice que la clase hace. Si la clase A usa directamente la clase C o D, se llama 'programación para una implementación', que no es una solución muy flexible. Pero si la clase A usa una referencia a la clase B, que luego se puede establecer en la implementación C o la implementación D, hace que las cosas sean más flexibles y mantenibles. ¿Es esto correcto?
Aviv Cohn
Si esto es correcto, entonces mi pregunta es: ¿hay más ejemplos concretos para 'programar en una interfaz', además del ejemplo común 'usar una referencia de interfaz en lugar de una referencia de clase concreta'?
Aviv Cohn
2
@AvivCohn Un poco tarde en esta respuesta, pero un ejemplo concreto es la red mundial. Durante la guerra de los navegadores (IE 4 era), los sitios web se escribieron no para lo que decía cualquier especificación, sino para las peculiaridades de algunos navegadores (Netscape o IE). Básicamente se trataba de programar la implementación en lugar de la interfaz.
Sebastian Redl el
9

Solo puedo hablar de mi experiencia personal, ya que nunca me la han enseñado formalmente.

Tu primer punto es correcto. La flexibilidad obtenida proviene de no poder invocar accidentalmente detalles de implementación de la clase concreta donde no deberían invocarse.

Por ejemplo, considere una ILoggerinterfaz que se implementa actualmente como una LogToEmailLoggerclase concreta . La LogToEmailLoggerclase expone todos los ILoggermétodos y propiedades, pero también tiene una propiedad específica de implementación sysAdminEmail.

Cuando se utiliza su registrador en su aplicación, no debería ser la preocupación del código consumidor configurar el sysAdminEmail. Esta propiedad debe establecerse durante la configuración del registrador y debe ocultarse del mundo.

Si estaba codificando contra la implementación concreta, podría establecer accidentalmente la propiedad de implementación al usar el registrador. Ahora, su código de aplicación está estrechamente acoplado a su registrador, y cambiar a otro registrador requerirá primero desacoplar su código del original.

En este sentido, la codificación de una interfaz afloja el acoplamiento .

Con respecto a su segundo punto: Otra razón que he visto para codificar una interfaz es reducir la complejidad del código.

Por ejemplo, imagina que tengo un juego con las siguientes interfaces I2DRenderable, I3DRenderable, IUpdateable. No es raro que los componentes de un solo juego tengan contenido renderizable en 2D y 3D. Otros componentes pueden ser solo 2D y otros solo 3D.

Si un módulo realiza la representación en 2D, entonces tiene sentido que mantenga una colección de I2DRenderables. No importa si los objetos de su colección también lo son I3DRenderableo IUpdateblesi otros módulos serán responsables de tratar esos aspectos de los objetos.

Almacenar los objetos renderizables como una lista de I2DRenderablemantiene baja la complejidad de la clase de renderizado. La lógica de renderizado y actualización 3D no es de su incumbencia, por lo que esos aspectos de sus objetos secundarios pueden y deben ignorarse.

En este sentido, la codificación de una interfaz mantiene baja la complejidad al aislar las preocupaciones .

MetaFight
fuente
4

Quizás hay dos usos de la palabra interfaz que se usa aquí. La interfaz a la que se refiere principalmente en su pregunta es una interfaz Java . Ese es específicamente un concepto de Java, más generalmente es una interfaz de lenguaje de programación.

Diría que programar en una interfaz es un concepto más amplio. Las API REST ahora populares que están disponibles para muchos sitios web son otro ejemplo del concepto más amplio de programación para una interfaz en un nivel superior. Al crear una capa entre el funcionamiento interno de su código y el mundo exterior (personas en Internet, otros programas, incluso otras partes del mismo programa) puede cambiar cualquier cosa dentro de su código siempre que no cambie lo que el mundo exterior está esperando, donde eso está definido por una interfaz o contrato que pretendes cumplir.

Eso le proporciona la flexibilidad para refactorizar su código interno sin tener que contar todas las demás cosas que dependen de su interfaz.

También significa que su código debería ser más estable. Al apegarse a la interfaz, no debería romper el código de otras personas. Cuando realmente tiene que cambiar la interfaz, puede lanzar una nueva versión principal (1.abc a 2.xyz) de la API que indica que hay cambios importantes en la interfaz de la nueva versión.

Como @Doval señala en los comentarios sobre esta respuesta, también hay una localidad de errores. Creo que todo esto se reduce a la encapsulación. Tal como lo usaría para objetos en un diseño orientado a objetos, este concepto también es útil en un nivel superior.

Encaitar
fuente
1
Un beneficio que generalmente se pasa por alto es la localidad de los errores. Supongamos que necesita un mapa y lo implementa utilizando un árbol binario. Para que esto funcione, las claves deben tener un cierto orden, y debe mantener invariante que las claves que son "menores que" la clave del nodo actual están en el subárbol izquierdo, mientras que las que son "mayores que" están activadas El subárbol correcto. Cuando oculta la implementación del Mapa detrás de una interfaz, si una búsqueda del Mapa sale mal, sabe que el error debe estar en el módulo del Mapa. Si está expuesto, el error podría estar en cualquier parte del programa. Para mí, este es el principal beneficio.
Doval
4

Una analogía del mundo real podría ayudar:

Un enchufe de electricidad de red en una interfaz.
Sí; esa cosa de tres clavijas en el extremo del cable de alimentación de su televisor, radio, aspiradora, lavadora, etc.

Cualquier dispositivo que tenga un enchufe principal (es decir, implemente la interfaz "tiene un enchufe de red") puede tratarse exactamente de la misma manera; todos se pueden enchufar a una toma de corriente y pueden extraer energía de esa toma.

Lo que cada aparato individuo hace es completamente diferente. No va a llegar muy lejos limpiando sus alfombras con el televisor y la mayoría de la gente no mira su lavadora para entretenerse. Pero todos estos dispositivos comparten el comportamiento común de poder enchufarse a una toma de corriente.

Eso es lo que te dan las interfaces. Comportamientos
unificados que pueden ser llevados a cabo por muchas clases diferentes de objetos, sin la necesidad de las complicaciones de la herencia.

Phill W.
fuente
1

El término "programación a una interfaz" está abierto a mucha interpretación. Interfaz en el desarrollo de software es una palabra muy común. Así es como explico el concepto a los desarrolladores junior que he entrenado a lo largo de los años.

En la arquitectura de software hay una amplia gama de límites naturales. Los ejemplos comunes incluyen

  • El límite de la red entre el cliente y los procesos del servidor.
  • El límite de API entre una aplicación y una biblioteca de terceros
  • Límite interno del código entre diferentes dominios comerciales dentro del programa

Lo que importa es que cuando existen estos límites naturales, se identifican y se especifica el contrato de cómo se comporta ese límite. Usted prueba su software no si el "otro lado" se comporta, sino si sus interacciones coinciden con la especificación.

Las consecuencias de esto son:

  • Los componentes externos se pueden intercambiar siempre que implementen la especificación
  • Punto natural para pruebas unitarias para validar el comportamiento correcto
  • Las pruebas de integración se vuelven importantes: ¿fue la especificación ambigua?
  • Como desarrollador, tiene un mundo de preocupación menor cuando trabaja en una tarea en particular

Si bien gran parte de esto puede relacionarse con clases e interfaces, es tan importante darse cuenta de que también se relaciona con modelos de datos, protocolos de red y, de manera más general, trabajar con múltiples desarrolladores

Michael Shaw
fuente