Interfaces en una clase abstracta

30

Mi compañero de trabajo y yo tenemos opiniones diferentes sobre la relación entre las clases base y las interfaces. Creo que una clase no debe implementar una interfaz a menos que esa clase se pueda usar cuando se requiere una implementación de la interfaz. En otras palabras, me gusta ver código como este:

interface IFooWorker { void Work(); }

abstract class BaseWorker {
    ... base class behaviors ...
    public abstract void Work() { }
    protected string CleanData(string data) { ... }
}

class DbWorker : BaseWorker, IFooWorker {
    public void Work() {
        Repository.AddCleanData(base.CleanData(UI.GetDirtyData()));
    }
}

DbWorker es lo que obtiene la interfaz IFooWorker, porque es una implementación instanciable de la interfaz. Cumple completamente el contrato. Mi compañero de trabajo prefiere lo casi idéntico:

interface IFooWorker { void Work(); }

abstract class BaseWorker : IFooWorker {
    ... base class behaviors ...
    public abstract void Work() { }
    protected string CleanData(string data) { ... }
}

class DbWorker : BaseWorker {
    public void Work() {
        Repository.AddCleanData(base.CleanData(UI.GetDirtyData()));
    }
}

Donde la clase base obtiene la interfaz, y en virtud de esto, todos los herederos de la clase base también pertenecen a esa interfaz. Esto me molesta pero no puedo encontrar razones concretas por las cuales, fuera de "la clase base no puede sostenerse por sí sola como una implementación de la interfaz".

¿Cuáles son los pros y los contras de su método frente al mío y por qué debería usarse uno sobre otro?

Bryan Boettcher
fuente
Su sugerencia se parece mucho a la herencia de diamantes , lo que puede causar mucha confusión más adelante.
Spoike
@Spoike: ¿Cómo ver eso?
Niing
@Niing el enlace que proporcioné en el comentario tiene un diagrama de clase simple y una explicación simple de lo que es. se llama un problema de "diamante" porque la estructura de herencia se dibuja básicamente como una forma de diamante (es decir, ♦ ️).
Spoike
@Spoike: ¿por qué esta pregunta causará la herencia de diamantes?
Niing
1
@Niing: Heredar BaseWorker e IFooWorker con el mismo método llamado Workes el problema de la herencia de diamantes (en ese caso, ambos cumplen un contrato sobre el Workmétodo). En Java, se requiere que implemente los métodos de la interfaz para evitar el problema de qué Workmétodo debe usar el programa de esa manera. Sin embargo, lenguajes como C ++ no desambigúan esto para usted.
Spoike

Respuestas:

24

Creo que una clase no debe implementar una interfaz a menos que esa clase se pueda usar cuando se requiere una implementación de la interfaz.

BaseWorkercumple ese requisito El hecho de que no pueda crear una instancia directa de un BaseWorkerobjeto no significa que no pueda tener un BaseWorker puntero que cumpla con el contrato. De hecho, ese es el objetivo de las clases abstractas.

Además, es difícil saber por el ejemplo simplificado que publicó, pero parte del problema puede ser que la interfaz y la clase abstracta son redundantes. A menos que tenga otras clases de implementación IFooWorkerque no se derivan BaseWorker, no necesita la interfaz en absoluto. Las interfaces son solo clases abstractas con algunas limitaciones adicionales que facilitan la herencia múltiple.

Una vez más, es difícil distinguir de un ejemplo simplificado, el uso de un método protegido, que se refiere explícitamente a la base de una clase derivada, y la falta de un lugar inequívoco para declarar la implementación de la interfaz son signos de advertencia de que está utilizando la herencia de manera inapropiada en lugar de composición. Sin esa herencia, toda su pregunta se vuelve discutible.

