Baixo acoplamento e coesão firme


11

Claro que depende da situação. Mas quando um objeto ou sistema de alavanca inferior se comunica com um sistema de nível superior, os retornos de chamada ou eventos devem ser preferidos a manter um ponteiro para o objeto de nível superior?

Por exemplo, temos uma worldclasse que possui uma variável de membro vector<monster> monsters. Quando a monsterclasse se comunica com o world, devo preferir usar uma função de retorno de chamada ou devo ter um ponteiro para a worldclasse dentro da monsterclasse?


Além da ortografia no título da pergunta, ela não é realmente formulada na forma de uma pergunta. Acho que reformulá-lo pode ajudar a solidificar o que você está perguntando aqui, já que não acho que você possa obter uma resposta útil para essa pergunta em sua forma atual. E também não é uma questão de design de jogo, é uma questão sobre a estrutura de programação (não tenho certeza se isso significa que a tag de design é ou não é apropriada, não consigo lembrar de onde descemos sobre 'design de software' e tags)
MrCranky 22/02

Respostas:


10

Existem três maneiras principais pelas quais uma classe pode conversar com outra sem estar intimamente ligada a ela:

  1. Através de uma função de retorno de chamada.
  2. Através de um sistema de eventos.
  3. Através de uma interface.

Os três estão intimamente relacionados. Um sistema de eventos de várias maneiras é apenas uma lista de retornos de chamada. Um retorno de chamada é mais ou menos uma interface com um único método.

Em C ++, eu raramente uso retornos de chamada:

  1. O C ++ não possui um bom suporte para retornos de chamada que retêm seu thisponteiro, portanto, é difícil usar retornos de chamada no código orientado a objetos.

  2. Um retorno de chamada é basicamente uma interface de método único não extensível. Com o tempo, percebo que quase sempre acabo precisando de mais de um método para definir essa interface e um único retorno de chamada raramente é suficiente.

Nesse caso, eu provavelmente faria uma interface. Na sua pergunta, você não explica exatamente o que monsterrealmente precisa se comunicar world. Supondo que eu faria algo como:

class IWorld {
public:
  virtual Monster* getNearbyMonster(const Position & position) = 0;
  virtual Item*    getItemAt(const Position & position) = 0;
};

class Monster {
public:
  void update(IWorld * world) {
    // Do stuff...
  }
}

class World : public IWorld {
public:
  virtual Monster* getNearbyMonster(const Position & position) {
    // ...
  }

  virtual Item*    getItemAt(const Position & position) {
    // ...
  }

  // Lots of other stuff that Monster should not have access to...
}

A idéia aqui é que você coloque apenas IWorld(que é um nome ruim) o mínimo Monsternecessário para acessar. Sua visão do mundo deve ser a mais estreita possível.


1
Os delegados +1 (retornos de chamada) geralmente se tornam mais numerosos com o passar do tempo. Dar uma interface aos monstros para que eles possam entender as coisas é um bom caminho a percorrer na minha opinião.
Michael Coleman

12

Não use uma função de retorno de chamada para ocultar qual função você está chamando. Se você colocar uma função de retorno de chamada, e essa função de retorno tiver uma e apenas uma função atribuída a ela, então você não está realmente quebrando o acoplamento. Você está apenas mascarando com outra camada de abstração. Você não está ganhando nada (além do tempo de compilação), mas está perdendo a clareza.

Eu não chamaria exatamente isso de melhor prática, mas é um padrão comum ter entidades contidas em algo para ter um ponteiro para seus pais.

Dito isto, pode valer a pena usar o padrão de interface para dar aos seus monstros um subconjunto limitado de funcionalidades que eles podem chamar no mundo.


+1 Dar ao monstro uma maneira limitada de chamar os pais é um bom meio caminho, na minha opinião.
Michael Coleman

7

Geralmente, tento evitar os links bidirecionais, mas, se precisar deles, garanto que existe um método para fazê-los e outro para quebrá-los, para que você nunca tenha inconsistências.

