Pregunta

Sé que en el diamante de la herencia se considera una mala práctica.Sin embargo, tengo 2 casos en los que siento que el diamante de la herencia podría encajar muy bien.Quiero preguntar, ¿me recomienda el uso de diamante de la herencia en estos casos, o es que hay otro diseño que podría ser mejor.

Caso 1: Quiero crear clases que representan los diferentes tipos de "Acciones" en mi sistema.Las acciones se clasifican por varios parámetros:

  • La acción se puede "Leer" o "Escribir".
  • La acción puede ser con retraso o sin retraso (no es sólo de 1 parámetro.Cambia el comportamiento de manera significativa).
  • La acción del "tipo de flujo" 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, voy a obedecer, que no hay 2 acciones (que hereda de la clase de Acción) va a aplicar el mismo método.

Caso 2: Me implementar el patrón de diseño composite para un "Comando" en mi sistema.Un comando puede ser leído, escrito, borrado, etc.También quiero tener una secuencia de comandos, que también puede ser leído, escrito, borrado, etc.Una secuencia de comandos puede contener otras secuencias de comandos.

Así que 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, "Moderno" de comandos.Tanto un comando y comando compuesto puede ser moderno.Ser "Moderno", añade una cierta lista de propiedades para un comando y comando compuesto (en su mayoría mismas propiedades para ambos).Quiero ser capaz de contener un puntero a CommandAbstraction, y se inicializa (a través de las nuevas), de acuerdo a las necesidades del tipo de comando.Así que me quiero hacer el siguiente diseño (además de las anteriores) :

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
};

Otra vez, me aseguraré de que no hay 2 clases que heredan de CommandAbstraction clase va a aplicar el mismo método.

Gracias.

¿Fue útil?

Solución

La herencia es las relaciones segundo más fuerte (más de acoplamiento) en C ++, precedido únicamente por amistad. Si se puede rediseñar en usar solamente composición se acopla de manera más flexible su código. Si no puede, entonces usted debe considerar si todas sus clases realmente debe heredar de la base. ¿Se debe a la aplicación o simplemente una interfaz? Va a querer usar cualquier elemento de la jerarquía como un elemento de base? O son simplemente deja en su jerarquía que son verdadera acción de? Si sólo hojas son actos y que están agregando comportamiento se puede considerar el diseño basado Política para este tipo de composición de comportamientos.

La idea es que los diferentes comportamientos (ortogonales) pueden ser definidas en el conjunto de clases pequeñas y luego se juntan para proporcionar el comportamiento completo real. En el ejemplo que voy a considerar sólo 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 diferentes instanciaciones de la plantilla se pueden almacenar (a través de punteros) en un contenedor o pasaron a funciones como argumentos y se les llaman polimórficamente.

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 a los métodos públicos de las implementaciones de políticas que sean accesibles a través de la creación de instancias de plantilla. Esto no permite el uso de agregación para combinar las políticas como no hay nuevos miembros función podría ser empujados en la interfaz de clase. En el ejemplo, la plantilla depende de la política de tener un método wait (), que es común a todas las políticas de espera. Ahora la espera de un período de tiempo necesita un período de tiempo fijo que se establece a través del tiempo () método público.

En el ejemplo, la política NOWAIT es sólo un ejemplo particular de la política WaitSeconds con el periodo establecido en 0. Esto fue intencional para marcar que la interfaz de la política no tiene por qué ser el mismo. Otra aplicación de las políticas de espera podría estar esperando en un número de milisegundos, reloj avanza, o hasta que algún evento externo, proporcionando una clase que se registra como una devolución de llamada para el evento dado.

Si usted no necesita el polimorfismo se puede sacar del ejemplo de la clase base y los métodos virtuales en total. Si bien esto puede parecer demasiado complejo para el ejemplo actual, puede decidir sobre la adición de otras políticas a la mezcla.

Si bien la adición de nuevos comportamientos ortogonales que implicaría un crecimiento exponencial en el número de clases si la herencia normal se utiliza (con el polimorfismo), con este método sólo se puede aplicar cada parte diferente separado y la cola juntos en la plantilla de acción.

Por ejemplo, usted podría hacer su acción periódica y añadir una política de salida que determinan cuándo salir del bucle periódica. Las primeras opciones que vienen a la mente son LoopPolicy_NRuns y LoopPolicy_TimeSpan, LoopPolicy_Until. Este método de política (exit () en mi caso) se llama una vez para cada bucle. La primera implementación cuenta el número de veces que se ha llamado un salidas después de un número fijo (fijado por el usuario, como el período se fijó en el ejemplo anterior). La segunda aplicación podría ejecutar periódicamente el proceso para un determinado período de tiempo, mientras que el último se ejecutará este proceso hasta un tiempo determinado (reloj).

