Em esta série de posts , Eric Lippert descreve um problema no design orientado a objetos usando assistentes e guerreiros como exemplos, em que:
abstract class Weapon { }
sealed class Staff : Weapon { }
sealed class Sword : Weapon { }
abstract class Player
{
public Weapon Weapon { get; set; }
}
sealed class Wizard : Player { }
sealed class Warrior : Player { }
e depois adiciona algumas regras:
- Um guerreiro só pode usar uma espada.
- Um assistente pode usar apenas uma equipe.
Ele então demonstra os problemas com os quais você se depara se tentar aplicar essas regras usando o sistema do tipo C # (por exemplo, responsabilizando a Wizard
classe por garantir que um assistente possa usar apenas uma equipe). Você viola o princípio de substituição de Liskov, corre o risco de exceções em tempo de execução ou acaba com um código difícil de estender.
A solução que ele apresenta é que nenhuma validação é feita pela classe Player. É usado apenas para rastrear o estado. Então, em vez de dar uma arma a um jogador:
player.Weapon = new Sword();
o estado é modificado por se de Command
acordo com Rule
s:
... criamos um
Command
objeto chamadoWield
que leva dois objetos de estado do jogo, aPlayer
e aWeapon
. Quando o usuário emite um comando para o sistema “esse assistente deve usar essa espada”, esse comando é avaliado no contexto de um conjunto deRule
s, que produz uma sequência deEffect
s. Temos umaRule
que diz que quando um jogador tenta empunhar uma arma, o efeito é que a arma existente, se houver, é descartada e a nova arma se torna a arma do jogador. Temos outra regra que fortalece a primeira regra, que diz que os efeitos da primeira regra não se aplicam quando um mago tenta empunhar uma espada.
Eu gosto dessa idéia em princípio, mas tenho uma preocupação sobre como ela pode ser usada na prática.
Nada parece impedir que um desenvolvedor de iludirem o Commands
e Rule
é simplesmente definindo o Weapon
em um Player
. A Weapon
propriedade precisa estar acessível pelo Wield
comando, portanto, não pode ser feita private set
.
Então, o que impede um desenvolvedor de fazer isso? Eles apenas precisam se lembrar de não?
Respostas:
Todo o argumento que uma série de postagens do blog leva até a Parte Cinco :
Armas, personagens, monstros e outros objetos do jogo não são responsáveis por verificar o que podem ou não fazer. O sistema de regras é responsável por isso. O
Command
objeto também não está fazendo nada com os objetos do jogo. Representa apenas a tentativa de fazer algo com eles. O sistema de regras verifica se o comando é possível e quando é o sistema de regras executa o comando chamando os métodos apropriados nos objetos do jogo.Se um desenvolvedor quiser criar um segundo sistema de regras que faça coisas com personagens e armas que o primeiro sistema de regras não permitiria, ele poderá fazer isso porque em C # você não pode (sem hacks de reflexão desagradáveis) descobrir de onde vem uma chamada de método de.
Uma solução alternativa que pode funcionar em algumas situações é colocar os objetos do jogo (ou suas interfaces) em um assembly com o mecanismo de regras e marcar como mutador qualquer método
internal
. Todos os sistemas que precisam de acesso somente leitura aos objetos do jogo estariam em uma montagem diferente, o que significa que eles só poderiam acessar ospublic
métodos. Isso ainda deixa a brecha dos objetos do jogo que chamam os métodos internos um do outro. Mas fazer isso seria um cheiro óbvio para o código, porque você concordou que as classes de objetos do jogo deveriam ser detentoras de estado.fonte
O problema óbvio do código original é que ele está fazendo Modelagem de Dados em vez de Modelagem de Objetos . Observe que não há absolutamente nenhuma menção aos requisitos comerciais reais no artigo vinculado!
Eu começaria tentando obter requisitos funcionais reais. Por exemplo: "Qualquer jogador pode atacar outros jogadores, ...". Aqui:
"Os jogadores podem empunhar uma arma usada no ataque, os Magos podem empunhar um Cajado, os Guerreiros uma Espada":
"Cada arma causa dano ao inimigo atacado". Ok, agora temos que ter uma interface comum para a Arma:
E assim por diante ... Por que não há
Wield()
noPlayer
? Porque não havia exigência de que qualquer jogador pudesse usar qualquer arma.Posso imaginar que haveria um requisito que diz: "Qualquer um
Player
pode tentar exercer algumWeapon
". Isso seria uma coisa completamente diferente no entanto. Eu modelaria talvez dessa maneira:Resumo: Modele os requisitos e apenas os requisitos. Não faça modelagem de dados, isso não é modelagem oo.
fonte
Uma maneira seria passar o
Wield
comando para oPlayer
. O jogador então executa oWield
comando, que verifica as regras apropriadas e retorna oWeapon
, com o qualPlayer
ele define seu próprio campo de arma. Dessa forma, o campo Arma pode ter um setter privado e só pode ser definido através da passagem de umWield
comando para o jogador.fonte
Nada impede que o desenvolvedor faça isso. Na verdade, Eric Lippert tentou muitas técnicas diferentes, mas todas tinham fraquezas. Esse era o ponto principal da série que impedir o desenvolvedor de fazer isso não é fácil e tudo o que ele tentou teve desvantagens. Finalmente, ele decidiu que usar um
Command
objeto com regras é o caminho a seguir.Com as regras, você pode definir a
Weapon
propriedade deWizard
a como a,Sword
mas quando você pedeWizard
para usar a arma (espada) e atacar, ela não terá nenhum efeito e, portanto, não mudará nenhum estado. Como ele diz abaixo:Em outras palavras, não podemos impor essa regra por meio de
type
relacionamentos que ele tentou de muitas maneiras diferentes, mas que não gostaram ou não deram certo. Portanto, a única coisa que ele disse que podemos fazer é fazer algo sobre isso em tempo de execução. Lançar uma exceção não era bom porque ele não a considera uma exceção.Ele finalmente escolheu a solução acima. Essa solução basicamente diz que você pode definir qualquer arma, mas quando você a produz, se não a arma certa, seria essencialmente inútil. Mas nenhuma exceção seria lançada.
Eu acho que é uma boa solução. Embora, em alguns casos, eu também siga o padrão de teste.
fonte
This solution basically says you can set any weapon but when you yield it, if not the right weapon, it would be essentially useless.
Não consegui encontrá-lo nessa série. Você poderia me indicar onde esta solução é proposta?that the existing weapon, if there is one, is dropped and the new weapon becomes the player’s weapon
. Enquanto a segunda regra quethat strengthens the first rule, that says that the first rule’s effects do not apply when a wizard tries to wield a sword.
Então, eu acho que existe uma regra verificando se a arma é espada, por isso não pode ser manejada por um wizzard, por isso não está definida. Em vez disso, soa um triste trombone.Wield
aqui. Eu acho que é um nome um pouco enganador para o comando. Algo comoChangeWeapon
seria mais preciso. Eu acho que você poderia ter um modelo diferente onde você pode definir qualquer arma, mas quando você a produz, se não a arma certa, seria essencialmente inútil . Isso parece interessante, mas não acho que seja o que Eric Lippert descreve.A primeira solução descartada do autor foi representar as regras pelo sistema de tipos. O sistema de tipos é avaliado em tempo de compilação. Se você desanexar as regras do sistema de tipos, elas não serão mais verificadas pelo compilador; portanto, não há nada que impeça um desenvolvedor de cometer um erro per se.
Mas esse problema é enfrentado por toda peça de lógica / modelagem que não é verificada pelo compilador e a resposta geral para isso é o teste (de unidade). Portanto, a solução proposta pelo autor precisa de um forte recurso de teste para contornar os erros dos desenvolvedores. Para enfatizar esse ponto de precisar de um forte recurso de teste para erros que só são detectados em tempo de execução, consulte este artigo de Bruce Eckel, que argumenta que você precisa trocar a digitação forte por testes mais fortes em linguagens dinâmicas.
Em conclusão, a única coisa que pode impedir os desenvolvedores de cometer erros é ter um conjunto de testes (unitários), verificando se todas as regras são respeitadas.
fonte
Talvez eu tenha perdido uma sutileza aqui, mas não tenho certeza se o problema está no sistema de tipos. Talvez seja com convenção em c #.
Por exemplo, você pode tornar esse tipo completamente seguro, protegendo o
Weapon
setterPlayer
. Em seguida, adicionesetSword(Sword)
esetStaff(Staff)
paraWarrior
e,Wizard
respectivamente, que chamam o setter protegido.Dessa forma, o relacionamento
Player
/Weapon
é estaticamente verificado e o código que não se importa pode usar apenas aPlayer
para obter aWeapon
.fonte
Weapon
a aPlayer
. Mas não existe um sistema de tipos em que você não conheça os tipos concretos em tempo de compilação que possam atuar nesses tipos concretos em tempo de compilação. Por definição. Esse esquema significa que é apenas o caso que precisa ser tratado em tempo de execução. Como tal, é realmente melhor do que qualquer um dos esquemas de Eric.Essa questão é efetivamente a mesma com o tópico bastante conhecido como " onde colocar a validação " (provavelmente observando também o ddd).
Portanto, antes de responder a essa pergunta, deve-se perguntar: qual é a natureza das regras que você deseja seguir? Eles são esculpidos em pedra e definem a entidade? A quebra dessas regras resulta em uma entidade que deixa de ser o que é? Se sim, além de manter essas regras na validação de comandos , coloque-as em uma entidade também. Portanto, se um desenvolvedor esquecer de validar o comando, suas entidades não estarão em um estado inválido.
Se não - bem, isso implica inerentemente que essas regras são específicas de comandos e não devem residir em entidades de domínio. Portanto, violar essas regras resulta em ações que não deveriam ter sido permitidas, mas não no estado de modelo inválido.
fonte