Karl Bielefeldt
fuente
1
+1 por señalar que solo porque BaseWorkerno se puede crear una instancia directamente, no significa que un determinado BaseWorker myWorkerno se implemente IFooWorker.
Avner Shahar-Kashtan
2
No estoy de acuerdo con que las interfaces sean solo clases abstractas. Las clases abstractas generalmente definen algunos detalles de implementación (de lo contrario, se hubiera utilizado una interfaz). Si esos detalles no son de su agrado, ahora debe cambiar cada uso de la clase base a otra cosa. Las interfaces son poco profundas; definen la relación "plug-and-socket" entre dependencias y dependientes. Como tal, el acoplamiento estrecho a cualquier detalle de implementación ya no es una preocupación. Si una clase que hereda la interfaz no es de su agrado, elimínela y coloque algo completamente diferente; su código podría importarle menos.
KeithS
55
Ambos tienen ventajas, pero generalmente la interfaz siempre debe usarse para la dependencia, ya sea que exista o no una clase abstracta que defina implementaciones base. La combinación de interfaz y clase abstracta da lo mejor de ambos mundos; la interfaz mantiene la relación de enchufe y zócalo de superficie poco profunda, mientras que la clase abstracta proporciona el código común útil. Puede eliminar y reemplazar cualquier nivel de esto debajo de la interfaz a voluntad.
KeithS
Por "puntero", ¿quiere decir una instancia de un objeto (a quien un puntero haría referencia)? Las interfaces no son clases, son tipos, y pueden actuar como un sustituto incompleto de la herencia múltiple, no como un reemplazo, es decir, "incluyen el comportamiento de múltiples fuentes en una clase".
samis
46

Tendría que estar de acuerdo con su compañero de trabajo.

En los dos ejemplos que da, BaseWorker define el método abstracto Work (), lo que significa que todas las subclases son capaces de cumplir con el contrato de IFooWorker. En este caso, creo que BaseWorker debería implementar la interfaz, y esa implementación sería heredada por sus subclases. Esto le ahorrará tener que indicar explícitamente que cada subclase es de hecho un IFooWorker (el principio DRY).

Si no estaba definiendo Work () como un método de BaseWorker, o si IFooWorker tenía otros métodos que las subclases de BaseWorker no querrían o necesitarían, entonces (obviamente) tendría que indicar qué subclases realmente implementan IFooWorker.

Matthew Flynn
fuente
11
+1 habría publicado lo mismo. Lo siento insta, pero creo que su compañero de trabajo también tiene razón en este caso, si algo garantiza cumplir un contrato, debe heredar ese contrato, ya sea que la garantía se cumpla mediante una interfaz, una clase abstracta o una clase concreta. En pocas palabras: el garante debe heredar el contrato que garantiza.
Jimmy Hoffa
2
Generalmente estoy de acuerdo, pero me gustaría señalar que palabras como "base", "estándar", "predeterminado", "genérico", etc. son olores de código en un nombre de clase. Si una clase base abstracta tiene casi el mismo nombre que una interfaz pero con una de esas palabras de comadreja incluidas, a menudo es un signo de abstracción incorrecta; Las interfaces definen lo que hace algo , la herencia de tipos define lo que es . Si tiene una IFooy una, BaseFooentonces implica que o bien IFoono es una interfaz de grano fino apropiada, o que BaseFooestá utilizando una herencia donde la composición podría ser una mejor opción.
Aaronaught
@Aaronaught Buen punto, aunque puede ser que realmente haya algo que todas o la mayoría de las implementaciones de IFoo necesiten heredar (como un identificador de un servicio o su implementación DAO).
Matthew Flynn
2
Entonces, ¿no debería llamarse a la clase base MyDaoFooo SqlFooo HibernateFooo lo que sea, para indicar que solo se está implementando un árbol de clases posible IFoo? Es aún más olor para mí si una clase base está acoplada a una biblioteca o plataforma específica y no hay mención de esto en el nombre ...
Aaronaught
@Aaronaught - Sí. Concuerdo completamente.
Matthew Flynn
11

Generalmente estoy de acuerdo con tu compañero de trabajo.

