DEV Community

Cover image for Reduzindo a quantidade de Branchs na criação de Objetos com uma estrutura plugável
Walter Alleyz
Walter Alleyz

Posted on

Reduzindo a quantidade de Branchs na criação de Objetos com uma estrutura plugável

Você já se deparou com o cenário em que criar objetos, de forma dinâmica, usando algum parâmetro era necessário?
Digamos que estejamos criando um sistema para academias, e para diferenciar os tipos de clientes nós vamos modelar classes diferentes. De início vamos ter os planos básico, avançado e de luxo e com o tempo podemos acrescentar novos planos a partir de novas atualizações.

public class Basico {}

public class Avcado {}

public class DeLuxo {}
Enter fullscreen mode Exit fullscreen mode

Podemos perceber que se o nosso cliente estiver buscando um sistema em que ele possa definir os tipos de planos, ele vai sofrer com forte dependência do time de desenvolvimento, pois para adicionar um plano ele precisará esperar uma nova atualização. Não é o objetivo desse artigo solucionar esse problema.

Normalmente para instanciar essas classes de forma dinâmica, nós poderíamos criar uma função para receber um parâmetro e retornar o objeto selecionado. Mas para isso, nossas classes precisam adquirir um super tipo que as torne polimórficas.

public interface UserPlan {
  void doSomething();
}
Enter fullscreen mode Exit fullscreen mode

Agora podemos implementar essa interface em nossas classes e criar o nosso método.

public UserPlan criarPlano(String plan) {
  if(plan.equals("basico")) return new Basico();
  else if(plan.equals("avancado")) return new Avcado();
  else return new DeLuxo();
}
Enter fullscreen mode Exit fullscreen mode

Muito fácil, não é mesmo? O problema que criamos utilizando desse padrão é que sempre que uma nova classe plano for criada, precisaremos editar essa função e adicionar uma nova branch. Se esse método estiver contido dentro da própria classe cliente (a classe que dependeria dessa função), teremos um problema em dobro, sempre tendo de abrir a classe para dar manutenção e tendo de alterar todos os pontos em que a criação desses objetos atinge.

Um exemplo simples de entender é que, se a classe cliente dependesse do nome do objeto criado para executar alguma ação, e a implementação dessa ação estivesse contida dentro da classe cliente, também precisaríamos editar esse código gerando um ciclo de retrabalho interminável.

public class Cliente {

  public void fazAlgoDeAcordoComObjeto(String plan) {
    UserPlan plan = criarPlano(plan);

    if(plan.getClass().getSimpleName().equals("Basico"))
    // implementa algo aqui

    else if(plan.getClass().getSimpleName().equals("Avcado"))
    // implementa algo aqui

    else
    // implementa algo aqui

  }
}
Enter fullscreen mode Exit fullscreen mode

Esse tipo de implementação cria uma dependência (ou acoplamento) terrivelmente abusiva, que nos traz um novo problema sempre que precisamos realizar alguma alteração.

Para contornar esse contra padrão e eliminar as branchs, podemos nos aproveitar de uma estrutura de dados que já utiliza comparação de acordo com um parâmetro específico; uma estrutura de chave-valor.

Vamos começar eliminando da classe cliente a implementação do método fazAlgoDeAcordoComObjeto, e utilizar injeção de dependência para chamar o método doSomething que as classes implementam por meio da interface UserPlan:

public class Cliente {

  private final UserPlan userPlan;

  public Cliente(UserPlan userPlan) {
    this.userPlan = userPlan;
  }

  public void fazAlgoDeAcordoComObjeto() {
    userPlan.doSomething();
  }

}
Enter fullscreen mode Exit fullscreen mode

Eliminamos vários pontos de manutenção distribuindo a responsabilidade.

Agora podemos criar uma classe utilitária que vai cuidar da criação do objeto para nós.
Essa classe vai possuir um HashMap (que recebe chave e valor) para guardar a chave de comparação e o método de criação do objeto.

public class UserPlanCreator {

  private static final Map<String, PlanCreator> plans = new HashMap<>();

  static {
    plans.put("basico", Basico::new);
    plans.put("avancado", Avcado::new);
    plans.put("luxo", DeLuxo::new);
  }

  private UserPlanCreator() {}

  public static UserPlan criarPlano(String plano) {
    return plans.get(plano).create();
  }

}
Enter fullscreen mode Exit fullscreen mode

Uma validação de nulo pode ser implementada no método criarPlano.

Você deve ter percebido que a nossa estrutura Map não retorna o super tipo e sim uma interface que a gente não implementou ainda. Eu fiz essa escolha de design, porque criar um objeto dentro de um iniciador estático iria tornar essa criação única, fazendo o método sempre retornar o mesmo objeto.
Para evitar esse comportamento, precisamos retornar o método de criar e não o objeto instanciado e nesse momento as interfaces funcionais podem nos ajudar.

Uma interface funcional é uma estrutura que possui um único método abstrato e pode ser utilizada para implementar lambdas ou referências de métodos. Assim seria a nossa interface funcional:

@FuncionalInterface
public interface PlanCreator {

  UserPlan create();
}
Enter fullscreen mode Exit fullscreen mode

Pronto, agora a gente só precisa garantir que o lambda, ou referência de método, retorne o objeto do tipo UserPlan (e por isso estamos usando um super tipo nas classes de planos 😀).

Apesar desse monte de código você vai perceber que as responsabilidades estão bem definidas, e a classe cliente só precisa chamar o método do objeto que ela vai receber, sem precisar lidar com a comparação e sem precisar conhecer os objetos que ela possui dependência. Ainda eliminamos vários pontos de manutenção, garantindo que quando uma nova classe for criada precisaremos alterar apenas uma estrutura do todo.

Por fim, nosso código para criar um plano ficaria assim:

public static void main(String[] args) {
  // geralmente a entrada do tipo do plano viria de algum input
  Client client = new Client(UserPlanCreator.criarPlano("basico"));
  client.doSomething();
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)