Pergunta

Eu sei que ter herança diamante é considerado uma má prática. No entanto, eu tenho 2 casos em que eu sinto que a herança diamante poderia encaixar muito bem. Eu quero perguntar, você recomendaria me usar a herança diamante nestes casos, ou há um outro projeto que poderia ser melhor.

Caso 1: Quero criar classes que representam diferentes tipos de "Ações" no meu sistema. As ações são classificados por vários parâmetros:

  • A ação pode ser "lida" ou "Write".
  • A ação pode ser com atraso ou sem demora (Não é apenas um parâmetro. Isso altera o comportamento de forma significativa).
  • "tipo de fluxo" da ação pode ser flowa ou FlowB.

Eu pretendo ter a seguinte projeto:

// 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  {/*...*/};
//...

Claro, eu vou obedecer que há 2 ações (herdam class action) irá implementar o mesmo método.

Caso 2: I implementar o padrão de projeto composto por um "Command" no meu sistema. Um comando pode ser lido, escrito, apagado, etc. Eu também quero ter uma sequência de comandos, que também podem ser lidos, escritos, deletados, etc. A sequência de comandos pode conter outras sequências de comandos.

Então, eu tenho o seguinte projeto:

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

Além disso, eu tenho um tipo especial de comandos, comandos "moderno". Ambos um comando e comando composto pode ser moderno. Ser "Modern", acrescenta uma determinada lista de propriedades para um comando e comando composto (principalmente mesmas propriedades para ambos). Eu quero ser capaz de manter um ponteiro para CommandAbstraction, e inicializá-lo (via nova) de acordo com o tipo necessária de comando. Então, eu quero fazer o seguinte projeto (além do acima):

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

Mais uma vez, eu vou ter certeza de que há 2 classes que herdam da classe CommandAbstraction irá implementar o mesmo método.

Obrigado.

Foi útil?

Solução

A herança é as relações de segundo mais forte (mais) de acoplamento em C ++, precedida apenas por amizade. Se você pode redesenhar a usar apenas a composição seu código será mais baixo acoplamento. Se não for possível, então você deve considerar se todas as suas classes realmente deve herdar a partir da base. É devido à implementação ou apenas uma interface? você vai querer usar qualquer elemento da hierarquia como um elemento de base? Ou são as folhas apenas na hierarquia que são reais Ação de? Se apenas folhas são ações e você está adicionando comportamento que você pode considerar o design baseado em políticas para este tipo de composição de comportamentos.

A idéia é que os diferentes comportamentos (ortogonais) pode ser definida em conjuntos pequenos da classe e, em seguida, agrupados para fornecer o comportamento completo real. No exemplo que irá considerar apenas uma política que define se a ação deve ser executada agora ou no futuro, e o comando para executar.

Eu fornecer uma classe abstrata para que as diferentes instâncias do modelo podem ser armazenados (através de ponteiros) em um recipiente ou passado para funções como argumentos e são chamados polymorphically.

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

O uso de herança permite que os métodos públicos de implementações de políticas para ser acessível através da instanciação de modelo. Isso não permite o uso de agregação para combinar as políticas como há novos membros de função poderia ser empurrado para a interface de classe. No exemplo, o modelo depende da política de ter um método wait (), que é comum a todas as políticas de espera. Agora à espera de um período de tempo precisa de um período de tempo fixo que é definido durante o período () Método público.

No exemplo, a política nowait é apenas um exemplo particular da política WaitSeconds com o conjunto período a 0. Esta foi intencional para marcar que a interface política não precisa ser o mesmo. Outra implementação de políticas de espera poderia estar esperando em um número de milissegundos, carrapatos de relógio, ou até que algum evento externo, fornecendo uma classe que registra como um retorno de chamada para o evento fornecido.

Se você não precisa de polimorfismo você pode tirar a partir do exemplo da classe base e os métodos virtuais completamente. Enquanto isto pode parecer excessivamente complexa para o exemplo atual, você pode decidir sobre a adição de outras políticas à mistura.

Ao adicionar novos comportamentos ortogonais implicaria um crescimento exponencial no número de classes se a herança simples é usado (com polimorfismo), com essa abordagem, você pode simplesmente implementar cada parte diferente separadamente e colá-la em conjunto no modelo de ação.

