Executando verificação de segurança...
5

Design Patterns: Factory - Primeiros passos com Typescript

O que são Design Patterns?

Olá devs. Hoje iremos começar a falar sobre design patterns, ou no português claro, padrões de projeto para desenvolvimento de software. No dia-a-dia da pessoa desenvolvedora, enfrentamos diversos problemas de refatoração, necessidade de reaproveitar partes do nosso código, escrever um código limpo e legível ou aumentar a performance do código para que ele consiga ser escalável.

Todos esses problemas foram compartilhados pela comunidade por muito tempo e o resultado para solucioná-los proposto pela comunidade de grandes engenheiros de software foi uma série de paradigmas para arquitetar um código.

O resultado de trabalhos assim que geraram a orientação a objetos, os 04 pilares da orientação a objetos, os paradigmas do SOLID e o design patterns.

O design patterns, ou padrões de projeto, são um conjunto de padrões de desenvolvimento de software que visa resolver um problema e melhorar a qualidade do seu código. Eles miram a criação de uma arquitetura de software que garanta resolver da melhor maneira problemas de escalabilidade, manutenibilidade e clareza do seu software.

Lembre-se, por ser padrões, eles não são blocos de código para se colocar no seu projeto mas formas de melhor escrever e planejar a arquitetura do seu código. Portanto, não se limitam a uma linguagem apenas mas a varias. Iremos fazer a prática com Typescript mas você pode ficar a vontade para implementar na linguagem de sua escolha.

Padrão Factory

O padrão Factory é um dos padrões criacionais (Factory, Abstract Factory, Builder, Prototype e Singleton), ou seja, um padrão de projeto relacionado à criação de objetos que propõe o desenvolvimento de um método fábrica responsável pela instância dos objetos de uma classe.

Com a implementação do padrão factory, seu código não irá instanciar mais nenhum objeto de classe relacionadas a domínios ou regras de negócio. O seu código irá chamar esse método fábrica que irá instanciar o objeto e devolvê-lo para você, ou seja, objetos de classes não serão criados mais com um new. Esses objetos criados e devolvidos pelos métodos fábrica são chamados de produtos.

Mas qual a vantagem de aplicar o padrão factory?

Primeiramente, você desacopla a instância do objeto do resto do seu código. Isso quer dizer que a regra de negócio que precisa de um objeto (produto) não precisa saber das regras do mesmo.

A segunda vantagem é que você, ao criar um método fábrica, tem acesso a todos os novos objetos criados. Se você precisa adicionar uma funcionalidade toda vez que um objeto é instanciado, você precisa apenas modificar o seu método fábrica e em todos os lugares que necessitam de um produto novo terão a funcionalidade aplicada. Sem esse padrão, você teria que procurar no seu código toda vez que o new de uma classe é chamado.

Nesse momento, você pode pensar: mas era só adicionar a funcionalidade que eu quero no construtor da classe. O problema é que você pode ferir 03 paradigmas do SOLID de uma só vez (S, O e D). Vamos a um exemplo para ilustrar melhor:

Digamos que você tenha um software responsável por uma loja de automóveis e nele existe uma classe responsável pela entidade Car. Essa classe está cheia de regras de negócio relacionadas aos cálculos dos impostos e documentos de um carro.

Então temos a seguinte modificação: 'Toda vez que um carro novo for instanciado, devemos registrar em um log a adição deste carro'. Aqui temos que criar uma classe Log responsável por gravar esses registros, sejam eles em arquivo ou utilizando um banco de dados.

Se chamarmos uma instância da classe Log dentro do construtor da classe Car, estamos ferindo o princípio de responsabilidade única, já que a classe Car tem que ser responsável apenas pelos carros.

Deixamos de aplicar o princípio aberto/fechado também já que uma classe está aberta a adição de regras mas fechada para modificações, ainda mais uma modificação em um construtor.

Por último, não aplicamos o princípio de inversão de dependência, já que a classe Car não pode ser reutilizada devido ao alto acoplamento com a classe Log. Imagine que você deseja reaproveitar todas as regras de negócios dos carros em outro sistema. Além da classe Car, você teria de levar também a classe Log.

A terceira vantagem é que você pode não criar um objeto novo com o new no método fábrica. Dependendo da aplicação, podemos retornar um objeto previamente criado ao invés de criar um novo. Neste caso, usamos um outro padrão de projeto criacional chamado Singleton


Iniciando a Prática

Para nossa prática, vamos criar um software de uma empresa aérea e a nossa primeira classe é a classe Airplane. Por enquanto, não teremos métodos complexos nem muitos atributos. Apenas os atributos prefix, manufacturer e aircraft para guardar de modo privado o prefixo, fabricante e modelo de uma aeronave.

