Interfaz y herencia: ¿Lo mejor de ambos mundos?

10

Descubrí las interfaces y comencé a amarlas. La belleza de una interfaz es que es un contrato, y cualquier objeto que cumpla ese contrato se puede usar donde se requiera esa interfaz.

El problema con una interfaz es que no puede tener una implementación predeterminada, lo cual es un problema para las propiedades mundanas y derrota a DRY. Esto también es bueno, porque mantiene la implementación y el sistema desacoplado. La herencia, por un lado, mantiene un acoplamiento más apretado y tiene el potencial de romper la encapsulación.

Caso 1 (Herencia con miembros privados, buena encapsulación, estrechamente acoplada)

class Employee
{
int money_earned;
string name;

public:
 void do_work(){money_earned++;};
 string get_name(return name;);
};


class Nurse : public Employee: 
{
   public:
   void do_work(/*do work. Oops, can't update money_earned. Unaware I have to call superclass' do_work()*/);

};

void HireNurse(Nurse *n)
{
   nurse->do_work();
)

Caso 2 (solo una interfaz)

class IEmployee
{
     virtual void do_work()=0;
     virtual string get_name()=0;
};

//class Nurse implements IEmployee.
//But now, for each employee, must repeat the get_name() implementation,
//and add a name member string, which breaks DRY.

Caso 3: (¿lo mejor de ambos mundos?)

Similar al caso 1 . Sin embargo, imagine que (hipotéticamente) C ++ no permitió métodos de anulación, excepto aquellos métodos que son puramente virtuales .

Entonces, en el Caso 1 , anular do_work () causaría un error en tiempo de compilación. Para solucionar esto, configuramos do_work () como virtual puro y agregamos un método separado increment_money_earned (). Como ejemplo:

class Employee
{
int money_earned;
string name;

public:
 virtual void do_work()=0;
 void increment_money_earned(money_earned++;);
 string get_name(return name;);
};


class Nurse : public Employee: 
{
   public:
   void do_work(/*do work*/ increment_money_earned(); ); .
};

Pero incluso esto tiene problemas. ¿Qué pasa si dentro de 3 meses, Joe Coder crea un empleado médico, pero se olvida de llamar a increment_money_earned () en do_work ()?


La pregunta:

  • ¿Es el caso 3 superior al caso 1 ? ¿Se debe a una "mejor encapsulación" o a un "acoplamiento más flexible", o alguna otra razón?

  • ¿Es el caso 3 superior al caso 2 porque se ajusta a DRY?

MustafaM
fuente
2
... ¿reinventan las clases abstractas o qué?
ZJR

Respuestas:

10

¡Una forma de resolver el problema de olvidar-llamar-a-la-superclase es devolver el control a la superclase! He repasado tu primer ejemplo para mostrar cómo (y lo hice compilar;)). Oh, yo también supongo que do_work()en el Employeese supone que es virtualen su primer ejemplo.

#include <string>

using namespace std;

class Employee
{
    int money_earned;
    string name;
    virtual void on_do_work() {}

    public:
        void do_work() { money_earned++; on_do_work(); }
        string get_name() { return name; }
};

class Nurse : public Employee
{
    void on_do_work() { /* do more work. Oh, and I don't have to call do_work()! */ }
};

void HireNurse(Nurse* nurse)
{
    nurse->do_work();
}

Ahora do_work()no se puede anular. Si desea extenderlo, debe hacerlo a través del on_do_work()cual do_work()tiene control.

Esto, por supuesto, también se puede usar con la interfaz de su segundo ejemplo si lo Employeeextiende. Por lo tanto, si te entiendo correctamente, creo que eso hace que este Caso 3 ¡pero sin tener que usar C ++ hipotético! Es SECO y tiene una fuerte encapsulación.

Gyan alias Gary Buyn
fuente
3
Y ese es el patrón de diseño conocido como "método de plantilla" ( en.wikipedia.org/wiki/Template_method_pattern ).
Joris Timmermans
Sí, esto cumple con el caso 3. Esto parece prometedor. Examinará en detalle. Además, este es algún tipo de sistema de eventos. ¿Hay un nombre para este 'patrón'?
MustafaM
@MadKeithV ¿estás seguro de que este es el 'método de plantilla'?
MustafaM
@illmath: sí, es un método público no virtual que delega partes de los detalles de su implementación en métodos virtuales protegidos / privados.
Joris Timmermans
@illmath No lo había pensado como un método de plantilla antes, pero creo que es un ejemplo básico de uno. Acabo de encontrar este artículo que tal vez quieras leer donde el autor cree que merece su propio nombre:
Idioma de
1

El problema con una interfaz es que no puede tener una implementación predeterminada, lo cual es un problema para las propiedades mundanas y derrota a DRY.

En mi opinión, las interfaces deberían tener solo métodos puros, sin una implementación predeterminada. No rompe el principio DRY de ninguna manera, porque las interfaces muestran cómo acceder a alguna entidad. Solo para referencias, estoy viendo la explicación DRY aquí :
"Cada conocimiento debe tener una representación única, inequívoca y autorizada dentro de un sistema".

Por otro lado, SOLID le dice que cada clase debe tener una interfaz.

¿Es el caso 3 superior al caso 1? ¿Se debe a una "mejor encapsulación" o a un "acoplamiento más flexible", o alguna otra razón?

No, el caso 3 no es superior al caso 1. Debe tomar una decisión. Si desea tener una implementación predeterminada, hágalo. Si quieres un método puro, ve con él.

¿Qué pasa si dentro de 3 meses, Joe Coder crea un empleado médico, pero se olvida de llamar a increment_money_earned () en do_work ()?