Si todavía me está siguiendo hasta aquí, me gustaría hacer algunos cambios de hecho. El primero de ellos es que en lugar de utilizar un comando de parámetro de plantilla que implementa un método execute () Me gustaría utilizar palabras funcionales y, probablemente, un constructor de plantilla que toma el comando a 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 ser obligados a punta de ejemplificación a cualquier función, funtor, o el método de usuario gratis de una clase.

Ahora tengo que ir, pero si usted está interesado puedo intentar publicar una versión modificada.

Otros consejos

Hay una diferencia entre la calidad de diseño de diamante herencia orientado a la implementación en que la aplicación se hereda (arriesgado), y subtipificación orientada a la herencia, donde se heredan interfaces o marcadores interfaces (a menudo útil).

En general, si se puede evitar el primer caso, es mejor desde algún punto del borde del método invocado exacta puede causar problemas, y la importancia de las bases virtuales, estados, etc., comienza a importar. De hecho, Java no se permitirá sacar algo por el estilo, sólo es compatible con la jerarquía de la interfaz.

Creo que la "limpia" el diseño que puede llegar a esto es a su vez de manera efectiva todas sus clases en el diamante en simulacros de interfaces (por no tener información de estado, y que tiene métodos virtuales puros). Esto reduce el impacto de la ambigüedad. Y, por supuesto, puede utilizar múltiples e incluso diamantes herencia de este al igual que lo utilice implementos en Java.

A continuación, tener un conjunto de implementaciones concretas de estas interfaces que se pueden implementar de diferentes maneras (por ejemplo, agregación, incluso herencia).

Encapsular este marco para que los clientes externos sólo reciben las interfaces y nunca interactúan directamente con los tipos de hormigón, y asegúrese de probar a fondo sus implementaciones.

Por supuesto, esto es un montón de trabajo, pero si usted está escribiendo una API central y reutilizable, esto podría ser la mejor opción.

Me encontré con este problema sólo en esta semana y encontré un artículo sobre DDJ que explica los problemas y cuando se debe o no debe estar preocupado por ellos. Aquí está:

"herencia múltiple consideran útiles"

"Los diamantes" en la jerarquía de herencia de interfaces es muy segura - es la herencia de código que obtenga en agua caliente.

Para conseguir la reutilización de código, te aconsejo que considerar los mixins (de google para C++ Mixins si usted no está familiarizado con el tequnique).Cuando el uso de mixins usted se siente como usted puede "ir de compras" para los fragmentos de código que usted necesita para implementar la clase sin necesidad de utilizar la herencia múltiple de estado de las clases.

Así, el patrón es - la herencia múltiple de interfaces y de una sola cadena de mixins (dándole la reutilización de código) para ayudar a implementar la clase concreta.

Espero que ayude!

Con el primer ejemplo .....

si su ActionRead ActionWrite tiene por qué ser subclases de acción en absoluto.

puesto que usted va a terminar con una clase concreta que será una acción de todos modos, sólo podría heredar actionread y actionwrite sin que sean en sí mismas acciones.

Sin embargo, usted podría inventar código que les obligaría a ser acciones. Pero, en general, me gustaría probar y acción separada, leer, escribir, y Delay y justo la clase concreta mezcla todos que juntos

sin necesidad de conocer más de lo que está haciendo, Yo probablemente reorganizar las cosas un poco. En lugar de sucesiones múltiple con todas estas versiones de la acción, Me gustaría hacer la lectura polimórficos y clases de escritura y la escritura, instanciado como delegados.

algo como el siguiente (que no tiene inheritence diamante):

A continuación les presento una de las muchas formas de ejecución opcional de retardo, y asumir la metodología de retardo es el mismo para todos los lectores. cada subclase podría tener su propia implementación de retraso en cuyo caso debe pasar hacia abajo para leer e instancia de la respectivo derivado clase de retardo.

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
    {};
};

Para el caso 2, no es un OneCommand sólo un caso especial de CompositeCommand? Si se elimina OneCommand y permitir a CompositeCommands tienen un solo elemento, creo que su diseño se simplifica:

              CommandAbstraction
                 /          \
                /            \
               /              \
        ModernCommand      CompositeCommand
               \               /
                \             /
                 \           /
             ModernCompositeCommand

Usted todavía tiene el diamante temida, pero creo que esto puede ser un caso aceptable para él.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top