Uma tarefa na minha aula de engenharia de software é projetar um aplicativo que possa desempenhar diferentes formas em um jogo específico. O jogo em questão é Mancala, alguns desses jogos são chamados Wari ou Kalah. Esses jogos diferem em alguns aspectos, mas, para minha pergunta, é importante saber que os jogos podem diferir no seguinte:
- A maneira pela qual o resultado de uma movimentação é tratado
- A maneira pela qual o final do jogo é determinado
- A maneira pela qual o vencedor é determinado
A primeira coisa que me veio à cabeça para projetar isso foi usar o padrão de estratégia; tenho uma variação nos algoritmos (as regras reais do jogo). O design pode ficar assim:
Então pensei comigo mesmo que, no jogo de Mancala e Wari, a maneira como o vencedor é determinado é exatamente o mesmo e o código seria duplicado. Eu não acho que isso seja, por definição, uma violação da 'regra única, um lugar' ou do princípio DRY, visto que uma mudança nas regras de Mancala não significaria automaticamente que essa regra também deveria ser alterada em Wari. No entanto, pelo feedback que recebi do meu professor, tive a impressão de encontrar um design diferente.
Eu então vim com isso:
Cada jogo (Mancala, Wari, Kalah, ...) teria apenas atributo do tipo de interface de cada regra, ou seja, WinnerDeterminer
e se houver uma versão do Mancala 2.0 que seja igual ao Mancala 1.0, exceto pela forma como o vencedor for determinado, ele poderá use as versões do Mancala.
Eu acho que a implementação dessas regras como padrão de estratégia é certamente válida. Mas o verdadeiro problema surge quando quero projetá-lo ainda mais.
Ao ler sobre o padrão do método de modelo, pensei imediatamente que poderia ser aplicado a esse problema. As ações que são executadas quando um usuário faz uma mudança são sempre as mesmas e na mesma ordem, a saber:
- depositar pedras nos buracos (isso é o mesmo para todos os jogos, então seria implementado no próprio método de modelo)
- determinar o resultado da mudança
- determinar se o jogo terminou por causa da jogada anterior
- se o jogo terminar, determine quem ganhou
Esses três últimos passos estão todos no meu padrão de estratégia descrito acima. Estou tendo muitos problemas para combinar esses dois. Uma solução possível que encontrei seria abandonar o padrão de estratégia e fazer o seguinte:
Realmente não vejo a diferença de design entre o padrão de estratégia e isso? Mas tenho certeza de que preciso usar um método de modelo (apesar de ter a mesma certeza de ter que usar um padrão de estratégia).
Também não posso determinar quem seria responsável pela criação do TurnTemplate
objeto, enquanto que com o padrão de estratégia sinto que tenho famílias de objetos (as três regras) que eu poderia criar facilmente usando um padrão de fábrica abstrato. Eu teria, então, um MancalaRuleFactory
, WariRuleFactory
, etc. e que iria criar as instâncias corretas das regras e mão-me de volta um RuleSet
objeto.
Digamos que eu use o padrão de fábrica abstrato estratégia e eu tenho um RuleSet
objeto que possui algoritmos para as três regras nele. A única maneira que sinto que ainda posso usar o padrão do método template é passar esse RuleSet
objeto para o meu TurnTemplate
. O 'problema' que então surge é que eu nunca precisaria de minhas implementações concretas do TurnTemplate
, essas classes se tornariam obsoletas. Nos meus métodos protegidos no TurnTemplate
eu poderia simplesmente chamar ruleSet.determineWinner()
. Como conseqüência, a TurnTemplate
classe não seria mais abstrata, mas precisaria se tornar concreta. Ainda é um padrão de método de modelo?
Para resumir, estou pensando da maneira certa ou estou perdendo algo fácil? Se estou no caminho certo, como posso combinar um padrão de estratégia e um padrão de método de modelo?
fonte
Respostas:
Depois de analisar seus designs, a primeira e a terceira iterações parecem ter designs mais elegantes. No entanto, você menciona que é estudante e seu professor lhe deu algum feedback. Sem saber exatamente qual é a sua tarefa ou o objetivo da classe ou mais informações sobre o que seu professor sugeriu, eu aceitaria qualquer coisa que eu disser abaixo com um pouco de sal.
No seu primeiro design, você declara
RuleInterface
ser uma interface que define como lidar com a vez de cada jogador, como determinar se o jogo terminou e como determinar um vencedor após o término do jogo. Parece que essa é uma interface válida para uma família de jogos que experimenta variações. No entanto, dependendo dos jogos, você pode ter um código duplicado. Concordo que a flexibilidade de alterar as regras de um jogo é uma coisa boa, mas também argumentaria que a duplicação de código é terrível para defeitos. Se você copiar / colar código defeituoso entre as implementações e houver um erro, agora você terá vários erros que precisam ser corrigidos em locais diferentes. Se você reescrever as implementações em momentos diferentes, poderá introduzir defeitos em locais diferentes. Nenhuma delas é desejável.Seu segundo design parece bastante complexo, com uma árvore de herança profunda. Pelo menos, é mais profundo do que eu esperaria para resolver esse tipo de problema. Você também está começando a dividir os detalhes da implementação em outras classes. Por fim, você está modelando e implementando um jogo. Essa pode ser uma abordagem interessante se você precisar combinar e combinar suas regras para determinar os resultados de uma jogada, o final do jogo e um vencedor, que não parecem estar nos requisitos mencionados. . Seus jogos são conjuntos de regras bem definidos e eu tentaria encapsular os jogos o máximo possível em entidades separadas.
Seu terceiro design é aquele que eu mais gosto. Minha única preocupação é que não esteja no nível certo de abstração. No momento, você parece estar modelando uma curva. Eu recomendaria considerar projetar o jogo. Considere que você tem jogadores que estão fazendo movimentos em algum tipo de tabuleiro, usando pedras. Seu jogo exige que esses atores estejam presentes. A partir daí, seu algoritmo não é
doTurn()
maisplayGame()
, que vai do movimento inicial para o movimento final, após o qual termina. Após a jogada de cada jogador, ele ajusta o estado do jogo, determina se o jogo está em um estado final e, se estiver, determina o vencedor.Eu recomendaria examinar mais de perto seus primeiro e terceiro projetos e trabalhar com eles. Também pode ajudar a pensar em termos de protótipos. Como seriam os clientes que usam essas interfaces? Uma abordagem de design faz mais sentido para implementar um cliente que realmente instancia um jogo e joga? Você precisa perceber com o que ele está interagindo. No seu caso particular, é a
Game
classe e quaisquer outros elementos associados - você não pode projetar isoladamente.Como você menciona que é estudante, gostaria de compartilhar algumas coisas de uma época em que eu era o TA de um curso de design de software:
fonte
GameTemplate
que parece muito melhor. Também me permite combinar um método de fábrica para inicializar jogadores, o tabuleiro etc.Sua confusão é justificada. O fato é que os padrões não são mutuamente exclusivos.
O método de modelo é a base para vários outros padrões, como Estratégia e Estado. Essencialmente, a interface Strategy contém um ou mais métodos de modelo, cada um exigindo que todos os objetos que implementam uma estratégia tenham (pelo menos) algo como um método doAction (). Isso permite que as estratégias sejam substituídas uma pela outra.
Em Java, uma interface nada mais é que um conjunto de métodos de modelo. Da mesma forma, qualquer método abstrato é essencialmente um método de modelo. Esse padrão (entre outros) era bem conhecido pelos designers da linguagem, então eles o incorporaram.
A @ThomasOwens oferece excelentes conselhos para abordar seu problema específico.
fonte
Se você está sendo distraído pelos padrões de design, meu conselho é que você protótipo do jogo primeiro, então os padrões devem aparecer em você. Eu não acho que seja realmente possível ou aconselhável tentar projetar um sistema perfeitamente primeiro e depois implementá-lo (de maneira semelhante, acho perplexo quando as pessoas tentam escrever programas inteiros primeiro e depois compilar, em vez de fazê-lo pouco a pouco .) O problema é que é improvável que você pense em todos os cenários com os quais sua lógica terá que lidar, e durante a fase de implementação você perderá toda a esperança ou tentará manter seu design defeituoso original e introduzir hacks, ou até pior não entregar nada.
fonte
Vamos ao que interessa. Não há absolutamente nenhuma necessidade de interface de jogo, nenhum padrão de design, nenhuma classe abstrata e nenhuma UML.
Se você tiver uma quantidade razoável de classes de suporte, como interface do usuário, simulação e qualquer outra coisa, basicamente todo o seu código específico da lógica do jogo será reutilizado de qualquer maneira. Além disso, seu usuário não muda seu jogo dinamicamente. Você não gira a 30Hz entre os jogos. Você joga um jogo por cerca de meia hora. Portanto, seu polimorfismo "dinâmico" não é realmente dinâmico. É bastante estático.
Portanto, a melhor maneira de ir aqui é usar uma abstração funcional genérica, como C #
Action
ou C ++std::function
, criar uma classe Mancala, Wari e Kalah, e partir daí.Feito.
Você não liga para jogos. Os jogos ligam para você.
fonte