Como passar dados para observador 'genérico'? Como argumentos ou como uma única estrutura?

StackOverflow https://stackoverflow.com/questions/3601886

  •  25-09-2019
  •  | 
  •  

Pergunta

Estou ocupado adicionando um mecanismo de observador genérico a um aplicativo C ++ legado (usando o Visual Studio 2010, mas não usando .NET, os delegados .NET estão fora de questão).

No design, quero separar a parte específica do aplicativo o máximo possível do mecanismo genérico do observador.

A maneira mais lógica de implementar observadores parece da seguinte maneira:

class IDoThisObserver
   {
   public:
      void handlDoThis(int arg1, int arg2) = 0;
   };

Para todos os tipos de observador (IDOTHISOBSERVER, IDOTHATOBSERVER, ...) Os argumentos dos métodos (manipulados, manipulados) são diferentes.

O que resta de uma maneira genérica de armazenar os observadores, assim:

template<typename T>
class ObserverContainer
   {
   public:
      void addObserver (T &t) {m_observers.push_back(&t);}
   private:
      std::list<T*> m_observers;
   };

Chamar um observador não pode ser generalizado, pois os argumentos são diferentes para cada tipo de observador.

Uma maneira alternativa seria 'embalar' todos os argumentos em um argumento, assim:

struct DoThisInfo
   {
   DoThisInfo (int arg1, int arg2) : m_arg1(arg1), m_arg2(arg2) {}
   int m_arg1;
   int m_arg2;
   };

E depois defina um observador mais genérico, assim:

template<typename T>
class IObserver
   {
   public:
      void notify(const T &t) = 0;
   };

E uma coleção desses observadores se tornaria a seguinte:

template<typename T>
class ObserverContainer
   {
   public:
      void addObserver (IObserver<T> &obs) {m_observers.push_back(&obs);}
   private:
      std::list<IObserver<T>*> m_observers;
   };

Agora, muito mais lógica pode ser adicionada centralmente a esse observador, incluindo chamar todos os observadores. O 'iniciador' da chamada só precisa criar e preencher a estrutura de notificação.

As aulas que desejam herdar de vários tipos de observadores precisam fazer assim:

class MyObserver : public IObserver<NotifyThis>, public IObserver<NotifyThat>
   {
   ...
   };

Qual dessas abordagens (observadores com vários argumentos explícitos ou com um argumento de estrutura) parece o melhor? Existem vantagens ou desvantagens para qualquer uma dessas abordagens?

EDITAR: Eu parecia um pouco mais longe para abordagens alternativas, e a abordagem de slot/sinal parece outro bom candidato. Existem desvantagens importantes no slot/sinalização que eu deveria conhecer?

Foi útil?

Solução

O design com o struct O argumento é definitivamente melhor, pois permite que o código genérico seja escrito no ObserverContainer. Geralmente, é uma boa prática de design substituir listas de argumentos longas por objetos que encapsulam os argumentos e este é um bom exemplo da recompensa. Criando uma abstração mais geral para o seu notify método (com a estrutura que você está definindo notify Como um método que leva um pedaço de "dados", enquanto com a lista de arg que você está definindo um método que leva dois números), você se permite escrever código genérico que usa o método e não precisa se preocupar com a composição exata do passado em pedaços de dados.

Outras dicas

Por que não apenas fazer:

class IObserver {
    // whatever is in common
};

class IDoThisObserver : public IObserver
{
   public:
      void handlDoThis(int arg1, int arg2) = 0;
};

class IDoThatObserver : public IObserver
{
   public:
      void handlDoThat(double arg1) = 0;
};

?

Então você tem:

class ObserverContainer
{
   public:
      void addObserver (IObserver* t) {m_observers.push_back(t);}
   private:
      std::list<IObserver*> m_observers;
};

Você já procurou o Boost.Signals? Melhor do que reimplementar a roda.

Quanto aos parâmetros: chamar um observador/slot deve ser conceitualmente o mesmo como se você chamasse uma função comum. A maioria das implementações do SignalsLots permite vários parâmetros, então use-o. E use sinais diferentes para diferentes tipos de observadores, não há necessidade de transmitir dados em variantes.

Duas desvantagens dos Observer-Pattern/SignalsLots que eu vi:
1) O fluxo do programa é difícil ou mesmo impossível de entender, olhando apenas para a fonte.
2) Programas fortemente dinâmicos com muitos observadores/sinalslots podem encontrar um "excluir isso"

