DEV Community

Abel Costa
Abel Costa

Posted on

Refatorando Ifs Aninhados com Chain of Responsibility

Funções ideais

Num mundo ideal funções são pequenas, muito bem definidas, possuem uma única responsabilidade e não crescem, mas no mundo real isso não é uma realidade, funções tendem a crescer, mesmo que tenham uma única função de fato.

Imagine uma função de uma aplicação veterinária que deve dizer ao usuário qual comida é adequada para um determinado animal, de forma simples a função seria mais ou menos dessa forma:

function properFoodForAnimal(anAnimal: string): string {
  let food;

  if ("dog" === anAnimal.toLowerCase()) food = "beef meat";

  if ("cat" === anAnimal.toLowerCase()) food = "fish meat";

  if ("owl" === anAnimal.toLowerCase()) food = "rat meat";

  if ("monkey" === anAnimal.toLowerCase()) food = "banana";

  if ("horse" === anAnimal.toLowerCase()) food = "corn";

  return `Animal ${anAnimal} will eat ${food}`;
}
Enter fullscreen mode Exit fullscreen mode

Note que a medida em que a veterinária "expandir o seu mercado" e novos animais forem atendidos essa função tende a crescer ganhando novos ifs.
Para evitar que isso aconteça podemos aplicar o design pattern chain of responsibility.

Chain of Responsibility

É um padrão de projeto comportamental muito utilizado quando temos uma cadeia (chain) de situações que lidam com o mesmo escopo como a escolha de uma comida, calculo de um preço, filtragem de campos dentre outras.
Esse padrão permite que vários objetos manipulem a solicitação sem que haja acoplamento entre o trecho de código que envia a requisição e seus receptores. Podemos facilmente identificar esse padrão no nosso código através da interface a qual a solução é implementada.

interface Handler {
    setNext(handler: Handler): Handler;

    handle(request: string): string;
}
Enter fullscreen mode Exit fullscreen mode

Procedimento

Sempre que vamos começar a refatorar um trecho de código temos de nos certificar que aquele trecho possui uma suite de testes robusta que vai nos assegurar de não causar nenhum bug, então a primeira coisa que vamos fazer é justamente escrever os testes para a função.

describe("Proper food for animal", () => {
  it("should return the correct phrase containing the proper for a dog", () => {
    let phrase = properFoodForAnimal("dog");

    expect(phrase).toBe("Animal dog will eat beef meat");
  });

  it("should return the correct phrase containing the proper for a cat", () => {
    let phrase = properFoodForAnimal("cat");

    expect(phrase).toBe("Animal cat will eat fish meat");
  });

  it("should return the correct phrase containing the proper for a owl", () => {
    let phrase = properFoodForAnimal("owl");

    expect(phrase).toBe("Animal owl will eat rat meat");
  });

  it("should return the correct phrase containing the proper for a monkey", () => {
    let phrase = properFoodForAnimal("monkey");

    expect(phrase).toBe("Animal monkey will eat banana");
  });

  it("should return the correct phrase containing the proper for a horse", () => {
    let phrase = properFoodForAnimal("horse");

    expect(phrase).toBe("Animal horse will eat corn");
  });
});
Enter fullscreen mode Exit fullscreen mode

Escritos os testes iniciamos a refatoração, primeiramente criando a interface AnimalFoodHandler que é nada além da interface genérica do pattern apresentada anteriormente.

interface ProperFoodHandler {
  setNext(handler: ProperFoodHandler): ProperFoodHandler;

  handle(anAnimal: string): string;
}
Enter fullscreen mode Exit fullscreen mode

Pode parecer desnecessário a primeira vista nomear essa interface de forma tão específica uma vez que a proposta é que ela seja genérica, mas isso impede outros programadores de aplicar handlers não adequados para aquela situação já que o intellisense do ts avisará que os objetos não são do mesmo tipo.
O próximo passo então é criarmos as classes handlers para cada animal que temos no código hoje, como todo handler possui a mesma estrutura e lida com o mesmo domínio podemos fazer o uso de herança e polimorfismo para deixar o nosso código ainda mais claro. Vamos criar uma classe abstrata de handler da qual se derivarão as classes filhas.

export class AbstractFoodHandler implements IFoodHandler {
  private nextHandler: IFoodHandler | undefined;

  next(handler: IFoodHandler): IFoodHandler {
    this.nextHandler = handler;

    return handler;
  }

  handle(anAnimal: string): string | null {
    if (this.nextHandler) {
      return this.nextHandler.handle(anAnimal);
    }

    return null;
  }
}
Enter fullscreen mode Exit fullscreen mode

Agora podemos ter handlers concretos derivado da classe abstrata de forma com que haja um handler para cada situação, assim sendo temos

DogFoodHandler.ts

export class DogFoodHandler extends AbstractFoodHandler {
  handle(anAnimal: string): string | null {
    if ("dog" === anAnimal.toLowerCase())
      return `Animal dog will eat beef meat`;

    return super.handle(anAnimal);
  }
}
Enter fullscreen mode Exit fullscreen mode

CatFoodHandler.ts

export class CatFoodHandler extends AbstractFoodHandler {
  handle(anAnimal: string): string | null {
    if ("cat" === anAnimal.toLowerCase())
      return `Animal cat will eat fish meat`;

    return super.handle(anAnimal);
  }
}
Enter fullscreen mode Exit fullscreen mode

OwlFoodHandler.ts

export class OwlFoodHandler extends AbstractFoodHandler {
  handle(anAnimal: string): string | null {
    if ("owl" === anAnimal) return `Animal owl will eat rat meat`;

    return super.handle(anAnimal);
  }
}
Enter fullscreen mode Exit fullscreen mode

MonkeyFoodHandler.ts

export class MonkeyFoodHandler extends AbstractFoodHandler {
  handle(anAnimal: string): string | null {
    if ("monkey" === anAnimal.toLowerCase())
      return `Animal monkey will eat banana`;

    return super.handle(anAnimal);
  }
}
Enter fullscreen mode Exit fullscreen mode

HorseFoodHandler.ts

export class HorseFoodHandler extends AbstractFoodHandler {
  handle(anAnimal: string): string | null {
    if ("horse" === anAnimal.toLowerCase()) return `Animal horse will eat corn`;

    return super.handle(anAnimal);
  }
}
Enter fullscreen mode Exit fullscreen mode

Ainda podemos criar um arquivo index.ts para facilitar a exportação de arquivos.

import { CatFoodHandler } from "./CatFoodHandler";
import { DogFoodHandler } from "./DogFoodHandler";
import { MonkeyFoodHandler } from "./MonkeyFoodHandler";
import { OwlFoodHandler } from "./OwlFoodHandler";
import { HorseFoodHandler } from "./HorseFoodHandler";

const catHandler = new CatFoodHandler();
const monkeyHandler = new MonkeyFoodHandler();
const owlHandler = new OwlFoodHandler();
const horseHandler = new HorseFoodHandler();

const handler = new DogFoodHandler();

handler
  .next(catHandler)
  .next(owlHandler)
  .next(monkeyHandler)
  .next(horseHandler);

export { handler };
Enter fullscreen mode Exit fullscreen mode

O código final da nosssa aplicação ficará assim:

export function properFoodForAnimal(anAnimal: string): string | null {
  return handler.handle(anAnimal);
}
Enter fullscreen mode Exit fullscreen mode

Assim, se quisermos adicionar uma nova condição dentro desse fluxo de código basta criarmos uma nova classe concreta de Handler, inicializá-la e colocá-la na cadeia dentro do arquivo index.ts.

Código fonte

Top comments (0)