Tomemos su modelo: la interfaz se implementa solo por las clases secundarias, a pesar de que la clase base también impone la exposición de los métodos IFooWorker. En primer lugar, es redundante; ya sea que la clase secundaria implemente la interfaz o no, deben anular los métodos expuestos de BaseWorker y, de la misma manera, cualquier implementación de IFooWorker debe exponer la funcionalidad tanto si reciben ayuda de BaseWorker como si no.

Esto además hace que su jerarquía sea ambigua; "Todos los IFooWorkers son BaseWorkers" no es necesariamente una declaración verdadera, y tampoco lo es "Todos los BaseWorkers son IFooWorkers". Entonces, si desea definir una variable de instancia, un parámetro o un tipo de retorno que podría ser cualquier implementación de IFooWorker o BaseWorker (aprovechando la funcionalidad expuesta común, que es una de las razones para heredar en primer lugar), ninguno de se garantiza que todo lo abarca; algunos BaseWorkers no serán asignables a una variable de tipo IFooWorker, y viceversa.

El modelo de su compañero de trabajo es mucho más fácil de usar y reemplazar. "Todos los BaseWorkers son IFooWorkers" es ahora una declaración verdadera; puede dar cualquier instancia de BaseWorker a cualquier dependencia de IFooWorker, sin problemas. La afirmación opuesta "Todos los IFooWorkers son BaseWorkers" no es cierta; eso le permite reemplazar BaseWorker con BetterBaseWorker y su código de consumo (que depende de IFooWorker) no tendrá que notar la diferencia.

KeithS
fuente
Me gusta el sonido de esta respuesta, pero no sigo la última parte. Estás sugiriendo que BetterBaseWorker sería una clase abstracta independiente que implementa IFooWorker, de modo que mi plugin hipotético podría simplemente decir "¡oh, muchacho! ¡El Mejor Trabajador Base finalmente está fuera!" y cambiar cualquier referencia de BaseWorker a BetterBaseWorker y llamarlo un día (¿tal vez jugar con los nuevos y geniales valores devueltos que me permiten quitar enormes fragmentos de mi código redundante, etc.)?
Anthony
Más o menos sí. La gran ventaja es que reducirá la cantidad de referencias a BaseWorker que tendrán que cambiar para ser BetterBaseWorkers; la mayor parte de su código no hará referencia a la clase concreta directamente, sino que usará IFooWorker, por lo que cuando cambie lo que se asigna a esas dependencias de IFooWorker (propiedades de clases, parámetros de constructores o métodos), el código que usa IFooWorker no debería ver ninguna diferencia en uso
KeithS
6

Necesito agregar algo de advertencia a estas respuestas. El acoplamiento de la clase base a la interfaz crea una fuerza en la estructura de esa clase. En su ejemplo básico, es obvio que los dos deben estar acoplados, pero eso puede no ser cierto en todos los casos.

Tome las clases de framework de colección de Java:

abstract class AbstractList
class LinkedList extends AbstractList implements Queue

El hecho de que LinkedList implemente el contrato de Cola no llevó la preocupación a AbstractList .

¿Cuál es la distinción entre los dos casos? El propósito de BaseWorker siempre fue (como lo comunica su nombre e interfaz) implementar operaciones en IWorker . El propósito de AbstractList y el de Queue son divergentes, pero un decadente de los primeros aún puede implementar el último contrato.

Mihai Danila
fuente
Esto sucede la mitad del tiempo. Él siempre prefiere implementar la interfaz en la clase base, y yo siempre prefiero implementarla en el concreto final. El escenario que presentó ocurre a menudo y es parte de la razón por la que las interfaces en la base me molestan.
Bryan Boettcher
1
Correcto, insta, y considero el uso de interfaces de esta manera como una faceta del uso excesivo de la herencia que vemos en muchos entornos de desarrollo. Como dijo GoF en su libro de patrones de diseño, preferir la composición sobre la herencia , y mantener la interfaz fuera de la clase base es una de las formas de promover ese principio.
Mihai Danila
1
Recibo la advertencia, pero notarás que la clase abstracta del OP incluía métodos abstractos que coinciden con la interfaz: un BaseWorker es implícitamente un IFooWorker. Hacerlo explícito hace que este hecho sea más útil. Sin embargo, existen numerosos métodos incluidos en Queue que no están incluidos en AbstractList y, como tal, una AbstractList no es una Cola, incluso si sus hijos pudieran estarlo. AbstractList y Queue son ortogonales.
Matthew Flynn
Gracias por esta vision; Estoy de acuerdo en que el caso del OP cae dentro de esa categoría, pero sentí que había una discusión más grande en busca de una regla más profunda, y quería compartir mis observaciones sobre una tendencia sobre la que he leído (e incluso tuve mi mismo por un breve tiempo) del uso excesivo de la herencia, tal vez porque esa es una de las herramientas en la caja de herramientas de OOP.
Mihai Danila
Dang, han pasado seis años. :)
Mihai Danila
2

