Como simplificar minhas classes stateful complexas e seus testes?


9

Estou em um projeto de sistema distribuído escrito em java, onde temos algumas classes que correspondem a objetos de negócios do mundo real muito complexos. Esses objetos têm muitos métodos correspondentes às ações que o usuário (ou outro agente) pode aplicar a esses objetos. Como resultado, essas classes se tornaram muito complexas.

A abordagem da arquitetura geral do sistema levou a muitos comportamentos concentrados em poucas classes e muitos cenários possíveis de interação.

Como exemplo, e para manter as coisas fáceis e claras, digamos que Robot e Car eram classes no meu projeto.

Portanto, na classe Robot, eu teria muitos métodos no seguinte padrão:

  • dormir(); isSleepAvaliable ();
  • acordado(); isAwakeAvaliable ();
  • caminhada (direção); isWalkAvaliable ();
  • atirar (direção); isShootAvaliable ();
  • turnOnAlert (); isTurnOnAlertAvailable ();
  • turnOffAlert (); isTurnOffAlertAvailable ();
  • recarrega(); isRechargeAvailable ();
  • desligar(); isPowerOffAvailable ();
  • stepInCar (Car); isStepInCarAvailable ();
  • stepOutCar (carro); isStepOutCarAvailable ();
  • Auto-destruição(); isSelfDestructAvailable ();
  • morrer(); isDieAvailable ();
  • Está vivo(); está acordado(); isAlertOn (); getBatteryLevel (); getCurrentRidingCar (); getAmmo ();
  • ...

Na classe Car, seria semelhante:

  • Ligar(); isTurnOnAvaliable ();
  • desligar(); isTurnOffAvaliable ();
  • caminhada (direção); isWalkAvaliable ();
  • reabastecer(); isRefuelAvailable ();
  • Auto-destruição(); isSelfDestructAvailable ();
  • batida(); isCrashAvailable ();
  • isOperational (); isOn (); getFuelLevel (); getCurrentPassenger ();
  • ...

Cada um deles (robô e carro) é implementado como uma máquina de estado, onde algumas ações são possíveis em alguns estados e outras não. As ações alteram o estado do objeto. Os métodos de ações são lançados IllegalStateExceptionquando chamados em um estado inválido e os isXXXAvailable()métodos informam se a ação é possível no momento. Embora alguns sejam facilmente dedutíveis do estado (por exemplo, no estado de sono, está disponível o estado de vigília), outros não são (para atirar, ele deve estar acordado, vivo, com munição e sem andar de carro).

Além disso, as interações entre os objetos também são complexas. Por exemplo, o carro pode conter apenas um passageiro do robô; portanto, se outro tentar entrar, uma exceção deve ser lançada; Se o carro bater, o passageiro deve morrer; Se o robô está morto dentro de um veículo, ele não pode sair, mesmo que o carro esteja ok; Se o robô estiver dentro de um carro, ele não poderá entrar em outro antes de sair; etc.

O resultado disso é que, como eu já disse, essas classes se tornaram realmente complexas. Para piorar as coisas, existem centenas de cenários possíveis quando o robô e o carro interagem. Além disso, grande parte dessa lógica precisa acessar dados remotos em outros sistemas. O resultado é que o teste de unidade tornou-se muito difícil e temos muitos problemas de teste, um causando o outro em um círculo vicioso:

  • As configurações dos casos de teste são muito complexas, porque precisam criar um mundo significativamente complexo para se exercitar.
  • O número de testes é enorme.
  • O conjunto de testes leva algumas horas para ser executado.
  • Nossa cobertura de teste é muito baixa.
  • O código de teste tende a ser escrito semanas ou meses depois do código que eles testam, ou nunca.
  • Muitos testes também são interrompidos, principalmente porque os requisitos do código testado foram alterados.
  • Alguns cenários são tão complexos que falham no tempo limite durante a instalação (configuramos um tempo limite em cada teste, nos piores casos com 2 minutos e, mesmo assim, com o tempo limite, garantimos que não seja um loop infinito).
  • Os erros entram regularmente no ambiente de produção.

Esse cenário de robô e carro é uma simplificação exagerada do que temos na realidade. Claramente, esta situação não é gerenciável. Então, estou pedindo ajuda e sugestões para: 1, reduzir a complexidade das classes; 2. Simplifique os cenários de interações entre meus objetos; 3. Reduza o tempo de teste e a quantidade de código a ser testado.

Edição:
Eu acho que não estava claro sobre as máquinas de estado. o robô é em si uma máquina de estado, com estados "adormecido", "acordado", "recarregando", "morto" etc. O carro é outra máquina de estado.

EDIT 2: Caso você esteja curioso sobre o que meu sistema realmente é, as classes que interagem são coisas como Servidor, Endereço IP, Disco, Backup, Usuário, SoftwareLicense, etc. O cenário Robô e Carro é apenas um caso que eu encontrei isso seria simples o suficiente para explicar meu problema.


você pensou em perguntar no Code Review.SE ? Fora isso, para o projeto como o seu eu começar a pensar sobre Extract Classe tipo refatoração
— mosquito

Eu considerei a Revisão de código, mas esse não é o lugar certo. O principal problema não está no código em si, mas na abordagem da arquitetura geral do sistema que leva a muitos comportamentos concentrados em poucas classes e a muitos cenários possíveis de interação.
— Victor Stafusa

