Eu quero distinguir entre duas maneiras diferentes de abordar a programação orientada a objetos
- Simulacionista: seus objetos representam objetos de domínio real, você os programou para lidar com qualquer funcionalidade relacionada a esse domínio. Os objetos programados dessa maneira provavelmente têm muitos colaboradores mutáveis e ocultos usados para implementar essa funcionalidade.
- Registros + funções: seus objetos são apenas pacotes de dados e funções que operam sobre esses dados. Objetos programados dessa maneira são mais propensos a serem imutáveis, a assumir menos responsabilidades e permitir a injeção de colaboradores.
Uma regra prática é que um objeto programado na primeira maneira terá mais métodos e mais void
métodos do que na segunda maneira. Digamos que íamos escrever um simulador de vôo e projetar uma classe de avião. Teríamos algo como:
class Plane {
void accelerate();
void deccelerate();
void toggleRightFlaps();
void toggleLeftFlaps();
void turnRudderRight();
void turnRudderLeft();
void deployLandingGear();
void liftLandingGear();
// etc.
void tick() throws PlaneCrashedException;
}
Talvez seja um pouco mais extremo do que se possa encontrar, mas é claro. Se você deseja implementar esse tipo de interface, você deve manter dentro do objeto:
- Toda a informação sobre o estado do equipamento do avião.
- Toda a informação sobre a velocidade / aceleração do avião.
- A taxa de atualização da nossa simulação (para implementar o tick).
- Detalhes completos sobre o modelo 3d da simulação e sua física para implementar o tick.
A gravação de um teste de unidade para um objeto gravado no modo é extremamente difícil porque:
- Você precisa fornecer todos os diferentes bits de dados e colaboradores que esse objeto precisa no início de seu teste (instatá-los pode ser realmente tedioso).
- Quando você deseja testar um método, enfrenta dois problemas: a) a interface frequentemente não expõe dados suficientes para você testar (então você acaba tendo que usar zombarias / reflexões para verificar as expectativas) b) existem muitos componentes ligados em um que você deve verificar se comportou em cada teste.
Basicamente, você começa com uma interface que parece razoável e se encaixa bem no domínio, mas a gentileza da simulação o levou a criar um objeto que é realmente difícil de testar.
No entanto, você pode criar objetos que atendem ao mesmo objetivo. Você gostaria de frear Plane
em pedaços menores. Tenha um PlaneParticle
que rastreie os bits físicos do avião, a posição, velocidade, aceleração, rotação, guinada, etc., etc., expondo e permitindo manipulá-los. Em seguida, um PlaneParts
objeto pode rastrear o status de. Você poderia enviar tick()
para um lugar completamente diferente, digamos, ter um PlanePhysics
objeto parametrizado por, por exemplo, a força da gravidade, que sabe como PlaneParticle
e PlaneParts
como cuspir um novo PlaneParticle
. Tudo isso pode ser totalmente imutável, embora não seja necessário, por exemplo.
Agora você tem essas vantagens em termos de teste:
- Cada componente individual tem menos a fazer e é mais fácil de configurar.
- Você pode testar seus componentes isoladamente.
- Esses objetos podem evitar a exposição de seus componentes internos (especialmente se forem imutáveis), portanto, não é preciso ter esperteza para medi-los.
Aqui está o truque: a segunda abordagem orientada a objetos que descrevi está muito próxima da programação funcional. Talvez em programas funcionais puros, seus registros e suas funções sejam separados e não unidos em objetos, definitivamente um programa funcional garantiria todas essas coisas. O que eu acho que realmente facilita o teste de unidade é
- Pequenas unidades (pequeno espaço de estado).
- Funções com entrada mínima (sem entradas ocultas).
- Funções com saída mínima.
A programação funcional incentiva essas coisas (mas é possível escrever programas ruins em qualquer paradigma), mas elas são alcançáveis em programas orientados a objetos. E enfatizaria ainda que a programação funcional e a programação orientada a objetos não são incompatíveis.