¿Por qué la mayoría de los arquitectos de sistemas insisten en programar primero una interfaz?

StackOverflow https://stackoverflow.com/questions/48605

Pregunta

Casi todos los libros de Java que leo hablan sobre el uso de la interfaz como una forma de compartir el estado y el comportamiento entre objetos que, cuando se "construyeron" por primera vez, no parecían compartir una relación.

Sin embargo, cada vez que veo arquitectos diseñando una aplicación, lo primero que hacen es empezar a programar en una interfaz.¿Cómo?¿Cómo sabes todas las relaciones entre objetos que ocurrirán dentro de esa interfaz?Si ya conoce esas relaciones, ¿por qué no simplemente ampliar una clase abstracta?

¿Fue útil?

Solución

Programar en una interfaz significa respetar el "contrato" creado al utilizar esa interfaz.Y entonces si tu IPoweredByMotor La interfaz tiene un start() método, futuras clases que implementen la interfaz, ya sean MotorizedWheelChair, Automobile, o SmoothieMaker, al implementar los métodos de esa interfaz, agregue flexibilidad a su sistema, porque una pieza de código puede arrancar el motor de muchos tipos diferentes de cosas, porque todo lo que esa pieza de código necesita saber es que responden a start().No importa cómo empiezan, solo que ellos debe comenzar.

Otros consejos

Gran pregunta.te referiré a Josh Bloch en Java efectivo, quien escribe (elemento 16) por qué preferir el uso de interfaces a clases abstractas.Por cierto, si no tienes este libro, ¡te lo recomiendo mucho!Aquí un resumen de lo que dice:

  1. Las clases existentes se pueden actualizar fácilmente para implementar una nueva interfaz. Todo lo que necesitas hacer es implementar la interfaz y agregar los métodos requeridos.Las clases existentes no se pueden adaptar fácilmente para ampliar una nueva clase abstracta.
  2. Las interfaces son ideales para definir mezclas. Una interfaz combinada permite que las clases declaren comportamientos opcionales adicionales (por ejemplo, Comparable).Permite mezclar la funcionalidad opcional con la funcionalidad principal.Las clases abstractas no pueden definir combinaciones: una clase no puede extenderse a más de un padre.
  3. Las interfaces permiten marcos no jerárquicos. Si tiene una clase que tiene la funcionalidad de muchas interfaces, puede implementarlas todas.Sin interfaces, tendría que crear una jerarquía de clases inflada con una clase para cada combinación de atributos, lo que daría como resultado una explosión combinatoria.
  4. Las interfaces permiten mejoras de funcionalidad segura. Puede crear clases contenedoras utilizando el patrón Decorator, un diseño robusto y flexible.Una clase contenedora implementa y contiene la misma interfaz, reenviando algunas funciones a los métodos existentes, mientras agrega comportamiento especializado a otros métodos.No puedes hacer esto con métodos abstractos; en su lugar, debes usar la herencia, que es más frágil.

¿Qué pasa con la ventaja de que las clases abstractas proporcionen una implementación básica?Puede proporcionar una clase de implementación esquelética abstracta con cada interfaz.Esto combina las virtudes de las interfaces y las clases abstractas.Las implementaciones esqueléticas brindan asistencia para la implementación sin imponer las severas restricciones que imponen las clases abstractas cuando sirven como definiciones de tipos.Por ejemplo, el Marco de colecciones define el tipo utilizando interfaces y proporciona una implementación esquelética para cada una.