Tudo à parte, eu gosto mais de observadores/sinalslots do que subclasse e, portanto, de alto acoplamento.

Eu não acho que nenhuma das suas abordagens se encaixaria na sua exigência como está. No entanto, uma pequena modificação usando um datacarrier que contém o conjunto de dados passou por todos os observadores em que cada observador saberia o que ler faria o truque. O código de amostra abaixo pode limpá -lo (observe que não compilei)

 enum Type {
    NOTIFY_THIS,
    NOTIFY_THAT
 };

 struct Data {
 virtual Type getType() = 0;
 };

 struct NotifyThisData: public Data {
    NotifyThisData(int _a, int _b):a(_a), b(_b) { }
    int a,b;
    Type getType() { return NOTIFY_THIS; }
 };

 struct NotifyThatData: public Data {
    NotifyThatData(std::string _str):str(_str) { }
    std::string str;
    Type getType() { return NOTIFY_THAT; }
 };

 struct DataCarrier {
    std::vector<Data*> m_TypeData;  
 };

 class IObserver {
 public:
     virtual void handle(DataCarrier& data) = 0;
 };

 class NotifyThis: public virtual IObserver {
 public:
         virtual void handle(DataCarrier& data) {
                 vector<Data*>::iterator iter = find_if(data.m_TypeData.begin(), data.m_TypeData.end(), bind2nd(functor(), NOTIFY_THIS);
                 if (iter == data.m_TypeData.end())
                         return;
                 NotifyThisData* d = dynamic_cast<NotifyThisData*>(*iter);
                 std::cout << "NotifyThis a: " << d->a << " b: " << d->b << "\n";
         }
 };

 class NotifyThat: public virtual IObserver {
 public:
         virtual void handle(DataCarrier& data) {
                 vector<Data*>::iterator iter = find_if(data.m_TypeData.begin(), data.m_TypeData.end(), bind2nd(functor(),NOTIFY_THAT);
                 if (iter == data.m_TypeData.end())
                         return;            
                 NotifyThatData* d = dynamic_cast<NotifyThatData*>(*iter);
                 std::cout << "NotifyThat str: " << d->str << "\n";
         }
 };

 class ObserverContainer
    {
    public:
       void addObserver (IObserver* obs) {m_observers.push_back(obs);}
       void notify(DataCarrier& d) {
                 for (unsigned i=0; i < m_observers.size(); ++i) {
                         m_observers[i]->handle(d);
                 }
         }
    private:
       std::vector<IObserver*> m_observers;
    };

 class MyObserver: public NotifyThis, public NotifyThat {
 public:
         virtual void handle(DataCarrier& data) { std::cout << "In MyObserver Handle data\n"; }
 };

 int main() {
         ObserverContainer container;
         container.addObserver(new NotifyThis());
         container.addObserver(new NotifyThat());
         container.addObserver(new MyObserver());

         DataCarrier d;
         d.m_TypeData.push_back(new NotifyThisData(10, 20));
         d.m_TypeData.push_back(new NotifyThatData("test"));

    container.notify(d);
    return 0;
 }

Dessa forma, você precisa modificar apenas a enumeração se você adicionar uma nova estrutura. Você também pode usar o boost :: shared_ptr para lidar com a bagunça dos ponteiros.

Eu não acertaria a sintaxe, então vou listar as declarações para ilustrar as estruturas. Um observador genérico pode esperar um parâmetro que seja subclassificado em formas específicas dos parâmetros necessários ou é uma estrutura, incluindo um mapeamento horizontal de todos os parâmetros primitivos que serão exigidos pelos seus observadores. Então o observador pode funcionar como um AbstractFactory e cada subclasse do ObserverContainer pode ser factiva e dothisobServerFactory. A fábrica construiria um observador e atribuiria uma configuração ao observador para dizer qual parâmetro esperar.

class AbstractObserverFactory {...};
class DoThatObserverFactory : AbstractObserverFactory {...};
class DoThisObserverFactory : AbstractObserverFactory {...};
class ObserverParam {...};
class DoThatObserverParam : ObserverParam {...};
class DoThisObserverParam : ObserverParam {...};
class Observer;
class DoThisObserver : public Observer
{
   public:
      void handlDoThis(DoThisObserverParam);
};
Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top