Muitas vezes, você pode evitar o link bidirecional inteiramente, transmitindo dados conforme necessário. Uma refatoração trivial é fazer com que, em vez de ter o monstro mantendo um vínculo com o mundo, você passe o mundo por referência aos métodos de monstro que precisam dele. Melhor ainda é passar apenas uma interface para os bits do mundo que o monstro precisa estritamente, o que significa que o monstro não depende da implementação concreta do mundo. Isso corresponde ao Princípio de Segregação de Interface e ao Princípio de Inversão de Dependência , mas não começa a introduzir o excesso de abstração que você pode obter com eventos, sinais + slots, etc.

De certa forma, você pode argumentar que o uso de um retorno de chamada é uma mini interface muito especializada, e isso é bom. Você precisa decidir se é possível atingir seus objetivos de maneira mais significativa por meio de uma coleção de métodos em um objeto de interface ou de vários tipos em retornos de chamada diferentes.


3

Eu tento evitar que objetos contidos chamem seu contêiner porque acho que isso gera confusão, torna-se muito fácil de justificar, fica muito usado e cria dependências que não podem ser gerenciadas.

Na minha opinião, a solução ideal é que as classes de nível superior sejam inteligentes o suficiente para gerenciar as classes de nível inferior. Por exemplo, o mundo que sabe determinar se ocorreu uma colisão entre um monstro e um cavaleiro sem saber sobre o outro é melhor comigo do que o monstro que pergunta ao mundo se colidiu com um cavaleiro.

Outra opção no seu caso, eu provavelmente descobriria por que a classe de monstros precisa saber sobre a classe mundial e você provavelmente descobrirá que há algo na classe mundial que pode ser dividido em uma classe própria que ela faz sentido para a classe monstro saber.


2

Você não vai muito longe sem eventos, obviamente, mas antes mesmo de começar a escrever (e projetar) um sistema de eventos, você deve fazer a pergunta real: por que o monstro se comunicaria com a classe mundial? Deveria mesmo?

Vamos dar uma situação "clássica", um monstro atacando um jogador.

Monstro está atacando: o mundo pode muito bem identificar a situação em que um herói está ao lado de um monstro e dizer ao monstro para atacar. Portanto, a função no monstro seria:

void Monster::attack(LivingCreature l)
{
  // Call to combat system
}

Mas o mundo (que já conhece o monstro) não precisa ser conhecido pelo monstro. Na verdade, o monstro pode ignorar a própria existência da classe World, que provavelmente é melhor.

A mesma coisa quando o monstro está se movendo (eu deixo os subsistemas pegar a criatura e lidar com a computação / intenção, o monstro é apenas um pacote de dados, mas muita gente diria que isso não é um verdadeiro POO).

Meu argumento é: eventos (ou retorno de chamada) são ótimos, é claro, mas não são a única resposta para todos os problemas que você enfrentar.


1

Sempre que posso, tento restringir a comunicação entre os objetos a um modelo de solicitação e resposta. Existe uma ordem parcial implícita nos objetos no meu programa, de modo que, entre dois objetos A e B, possa haver uma maneira de A chamar direta ou indiretamente um método de B ou de B chamar direta ou indiretamente um método de A , mas nunca é possível que A e B chamem mutuamente os métodos uns dos outros. Às vezes, é claro, você deseja ter uma comunicação com o chamador de um método. Há algumas maneiras pelas quais eu gosto de fazer isso, e nenhuma delas é de retorno de chamada.

Uma maneira é incluir mais informações no valor de retorno da chamada do método, o que significa que o código do cliente decide o que fazer com ele após o procedimento retornar o controle para ele.

