A un colega mío se le ocurrió una regla general para elegir entre crear una clase base o una interfaz.
Él dice:
Imagine cada nuevo método que está a punto de implementar. Para cada uno de ellos, considere esto: ¿este método será implementado por más de una clase exactamente de esta forma, sin ningún cambio? Si la respuesta es "sí", cree una clase base. En cualquier otra situación, cree una interfaz.
Por ejemplo:
Considere las clases
cat
ydog
, que extienden la clasemammal
y tienen un único métodopet()
. Luego agregamos la clasealligator
, que no extiende nada y tiene un solo métodoslither()
.Ahora, queremos agregar un
eat()
método a todos ellos.Si la implementación del
eat()
método será exactamente la misma paracat
,dog
yalligator
, deberíamos crear una clase base (digamos,animal
), que implemente este método.Sin embargo, si su implementación
alligator
difiere de la manera más leve, deberíamos crear unaIEat
interfaz y hacerlamammal
ealligator
implementarla.
Insiste en que este método cubre todos los casos, pero me parece una simplificación excesiva.
¿Vale la pena seguir esta regla de oro?
fuente
alligator
La implementación deeat
difiere, por supuesto, en que aceptacat
ydog
como parámetros.is a
y las interfaces sonacts like
ois
. Entonces un perrois a
mamífero yacts like
un comedor. Esto nos diría que los mamíferos deberían ser una clase y el comedor debería ser una interfaz. Siempre ha sido una guía muy útil. Nota al margen: Un ejemplo deis
seríaThe cake is eatable
oThe book is writable
.Respuestas:
No creo que esta sea una buena regla general. Si le preocupa la reutilización del código, puede implementar un
PetEatingBehavior
rol que defina las funciones de alimentación para gatos y perros. Entonces puede tenerIEat
y reutilizar el código juntos.En estos días veo cada vez menos razones para usar la herencia. Una gran ventaja es la facilidad de uso. Tome un marco GUI. Una forma popular de diseñar una API para esto es exponer una gran clase base y documentar qué métodos puede anular el usuario. Por lo tanto, podemos ignorar todas las otras cosas complejas que nuestra aplicación necesita administrar, ya que la clase base proporciona una implementación "predeterminada". Pero aún podemos personalizar las cosas reimplementando el método correcto cuando sea necesario.
Si se adhiere a la interfaz de su API, su usuario generalmente tiene más trabajo que hacer para que los ejemplos simples funcionen. Pero la API con interfaces a menudo está menos acoplada y es más fácil de mantener por la sencilla razón de que
IEat
contiene menos información que laMammal
clase base. Por lo tanto, los consumidores dependerán de un contrato más débil, obtendrá más oportunidades de refactorización, etc.Para citar a Rich Hickey: ¡Simple! = Fácil
fuente
PetEatingBehavior
implementación?PetEatingBehavior
es, probablemente, el equivocado. No puedo darle una implementación ya que este es solo un ejemplo de juguete. Pero puedo describir los pasos de refactorización: tiene un constructor que toma toda la información utilizada por gatos / perros para definir eleat
método (instancia de dientes, instancia de estómago, etc.). Solo contiene el código común a los gatos y perros utilizados en sueat
método. Simplemente tiene que crear unPetEatingBehavior
miembro y reenviarlo a dog.eat/cat.eat.Es una regla general decente, pero conozco algunos lugares donde la violé.
Para ser justos, uso las clases base (abstractas) mucho más que mis pares. Hago esto como programación defensiva.
Para mí (en muchos idiomas), una clase base abstracta agrega la restricción clave de que solo puede haber una clase base. Al elegir usar una clase base en lugar de una interfaz, usted dice "esta es la funcionalidad principal de la clase (y cualquier heredero), y no es apropiado mezclar willy-nilly con otra funcionalidad". Las interfaces son lo opuesto: "Este es un rasgo de un objeto, no necesariamente su responsabilidad principal".
Este tipo de opciones de diseño crean guías implícitas para sus implementadores sobre cómo deben usar su código y, por extensión, escriben su código. Debido a su mayor impacto en la base de código, tiendo a tener en cuenta estas cosas con más fuerza al tomar la decisión de diseño.
fuente
El problema con la simplificación de su amigo es que determinar si un método alguna vez tendrá que cambiar es excepcionalmente difícil. Es mejor usar una regla que hable de la mentalidad detrás de las superclases y las interfaces.
En lugar de averiguar qué debe hacer al observar cuál cree que es la funcionalidad, observe la jerarquía que está tratando de crear.
Así es como un profesor mío enseñó la diferencia:
Las superclases deberían ser
is a
y las interfaces sonacts like
ois
. Entonces un perrois a
mamífero yacts like
un comedor. Esto nos diría que los mamíferos deberían ser una clase y el comedor debería ser una interfaz. Un ejemplo deis
seríaThe cake is eatable
oThe book is writable
(haciendo ambaseatable
ewritable
interfaces).Usar una metodología como esta es razonablemente simple y fácil, y hace que codifique la estructura y los conceptos en lugar de lo que cree que hará el código. Esto hace que el mantenimiento sea más fácil, que el código sea más legible y que el diseño sea más sencillo de crear.
Independientemente de lo que el código pueda decir realmente, si usa un método como este, podría volver dentro de cinco años y usar el mismo método para volver sobre sus pasos y descubrir fácilmente cómo se diseñó su programa y cómo interactúa.
Solo mis dos centavos.
fuente
A pedido de exizt, estoy ampliando mi comentario a una respuesta más larga.
El mayor error que comete la gente es creer que las interfaces son simplemente clases abstractas vacías. Una interfaz es una forma para que un programador diga: "No me importa lo que me des, siempre y cuando siga esta convención".
La biblioteca .NET sirve como un maravilloso ejemplo. Por ejemplo, cuando escribe una función que acepta un
IEnumerable<T>
, lo que está diciendo es: "No me importa cómo está almacenando sus datos. Solo quiero saber que puedo usar unforeach
bucle.Esto lleva a un código muy flexible. De repente, para integrarse, todo lo que necesita hacer es seguir las reglas de las interfaces existentes. Si la implementación de la interfaz es difícil o confusa, entonces tal vez sea una pista de que estás tratando de meter una clavija cuadrada en un agujero redondo.
Pero luego surge la pregunta: "¿Qué pasa con la reutilización del código? Mis profesores de CS me dijeron que la herencia era la solución a todos los problemas de reutilización del código y que la herencia le permite escribir una vez y usar en todas partes y curaría la menangitis en el proceso de rescate de huérfanos de los mares crecientes y no habría más lágrimas y así sucesivamente, etc., etc., etc. "
Usar la herencia solo porque te gusta el sonido de las palabras "reutilización de código" es una muy mala idea. La guía de estilo de código de Google hace este punto bastante conciso:
Para ilustrar por qué la herencia no siempre es la respuesta, voy a usar una clase llamada MySpecialFileWriter †. Una persona que cree ciegamente que la herencia es la solución a todos los problemas, argumentaría que debe tratar de heredar
FileStream
, para no duplicarFileStream
el código. Las personas inteligentes reconocen que esto es estúpido. Simplemente debe tener unFileStream
objeto en su clase (ya sea como una variable local o miembro) y usar su funcionalidad.El
FileStream
ejemplo puede parecer artificial, pero no lo es. Si tiene dos clases que implementan la misma interfaz exactamente de la misma manera, entonces debe tener una tercera clase que encapsule cualquier operación que esté duplicada. Su objetivo debe ser escribir clases que sean bloques reutilizables autónomos que se puedan juntar como legos.Esto no significa que la herencia deba evitarse a toda costa. Hay muchos puntos a considerar, y la mayoría se cubrirá investigando la pregunta "Composición vs. Herencia". Nuestro propio Stack Overflow tiene algunas buenas respuestas sobre el tema.
Al final del día, los sentimientos de su compañero de trabajo carecen de la profundidad o comprensión necesarias para tomar una decisión informada. Investiga el tema y descúbrelo por ti mismo.
† Al ilustrar la herencia, todos usan animales. Eso es inútil. En 11 años de desarrollo, nunca he escrito una clase llamada
Cat
, así que no voy a usarla como ejemplo.fuente
FileStream
)Los ejemplos rara vez hacen un punto, especialmente cuando se trata de automóviles o animales. La forma de comer (o procesar alimentos) de un animal no está realmente vinculada a su herencia, no existe una única forma de comer que se aplique a todos los animales. Esto depende más de sus capacidades físicas (las formas de entrada, procesamiento y salida, por así decirlo). Los mamíferos pueden ser carnívoros, herbívoros u omnívoros, por ejemplo, mientras que las aves, los mamíferos y los peces pueden comer insectos. Este comportamiento se puede capturar con ciertos patrones.
En este caso, es mejor abstraer el comportamiento, como este:
O implemente un patrón de visitante donde
FoodProcessor
intente alimentar animales compatibles (ICarnivore : IFoodEater
,MeatProcessingVisitor
). La idea de los animales vivos puede dificultar que pienses en extraer su tracto digestivo y reemplazarlo por uno genérico, pero realmente les estás haciendo un favor.Esto hará que su animal sea una clase base, y aún mejor, una que nunca tendrá que cambiar nuevamente al agregar un nuevo tipo de alimento y un procesador acorde, para que pueda concentrarse en las cosas que hacen que esta clase de animal específica sea trabajando en tan único.
Entonces, sí, las clases base tienen su lugar, la regla general se aplica (a menudo, no siempre, ya que es una regla general), pero sigue buscando comportamientos que puedas aislar.
fuente
IConsumer
interfaz? Los carnívoros pueden consumir carne, los herbívoros consumen plantas y las plantas consumen luz solar y agua.Una interfaz es realmente un contrato. No hay funcionalidad Depende del implementador implementar. No hay elección ya que uno debe implementar el contrato.
Las clases abstractas dan opciones a los implementadores. Se puede elegir tener un método que todos deben implementar, proporcionar un método con funcionalidad básica que se pueda anular o proporcionar alguna funcionalidad básica que todos los herederos puedan usar.
Por ejemplo:
Yo diría que su regla general también podría ser manejada por una clase base. En particular, dado que muchos mamíferos probablemente "comen" lo mismo, usar una clase base puede ser mejor que una interfaz, ya que uno podría implementar un método de alimentación virtual que la mayoría de los herederos podrían usar y luego los casos excepcionales podrían anular.
Ahora bien, si la definición de "comer" varía ampliamente de un animal a otro, tal vez la interfaz sería mejor. Esto es a veces un área gris y depende de la situación individual.
fuente
No.
Las interfaces tratan sobre cómo se implementan los métodos. Si basa su decisión de usar la interfaz cada vez que usa los métodos, entonces está haciendo algo mal.
Para mí, la diferencia entre la interfaz y la clase base se trata de dónde se encuentran los requisitos. Usted crea la interfaz, cuando algún fragmento de código requiere una clase con una API específica y un comportamiento implícito que surge de esta API. No puede crear una interfaz sin un código que realmente lo llame.
Por otro lado, las clases base suelen ser una forma de reutilizar el código, por lo que rara vez se molesta con el código que llamará a esta clase base y solo tendrá en cuenta la jerarquía de objetos de los que forma parte esta clase base.
fuente
eat()
en cada animal? Podemos hacer que lomammal
implementemos, ¿no? Sin embargo, entiendo su punto sobre los requisitos.Realmente no es una buena regla general porque si desea implementaciones diferentes, siempre podría tener una clase base abstracta con un método abstracto. Se garantiza que cada heredero tendrá ese método, pero cada uno define su propia implementación. Técnicamente, podría tener una clase abstracta con nada más que un conjunto de métodos abstractos, y sería básicamente lo mismo que una interfaz.
Las clases base son útiles cuando desea una implementación real de algún estado o comportamiento que pueda usarse en varias clases. Si se encuentra con varias clases que tienen campos y métodos idénticos con implementaciones idénticas, probablemente pueda refactorizarlo en una clase base. Solo debes tener cuidado en cómo heredas. Cada campo o método existente en la clase base debe tener sentido en el heredero, especialmente cuando se convierte como la clase base.
Las interfaces son útiles cuando necesita interactuar con un conjunto de clases de la misma manera, pero no puede predecir o no le importa su implementación real. Tomemos, por ejemplo, algo como una interfaz de colección. El Señor solo conoce todas las innumerables formas en que alguien podría decidir implementar una colección de artículos. ¿Lista enlazada? ¿Lista de arreglo? ¿Apilar? ¿Cola? Este método SearchAndReplace no le importa, solo necesita pasarle algo a lo que pueda llamar Agregar, Eliminar y GetEnumerator.
fuente
Estoy viendo las respuestas a esta pregunta por mí mismo, para ver lo que otras personas tienen que decir, pero diré tres cosas que me inclino a pensar sobre esto:
La gente pone demasiada fe en las interfaces y muy poca fe en las clases. Las interfaces son esencialmente clases básicas muy diluidas, y hay ocasiones en que el uso de algo ligero puede conducir a cosas como código repetitivo excesivo.
No tiene nada de malo anular un método, invocarlo
super.method()
y dejar que el resto de su cuerpo simplemente haga cosas que no interfieran con la clase base. Esto no viola el Principio de sustitución de Liskov .Como regla general, ser así de rígido y dogmático en ingeniería de software es una mala idea, incluso si algo es generalmente una mejor práctica.
Entonces tomaría lo que se dijo con un grano de sal.
fuente
People put way too much faith into interfaces, and too little faith into classes.
Gracioso. Tiendo a pensar que lo contrario es cierto.Quiero tomar un enfoque lingual y semántico hacia esto. No hay una interfaz para
DRY
. Si bien puede usarse como un mecanismo para la centralización junto con tener una clase abstracta que lo implemente, no está destinado a DRY.Una interfaz es un contrato.
IAnimal
La interfaz es un contrato de todo o nada entre el cocodrilo, el gato, el perro y la noción de un animal. Lo que dice esencialmente es "si quieres ser un animal, debes tener todos estos atributos y características, o ya no eres un animal".La herencia del otro lado es un mecanismo de reutilización. En este caso, creo que tener un buen razonamiento semántico puede ayudarlo a decidir qué método debe ir a dónde. Por ejemplo, aunque todos los animales pueden dormir, y duermen igual, no usaré una clase base para ello. Primero puse el
Sleep()
método en laIAnimal
interfaz como un énfasis (semántico) para lo que se necesita para ser un animal, luego uso una clase abstracta base para gato, perro y cocodrilo como técnica de programación para no repetirme.fuente
No estoy seguro de que esta sea la mejor regla general que existe. Puede poner un
abstract
método en la clase base y hacer que cada clase lo implemente. Como todomammal
tiene que comer, tendría sentido hacer eso. Entonces cada unomammal
puede usar su únicoeat
método. Por lo general, solo uso interfaces para la comunicación entre objetos, o como marcador para marcar una clase como algo. Creo que el uso excesivo de interfaces hace que el código sea descuidado.También estoy de acuerdo con @Panzercrisis en que una regla general debería ser solo una pauta general. No es algo que deba estar escrito en piedra. Sin duda habrá momentos en los que simplemente no funcionará.
fuente
Las interfaces son tipos. No proporcionan implementación. Simplemente definen el contrato para manejar ese tipo. Este contrato debe cumplirlo cualquier clase que implemente la interfaz.
El uso de interfaces y clases abstractas / base debe determinarse por los requisitos de diseño.
Una sola clase no requiere una interfaz.
Si dos clases son intercambiables dentro de una función, deben implementar una interfaz común. Cualquier funcionalidad que se implemente de manera idéntica podría refactorizarse en una clase abstracta que implemente la interfaz. La interfaz podría considerarse redundante en ese punto si no hay una funcionalidad distintiva. Pero entonces, ¿cuál es la diferencia entre las dos clases?
Usar clases abstractas como tipos es generalmente desordenado a medida que se agrega más funcionalidad y se expande una jerarquía.
Favorezca la composición sobre la herencia: todo esto desaparece.
fuente