La programación de interfaces proporciona varios beneficios:

  1. Requerido para patrones de tipo GoF, como el patrón de visitante

  2. Permite implementaciones alternativas.Por ejemplo, pueden existir múltiples implementaciones de objetos de acceso a datos para una única interfaz que abstrae el motor de base de datos en uso (AccountDaoMySQL y AccountDaoOracle pueden implementar AccountDao).

  3. Una clase puede implementar múltiples interfaces.Java no permite la herencia múltiple de clases concretas.

  4. Detalles de implementación de resúmenes.Las interfaces pueden incluir solo métodos API públicos, ocultando detalles de implementación.Los beneficios incluyen una API pública claramente documentada y contratos bien documentados.

  5. Utilizado en gran medida por los marcos modernos de inyección de dependencia, como http://www.springframework.org/.

  6. En Java, las interfaces se pueden utilizar para crear servidores proxy dinámicos: http://java.sun.com/j2se/1.5.0/docs/api/java/lang/reflect/Proxy.html.Esto se puede utilizar de forma muy eficaz con marcos como Spring para realizar programación orientada a aspectos.Los aspectos pueden agregar funcionalidades muy útiles a las clases sin agregar código Java directamente a esas clases.Ejemplos de esta funcionalidad incluyen registro, auditoría, monitoreo del desempeño, demarcación de transacciones, etc. http://static.springframework.org/spring/docs/2.5.x/reference/aop.html.

  7. Implementaciones simuladas, pruebas unitarias: cuando las clases dependientes son implementaciones de interfaces, se pueden escribir clases simuladas que también implementen esas interfaces.Las clases simuladas se pueden utilizar para facilitar las pruebas unitarias.

Creo que una de las razones por las que los desarrolladores han abandonado en gran medida las clases abstractas podría ser un malentendido.

Cuando el Pandilla de cuatro escribió:

Programa para una interfaz, no una implementación.

No existía nada parecido a una interfaz Java o C#.Estaban hablando del concepto de interfaz orientada a objetos que tiene cada clase.Erich Gamma lo menciona en esta entrevista.

Creo que seguir todas las reglas y principios mecánicamente sin pensar conduce a un código base difícil de leer, navegar, comprender y mantener.Recordar:Lo más simple que podría funcionar.

¿Cómo?

Porque eso es lo que dicen todos los libros.Al igual que los patrones GoF, muchas personas lo ven como universalmente bueno y nunca piensan si es realmente el diseño correcto o no.

¿Cómo sabes todas las relaciones entre objetos que ocurrirán dentro de esa interfaz?

No lo haces, y eso es un problema.

Si ya conoce esas relaciones, ¿por qué no solo extender una clase abstracta?

Razones para no extender una clase abstracta:

  1. Tiene implementaciones radicalmente diferentes y crear una clase base decente es demasiado difícil.
  2. Necesitas quemar tu única clase base para otra cosa.

Si ninguno de los dos aplica, continúe y use una clase abstracta.Esto le ahorrará mucho tiempo.

Preguntas que no hiciste:

¿Cuáles son las desventajas de usar una interfaz?

No puedes cambiarlos.A diferencia de una clase abstracta, una interfaz está escrita en piedra.Una vez que tenga uno en uso, extenderlo romperá el código, punto.

¿Realmente necesito cualquiera de los dos?

La mayoria del tiempo, no.Piense muy bien antes de construir cualquier jerarquía de objetos.Un gran problema en lenguajes como Java es que facilita demasiado la creación de jerarquías de objetos masivas y complicadas.

Considere el ejemplo clásico que LameDuck hereda de Duck.Suena fácil, ¿no?

Bueno, eso es hasta que necesites indicar que el pato ha resultado herido y ahora está cojo.O indicar que el pato cojo se ha curado y puede volver a caminar.Java no le permite cambiar el tipo de objeto, por lo que usar subtipos para indicar cojera en realidad no funciona.

Programar a una interfaz significa respetar el "contrato" creado mediante el uso de esa interfaz

Esto es lo que más se malinterpreta acerca de las interfaces.

No hay forma de hacer cumplir dicho contrato con las interfaces.Las interfaces, por definición, no pueden especificar ningún comportamiento en absoluto.Las clases son donde ocurre el comportamiento.

Esta creencia errónea está tan extendida que muchas personas la consideran sabiduría convencional.Sin embargo, está mal.

Entonces esta declaración en el OP