A outra maneira é chamar um objeto filho mútuo. Ou seja, se A chama um método em B, e B precisa comunicar algumas informações para A, B chama um método em C, onde A e B podem chamar C, mas C não pode chamar A ou B. O objeto A seria responsável por obter as informações de C após B retorna o controle para A. Observe que isso não é fundamentalmente diferente da primeira maneira que propus. O objeto A ainda pode recuperar as informações apenas de um valor de retorno; nenhum dos métodos do objeto A é invocado por B ou C. Uma variação desse truque é passar C como parâmetro ao método, mas as restrições no relacionamento de C com A e B ainda se aplicam.

Agora, a questão importante é por que insisto em fazer as coisas dessa maneira. Existem três razões principais:

  • Isso mantém meus objetos mais fracamente acoplados. Meus objetos podem encapsular outros objetos, mas eles nunca dependerão do contexto do chamador e o contexto nunca dependerá dos objetos encapsulados.
  • Isso mantém meu fluxo de controle fácil de raciocinar. É bom poder assumir que o único código que pode alterar o estado interno selfenquanto um método em execução é esse e não o outro. Esse é o mesmo tipo de raciocínio que pode levar alguém a colocar mutexes em objetos simultâneos.
  • Ele protege invariantes nos dados encapsulados dos meus objetos. É permitido que métodos públicos dependam de invariantes, e esses invariantes podem ser violados se um método puder ser chamado externamente enquanto outro já estiver em execução.

Não sou contra todos os usos de retornos de chamada. De acordo com minha política de nunca "ligar para o chamador", se um objeto A chama um método em B e passa um retorno de chamada para ele, o retorno de chamada pode não alterar o estado interno de A e isso inclui os objetos encapsulados por A e o objetos no contexto de A. Em outras palavras, o retorno de chamada só pode invocar métodos em objetos dados a ele por B. O retorno de chamada, na verdade, está sob as mesmas restrições que B.

Um último ponto a perder é que permitirei a invocação de qualquer função pura, independentemente dessa ordem parcial de que estou falando. As funções puras são um pouco diferentes dos métodos, pois não podem mudar ou depender de estados mutáveis ​​ou efeitos colaterais; portanto, não há preocupação em confundir assuntos.


0

Pessoalmente? Eu apenas uso um singleton.

Sim, ok, design ruim, não orientado a objetos, etc. Você sabe o que? Eu não me importo . Estou escrevendo um jogo, não uma demonstração de tecnologia. Ninguém vai me dar nota no código. O objetivo é criar um jogo divertido, e tudo o que estiver no meu caminho resultará em um jogo menos divertido.

Você já teve dois mundos funcionando ao mesmo tempo? Talvez! Talvez você vá. Mas a menos que você possa pensar nessa situação agora, provavelmente não o fará.

Então, minha solução: faça um mundo único. Chame funções nele. Seja feito com toda a bagunça. Você pode passar um parâmetro extra para cada função - e não se engane, é para isso que isso leva. Ou você pode simplesmente escrever um código que funcione.

Fazer dessa maneira requer um pouco de disciplina para limpar as coisas quando fica bagunçado (é "quando", não "se"), mas não há como impedir que o código fique bagunçado - você tem o problema do espaguete ou os milhares problema de camadas de abstração. Pelo menos dessa forma, você não está escrevendo grandes quantidades de código desnecessário.

E se você decidir que não quer mais um singleton, geralmente é bastante simples se livrar dele. Dá algum trabalho, é preciso passar um bilhão de parâmetros, mas esses são os parâmetros que você precisaria passar de qualquer maneira.


2
Eu provavelmente diria isso de forma mais sucinta: "Não tenha medo de refatorar".
Tétrada

Singletons são maus! Globais são muito melhores. Todas as virtudes singleton que você listou são as mesmas para um global. Uso ponteiros globais (na verdade funções globais retornando referências) para meu subsistema e os inicializo / destruo na função principal. Os indicadores Globals evitam problemas de ordem de inicialização, singletons pendurados durante o desmembramento, construção não trivial de singletons etc. Repito que singletons são maus.
Deft_code

@ Tetrad, altamente concordado. É uma das melhores habilidades que você pode ter.
ZorbaTHut
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.