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?
fuente
Respuestas:
¡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 elEmployee
se supone que esvirtual
en su primer ejemplo.Ahora
do_work()
no se puede anular. Si desea extenderlo, debe hacerlo a través delon_do_work()
cualdo_work()
tiene control.Esto, por supuesto, también se puede usar con la interfaz de su segundo ejemplo si lo
Employee
extiende. 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.fuente
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.
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.
Entonces Joe Coder debería obtener lo que se merece por ignorar las pruebas de unidades que fallan. Él probó esta clase, ¿no? :)
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.
fuente
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 IEmployee
para obtener efectivamente la misma ventaja. Hay numerosas formas de lidiar con tales cosas.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.
fuente
get_name
. Todas sus implementaciones propuestas compartirían la misma implementación deget_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.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.
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
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.
fuente
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.
fuente