9 alternativas para uma equipe ruim (padrão de design)

imagem

O que é isso e por quê?


Ao projetar, um desenvolvedor pode encontrar um problema: criaturas e objetos podem ter habilidades diferentes em diferentes combinações. Os sapos pulam e nadam, os patos nadam e voam, mas não com um peso, e os sapos podem voar com galhos e patos. Portanto, é conveniente mudar de herança para composição e adicionar habilidades dinamicamente. A necessidade de animar sapos voadores levou a uma rejeição injustificada dos métodos de habilidade e a transferência de seu código para as equipes em uma das implementações. Aqui está:

class CastSpellCommand extends Command { constructor (source, target, spell) { this.source = source; this.target = target; this.spell = spell; } execute () { const spellAbility = this.source.getAbility(SpellCastAbility); //      if (!cond) return error if (spellAbility == null) { throw new Error('NoSpellCastAbility'); } this.addChildren(new PayManaCommand(this.source, this.spell.manaCost)); this.addChildren(this.spell.getCommands(this.source, this.target)); //   : healthAbility.health = Math.max( 0, resultHealth ); } } // : async onMeleeHit (meleeHitCommand) { await view.drawMeleeHit( meleeHitCommand.source, meleeHitCommand.target ); } async onDealDamage (dealDamageCommand) { await view.showDamageNumbers( dealDamageCommand.target, dealDamageCommand.damage ); } 

O que pode ser feito?


Considere várias abordagens de natureza diferente:

Observador


 class Executor extends Observer {/* ... */} class Animator extends Observer {/* ... */} 

Uma solução clássica bem conhecida dos programadores. Você só precisa alterá-lo minimamente para verificar os valores retornados pelos observadores:

 this.listeners.reduce((result, listener) => result && listener(action), true) 

Desvantagem: os observadores devem assinar os eventos na ordem correta.

Se você manipular erros, o animador também poderá mostrar animações de ações com falha. Você pode passar o valor anterior para os observadores; conceitualmente, a solução permanece a mesma. Se métodos de observação ou funções de retorno de chamada são chamados, se um loop regular é usado em vez de convolução, os detalhes não são tão significativos.

Deixe como está


E de fato. A abordagem atual tem desvantagens e vantagens:

  1. Testar a capacidade de executar um comando requer a execução de um comando
  2. Argumentos em uma ordem de mudança, condições, prefixos de método são conectados
  3. Dependências de loop (comando <ortografia <comando)
  4. Entidades adicionais para cada ação (o método é substituído pelo método, classe e seu construtor)
  5. Excesso de conhecimento e ações de uma equipe individual: da mecânica do jogo aos erros de sincronização e manipulação direta das propriedades de outras pessoas
  6. A interface é enganosa (executa não apenas chamadas, mas também adiciona comandos via addChildren; o que, obviamente, faz o oposto)
  7. Necessidade duvidosa e implementação de instruções recursivas per se
  8. A classe despachante, se houver, não executa suas funções
  9. [+] Alegadamente, a única maneira de animar na prática, se as animações precisarem de dados completos (indicados como o principal motivo)
  10. [+] Provavelmente outros motivos

Algumas das deficiências podem ser tratadas separadamente, mas o restante exige mudanças mais drásticas.

ad hoc


  • As condições para a execução da equipe, especialmente a mecânica do jogo, devem ser retiradas das equipes e executadas separadamente. As condições podem mudar no tempo de execução, e destacar os botões inativos em cinza ocorre na prática muito antes do início do trabalho de animação, sem mencionar a lógica. Para evitar a cópia, pode fazer sentido armazenar condições gerais em protótipos de capacidade.
  • Métodos de retorno, em combinação com o parágrafo anterior, a necessidade de tais verificações desaparecerá:

     const spellAbility = this.source.getAbility(SpellCastAbility); //      if (!cond) return error if (spellAbility == null) { throw new Error('NoSpellCastAbility'); } 

    O próprio mecanismo javascript mostrará o TypeError correto quando o método for chamado por engano.
  • A equipe também não precisa desse conhecimento:

     healthAbility.health = Math.max( 0, resultHealth ); 
  • Para resolver o problema dos argumentos que mudam de lugar, eles podem ser passados ​​pelo objeto.
  • Embora o código de chamada não esteja disponível para estudo, parece que a maioria das deficiências cresce devido à maneira não ideal de invocar ações do jogo. Por exemplo, os manipuladores de botão acessam entidades específicas. Portanto, substituí-los em manipuladores por comandos específicos parece bastante natural. Se você tem um expedidor, é muito mais fácil chamar uma animação após a ação, você pode transferir as mesmas informações para ela, para que não haja falta de dados.

Fila


Para mostrar a animação da ação após a conclusão da ação, basta adicioná-los à fila e executá-los aproximadamente como na solução 1.

 [ [ walkRequirements, walkAction, walkAnimation ], [ castRequirements, castAction, castAnimation ], // ... ] 

Não importa quais entidades estão na matriz: funções banidas com os parâmetros necessários, instâncias de classes personalizadas ou objetos comuns.
O valor dessa solução é a simplicidade e a transparência; é fácil criar uma janela deslizante para visualizar os N últimos comandos.

Adequado para prototipagem e depuração.

Classe de subestudo


Nós fazemos uma aula de animação para a habilidade.

 class MovementAbility { walk (...args) { // action } } class AnimatedMovementAbility { walk (...args) { // animation } } 

Se for impossível fazer alterações na classe de chamada, herdamos dela ou decoramos o método desejado para que ele chame a animação. Ou transmitimos animação em vez de habilidade, eles têm a mesma interface.

Bem adequados quando você precisa praticamente do mesmo conjunto de métodos, eles podem ser verificados e testados automaticamente.

Combinações de métodos


 const AnimatedMovementAbility = combinedClass(MovementAbility, { ['*:before'] (method, ...args) { // call requirements }, ['*:after'] (method, ...args) { // call animations } }) 

Seria uma oportunidade interessante com suporte ao idioma nativo.
É bom usá-lo se essa opção for mais produtiva, embora um proxy seja realmente necessário.

Proxies


Nós envolvemos habilidades em proxies, capturamos métodos em getters.

 new Proxy(new MovementAbility, {/* handler */}) 

Desvantagem: muitas vezes mais lenta que as chamadas regulares, o que não é tão importante para a animação. Em um servidor que processa milhões de objetos, a desaceleração seria perceptível, mas o servidor não precisa de animação.

Promessa


Você pode construir cadeias a partir do Promise, mas há outra opção (ES2018):

 for await (const action of actionDispatcher.getActions()) { // } 

getActions retorna um iterador de ação assíncrono. O próximo método do iterador retorna a promessa adiada da próxima ação. Depois de processar eventos do usuário e do servidor, chamamos resolve (), cria uma nova promessa.

Melhor equipe


Crie objetos como este:

 {actor, ability, method, options} 

O código se resume a verificar e chamar o método de capacidade com parâmetros. A opção mais fácil e produtiva.

Nota


Source: https://habr.com/ru/post/pt439506/


All Articles