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?
fuente
Work
es el problema de la herencia de diamantes (en ese caso, ambos cumplen un contrato sobre elWork
método). En Java, se requiere que implemente los métodos de la interfaz para evitar el problema de quéWork
método debe usar el programa de esa manera. Sin embargo, lenguajes como C ++ no desambigúan esto para usted.Respuestas:
BaseWorker
cumple ese requisito El hecho de que no pueda crear una instancia directa de unBaseWorker
objeto no significa que no pueda tener unBaseWorker
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
IFooWorker
que no se derivanBaseWorker
, 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.
fuente
BaseWorker
no se puede crear una instancia directamente, no significa que un determinadoBaseWorker myWorker
no se implementeIFooWorker
.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.
fuente
IFoo
y una,BaseFoo
entonces implica que o bienIFoo
no es una interfaz de grano fino apropiada, o queBaseFoo
está utilizando una herencia donde la composición podría ser una mejor opción.MyDaoFoo
oSqlFoo
oHibernateFoo
o lo que sea, para indicar que solo se está implementando un árbol de clases posibleIFoo
? 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 ...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.
fuente
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:
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.
fuente
Yo haría la pregunta, ¿qué sucede cuando cambias
IFooWorker
, como agregar un nuevo método?Si
BaseWorker
implementa 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.
fuente
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.
fuente
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:
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.
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:
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.
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.
fuente
Hay otro aspecto de por qué
DbWorker
debería implementarseIFooWorker
, 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
DbWorker
se considera que no extiende laBaseWorker
clase abstracta ,DbWorker
pierde laIFooWorker
interfaz. Esto podría, pero no necesariamente, tener un impacto en los clientes que lo consumen si esperan laIFooWorker
interfaz.fuente
Para empezar, me gusta definir interfaces y clases abstractas de manera diferente:
En su caso, todos los trabajadores implementan
work()
porque eso es lo que el contrato les exige. Por lo tanto, su clase baseBaseworker
debe 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 necesitanwork()
. 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,
DbWorker
satisface laIFooWorker
interfaz, pero si hay una forma específica en que esta clase lo hacework()
, entonces realmente necesita sobrecargar la definición heredadaBaseWorker
. La razón por la que no debe implementar la interfazIFooWorker
directamente es que esto no es algo especialDbWorker
. Si hiciera eso cada vez que implementara clases similaresDbWorker
, 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.
fuente
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.
fuente