¿Es “si un método se reutiliza sin cambios, coloque el método en una clase base, de lo contrario cree una interfaz”, una buena regla general?

10

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 caty dog, que extienden la clase mammaly tienen un único método pet(). Luego agregamos la clase alligator, que no extiende nada y tiene un solo método slither().

Ahora, queremos agregar un eat()método a todos ellos.

Si la implementación del eat()método será exactamente la misma para cat, dogy alligator, deberíamos crear una clase base (digamos, animal), que implemente este método.

Sin embargo, si su implementación alligatordifiere de la manera más leve, deberíamos crear una IEatinterfaz y hacerla mammale alligatorimplementarla.

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?

sbichenko
fuente
27
alligatorLa implementación de eatdifiere, por supuesto, en que acepta caty dogcomo parámetros.
sbichenko
1
Donde realmente desea una clase base abstracta para compartir implementaciones, pero debe usar interfaces para una extensibilidad correcta, a menudo puede usar rasgos en su lugar. Es decir, si su idioma lo admite.
amon
2
Cuando te pintas en un rincón, es mejor estar en el más cercano a la puerta.
JeffO
10
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 reutilización del código debe realizarse (idealmente) a través de la composición. Tu compañero de trabajo está equivocado.
Riwalk
2
Un profesional de CS mío enseñó que las superclases deberían ser is ay las interfaces son acts likeo is. Entonces un perro is amamífero y acts likeun 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 de issería The cake is eatableo The book is writable.
MirroredFate

Respuestas:

13

No creo que esta sea una buena regla general. Si le preocupa la reutilización del código, puede implementar un PetEatingBehaviorrol que defina las funciones de alimentación para gatos y perros. Entonces puede tener IEaty 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 IEatcontiene menos información que la Mammalclase 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

Simon Bergot
fuente
¿Podría proporcionar un ejemplo básico de PetEatingBehaviorimplementación?
sbichenko
1
@exizt Primero, soy malo para elegir nombres. Así PetEatingBehaviores, 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 el eatmétodo (instancia de dientes, instancia de estómago, etc.). Solo contiene el código común a los gatos y perros utilizados en su eatmétodo. Simplemente tiene que crear un PetEatingBehaviormiembro y reenviarlo a dog.eat/cat.eat.
Simon Bergot el
No puedo creer que esta sea la única respuesta de muchos para alejar el OP de las herencias :( ¡La herencia no es para reutilizar el código!
Danny Tuppeny,
1
@DannyTuppeny ¿Por qué no?
sbichenko
44
@exizt Heredar de una clase es un gran problema; estás tomando una dependencia (probablemente permanente) de algo, de lo cual en muchos idiomas solo puedes tener uno. La reutilización de código es una cosa bastante trivial para usar esta clase base a menudo solo. ¿Qué pasa si terminas con métodos donde quieres compartirlos con otra clase, pero ya tiene una clase base debido a la reutilización de otros métodos (o incluso, que es parte de una jerarquía "real" utilizada polimórficamente (?)). Use la herencia cuando la Clase A es un tipo de Clase B (por ejemplo, un automóvil hereda del vehículo), no cuando comparte algunos detalles de implementación interna :-)
Danny Tuppeny
11

¿Vale la pena seguir esta regla de oro?

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.

Telastyn
fuente
1
Al releer esta respuesta 3 años después, considero que este es un gran punto: al elegir usar una clase base en lugar de una interfaz, estás diciendo "esta es la funcionalidad central de la clase (y cualquier heredero), y es inapropiado mezclar willy-nilly con otra funcionalidad "
sbichenko
9

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 ay las interfaces son acts likeo is. Entonces un perro is amamífero y acts likeun comedor. Esto nos diría que los mamíferos deberían ser una clase y el comedor debería ser una interfaz. Un ejemplo de issería The cake is eatableo The book is writable(haciendo ambas eatablee writableinterfaces).

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.

Destino Espejado
fuente
6

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 un foreachbucle.

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:

