Design Patterns: Factory - Primeiros passos com Typescript 🇧🇷
Tags: typescript, braziliandevs, architecture, webdev
Published: 2023-03-10 20:04:15

## O que são Design Patterns?  
<p> </p>

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bgoe310p1ygluy6l9sgp.png)

```ts
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:

```ts
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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cjrfwp319d9ltsvdlh8k.png)

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

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

```
e na utilização, teremos

```ts
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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/36dsptrjl5a1e1nji5wl.png)

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

```ts
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

```ts
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}`);
    }
}

```

```ts
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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yne77bsy9xayers6ejtx.png)

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

```ts
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

```ts
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`.

```ts
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:

```ts

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`.

```ts

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3j741pbzqjwzk4cwqre8.png)

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.


<h3 style="color:#000066">Danilo Silva</h3>
<img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tj4i67b3yid2wlx90oky.png" />

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.

<a href="https://www.linkedin.com/in/danilo-silva-44518956/">Linkedin</a>
<a href="https://github.com/Danilo-Oliveira-Silva">Github</a>
<a href="mailto:danilo.o.s@hotmail.com">E-mail</a>