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)