@gnat Você pode fornecer um exemplo de como eu implementaria Extract Class no cenário fornecido de Robot and Car?
— Victor Stafusa

Eu extrairia coisas relacionadas a carros do Robot em uma classe separada. Eu também extrairia todos os métodos relacionados ao sono + acordado em uma classe dedicada. Outros "candidatos" que parecem merecer extração são os métodos de energia + recarga, coisas relacionadas ao movimento. Etc. Observe que, como se trata de refatoração, a API externa do robô provavelmente deve permanecer; no primeiro estágio, eu modificaria apenas os internos. BTDTGTTS
— mosquito

Esta não é uma pergunta de revisão de código - a arquitetura está fora de tópico.
— Michael K

Respostas:


8

O padrão de design do Estado pode ser útil, se você ainda não o estiver usando.

A idéia central é que você cria uma classe interna para cada estado diferente - assim para continuar suas exemplo, SleepingRobot, AwakeRobot, RechargingRobote DeadRobottudo seria aulas, a implementação de uma interface comum.

Os métodos na Robotclasse (like sleep()e isSleepAvaliable()) têm implementações simples que delegam à classe interna atual.

Alterações de estado são implementadas trocando a classe interna atual por outra diferente.

A vantagem dessa abordagem é que cada uma das classes de estado é dramaticamente mais simples (pois representa apenas um estado possível) e pode ser testada independentemente. Dependendo da linguagem de implementação (não especificada), você ainda pode estar restrito a ter tudo no mesmo arquivo ou pode dividir as coisas em arquivos de origem menores.


Eu estou usando java.
— Victor Stafusa

Boa sugestão. Dessa forma, cada implementação tem um foco claro que pode ser testado individualmente sem que uma classe junit de 2.000 linhas teste todos os estados simultaneamente.
— 27412 OliverS

3

Não conheço seu código, mas, tomando o exemplo do método "sleep", assumirei que é algo parecido com o seguinte código "simplista":

public void sleep() {
 if(!dead && awake) {
  sleeping = true;
  awake = false;
  this.updateState(SLEEPING);
 }
 throw new IllegalArgumentException("robot is either dead or not awake");
}

Eu acho que você precisa fazer a diferença entre testes de integração e testes de unidade . Escrever um teste executado em todo o estado da máquina é certamente uma grande tarefa. Escrever testes de unidade menores que testam se seu método de dormir funciona corretamente é mais fácil. Neste ponto, você não precisa saber se o estado da máquina foi atualizado corretamente ou se o "carro" respondeu corretamente ao fato de que o estado da máquina foi atualizado pelo "robô" ... etc.

Dado o código acima, eu zombaria do objeto "machineState" e meu primeiro teste seria:

testSleep_dead() {
 robot.dead = true;
 robot.awake = false;
 robot.setState(AWAKE);
 try {
  robot.sleep();
  fail("should have got an exception");
 } catch(Exception e) {
  assertTrue(e instanceof IllegalArgumentException);
  assertEquals("robot is either dead or not awake", e.getMessage());
 }
}

Minha opinião pessoal é que escrever esses pequenos testes unitários deve ser a primeira coisa a fazer. Você escreveu isso:

As configurações dos casos de teste são muito complexas, porque precisam criar um mundo significativamente complexo para se exercitar.

A execução desses pequenos testes deve ser muito rápida e você não deve ter nada para inicializar como seu "mundo complexo". Por exemplo, se for um aplicativo baseado em um contêiner do IOC (por exemplo, Spring), você não precisará inicializar o contexto durante os testes de unidade.

Depois de cobrir uma boa porcentagem do seu código complexo com testes de unidade, você poderá começar a criar testes de integração mais demorados e mais complexos.

Finalmente, isso pode ser feito se o seu código for complexo (como você disse que é agora) ou depois de refatorar.


Eu acho que não estava claro sobre as máquinas de estado. o robô é em si uma máquina de estado, com estados "adormecido", "acordado", "recarregando", "morto" etc. O carro é outra máquina de estado.
— Victor Stafusa

@ Victor OK, fique à vontade para corrigir meu código de exemplo, se quiser. A menos que você me diga o contrário, acho que meu argumento sobre os testes de unidade ainda é válido, espero que pelo menos.
— Jalayn

Eu corrigi o exemplo. Não tenho privilégios para torná-lo facilmente visível, por isso deve ser revisado por pares primeiro. Seu comentário é útil.
— Victor Stafusa

2

Eu estava lendo a seção "Origem" do artigo da Wikipedia sobre o Princípio de Segregação de Interface , e me lembrei dessa pergunta.

Vou citar o artigo. O problema: "... uma classe principal de trabalho ... uma classe gorda com vários métodos específicos para uma variedade de clientes diferentes". A solução: "... uma camada de interfaces entre a classe Job e todos os seus clientes ..."

Seu problema parece uma permutação da que a Xerox tinha. Em vez de uma classe gorda, você tem duas, e essas duas classes gordas estão conversando entre si em vez de muitos clientes.

Você pode agrupar os métodos por tipo de interação e criar uma classe de interface para cada tipo? Por exemplo: RobotPowerInterface, RobotNavigationInterface, RobotAlarmInterface classes?

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.