Casi todos los libros de Java que leí hablan sobre el uso de la interfaz como una forma de compartir el estado y el comportamiento entre los objetos

simplemente no es posible.Las interfaces no tienen estado ni comportamiento.Pueden definir propiedades que las clases de implementación deben proporcionar, pero eso es lo más parecido que pueden llegar.No se puede compartir comportamiento utilizando interfaces.

Se puede suponer que la gente implementará una interfaz para proporcionar el tipo de comportamiento implícito en el nombre de sus métodos, pero eso no es lo mismo.Y no impone ninguna restricción sobre cuándo se llaman dichos métodos (por ejemplo, que se debe llamar a Inicio antes que a Detener).

Esta declaración

Requerido para patrones de tipo GoF, como el patrón de visitante

también es incorrecto.El libro de GoF utiliza exactamente cero interfaces, ya que no eran una característica de los lenguajes utilizados en ese momento.Ninguno de los patrones requiere interfaces, aunque algunos pueden usarlas.En mi opinión, el patrón Observer es uno en el que las interfaces pueden desempeñar un papel más elegante (aunque el patrón normalmente se implementa mediante eventos hoy en día).En el patrón Visitante, casi siempre se da el caso de que se requiere una clase Visitante base que implemente el comportamiento predeterminado para cada tipo de nodo visitado, IME.