Nos métodos, teremos os getters apenas e todos os atributos serão atribuídos no construtor da classe Airplane. Nossa classe Airplane implementará uma interface com o nome IAirplane.

Classe Airplane

interface IAirplane {
    prefix: string;
    manufacturer: string;
    aircraft: string;
}

class Airplane implements IAirplane {
    constructor(private _prefix: string,
        private _manufacturer: string,
        private _aircraft: string) {}
    
    get prefix(): string {
        return this._prefix
    }

    get manufacturer(): string {
        return this._manufacturer;
    }

    get aircraft(): string {
        return this._aircraft;
    }

}

A partir desse momento, toda vez que precisássemos de uma instância da classe Airplane iríamos simplesmente chamar o método new com o código:

const embraerE195 = new Airplane('PR-ABC','Embraer','E195');

Entretanto iremos aplicar o padrão de projeto Factory. Sua implementação será através de uma nova classe chamada AirplaneFactory. Essa classe terá o método fábrica chamado create que irá gerar um novo produto do tipo Airplane. Portanto, no nosso código, a classe Factory será a única que poderemos instanciar com o método new.

Existem algumas vertentes que sugerem que tanto a classe Factory quanto o método Factory sejam estáticos. Dessa forma, o método new seria, de fato, chamado apenas dentro dos métodos factory. Entretanto essa é uma forma equivocada de se construir a factory já que a classe estática impede que a mesma seja estendida. Veremos mais adiante sobre a possibilidade de criar produtos abstratos e concretos.

Classe Airplane e classe factory

Com isso, teremos no nosso código a classe e método factory

class AirplaneFactory {
    public create (prefix: string, manufacturer: string, aircraft: string): Airplane {
        return new Airplane(prefix, manufacturer, aircraft);
    } 
};

e na utilização, teremos

const airplaneFactory = new AirplaneFactory();

const embraerE195 = airplaneFactory.create('PR-ABC','Embraer','E195');

Note que sempre o método fábrica deve ter um produto do mesmo tipo da classe da regra de negócio, mesmo que seja um produto abstrato.


Aumentando a complexidade: Classes concretas e abstratas.

Iremos aumentar a complexidade do nosso exemplo. Teremos que na nossa regra de negócio, o avião não é uma entidade fechada mas que pode ser estendida para aviões de passageiros e aviões de carga. Também iremos definir que a classe Avião será abstrata, ou seja, não poderemos instanciar um objeto avião, apenas Avião de passageiros ou avião de carga.

No nosso novo diagrama, temos as classes PassengerAirplane e CargoAirplane, cada um com um atributo a mais em relação à classe abstrata Airplane. Também definimos as interfaces estendidas IPassengerAirplane e ICargoAirplane.

Classes abstratas e concretas

Iremos então modificar a classe Airplane para que seja abstrata

abstract class Airplane implements IAirplane {
    constructor(private _prefix: string,
        private _manufacturer: string,
        private _aircraft: string) {}
    
    get prefix(): string {
        return this._prefix
    }

    get manufacturer(): string {
        return this._manufacturer;
    }

    get aircraft(): string {
        return this._aircraft;
    }
}

Então iremos criar as classes concretas

interface IPassengerAirplane extends IAirplane {
    passengerCapacity: number;
    buyTicket(): void;
}


class PassengerAirplane extends Airplane implements IPassengerAirplane {
    
    constructor(prefix: string, manufacturer: string, aircraft: string, private _passengerCapacity: number) {
        super(prefix, manufacturer, aircraft);
    }
    
    get prefix(): string {
        return super.prefix
    }

    get manufacturer(): string {
        return super.manufacturer;
    }

    get aircraft(): string {
        return super.aircraft;
    }

    get passengerCapacity(): number {
        return this._passengerCapacity;
    }

    public buyTicket(): void {
        console.log(`New ticket emitted to ${this.manufacturer} ${this.aircraft} - Prefix: ${this.prefix}`);
    }
}

interface ICargoAirplane extends IAirplane {
    payload: number;
    loadCargo(weight: number)
}

class CargoAirplane extends Airplane implements ICargoAirplane {
    constructor(prefix: string, manufacturer: string, aircraft: string, private _payload: number) {
        super(prefix, manufacturer, aircraft);
    }

    get prefix(): string {
        return super.prefix
    }

    get manufacturer(): string {
        return super.manufacturer;
    }

    get aircraft(): string {
        return super.aircraft;
    }

