tipos - Herencia de diamantes(C++)
porque java no utiliza herencia multiple (7)
Sé que tener herencia de diamantes se considera una mala práctica. Sin embargo, tengo 2 casos en los que siento que la herencia de diamantes podría encajar muy bien. Quiero preguntar, ¿me recomendaría usar herencia de diamantes en estos casos, o hay otro diseño que podría ser mejor?
Caso 1: Quiero crear clases que representen diferentes tipos de "Acciones" en mi sistema. Las acciones se clasifican por varios parámetros:
- La acción puede ser "Leer" o "Escribir".
- La acción puede ser con retraso o sin demora (no es solo 1 parámetro. Cambia el comportamiento significativamente).
- El "tipo de flujo" de la acción puede ser FlowA o FlowB.
Tengo la intención de tener el siguiente diseño:
// abstract classes
class Action
{
// methods relevant for all actions
};
class ActionRead : public virtual Action
{
// methods related to reading
};
class ActionWrite : public virtual Action
{
// methods related to writing
};
class ActionWithDelay : public virtual Action
{
// methods related to delay definition and handling
};
class ActionNoDelay : public virtual Action {/*...*/};
class ActionFlowA : public virtual Action {/*...*/};
class ActionFlowB : public virtual Action {/*...*/};
// concrete classes
class ActionFlowAReadWithDelay : public ActionFlowA, public ActionRead, public ActionWithDelay
{
// implementation of the full flow of a read command with delay that does Flow A.
};
class ActionFlowBReadWithDelay : public ActionFlowB, public ActionRead, public ActionWithDelay {/*...*/};
//...
Por supuesto, obedeceré que no hay 2 acciones (que hereden de la clase Action) implementarán el mismo método.
Caso 2: Implemento el patrón de diseño compuesto para un "Comando" en mi sistema. Se puede leer, escribir, borrar, etc., un comando. También quiero tener una secuencia de comandos, que también se puedan leer, escribir, eliminar, etc. Una secuencia de comandos puede contener otras secuencias de comandos.
Entonces tengo el siguiente diseño:
class CommandAbstraction
{
CommandAbstraction(){};
~CommandAbstraction()=0;
void Read()=0;
void Write()=0;
void Restore()=0;
bool IsWritten() {/*implemented*/};
// and other implemented functions
};
class OneCommand : public virtual CommandAbstraction
{
// implement Read, Write, Restore
};
class CompositeCommand : public virtual CommandAbstraction
{
// implement Read, Write, Restore
};
Además, tengo un tipo especial de comandos, comandos "Modernos". Tanto un comando como un comando compuesto pueden ser modernos. Al ser "Moderno" se agrega una cierta lista de propiedades a un comando compuesto y compuesto (en su mayoría, las mismas propiedades para ambos). Quiero poder mantener un puntero a CommandAbstraction e inicializarlo (a través de nuevo) de acuerdo con el tipo de comando necesario. Así que quiero hacer el siguiente diseño (además de lo anterior):
class ModernCommand : public virtual CommandAbstraction
{
~ModernCommand()=0;
void SetModernPropertyA(){/*...*/}
void ExecModernSomething(){/*...*/}
void ModernSomethingElse()=0;
};
class OneModernCommand : public OneCommand, public ModernCommand
{
void ModernSomethingElse() {/*...*/};
// ... few methods specific for OneModernCommand
};
class CompositeModernCommand : public CompositeCommand, public ModernCommand
{
void ModernSomethingElse() {/*...*/};
// ... few methods specific for CompositeModernCommand
};
Nuevamente, me aseguraré de que no haya 2 clases heredadas de la clase CommandAbstraction que implementen el mismo método.
Gracias.
"Diamantes" en la jerarquía de herencia de las interfaces es bastante seguro: es una herencia de código que te lleva al agua caliente.
Para obtener la reutilización del código, te aconsejo que consideres mixins (google para C ++ Mixins si no estás familiarizado con el tequnique). Cuando usa mixins, siente que puede "ir de compras" por los fragmentos de código que necesita para implementar su clase sin usar herencia múltiple de clases con estado.
Entonces, el patrón es: herencia múltiple de interfaces y una única cadena de mixins (que le da reutilización de código) para ayudar a implementar la clase concreta.
¡Espero que ayude!
Con el primer ejemplo .....
es si ActionRead ActionWrite necesita ser subclases de acción en absoluto.
ya que vas a terminar con una clase concreta que será una acción de todos modos, puedes heredar actionread y actionwrite sin que sean acciones en sí mismas.
sin embargo, podrías inventar un código que requiera que sean acciones. Pero, en general, trataría de separar Acción, Leer, Escribir y Demorar y solo la clase concreta mezcla todo eso junto
Existe una diferencia de calidad de diseño entre la herencia de diamante orientada a la implementación donde la implementación es heredada (arriesgada) y la herencia orientada a la subtipificación donde las interfaces o las interfaces de marcador se heredan (a menudo son útiles).
En general, si puede evitar lo primero, estará mejor ya que en algún momento el método invocado exacto puede causar problemas, y la importancia de las bases virtuales, estados, etc. comienza a importar. De hecho, Java no le permitiría obtener algo así, solo admite la jerarquía de la interfaz.
Creo que el diseño "más limpio" que puede ofrecer es convertir efectivamente todas sus clases en el diamante en interfaces simuladas (al no tener información de estado y tener métodos virtuales puros). Esto reduce el impacto de la ambigüedad. Y, por supuesto, puede usar herencia de diamantes múltiple e incluso para esto como lo haría con los implementos en Java.
Luego, tenga un conjunto de implementaciones concretas de estas interfaces que se puedan implementar de diferentes maneras (por ejemplo, agregación, incluso herencia).
Encapsule este marco para que los clientes externos solo obtengan las interfaces y nunca interactúen directamente con los tipos concretos, y asegúrese de probar exhaustivamente sus implementaciones.
Por supuesto, esto es mucho trabajo, pero si estás escribiendo una API central y reutilizable, esta podría ser tu mejor opción.
Me encontré con este problema esta semana y encontré un artículo sobre DDJ que explicaba los problemas y cuándo debería o no debería preocuparse por ellos. Aquí está:
Para el caso 2, ¿no es un OneCommand
solo un caso especial de CompositeCommand
? Si eliminas OneCommand
y permites que CompositeCommand
s solo tenga un elemento, creo que tu diseño se simplifica:
CommandAbstraction
/ /
/ /
/ /
ModernCommand CompositeCommand
/ /
/ /
/ /
ModernCompositeCommand
Todavía tienes el temido diamante, pero creo que este puede ser un caso aceptable.
Sin saber más de lo que estás haciendo, probablemente reorganice las cosas un poco. En lugar de la herencia múltiple con todas estas versiones de acción, haría lecturas polimórficas y clases de escritura y escritura, instanciadas como delegadas.
Algo como lo siguiente (que no tiene herencia de diamantes):
Aquí presento una de las muchas maneras de implementar el retraso opcional, y supongo que la metodología de demora es la misma para todos los lectores. cada subclase podría tener su propia implementación de retraso, en cuyo caso pasaría a Lectura e instancia de la respectiva clase Delay derivada.
class Action // abstract
{
// Reader and writer would be abstract classes (if not interfaces)
// from which you would derive to implement the specific
// read and write protocols.
class Reader // abstract
{
Class Delay {...};
Delay *optional_delay; // NULL when no delay
Reader (bool with_delay)
: optional_delay(with_delay ? new Delay() : NULL)
{};
....
};
class Writer {... }; // abstract
Reader *reader; // may be NULL if not a reader
Writer *writer; // may be NULL if not a writer
Action (Reader *_reader, Writer *_writer)
: reader(_reader)
, writer(_writer)
{};
void read()
{ if (reader) reader->read(); }
void write()
{ if (writer) writer->write(); }
};
Class Flow : public Action
{
// Here you would likely have enhanced version
// of read and write specific that implements Flow behaviour
// That would be comment to FlowA and FlowB
class Reader : public Action::Reader {...}
class Writer : public Action::Writer {...}
// for Reader and W
Flow (Reader *_reader, Writer *_writer)
: Action(_reader,_writer)
, writer(_writer)
{};
};
class FlowA :public Flow // concrete
{
class Reader : public Flow::Reader {...} // concrete
// The full implementation for reading A flows
// Apparently flow A has no write ability
FlowA(bool with_delay)
: Flow (new FlowA::Reader(with_delay),NULL) // NULL indicates is not a writer
{};
};
class FlowB : public Flow // concrete
{
class Reader : public Flow::Reader {...} // concrete
// The full implementation for reading B flows
// Apparently flow B has no write ability
FlowB(bool with_delay)
: Flow (new FlowB::Reader(with_delay),NULL) // NULL indicates is not a writer
{};
};
La herencia es la segunda relación más fuerte (más acoplamiento) en C ++, precedida solo por la amistad. Si puede rediseñar para usar solo composición, su código estará más débilmente acoplado. Si no puede, entonces debería considerar si todas sus clases realmente deberían heredar de la base. ¿Se debe a la implementación o solo a una interfaz? ¿Querrá usar algún elemento de la jerarquía como elemento base? ¿O solo son hojas en su jerarquía que son acciones reales? Si solo las hojas son acciones y agrega comportamiento, puede considerar el diseño basado en políticas para este tipo de composición de comportamientos.
La idea es que diferentes comportamientos (ortogonales) se pueden definir en pequeños conjuntos de clases y luego agruparse para proporcionar el comportamiento real completo. En el ejemplo consideraré solo una política que define si la acción se va a ejecutar ahora o en el futuro, y el comando a ejecutar.
Proporciono una clase abstracta para que se puedan almacenar diferentes instancias de la plantilla (a través de punteros) en un contenedor o pasarlas a funciones como argumentos y obtener una llamada polimórfica.
class ActionDelayPolicy_NoWait;
class ActionBase // Only needed if you want to use polymorphically different actions
{
public:
virtual ~Action() {}
virtual void run() = 0;
};
template < typename Command, typename DelayPolicy = ActionDelayPolicy_NoWait >
class Action : public DelayPolicy, public Command
{
public:
virtual run() {
DelayPolicy::wait(); // inherit wait from DelayPolicy
Command::execute(); // inherit command to execute
}
};
// Real executed code can be written once (for each action to execute)
class CommandSalute
{
public:
void execute() { std::cout << "Hi!" << std::endl; }
};
class CommandSmile
{
public:
void execute() { std::cout << ":)" << std::endl; }
};
// And waiting behaviors can be defined separatedly:
class ActionDelayPolicy_NoWait
{
public:
void wait() const {}
};
// Note that as Action inherits from the policy, the public methods (if required)
// will be publicly available at the place of instantiation
class ActionDelayPolicy_WaitSeconds
{
public:
ActionDelayPolicy_WaitSeconds() : seconds_( 0 ) {}
void wait() const { sleep( seconds_ ); }
void wait_period( int seconds ) { seconds_ = seconds; }
int wait_period() const { return seconds_; }
private:
int seconds_;
};
// Polimorphically execute the action
void execute_action( Action& action )
{
action.run();
}
// Now the usage:
int main()
{
Action< CommandSalute > salute_now;
execute_action( salute_now );
Action< CommandSmile, ActionDelayPolicy_WaitSeconds > smile_later;
smile_later.wait_period( 100 ); // Accessible from the wait policy through inheritance
execute_action( smile_later );
}
El uso de la herencia permite que los métodos públicos de las implementaciones de políticas sean accesibles a través de la instanciación de plantillas. Esto no permite el uso de agregación para combinar las políticas, ya que no se pueden insertar nuevos miembros de funciones en la interfaz de clase. En el ejemplo, la plantilla depende de que la política tenga un método wait (), que es común para todas las políticas en espera. Ahora, la espera de un período de tiempo necesita un período de tiempo fijo que se establece a través del método público de período ().
En el ejemplo, la política No Espere es solo un ejemplo particular de la política WaitSeconds con el período establecido en 0. Esto fue intencional para marcar que la interfaz de la política no tiene que ser la misma. Otra implementación de política de espera podría estar esperando en un número de milisegundos, marcaciones de reloj o hasta algún evento externo, proporcionando una clase que se registre como una devolución de llamada para el evento dado.
Si no necesita polimorfismo, puede sacar del ejemplo la clase base y los métodos virtuales por completo. Si bien esto puede parecer demasiado complejo para el ejemplo actual, puede decidir agregar otras políticas a la mezcla.
Si bien agregar nuevos comportamientos ortogonales implicaría un crecimiento exponencial en el número de clases si se utiliza la herencia simple (con polimorfismo), con este enfoque puede implementar cada parte diferente por separado y pegarla en la plantilla de Acción.
Por ejemplo, puede hacer que su acción sea periódica y agregar una política de salida que determine cuándo salir del ciclo periódico. Las primeras opciones que se me ocurren son LoopPolicy_NRuns y LoopPolicy_TimeSpan, LoopPolicy_Until. Este método de política (exit () en mi caso) se llama una vez para cada ciclo. La primera implementación cuenta el número de veces que se ha llamado una salida después de un número fijo (fijado por el usuario, ya que el período se corrigió en el ejemplo anterior). La segunda implementación ejecutaría periódicamente el proceso durante un período de tiempo determinado, mientras que la segunda ejecutará este proceso hasta un tiempo determinado (reloj).
Si todavía me siguen hasta aquí, de hecho haré algunos cambios. El primero es que en lugar de usar un parámetro de plantilla Command que implementa un método execute () usaría funtores y probablemente un constructor con plantilla que toma el comando para ejecutar como parámetro. La razón es que esto hará que sea mucho más extensible en combinación con otras bibliotecas como boost :: bind o boost :: lambda, ya que en ese caso los comandos podrían vincularse en el punto de instanciación a cualquier función libre, functor o método miembro. de una clase.
Ahora me tengo que ir, pero si estás interesado, puedo intentar publicar una versión modificada.