Personalmente, creo que la respuesta a la pregunta es triple:

  1. Muchas personas ven las interfaces como una solución milagrosa (estas personas generalmente trabajan bajo el malentendido del "contrato" o piensan que las interfaces desacoplan mágicamente su código).

  2. La gente de Java está muy centrada en el uso de marcos, muchos de los cuales (con razón) requieren clases para implementar sus interfaces.

  3. Las interfaces eran la mejor manera de hacer algunas cosas antes de que se introdujeran los genéricos y las anotaciones (atributos en C#).

Las interfaces son una característica del lenguaje muy útil, pero se abusa mucho de ellas.Los síntomas incluyen:

  1. Una interfaz solo la implementa una clase

  2. Una clase implementa múltiples interfaces.A menudo promocionada como una ventaja de las interfaces, generalmente significa que la clase en cuestión está violando el principio de separación de preocupaciones.

  3. Existe una jerarquía de herencia de interfaces (a menudo reflejada en una jerarquía de clases).Esta es la situación que intenta evitar utilizando interfaces en primer lugar.Demasiada herencia es mala, tanto para las clases como para las interfaces.

Todas estas cosas son olores de código, en mi opinión.

Es una forma de promover la libertad acoplamiento.

Con un acoplamiento bajo, un cambio en un módulo no requerirá un cambio en la implementación de otro módulo.

Un buen uso de este concepto es Patrón abstracto de fábrica.En el ejemplo de Wikipedia, la interfaz GUIFactory produce la interfaz Button.La fábrica concreta puede ser WinFactory (que produce WinButton) u OSXFactory (que produce OSXButton).Imagínese si está escribiendo una aplicación GUI y tiene que revisar todas las instancias de OldButton clase y cambiarlos a WinButton.Luego, el año que viene, tendrás que añadir OSXButton versión.

En mi opinión, esto se ve con tanta frecuencia porque es una muy buena práctica que a menudo se aplica en situaciones equivocadas.

Las interfaces tienen muchas ventajas en relación con las clases abstractas:

  • Puede cambiar las implementaciones sin reconstruir el código que depende de la interfaz.Esto es útil para:clases de proxy, inyección de dependencia, AOP, etc.
  • Puede separar la API de la implementación en su código.Esto puede ser bueno porque hace que sea obvio cuando estás cambiando el código que afectará a otros módulos.
  • Permite a los desarrolladores que escriben código que depende de su código simular fácilmente su API con fines de prueba.

Obtiene la mayor ventaja de las interfaces cuando trabaja con módulos de código.Sin embargo, no existe una regla sencilla para determinar dónde deben estar los límites de los módulos.Por lo tanto, es fácil abusar de esta práctica recomendada, especialmente cuando se diseña algún software por primera vez.

Supongo (con @ eed3s9n) que es para promover un acoplamiento flexible.Además, sin interfaces las pruebas unitarias se vuelven mucho más difíciles, ya que no puedes simular tus objetos.

¿Por qué se extiende es malo?.Este artículo es prácticamente una respuesta directa a la pregunta formulada.No se me ocurre casi ningún caso en el que realmente necesidad una clase abstracta y muchas situaciones en las que es una mala idea.Esto no significa que las implementaciones que utilizan clases abstractas sean malas, pero tendrá que tener cuidado de no hacer que el contrato de interfaz dependa de artefactos de alguna implementación específica (ejemplo:la clase Stack en Java).

Una cosa más:No es necesario, ni es una buena práctica, tener interfaces en todas partes.Normalmente, debe identificar cuándo necesita una interfaz y cuándo no.En un mundo ideal, el segundo caso debería implementarse como clase final la mayor parte del tiempo.

Aquí hay algunas respuestas excelentes, pero si está buscando una razón concreta, no busque más que las pruebas unitarias.

Considere que desea probar un método en la lógica empresarial que recupere la tasa impositiva actual para la región donde se produce una transacción.Para hacer esto, la clase de lógica de negocios tiene que comunicarse con la base de datos a través de un Repositorio:

interface IRepository<T> { T Get(string key); }

class TaxRateRepository : IRepository<TaxRate> {
    protected internal TaxRateRepository() {}
    public TaxRate Get(string key) {
    // retrieve an TaxRate (obj) from database
    return obj; }
}

En todo el código, utilice el tipo IRepository en lugar de TaxRateRepository.

El repositorio tiene un constructor no público para alentar a los usuarios (desarrolladores) a utilizar la fábrica para crear instancias del repositorio:

public static class RepositoryFactory {

    public RepositoryFactory() {
        TaxRateRepository = new TaxRateRepository(); }

    public static IRepository TaxRateRepository { get; protected set; }
    public static void SetTaxRateRepository(IRepository rep) {
        TaxRateRepository = rep; }
}

La fábrica es el único lugar donde se hace referencia directamente a la clase TaxRateRepository.

Entonces necesitas algunas clases de apoyo para este ejemplo:

class TaxRate {
    public string Region { get; protected set; }
    decimal Rate { get; protected set; }
}

static class Business {
    static decimal GetRate(string region) { 
        var taxRate = RepositoryFactory.TaxRateRepository.Get(region);
        return taxRate.Rate; }
}

Y también hay otra implementación de IRepository: la maqueta:

class MockTaxRateRepository : IRepository<TaxRate> {
    public TaxRate ReturnValue { get; set; }
    public bool GetWasCalled { get; protected set; }
    public string KeyParamValue { get; protected set; }
    public TaxRate Get(string key) {
        GetWasCalled = true;
        KeyParamValue = key;
        return ReturnValue; }
}

Debido a que el código en vivo (Clase Business) usa una Fábrica para obtener el Repositorio, en la prueba unitaria usted conecta el MockRepository para TaxRateRepository.Una vez realizada la sustitución, puede codificar el valor de retorno y hacer que la base de datos sea innecesaria.

class MyUnitTestFixture { 
    var rep = new MockTaxRateRepository();

    [FixtureSetup]
    void ConfigureFixture() {
        RepositoryFactory.SetTaxRateRepository(rep); }

    [Test]
    void Test() {
        var region = "NY.NY.Manhattan";
        var rate = 8.5m;
        rep.ReturnValue = new TaxRate { Rate = rate };

        var r = Business.GetRate(region);
        Assert.IsNotNull(r);
        Assert.IsTrue(rep.GetWasCalled);
        Assert.AreEqual(region, rep.KeyParamValue);
        Assert.AreEqual(r.Rate, rate); }
}

Recuerde, solo desea probar el método de lógica de negocios, no el repositorio, la base de datos, la cadena de conexión, etc.Hay diferentes pruebas para cada uno de ellos.Al hacerlo de esta manera, puedes aislar completamente el código que estás probando.

Un beneficio adicional es que también puede ejecutar la prueba unitaria sin una conexión a la base de datos, lo que la hace más rápida y portátil (piense en un equipo de múltiples desarrolladores en ubicaciones remotas).

Otro beneficio adicional es que puede utilizar el proceso de desarrollo basado en pruebas (TDD) para la fase de implementación del desarrollo.No uso estrictamente TDD, sino una combinación de TDD y codificación de la vieja escuela.

En un sentido, creo que su pregunta se reduce simplemente a "¿Por qué usar interfaces y no clases abstractas?" Técnicamente, puede lograr un acoplamiento suelto con ambos: la implementación subyacente aún no está expuesta al código de llamadas, y puede usar el patrón de fábrica abstracto para devolver una implementación subyacente (implementación de interfaz frente aextensión de clase abstracta) para aumentar la flexibilidad de su diseño.De hecho, se podría argumentar que las clases abstractas le brindan un poco más, ya que le permiten requerir implementaciones para satisfacer su código ("DEBE implementar start()") y proporcionar implementaciones predeterminadas ("Tengo un paint() estándar que usted puede anular si lo desea"): con las interfaces, se deben proporcionar implementaciones, lo que con el tiempo puede provocar problemas de herencia frágiles a través de cambios en la interfaz.

Sin embargo, fundamentalmente uso interfaces debido principalmente a la restricción de herencia única de Java.Si mi implementación DEBE heredar de una clase abstracta para ser utilizada llamando al código, eso significa que pierdo la flexibilidad de heredar de otra cosa, aunque eso pueda tener más sentido (por ejemplo,para reutilización de código o jerarquía de objetos).

Una razón es que las interfaces permiten crecimiento y extensibilidad.Digamos, por ejemplo, que tiene un método que toma un objeto como parámetro,

Public void Drink (café somedrink) {

}

Ahora digamos que desea utilizar exactamente el mismo método, pero pasando un objeto hotTea.Bueno, no puedes.Simplemente codificaste el método anterior para usar solo objetos de café.Quizás eso sea bueno, quizás eso sea malo.La desventaja de lo anterior es que te limita estrictamente a un tipo de objeto cuando deseas pasar todo tipo de objetos relacionados.

Al usar una interfaz, digamos IHotDrink,

interfaz IHotDrink { }

y reescribiendo su método anterior para usar la interfaz en lugar del objeto,

Public void Drink (ihotdrink somedrink) {

}

Ahora puedes pasar todos los objetos que implementan la interfaz IHotDrink.Claro, puedes escribir exactamente el mismo método que hace exactamente lo mismo con un parámetro de objeto diferente, pero ¿por qué?De repente estás manteniendo un código inflado.

Se trata de diseñar antes de codificar.

Si no conoce todas las relaciones entre dos objetos después de haber especificado la interfaz, entonces no ha hecho un buen trabajo al definir la interfaz, lo cual es relativamente fácil de solucionar.

Si te sumergiste directamente en la codificación y te diste cuenta a mitad de camino de que te falta algo, es mucho más difícil de arreglar.

Podrías ver esto desde una perspectiva de Perl/python/ruby:

  • cuando pasas un objeto como parámetro a un método no pasas su tipo, solo sabes que debe responder a algunos métodos

Creo que considerar las interfaces de Java como una analogía explicaría mejor esto.Realmente no pasas un tipo, simplemente pasas algo que responde a un método (un rasgo, por así decirlo).

Creo que la razón principal para usar interfaces en Java es la limitación a la herencia única.En muchos casos, esto conduce a complicaciones innecesarias y duplicación de código.Eche un vistazo a los rasgos en Scala: http://www.scala-lang.org/node/126 Los rasgos son un tipo especial de clases abstractas, pero una clase puede ampliar muchas de ellas.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top