    get payload(): number {
        return this._payload;
    }

    public loadCargo(weight: number){
        console.log(`${weight} loaded to ${this.manufacturer} ${this.aircraft} - Prefix: ${this.prefix}`);
    }
}

Por fim, devemos alterar a nossa classe e método fábrica. Nesse momento, como temos uma fábrica produzindo um produto do tipo Airplane, devemos manter os produtos concretos respeitando esse tipo e se temos dois produtos possíveis (avião de passageiros e avião de carga), teremos duas fábricas concretas respeitando uma fábrica abstrata.

A fábrica abstrata deixa de ter a implementação do método fábrica mas apenas a definição de um método abstrato.
As fábricas concretas estendem a fábrica abstrata e implementam os métodos fábrica concretos de acordo com suas especificidades.

Factory abstrata e concreta

Note pelo diagrama de classe que tanto as classes Airplane como AirplaneFactory são classes abstratas. Dessa maneira, as fábricas PassengerAirplaneFactory e CargoAirplaneFactory se tornam as fábricas concretas que geram os produtos concretos PassengerAirplane e CargoAirplane.

As fábricas concretas implementam o método create que agora é um método abstrato na AirplaneFactory. Apesar dos produtos serem diferentes, os produtos respeitam a interface IAirplane. Isso demonstra o fato de não criarmos o método fábrica como estático.

Vamos às implementações

abstract class AirplaneFactory {
    public abstract create (prefix: string, 
       manufacturer: string, 
       aircraft: string, 
       payload: number, 
       passengerCapacity: number): Airplane
};

class PassengerAirplaneFactory extends AirplaneFactory {
    public create (prefix: string, 
       manufacturer: string, 
       aircraft: string, 
       passengerCapacity: number): PassengerAirplane {
        return new PassengerAirplane(prefix,
           manufacturer,
           aircraft,
           passengerCapacity);
    } 
};

class CargoAirplaneFactory extends AirplaneFactory {
    public create (prefix: string,
       manufacturer: string, 
       aircraft: string, 
       payload: number): CargoAirplane {
        return new CargoAirplane(prefix,
           manufacturer,
           aircraft,
           payload);
    }
};

Por fim, vamos implementar as nossas classes fábricas concretas

const passengerAirplaneFactory = new 
    PassengerAirplaneFactory();

const cargoAirplaneFactory = new 
    CargoAirplaneFactory();

const E195 = passengerAirplaneFactory
    .create('PR-ABC', 
        'Embraer', 
        'E195', 
         118);

const KC390 = cargoAirplaneFactory
    .create('PR-DEF', 
        'Boeing', 
        'B747', 
         137);

E195.buyTicket();
KC390.loadCargo(100);

Note que no exemplo, as únicas instâncias com o método new são das nossas classes factory. Criamos dois objetos, um chamado E195 criado como um avião de passageiros com a fábrica concreta PassengerAirplaneFactory e outro objeto KC390 criado como um avião de carga com a fábrica concreta CargoAirplaneFactory.

Temos um exemplo chamando os métodos buyTicket e loadCargo para cada um desses objetos apesar de ambos serem do tipo Airplane.

Podemos nos questionar sobre o uso da classe AirplaneFactory. Se não podemos instanciar ela, não seria mais simples implementar apenas as fábricas concretas? A resposta é não, pois assim, perdemos a herança já que os dois produtos são produtos concretos de uma classe abstrata Airplane. Caso precisássemos adicionar uma classe para controlar aviões comerciais, todos eles devem respeitar a interface IAirplane e devem ser produtos do tipo Airplane.

Além disso, se você se questionou se podemos criar uma classe Factory concreta que tenha a capacidade de criar ambos os tipos de produto, a resposta é sim.

Nesse caso, temos o próximo padrão de projeto que é o Abstract Factory ou fábrica abstrata. Não trataremos dele aqui mas nesse padrão, a fábrica abstrata fica responsável por criar famílias de produtos concretos sem a necessidade de especificar a classe concreta. Mas lembre-se, que o AirplaneFactory do nosso exemplo não é um abstract factory. Desse modo, tornar uma classe fábrica abstrata não se enquadra no padrão de projeto Abstract factory.


Testando o Factory

Podemos fazer testes do nosso Factory para conferir se os produtos estão respeitando as instâncias das classes de domínio, especialmente nos casos de classes concretas e abstratas que devem implementar uma mesma interface.

Vamos realizar um teste em Jest mas você pode utilizar a biblioteca de testes da sua escolha e de acordo com a linguagem de programação que está utilizando.

