Extensibilidade com interfaces e contratos

Resumen

A extensibilidade é uma das qualidades mais valiosas de um bom design de software porque permite adicionar funcionalidades novas sem reescrever o que já funciona. Se programas todos os dias, esta ideia importa-te: define se o teu código sobrevive às mudanças ou se desmorona a cada novo requisito.

A frase que abre a aula resume tudo: a mudança é a única constante. E no desenvolvimento de software, isso traduz-se numa pergunta diária, como adiciono coisas novas sem partir o que já existe?

O que significa extensibilidade no desenvolvimento de software?

A extensibilidade é a capacidade de adicionar elementos novos sem modificar de forma significativa o código existente. A ideia é simples: quanto menos tocas no que já funciona, menos riscos introduzes.

O truque está em desenhar pontos de extensão claros e alterações compreensíveis. Para isso, o código apoia-se em duas peças centrais que podes tratar como sinónimos na prática: contratos e interfaces.

O que é uma interface em programação? É um contrato que define quais métodos uma classe tem de implementar. Quem assina esse contrato compromete-se a cumprir todas as suas regras à risca.

Quais são os desafios reais de escrever código extensível?

Antes de pensares em padrões, há dois obstáculos que costumam aparecer em qualquer equipa.

  • Compreender bem o problema antes de saltar para o código. Para o caso, o objetivo é parar um momento e perguntar se a abordagem faz sentido e é simples.
  • Chegar a consenso sobre padrões e convenções de equipa. Se não existe documentação, é hora de a começar a escrever e registar cada decisão.
  • Manter um rastreamento dessas convenções para que adicionar coisas no futuro seja mais barato.

E aqui vem o detalhe interessante: muitas vezes saltamos direto para o teclado. Às vezes é inevitável, mas o ideal é dedicar tempo a entender o quê e o porquê.

Como aplicar contratos e interfaces num exemplo prático?

Imagina um cenário típico de front-end: um serviço de utilizadores depende de um gateway HTTP, que por sua vez depende de um módulo HTTP para falar com o backend. Tudo bem até alguém aparecer com um requisito novo.

E se mudarmos de REST para GraphQL?

A equipa decide trocar a abordagem REST por GraphQL, mas alguns serviços têm de continuar a usar a versão antiga. Como garantes que ambas as implementações sigam as mesmas regras?

A resposta é definir uma interface chamada gateway que obriga qualquer classe a implementar dois métodos: fetchMany e fetchOne. A partir daí, criam-se duas classes concretas:

  • REST Gateway, que implementa o contrato gateway com lógica REST.
  • GraphQL Gateway, que implementa o mesmo contrato com lógica GraphQL.
  • Qualquer futuro gateway que apareça, basta implementar a mesma interface.

A implementação interna de cada método é diferente porque as considerações técnicas mudam, mas as regras externas são idênticas. É aquela frase clássica do duck typing: se o teu objeto tem fetchMany e fetchOne, é um gateway.

Como fica o serviço de utilizadores depois desta mudança?

O serviço de utilizadores passa a ter um atributo chamado serviceGateway cujo tipo de dados é gateway, ou seja, a interface abstrata, não uma implementação concreta. No construtor instancia-se, por exemplo, um GraphQL Gateway.

Se amanhã quiseres trocar para um REST Gateway, a alteração é mínima. Porquê? Porque o contrato é o mesmo e ambos os gateways expõem fetchMany e fetchOne.

Porque é melhor depender de uma interface em vez de uma classe concreta? Porque podes trocar implementações sem reescrever o código que as consome. A interface fixa as regras; a classe concreta fica intercambiável.

Por que esta abordagem torna o código mais robusto?

O diagrama final mostra a relação com clareza: o serviço de utilizadores é composto por um gateway, que é uma interface implementada tanto por GraphQL Gateway como por REST Gateway. Repara na palavra composto, importa.

Com esta estrutura ganhas três benefícios concretos:

  1. Adicionar um novo gateway significa apenas criar uma classe nova que implemente os métodos do contrato.
  2. Trocar o gateway atual por outro envolve mudar uma linha no construtor.
  3. O código consumidor não sabe nem precisa saber qual implementação está por baixo.

Em pseudocódigo, a ideia central parece-se com isto:

typescript interface Gateway { fetchMany(): any; fetchOne(): any; }

class RestGateway implements Gateway { /* lógica REST / } class GraphQLGateway implements Gateway { / lógica GraphQL */ }

class UserService { serviceGateway: Gateway; constructor() { this.serviceGateway = new GraphQLGateway(); } }

A chave aqui é a palavra abstração. Em vez de o serviço depender diretamente de GraphQL Gateway e usá-lo como tal, depende de uma interface mais abstrata. Essa abstração é o que te dá liberdade para trocar peças sem partir o resto.

E tu, já tinhas trabalhado com contratos e interfaces antes? Conta nos comentários como aplicas isto no teu código.