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 world
classe que possui uma variável de membro vector<monster> monsters
. Quando a monster
classe se comunica com o world
, devo preferir usar uma função de retorno de chamada ou devo ter um ponteiro para a world
classe dentro da monster
classe?
architecture
software-engineering
hidayat
fonte
fonte
Respostas:
Existem três maneiras principais pelas quais uma classe pode conversar com outra sem estar intimamente ligada a ela:
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:
O C ++ não possui um bom suporte para retornos de chamada que retêm seu
this
ponteiro, portanto, é difícil usar retornos de chamada no código orientado a objetos.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
monster
realmente precisa se comunicarworld
. Supondo que eu faria algo como:A idéia aqui é que você coloque apenas
IWorld
(que é um nome ruim) o mínimoMonster
necessário para acessar. Sua visão do mundo deve ser a mais estreita possível.fonte
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.
fonte
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.
fonte
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.
fonte
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:
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.
fonte
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:
self
enquanto 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.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.
fonte
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.
fonte