Primeiro irei testar o PassengerAirplaneFactory. Como em todos os it eu irei usar uma instância nova do factory, irei colocar no beforeEach.

let passengerAirplaneFactory;
    beforeEach(() => {
        passengerAirplaneFactory = new 
             PassengerAirplaneFactory();
    });

Depois irei testar cinco pontos:

  • Se a PassengerAirplaneFactory é uma instância dela mesma.
  • Se a PassengerAirplaneFactory é uma instância da AirplaneFactory.
  • Se a PassengerAirplaneFactory cria um produto do tipo Airplane e do tipo PassengerAirplaneFactory.
  • E se a PassengerAirplaneFactory cria um produto que não é do tipo CargoAirplaneFactory.

Com todos esses testes, o código para testar a PassengerAirplaneFactory ficará da seguinte maneira:


describe('Passenger airplane factory', () => {

    let passengerAirplaneFactory;
    beforeEach(() => {
        passengerAirplaneFactory = new 
            PassengerAirplaneFactory();
    });

    it('is a instance of Airplane factory', () => {             
        expect(passengerAirplaneFactory)
            .toBeInstanceOf(AirplaneFactory);
    });

    it('is a instance of Passenger airplane factory', () => {
        expect(passengerAirplaneFactory)
            .toBeInstanceOf(PassengerAirplaneFactory);
    });
    it('creates a airplane and passenger 
    airplane product', () => {
        const E195 = passengerAirplaneFactory
            .create('PR-ABC', 
                'Embraer', 
                'E195', 
                 118); 
        expect(E195).toBeInstanceOf(Airplane);
        expect(E195).toBeInstanceOf(PassengerAirplane);
    });
    it('does not create a cargo airplane product', () => {
        const E195 = passengerAirplaneFactory
            .create('PR-ABC', 
                'Embraer', 
                'E195', 
                118); 
        expect(E195).not.toBeInstanceOf(CargoAirplane);
    });
});

Para os testes do CargoAirplaneFactory, temos as mesmas condições do caso do PassengerAirplaneFactory mas testando se ele gera produtos de CargoAirplane e Airplane e não de PassengerAirplane.


describe('Cargo airplane factory', () => {

    let cargoAirplaneFactory;
    beforeEach(() => {
        cargoAirplaneFactory = new CargoAirplaneFactory();
    });

    it('is a instance of Airplane factory', () => {    
        expect(cargoAirplaneFactory)
                .toBeInstanceOf(AirplaneFactory);
    });

    it('is a instance of Cargo airplane factory', () => {            
        expect(cargoAirplaneFactory)
            .toBeInstanceOf(CargoAirplaneFactory);
    });

    it('creates a airplane and cargo airplane product', () => 
    {
        const B747 = cargoAirplaneFactory
            .create('PR-DEF', 'Boeing', 'B747', 137); 
        expect(B747).toBeInstanceOf(Airplane);
        expect(B747).toBeInstanceOf(CargoAirplane);
    });

    it('does not create a passenger airplane product', () => 
    {
        const B747 = cargoAirplaneFactory
              .create('PR-DEF', 'Boeing', 'B747', 137); 
        expect(B747).not.toBeInstanceOf(PassengerAirplane);
    });
});

Por último e não menos importante, vamos verificar o resultado do nosso teste.

Testes no padrão Factory

Finalizando, esse foi o padrão Factory. Espero que essa breve introdução tenha ajudado na compreensão do início dos Design Patterns e te estimule a continuar aplicando tanto esse padrão quanto os outros. Bons estudos.

Danilo Silva

Desenvolvedor de software experiente em boas práticas, clean code e no desenvolvimento de software embarcado e de integração com hardwares de controle e telecomunicação.

Linkedin
Github
E-mail

Carregando publicação patrocinada...
2
1

Com certeza shedyhs.
O refactoring guru é um excelente guia dos padrões de projeto.

O livro deles, mergulho nos padrões de projeto, também é um excelente guia para estudar POO do zero antes de entrar no design patterns.

A grande dificuldade que enfrento em algumas literaturas são os exemplos. Geralmente os exemplos são ótimos para explicar um paradigma ou padrão de projeto mas cada um tem seu exemplo e as vezes eles não se contextualizam. Daí vinha uma grande dificuldade de trazer para o meu software.

Pretendo escrever outros artigos sobre os padrões seguindo o exemplo da aplicação de uma empresa aérea como comecei aqui no padrão Factory.

1

com certeza um dos maiores benefícios de usar o facotory (e desigb patterns em geral) é a maior facilidade em realizar os testes.

excelente tutorial, parabens Danilo!