Entonces Joe Coder debería obtener lo que se merece por ignorar las pruebas de unidades que fallan. Él probó esta clase, ¿no? :)

¿Qué caso es el mejor para un proyecto de software que podría tener 40,000 líneas de código?

Una talla no sirve para todos. Es imposible saber cuál es mejor. Hay algunos casos en los que uno encajaría mejor que el otro.

Tal vez debería aprender algunos patrones de diseño en lugar de tratar de inventar algunos propios.


Me acabo de dar cuenta de que está buscando un patrón de diseño de interfaz no virtual , porque así es como se ve su clase case 3.

BЈовић
fuente
Gracias por el comentario. He actualizado el Caso 3 para aclarar mi intención.
MustafaM
1
Voy a tener que -1 aquí. No hay ninguna razón para decir que todas las interfaces deben ser puras, o que todas las clases deben heredar de una interfaz.
DeadMG
@DeadMG ISP
B 17овић
@VJovic: Hay una gran diferencia entre SOLID y "Todo debe heredar de una interfaz".
DeadMG
"Una talla no sirve para todos" y "aprender algunos patrones de diseño" son correctos: el resto de su respuesta viola su propia sugerencia de que una talla no sirve para todos.
Joris Timmermans
0

Las interfaces pueden tener implementaciones predeterminadas en C ++. No hay nada que diga que una implementación predeterminada de una función no depende únicamente de otros miembros virtuales (y argumentos), por lo que no aumenta ningún tipo de acoplamiento.

Para el caso 2, DRY está reemplazando aquí. La encapsulación existe para proteger su programa del cambio, de diferentes implementaciones, pero en este caso, no tiene implementaciones diferentes. Entonces la encapsulación YAGNI.

De hecho, las interfaces en tiempo de ejecución generalmente se consideran inferiores a sus equivalentes en tiempo de compilación. En el caso de tiempo de compilación, puede tener tanto el caso 1 como el caso 2 en el mismo paquete, sin mencionar sus numerosas ventajas. O incluso en tiempo de ejecución, simplemente puede hacer Employee : public IEmployeepara obtener efectivamente la misma ventaja. Hay numerosas formas de lidiar con tales cosas.

Case 3: (best of both worlds?)

Similar to Case 1. However, imagine that (hypothetically)

Dejé de leer. YAGNI C ++ es lo que es C ++, y el comité de Estándares nunca, nunca va a implementar tal cambio, por excelentes razones.

DeadMG
fuente
Dices "no tienes implementaciones diferentes". Pero lo hago. Tengo la implementación de Nurse de Employee, y podría tener otras implementaciones más tarde (un Doctor, un Conserje, etc.). He actualizado el Caso 3 para que quede más claro lo que quise decir.
MustafaM
@illmath: Pero no tienes otras implementaciones de get_name. Todas sus implementaciones propuestas compartirían la misma implementación de get_name. Además, como dije, no hay razón para elegir, puedes tener ambas. Además, el caso 3 no tiene ningún valor. Puede anular virtuales no puros, así que olvídese de un diseño donde no pueda.
DeadMG
¡Las interfaces no solo pueden tener implementaciones predeterminadas en C ++, sino que también pueden tener implementaciones predeterminadas y seguir siendo abstractas! es decir, IMethod virtual vacío () = 0 {std :: cout << "Ni!" << std :: endl; }
Joris Timmermans
@MadKeithV: No creo que pueda definirlos en línea, pero el punto sigue siendo el mismo.
DeadMG
@MadKeith: Como si Visual Studio hubiera sido una representación particularmente precisa de Standard C ++.
DeadMG
0

¿Es el caso 3 superior al caso 1? ¿Se debe a una "mejor encapsulación" o a un "acoplamiento más flexible", o alguna otra razón?

Por lo que veo en su implementación, su implementación de Caso 3 requiere una clase abstracta que puede implementar métodos virtuales puros que luego pueden cambiarse en la clase derivada. El caso 3 sería mejor ya que la clase derivada puede cambiar la implementación de do_work cuando sea necesario y todas las instancias derivadas básicamente pertenecerían al tipo abstracto base.

Qué caso es el mejor para un proyecto de software que podría tener 40,000 líneas de código.

Diría que depende puramente de su diseño de implementación y del objetivo que desea lograr. La clase abstracta y las interfaces se implementan en función del problema que debe resolverse.

Editar en la pregunta

¿Qué pasa si dentro de 3 meses, Joe Coder crea un empleado médico, pero se olvida de llamar a increment_money_earned () en do_work ()?

Se pueden realizar pruebas unitarias para verificar si cada clase confirma el comportamiento esperado. Entonces, si se aplican las pruebas unitarias adecuadas, se pueden evitar errores cuando Joe Coder implementa la nueva clase.

Karthik Sreenivasan
fuente
0

El uso de interfaces solo rompe DRY si cada implementación es un duplicado de cada una. Puede resolver este dilema aplicando tanto la interfaz como la herencia, sin embargo, hay algunos casos en los que puede desear implementar la misma interfaz en varias clases, pero varía el comportamiento en cada una de las clases, y esto seguirá siendo el principio de SECO. Si elige usar cualquiera de los 3 enfoques que ha descrito, todo se reduce a las elecciones que necesita hacer para aplicar la mejor técnica para que coincida con una situación dada. Por otro lado, probablemente encontrará que con el tiempo, usa más interfaces y aplica la herencia solo donde desea eliminar la repetición. Eso no quiere decir que este es el único razón para la herencia, pero es mejor minimizar el uso de la herencia para permitirle mantener sus opciones abiertas si encuentra que su diseño necesita cambiar más adelante, y si desea minimizar el impacto en las clases descendientes de los efectos de un cambio introduciría en una clase para padres.

S.Robins
fuente