image

Acesse bootcamps ilimitados e +650 cursos pra sempre

60
%OFF
Article image
Alan Ribeiro
Alan Ribeiro06/08/2024 09:39
Compartilhe

Programação Orientada a Objetos e Test Drive Development

  • #TDD
  • #POO

A Programação Orientada a Objetos (POO) e o Test-Driven Development (TDD) são duas práticas fundamentais que, quando combinadas, levam a um desenvolvimento de software mais robusto e eficiente. Neste artigo, vamos explorar a importância de projetar classes e objetos de um sistema com a capacidade de serem facilmente testados. Dessa forma, conseguimos construir estruturas melhores, mais simples e, acima de tudo, confiáveis.

A Necessidade de Classes Testáveis

Ao desenvolver um sistema, é crucial que as classes sejam projetadas com a testabilidade em mente. Classes testáveis são, geralmente, mais simples e possuem responsabilidades bem definidas. Isso se alinha ao Princípio da Responsabilidade Única (Single Responsibility Principle), um dos princípios do Clean Code, que afirma que uma classe deve ter apenas uma razão para ser modificada.

Quando pensamos em classes testáveis, estamos também promovendo uma arquitetura que aumenta a coesão e reduz o acoplamento. A coesão se refere ao grau em que os métodos e propriedades de uma classe estão relacionados e trabalham juntos para realizar uma única tarefa. Classes com alta coesão são mais fáceis de entender, manter e testar, pois cada classe possui uma responsabilidade bem definida.

Por outro lado, o acoplamento refere-se ao grau de dependência entre classes. Classes com alto acoplamento dependem fortemente umas das outras, tornando difícil testá-las de forma isolada, já que mudanças em uma classe podem impactar outras classes dependentes. Isso pode levar a um código frágil e difícil de manter.

Para resolver esse problema, podemos aplicar o Princípio de Inversão de Dependências (Dependency Inversion Principle). Este princípio sugere que classes de alto nível não devem depender de classes de baixo nível, mas ambas devem depender de abstrações (interfaces ou classes abstratas). Ao seguir este princípio, reduzimos o acoplamento entre as classes, permitindo que cada uma possa ser testada de forma independente. Além disso, isso facilita a substituição e a modificação de partes do sistema sem afetar outras partes, resultando em um código mais flexível e robusto.

Você pode estar um pouco confuso com esses conceitos, então vamos trazer a inversão de dependência na prática.

Sem a Inversão de Dependência:

class EmailService {
  sendEmail(message: string): void {
      // Lógica para enviar e-mail
      console.log(`Email enviado: ${message}`);
  }
}


class OrderService {
  private emailService: EmailService = new EmailService();


  processOrder(order: string): void {
      // Lógica para processar o pedido
      console.log(`Pedido processado: ${order}`);
      this.emailService.sendEmail(`Confirmação de pedido: ${order}`);
  }
}


// Uso das classes
const orderService = new OrderService();
orderService.processOrder("Pedido #1234");

O mesmo código com o princípio de inversão de dependência.

interface NotificationService {
  sendNotification(message: string): void;
}



class EmailService implements NotificationService {
  sendNotification(message: string): void {
      // Lógica para enviar e-mail
      console.log(`Email enviado: ${message}`);
  }
}



class OrderService {
  private notificationService: NotificationService;



  // Injeção de dependência através do construtor
  constructor(notificationService: NotificationService) {
      this.notificationService = notificationService;
  }



  processOrder(order: string): void {
      // Lógica para processar o pedido
      console.log(`Pedido processado: ${order}`);
      this.notificationService.sendNotification(`Confirmação de pedido: ${order}`);
  }
}