Yo haría la pregunta, ¿qué sucede cuando cambias IFooWorker, como agregar un nuevo método?

Si BaseWorkerimplementa la interfaz, por defecto tendrá que declarar el nuevo método, incluso si es abstracto. Por otro lado, si no implementa la interfaz, solo obtendrá errores de compilación en las clases derivadas.

Por esa razón, haría que la clase base implementara la interfaz , ya que podría implementar toda la funcionalidad para el nuevo método en la clase base sin tocar las clases derivadas.

Scott Whitlock
fuente
1

Primero piense en qué es una clase base abstracta y qué es una interfaz. Considere cuándo usaría uno u otro y cuándo no lo haría.

Es común que las personas piensen que ambos son conceptos muy similares, de hecho, es una pregunta de entrevista común (la diferencia entre los dos es ??)

Entonces, una clase base abstracta le brinda algo que las interfaces no pueden, que son implementaciones predeterminadas de métodos. (Hay otras cosas, en C # puede tener métodos estáticos en una clase abstracta, no en una interfaz, por ejemplo).

Como ejemplo, un uso común de esto es con IDisposable. La clase abstracta implementa el método Dispose de IDisposable, lo que significa que cualquier versión derivada de la clase base será automáticamente desechable. Luego puedes jugar con varias opciones. Make Dispose abstract, obligando a las clases derivadas a implementarlo. Proporcione una implementación virtual predeterminada, para que no lo hagan, o hágalo virtual o abstracto y haga que llame a métodos virtuales llamados cosas como BeforeDispose, AfterDispose, OnDispose o Disposing, por ejemplo.

Por lo tanto, cada vez que todas las clases derivadas necesitan admitir la interfaz, ésta pasa a la clase base. Si solo uno o algunos necesitan esa interfaz, iría a la clase derivada.

Todo eso es realmente una gran simplificación. Otra alternativa es tener clases derivadas que ni siquiera implementen las interfaces, sino que las proporcionen a través de un patrón adaptador. Un ejemplo de esto que vi recientemente fue en IObjectContextAdapter.

Ian
fuente
1

Todavía estoy a años de comprender completamente la distinción entre una clase abstracta y una interfaz. Cada vez que creo tener una idea de los conceptos básicos, busco en stackexchange y estoy dos pasos atrás. Pero algunas reflexiones sobre el tema y la pregunta de los OP:

Primero:

Hay dos explicaciones comunes de una interfaz:

  1. Una interfaz es una lista de métodos y propiedades que cualquier clase puede implementar, y al implementar una interfaz, una clase garantiza que esos métodos (y sus firmas) y esas propiedades (y sus tipos) estarán disponibles al "interactuar" con esa clase o Un objeto de esa clase. Una interfaz es un contrato.

  2. Las interfaces son clases abstractas que no pueden / no pueden hacer nada. Son útiles porque puede implementar más de una, a diferencia de las clases principales para padres. Al igual, podría ser un objeto de la clase BananaBread y heredar de BaseBread, pero eso no significa que no pueda implementar las interfaces IWithNuts e ITastesYummy. Incluso podría implementar la interfaz IDoesTheDishes, porque no soy solo pan, ¿sabes?

Hay dos explicaciones comunes de una clase abstracta:

  1. Una clase abstracta es, ya sabes, lo que no puede hacer. Es como, la esencia, en realidad no es algo real. Espera, esto te ayudará. Un barco es una clase abstracta, pero un sexy Playboy Yacht sería una subclase de BaseBoat.

  2. Leí un libro sobre clases abstractas, y tal vez deberías leer ese libro, porque probablemente no lo entiendas y lo harás mal si no lo has leído.

Por cierto, el libro Citers siempre parece impresionante, incluso si todavía me alejo confundido.

Segundo:

En SO, alguien hizo una versión más simple de esta pregunta, el clásico, "¿por qué usar interfaces? ¿Cuál es la diferencia? ¿Qué me estoy perdiendo?" Y una respuesta usó un piloto de la fuerza aérea como un ejemplo simple. No aterrizó del todo, pero generó algunos comentarios excelentes, uno de los cuales mencionaba que la interfaz IFlyable tenía métodos como takeOff, pilotEject, etc. Y esto realmente me pareció un ejemplo real de por qué las interfaces no son solo útiles, pero crucial Una interfaz hace que un objeto / clase sea intuitivo, o al menos da la sensación de que lo es. Una interfaz no es para el beneficio del objeto o los datos, sino para algo que necesita interactuar con ese objeto. El clásico Fruit-> Apple-> Fuji or Shape-> Triangle-> Los ejemplos equiláteros de herencia son un gran modelo para comprender taxonómicamente un objeto dado en función de sus descendientes. Informa al consumidor y a los procesadores sobre sus cualidades generales, comportamientos, si el objeto es un grupo de cosas, controlará su sistema si lo coloca en el lugar equivocado, o describe datos sensoriales, un conector a un almacén de datos específico, o datos financieros necesarios para hacer la nómina.

Un objeto Plane específico puede o no tener un método para un aterrizaje de emergencia, pero me enojaré si asumo su emergencia y me gusta como cualquier otro objeto volador solo para despertar cubierto en DumbPlane y aprender que los desarrolladores fueron con quickLand porque pensaron que todo era lo mismo. Al igual que me sentiría frustrado si cada fabricante de tornillos tuviese su propia interpretación de righty tighty o si mi televisor no tuviera el botón de aumento de volumen por encima del botón de disminución de volumen.

Las clases abstractas son el modelo que establece lo que cualquier objeto descendiente debe tener para calificar como esa clase. Si no tienes todas las cualidades de un pato, no importa que tengas implementada la interfaz IQuack, eres un pingüino extraño. Las interfaces son las cosas que tienen sentido incluso cuando no puedes estar seguro de otra cosa. Jeff Goldblum y Starbuck pudieron volar naves espaciales alienígenas porque la interfaz era confiablemente similar.

Tercero:

Estoy de acuerdo con tu compañero de trabajo, porque a veces necesitas aplicar ciertos métodos desde el principio. Si está creando un ORM de registro activo, necesita un método de guardar. Esto no depende de la subclase que se puede instanciar. Y si la interfaz ICRUD es lo suficientemente portátil como para no estar exclusivamente acoplada a una clase abstracta, otras clases pueden implementarla para que sean confiables e intuitivos para cualquiera que ya esté familiarizado con cualquiera de las clases descendientes de esa clase abstracta.

Por otro lado, hubo un gran ejemplo anterior de cuándo no saltar para vincular una interfaz a la clase abstracta, porque no todos los tipos de lista implementarán (o deberían) una interfaz de cola. Dijiste que este escenario ocurre la mitad del tiempo, lo que significa que tú y tu compañero de trabajo están equivocados la mitad del tiempo y, por lo tanto, lo mejor que puedes hacer es discutir, debatir, considerar y, si resultan ser correctos, reconocer y aceptar el acoplamiento. . Pero no se convierta en un desarrollador que siga una filosofía incluso cuando no sea la mejor para el trabajo en cuestión.

Antonio
fuente
0

Hay otro aspecto de por qué DbWorkerdebería implementarse IFooWorker, como usted sugiere.

En el caso del estilo de su compañero de trabajo, si por alguna razón se produce una refactorización posterior y DbWorkerse considera que no extiende la BaseWorkerclase abstracta , DbWorkerpierde la IFooWorkerinterfaz. Esto podría, pero no necesariamente, tener un impacto en los clientes que lo consumen si esperan la IFooWorkerinterfaz.

Frederik Krautwald
fuente
-1

Para empezar, me gusta definir interfaces y clases abstractas de manera diferente:

Interface: a contract that each class must fulfill if they are to implement that interface
Abstract class: a general class (i.e vehicle is an abstract class, while porche911 is not)

En su caso, todos los trabajadores implementan work()porque eso es lo que el contrato les exige. Por lo tanto, su clase base Baseworkerdebe implementar ese método explícitamente o como una función virtual. Le sugiero que ponga este método en su clase base debido al simple hecho de que todos los trabajadores lo necesitan work(). Por lo tanto, es lógico que su clase base (aunque no pueda crear un puntero de ese tipo) lo incluya como una función.

Ahora, DbWorkersatisface la IFooWorkerinterfaz, pero si hay una forma específica en que esta clase lo hace work(), entonces realmente necesita sobrecargar la definición heredada BaseWorker. La razón por la que no debe implementar la interfaz IFooWorkerdirectamente es que esto no es algo especial DbWorker. Si hiciera eso cada vez que implementara clases similares DbWorker, estaría violando DRY .

Si dos clases implementan la misma función de diferentes maneras, puede comenzar a buscar la mejor superclase común. La mayoría de las veces encontrarás uno. Si no, sigue buscando o desiste y acepta que no tienen suficientes cosas en común para formar una clase base.

Arnab Datta
fuente
-3

Wow, todas estas respuestas y ninguna que indique que la interfaz en el ejemplo no sirve para nada. No utiliza la herencia y una interfaz para el mismo método, no tiene sentido. Usas uno u otro. Hay muchas preguntas en este foro con respuestas que explican los beneficios de cada uno y los senarios que requieren uno u otro.

Martin Maat
fuente
3
Esa es una falsedad patente. La interfaz es un contrato mediante el cual se espera que el sistema se comporte, independientemente de la implementación. La clase base heredada es una de las muchas implementaciones posibles para esta interfaz. Un sistema de escala lo suficientemente grande tendrá múltiples clases base que satisfagan las diversas interfaces (generalmente cerradas con genéricos), que es exactamente lo que hizo esta configuración y por qué fue útil dividirlas.
Bryan Boettcher el
Es un muy mal ejemplo desde una perspectiva OOP. Como dice "la clase base es solo una implementación" (debería ser o no tendría sentido para la interfaz), está diciendo que habrá clases que no sean trabajadores que implementarán la interfaz IFooWorker. ¡Entonces tendrá una clase base que es un trabajador especializado en el sector de la alimentación (deberíamos suponer que también podría haber trabajadores de bar) y tendría otros trabajadores que ni siquiera son trabajadores básicos! Eso es al revés. En más de una forma.
Martin Maat
1
Quizás esté atrapado en la palabra "Trabajador" en la interfaz. ¿Qué pasa con "Datasource"? Si tenemos un IDatasource <T>, podemos tener trivialmente un SqlDatasource <T>, RestDatasource <T>, MongoDatasource <T>. El negocio decide qué cierres de la interfaz genérica van a las implementaciones de cierre de las fuentes de datos abstractas.
Bryan Boettcher
Puede tener sentido usar la herencia solo por DRY y poner una implementación común en una clase base. Pero no alinearía ningún método anulado con ningún método de interfaz, la clase base contendría una lógica de soporte general. Si se alinearan, eso demostraría que la herencia resuelve bien su problema y la interfaz no serviría para nada. Utiliza una interfaz en caso de que la herencia no resuelva su problema porque las características comunes que desea modelar no se ajustan a un árbol de clases singke. Y sí, la redacción del ejemplo hace que sea aún más erróneo.
Martin Maat