La composición es a menudo más apropiada que la herencia. ... [B] ya que el código que implementa una subclase se extiende entre la base y la subclase, puede ser más difícil entender una implementación. La subclase no puede anular funciones que no son virtuales, por lo que la subclase no puede cambiar la implementación.

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 duplicar FileStreamel código. Las personas inteligentes reconocen que esto es estúpido. Simplemente debe tener un FileStreamobjeto en su clase (ya sea como una variable local o miembro) y usar su funcionalidad.

El FileStreamejemplo 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.

Riwalk
fuente
Entonces, si te entiendo correctamente, la inyección de dependencia proporciona las mismas capacidades de reutilización de código que la herencia. ¿Pero para qué usarías la herencia? ¿Proporcionarías un caso de uso? (Realmente aprecié el que tenía FileStream)
sbichenko
1
@exizt, no, no proporciona las mismas capacidades de reutilización de código. Proporciona mejores capacidades de reutilización de código (en muchos casos) ya que mantiene las implementaciones bien contenidas. Las clases interactúan explícitamente a través de sus objetos y su API pública, en lugar de obtener capacidades implícitamente y cambiar por completo la mitad de la funcionalidad al sobrecargar las funciones existentes.
Riwalk
4

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:

public interface IFoodEater
{
    void Eat(IFood food);
}

public class Animal : IFoodEater
{
    private IFoodProcessor _foodProcessor;
    public Animal(IFoodProcessor foodProcessor)
    {
        _foodProcessor = foodProcessor;
    }

    public void Eat(IFood food)
    {
        _foodProcessor.Process(food);
    }
}

O implemente un patrón de visitante donde FoodProcessorintente 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.

CodeCaster
fuente
1
El IFoodEater es algo redundante. ¿Qué más esperas poder comer? Sí, los animales como ejemplos son algo terrible.
Eufórico el
1
¿Qué pasa con las plantas carnívoras? :) En serio, un problema importante con el no uso de interfaces en este caso es que has vinculado íntimamente el concepto de comer con animales y es mucho más trabajo introducir nuevas abstracciones para las cuales la clase base Animal no es apropiada.
Julia Hayward
1
Creo que "comer" es la idea equivocada aquí: ir más abstracto. ¿Qué tal una IConsumerinterfaz? Los carnívoros pueden consumir carne, los herbívoros consumen plantas y las plantas consumen luz solar y agua.
2

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:

public abstract class Person
    {
        /// <summary>
        /// Inheritors must implement a hello
        /// </summary>
        /// <returns>Hello</returns>
        public abstract string SayHello();
        /// <summary>
        /// Inheritors can use base functionality, or override
        /// </summary>
        /// <returns></returns>
        public virtual string SayGoodBye()
        {
            return "Good Bye";
        }
        /// <summary>
        /// Base Functionality that is inherited 
        /// </summary>
        public void DoSomething()
        {
            //Do Something Here
        }
    }

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.

Jon Raynor
fuente
1

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.

Eufórico
fuente
Espera, ¿por qué volver a implementar eat()en cada animal? Podemos hacer que lo mammalimplementemos, ¿no? Sin embargo, entiendo su punto sobre los requisitos.
sbichenko
@exizt: Lo siento, leí tu pregunta incorrectamente.
Eufórico el
1

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.

usuario1100269
fuente
1

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:

  1. 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.

  2. 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 .

  3. 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.

Panzercrisis
fuente
55
People put way too much faith into interfaces, and too little faith into classes.Gracioso. Tiendo a pensar que lo contrario es cierto.
Simon Bergot el
1
"Esto no viola el Principio de sustitución de Liskov". Pero está muy cerca de convertirse en una violación de LSP. Mejor prevenir que curar.
Eufórico el
1

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.

IAnimalLa 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 la IAnimalinterfaz 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.

Saeed Neamati
fuente
0

No estoy seguro de que esta sea la mejor regla general que existe. Puede poner un abstractmétodo en la clase base y hacer que cada clase lo implemente. Como todo mammaltiene que comer, tendría sentido hacer eso. Entonces cada uno mammalpuede usar su único eatmé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á.

Jason Crosby
fuente
-1

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.

usuario108620
fuente