// Uso das classes com injeção de dependência
const emailService = new EmailService();
const orderService = new OrderService(emailService);
orderService.processOrder("Pedido #1234");
  1. Sem o Princípio de Inversão de Dependências:
  • A classe OrderService depende diretamente da classe EmailService.
  • Isso torna OrderService fortemente acoplada a EmailService, dificultando a modificação e os testes isolados.
  1. Com o Princípio de Inversão de Dependências:
  • Introduzimos a interface NotificationService que define o método sendNotification.
  • A classe EmailService implementa NotificationService, seguindo o princípio de que dependências devem ser de abstrações.
  • A classe OrderService agora depende da abstração NotificationService, e não de uma implementação concreta.
  • Usamos injeção de dependência para fornecer a implementação específica (EmailService) a OrderService, reduzindo o acoplamento e aumentando a flexibilidade.

Essa refatoração torna OrderService mais flexível e fácil de testar, pois podemos fornecer diferentes implementações de NotificationService sem modificar OrderService.

Na hora de construir testes para essa classe OrderService, podemos injetar um outro objeto falso do tipo NotificationService que não irá enviar de fato emails, mas apenas verificar se o método foi chamado. Assim o teste fica concentrado apenas nos métodos da classe sem se preocupar se o email foi enviado ou não. Já para os testes de uma classe que implementa a interface NotificationService a preocupação é com o envio da mensagem então não há que se preocupar com o processamento da OrderService.

TDD: Programando com Segurança

Programar com TDD é como dirigir um carro com cinto de segurança. No desenvolvimento de software, acidentes acontecem, como bugs e regressões. Ter uma suíte de testes robusta atua como um cinto de segurança, prevenindo que esses acidentes se tornem trágicos. O TDD nos encoraja a escrever testes antes mesmo de escrever o código de produção, o que garante que estamos sempre focados em escrever apenas o código necessário para passar os testes.

Além disso, o TDD promove uma mentalidade de pequenos passos, onde escrevemos um teste, vemos ele falhar, escrevemos o código mínimo necessário para passar o teste e então refatoramos. Esse ciclo contínuo de feedback ajuda a manter o código limpo e alinhado aos princípios do Clean Code, como:

- Simplicidade: Escrever o código mais simples possível para fazer o teste passar.

- Legibilidade: Testes atuam como documentação viva, explicando como o código deve se comportar.

- Manutenibilidade: Código testável é mais fácil de modificar e expandir com segurança.

Clean Code e Testes Unitários

Os princípios do Clean Code são essenciais para escrever classes que não apenas funcionam, mas que também são fáceis de entender e manter. Alguns desses princípios incluem:

- SRP (Single Responsibility Principle): Cada classe deve ter uma única responsabilidade.

- OCP (Open/Closed Principle): Classes devem ser abertas para extensão, mas fechadas para modificação.

- LSP (Liskov Substitution Principle): Subtipos devem ser substituíveis por seus tipos base sem alterar o comportamento do programa.

- ISP (Interface Segregation Principle): Múltiplas interfaces específicas são melhores do que uma única interface geral.

- DIP (Dependency Inversion Principle): Dependa de abstrações, não de concretizações.

Sem esses princípios, as classes tendem a se tornar "sujas", ou seja, cheias de responsabilidades, altamente acopladas e difíceis de testar. Implementar TDD ajuda a reforçar esses princípios, pois ao escrever testes unitários, somos forçados a criar interfaces claras e dependências explícitas, resultando em um código mais limpo.

Conclusão

Unit tests e TDD deveriam ser aprendidos junto com a Programação Orientada a Objetos, pois ajudam a compreender o paradigma orientado a objetos ao mesmo tempo em que constroem códigos de alta qualidade. Ao integrar TDD e os princípios do Clean Code no nosso fluxo de trabalho, não apenas melhoramos a qualidade do nosso código, mas também aumentamos nossa confiança no desenvolvimento, sabendo que nossos testes estão lá para nos proteger de eventuais falhas.

Em resumo, TDD e POO são práticas que se complementam, promovendo a criação de software que é robusto, bem estruturado e preparado para enfrentar os desafios do desenvolvimento de software moderno.

Compartilhe
Comentários (0)