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 IllegalStateException
quando 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.
fonte
Respostas:
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
,RechargingRobot
eDeadRobot
tudo seria aulas, a implementação de uma interface comum.Os métodos na
Robot
classe (likesleep()
eisSleepAvaliable()
) 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.
fonte
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":
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:
Minha opinião pessoal é que escrever esses pequenos testes unitários deve ser a primeira coisa a fazer. Você escreveu isso:
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.
fonte
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?
fonte