Por exemplo, você poderia fazer a sua acção periódica e adicionar uma política de saída que determinar quando sair do loop periódica. Primeiras opções que vêm à mente são LoopPolicy_NRuns e LoopPolicy_TimeSpan, LoopPolicy_Until. Este método de política (exit () no meu caso) é chamado uma vez para cada loop. As primeiras contagens de implementação do número de vezes que foi chamado um sai após um número fixo (fixada pelo utilizador, como foi período fixo no exemplo acima). A segunda implementação, periodicamente executar o processo para um determinado período de tempo, enquanto o último será executado este processo até um determinado tempo (relógio).

Se você ainda está seguindo-me até aqui, eu seria realmente fazer algumas mudanças. A primeira é que em vez de usar um modelo de parâmetro de comando que implementa um método execute () eu usaria functors e, provavelmente, um construtor templated que leva o comando para executar como parâmetro. A base racional é que isso irá torná-lo muito mais extensível em combinação com outras bibliotecas como impulso :: ligamento ou impulso :: lambda, uma vez que, em que os comandos de caso pode ser ligado no ponto de instanciação para qualquer função, functor, ou método membro livre de uma classe.

Agora eu tenho que ir, mas se você estiver interessado eu posso tentar postar uma versão modificada.

Outras dicas

Há uma diferença de qualidade de concepção entre a herança diamante centrada na aplicação onde a implementação é herdada (arriscado), e subtipagem orientada herança onde interfaces ou marcadores-interfaces são herdadas (muitas vezes útil).

Geralmente, se você pode evitar a primeira, você está melhor desde algum lugar abaixo da linha o método exato invocado pode causar problemas, ea importância de bases virtuais, estados, etc., começa a importar. Na verdade, Java não permitiria que você para puxar algo como isso, ele suporta apenas a hierarquia interface.

Eu acho que o "mais limpo" Design você pode vir para cima para isto é para ligar eficazmente todas as suas classes no diamante em mock-as interfaces (por não ter informações de estado, e ter métodos virtuais puros). Isso reduz o impacto da ambiguidade. E, claro, você pode usar a herança diamante múltipla e até mesmo para isto apenas como você usaria implementos em Java.

Então, temos um conjunto de implementações concretas dessas interfaces que podem ser implementadas de diferentes maneiras (agregação por exemplo,, mesmo de herança).

Encapsular esse quadro para que os clientes externos só obter as interfaces e nunca interagem diretamente com os tipos de concreto, e certifique-se de testar exaustivamente suas implementações.

Claro, isso é um monte de trabalho, mas se você está escrevendo uma API central e reutilizável, esta pode ser sua melhor aposta.

Eu corri para este problema só esta semana e encontrei um artigo sobre DDJ que explicou os problemas e quando você deve ou não deve se preocupar com eles. Aqui está:

"Herança múltipla considerado útil"

"Diamonds" na hierarquia de herança de interfaces é bastante seguro. - É herança de código que get é você em água quente

Para obter a reutilização de código, eu aconselho você a considerar mixins (google para Mixins C ++, se você não estiver familiarizado com o tequnique). Ao usar mixins você sente que pode "ir às compras" para os trechos de código que você precisa para implementar você classe sem usar herança múltipla de aulas com estado.

Assim, o padrão é -. Herança múltipla de interfaces e uma única cadeia de mixins (dando-lhe a reutilização de código) para ajudar a implementar a classe concreta

Espero que ajude!

Com o primeiro exemplo .....

suas se ActionRead ActionWrite precisam ser subclasses de ação.

desde que você está indo para acabar com uma classe concreta que será uma ação de qualquer maneira, você poderia actionread apenas herdar e actionwrite sem que eles sejam ações em si mesmos.

, porém, você pode inventar um código que exigiria que fossem ações. Mas, em geral eu tentar separar Ação, ler, escrever, e Delay e apenas a classe concreta misturas todos os que juntos

Com a saber mais sobre o que você está fazendo, Eu provavelmente iria reorganizar as coisas um pouco. Em vez de múltipla inheritence com todas essas versões de ação, Gostaria de fazer a leitura polimórfica e escrever e escrever classes, instanciated como delegados.

Algo como o seguinte (que não tem inheritence diamante):

Aqui eu apresento uma das muitas maneiras de aplicação Delay opcional, e assumir a metodologia de atraso é o mesmo para todos os leitores. cada subclasse pode ter sua própria implementação de atraso caso em que você deve passar para baixo para ler e instância do respectiva classe 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
    {};
};

Para o caso 2, não é um OneCommand apenas um caso especial de CompositeCommand? Se você eliminar OneCommand e permitir CompositeCommands ter apenas um elemento, eu acho que seu projeto fica mais simples:

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

Você ainda tem o diamante temido, mas acho que isso pode ser um caso aceitável para ele.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top