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}`;
}
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;
}
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");
});
});
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;
}
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;
}
}
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);
}
}
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);
}
}
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);
}
}
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);
}
}
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);
}
}
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 };
O código final da nosssa aplicação ficará assim:
export function properFoodForAnimal(anAnimal: string): string | null {
return handler.handle(anAnimal);
}
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
.
Top comments (0)