Inversão de controle Parte 2: Factory (Fábricas)

Robôs construindo um carro

Olá! Tudo bem? Este é o segundo artigo de uma série sobre inversão de controle. No primeiro artigo, abordamos aspectos conceituais desta técnica. Neste artigo, vamos nos aprofundar no padrão de projeto Factory e em como ele pode contribuir para a Inversão de Controle.

Mas primeiro, uma dica!

O nome Kent Beck reside no panteão dos grandes escritores de arquitetura e engenharia de software. Entre as várias obras dele, existe uma que eu aconselho a sua leitura: Padrões de Implementação – um catálogo de padrões indispensáveis para o dia a dia do programador. Esse livro é praticamente uma obra que te ensina a programar. Não estou falando que ele vai te ensinar a sintaxe de uma linguagem específica (ainda que ele utilize java como exemplo). Ele vai te ensinar todas as artimanhas para ser um bom programador e escrever código de qualidade. Sem dúvida é um complemento importante para o conhecimento adquirido após a leitura do Clean Code (MARTIN, 2008).

Mas por que estou falando desse livro agora?

Beck reserva o capítulo 7 para falar sobre comportamento do software. E imagine qual o assunto que ele aborda? O fluxo do software! Entrar em maiores detalhes iria fugir do escopo desse artigo. Então, se você é daqueles que acredita que inversão de controle só é possível por meio de frameworks, dê uma olhada em como o Kent Beck constrói a inversão apenas definindo como as mensagens entre os objetos são executadas.

Agora sim, vamos às fábricas!

Factory: O que são? Pra quê servem? Como são?

O catálogo de padrões de projeto mais famoso, com certeza, é o trabalho do GoF: Desing Patterns. Bastante detalhista, o livro aborda vinte e três padrões de projeto, explicando sua intenção, motivação, aplicabilidade, estrutura, seus impactos no projeto e sua mecânica de implementação.  

Um dos conjuntos de padrões abordados, e que interessam para nós neste momento, são os padrões de criação. Os padrões desse conjunto se ocupam em encapsular a lógica de criação dos objetos, tornando o sistema independente da forma como os seus objetos são instanciados. Os cinco padrões de criação abordados são: Abstract Factory, Builder, Factory Method, Prototype e Singleton. Como você já deve ter imaginado, vamos falar apenas dos padrões Abstract Factory e Factory Method.

Abstract Factory

Você vai utilizar o padrão Abstract Factory quando quiser garantir que apenas objetos de uma mesma família sejam sempre criados. Não entendeu? Talvez com um exemplo fique um pouco melhor:

O desafio é criar um conjunto de classes responsável por abstrair o processo de escrita de SQL. Selects básicos são fáceis de implementar. Dificilmente um banco fugiria do padrão ANSI. No entanto, quando é necessário implementar pesquisas mais específicas como “os primeiros X registros, pulando Y registros”, cada banco possui uma implementação diferente. Como proceder?

Tenho certeza que você já viu algo parecido para solucionar este tipo de questão: uma cláusula condicional que testa qual banco estamos acessando. E dependendo do resultado, retorna um texto diferente:

if (banco == TipoBanco.SQLServer)
{
  retorno = "top {0}";
} 
else if (banco == TipoBanco.MySQL || banco == TipBanco.PostgreSQL) 
{
  retorno = "limit {0}";
}
else if (banco == TipoBanco.Oracle)
{
  retorno = "rownum < {0}";
}
else if (banco == TipoBanco.FireBird)
{
  retorno = "first {0}";
}

return String.Format(retorno, qtdRegistros);

O problema de um código como esse é a escalabilidade. No momento em que um novo banco de dados tiver de ser suportado, você terá que varrer o sistema em busca das chamadas específicas para cada banco e então acrescentar o novo código.

Outro ponto importante a ser lembrado é que a classe que for montar o nosso select precisa saber exatamente onde colocar o código retornado. Isto porque o SQLServer e o Firebird montam a sentença com Top e Limit, respectivamente, no início do select. Já o Oracle, por exemplo, coloca o código no final da expressão. O desenvolvedor ou desenvolvedora terá que tomar muito cuidado para não misturar o código entre os bancos. Basta esquecer uma variável e pronto: nada funciona.

A Abstract Factory, além de dispensar essa quantidade de IF’s, ainda garante que todos os objetos instanciados pertençam a uma mesma família. Se eu começar a escrever um Select para Firebird, com abstract factory, eu posso ter certeza absoluta que todos os objetos instanciados pertencem ao grupo Firebird.

Antes de mostrar um exemplo, deixe te mostrar a estrutura do padrão:

Resultado de imagem para abstract factory pattern
Disponível em: https://upload.wikimedia.org/wikipedia/commons/thumb/9/9d/Abstract_factory_UML.svg/1200px-Abstract_factory_UML.svg.png

Isto é uma Factory?

Na imagem, nós temos uma classe Client. A sua implementação não nos importa. Ela está ali apenas para sinalizar a entidade de código que está utilizando a nossa classe. A classe Client solicita implementações de AbstractProductAe AbstractProductB. Estas são interfaces que abstraem os produtos concretos ProductA1, ProductA2, ProductB1 e ProductB2. Estes produtos, voltando ao nosso exemplo de criação de SQL, poderiam ser as classes responsáveis por gerar o código dos “retorne os primeiros X registros” (product) específicos de cada banco (A1 e A2).

As implementações são solicitadas para a Abstract Factory. Na imagem ela está representada como uma interface. Esta interface precisa possuir os métodos que retornem os produtos esperados. Seria algo como:

    public interface IAbstractFactory
    {
        ITable Table();
        IField Field();
        IFirstRegistries FirstRegistries();
    }

Acontece que o banco de dados precisa ser informado em algum ponto. Neste caso, o padrão prevê a criação de uma classe que abstraia a escolha das fábricas: A AbstractFactory. Mesmo com esse nome, não estamos falando de uma classe abstrata. Ela é concreta (ou seja, você deve instanciá-la diretamente). O que a torna “abstrata” é justamente o fato de que ela não retorna os produtos: ela delega a criação dos objetos para outras fabricas, que na imagem são as ConcreteFactory1 e ConcreteFactory2. Estas classes também implementam a interface AbstractFactory. Mas ao invés de delegarem a criação de objetos para outra classe, elas mesmas fazem a criação.

No nosso exemplo, a AbstractFactory faz a escolha da família de objetos a serem criados no seu construtor.

        private IAbstractFactory factory;
        public AbstractFactory(DBKind dbKind)
        {
            switch (dbKind)
            {
                case DBKind.SQLServer:
                    factory = new SQLServerFactory();
                    break;
                case DBKind.Firebird:
                    factory = new FirebirdFactory();
                    break;
                default:
                    factory = new SQLAnsi();
                    break;
            }
        }

Nos métodos que retornam os objetos, a AbstractFactory delega ao seu objeto factory a reponsabilidade de retornar a instância desejada:

        public IFirstRegistries FirstRegistries()
        {
            return factory.FirstRegistries();
        }

Veja o código completo:

namespace DB1.AbstractFactory
{
    #region Abstract Products
    public interface ITable {}
    public interface Ifield {}
    public interface IFirstRegistries { }
    #endregion

    #region AbstractFactory
    public interface IAbstractFactory
    {
        ITable Table();
        IField Field();
        IFirstRegistries FirstRegistries();
    }
    #endregion

    #region ConcreteFactory
    public class FirebirdFactory : IAbstractFactory
    {
        public IField Field()
        {
            throw new System.NotImplementedException();
        }

        public IFirstRegistries FirstRegistries()
        {
            throw new System.NotImplementedException();
        }

        public ITable Table()
        {
            throw new System.NotImplementedException();
        }
    }

    public class SQLServerFactory : IAbstractFactory
    {
        public IField Field()
        {
            throw new System.NotImplementedException();
        }

        public IFirstRegistries FirstRegistries()
        {
            throw new System.NotImplementedException();
        }

        public ITable Table()
        {
            throw new System.NotImplementedException();
        }
    }

    public class SQLAnsi : IAbstractFactory
    {
        public IField Field()
        {
            throw new System.NotImplementedException();
        }

        public IFirstRegistries FirstRegistries()
        {
            throw new System.NotImplementedException();
        }

        public ITable Table()
        {
            throw new System.NotImplementedException();
        }
    }
    
    enum DBKind { SQLServer, Firebird }
    public class AbstractFactory : IAbstractFactory
    {
        private IAbstractFactory factory;
        public AbstractFactory(DBKind dbKind)
        {
            switch (dbKind)
            {
                case DBKind.SQLServer:
                    factory = new SQLServerFactory();
                    break;
                case DBKind.Firebird:
                    factory = new FirebirdFactory();
                    break;
                default:
                    factory = new SQLAnsi();
                    break;
            }
        }
        public IField Field()
        {
            return factory.Field();
        }

        public IFirstRegistries FirstRegistries()
        {
            return factory.FirstRegistries();
        }

        public ITable Table()
        {
           return factory.Table();
        }
    }
    #endregion
}

Não é muito difícil, não é mesmo? Vamos conhecer melhor agora o padrão Factory method.

Factory Method

Esse padrão é utilizando quando você tem um algoritmo distribuído entre uma classe base e suas classes filhas. Contudo, quando a classe base não sabe criar um determinado objeto, ou o objeto padrão precisa ser substituído pelas classes filhas, utilizamos esse padrão.

(Abre parênteses)

Se você estuda padrões de projeto e pensou direto no padrão Template Method, você está certo.

(Fecha parênteses)

Vamos ao exemplo: Uma pizza!

Digamos que você precisa criar classes que saibam implementar pizzas e enviar para o delivery. O algoritmo básico é: Criar os ingredientes; misturá-los e então – como somos hi-tech – enviar via drone. O código ficaria mais ou menos assim:

    public interface IIngredient { }

    public class Muzzarela : IIngredient { }
    public class Ham : IIngredient { }
    public class Pepperoni : IIngredient { }    
    public class Eggs : IIngredient { }

    public abstract class Pizza
    {
        List<IIngredient> _ingredients;

        public Pizza()
        {
            CatchIngredients();
            AssembleIngredients();
            SendoToDeliver();
        }

        protected abstract void CatchIngredients();

        private void SendoToDeliver()
        {
            Console.WriteLine("Sending by drone");
        }

        private void AssembleIngredients()
        {
            foreach (var ingredient in Ingredients)            
                Console.WriteLine("Mix with {0}", ingredient.GetType().Name);            
        }

        public List<IIngredient> Ingredients { get => _ingredients; }
    }

Como você pode perceber, todo o algoritmo está definido na classe abstrata Pizza. Ela sabe quase tudo, exceto qual é a receita. O método responsável por juntar os ingredientes é CatchIngredients(), que será implementado pela classe filha. Vamos fazer uma pizza de pepperoni?

    public class PepperoniPizza : Pizza
    {
        protected override void CatchIngredients()
        {
            Ingredients.Add(new Pepperoni());
            Ingredients.Add(new Ham());
            Ingredients.Add(new Muzzarela());
            // No eggs today
        }
    }

Desculpe. Eu realmente não sei fazer pizza. Mas faça de conta que em uma pizza de pepperoni vai… Pepperoni, presunto e muzzarela. Se eu quiser criar uma pizza de pepperoni, basta declarar uma variável do tipo Pizza e instanciar um objeto filho. Pepperoni, no caso:

Pizza pizza = new Pepperoni();

Este padrão é ainda mais fácil, não é mesmo?

Quando vou utilizar o Desing Pattern Factory em Inversão de Controle?

Fábricas ajudam a diminuir o acoplamento e também a minimizar a repetição de código, o que por si só já é uma grande vantagem. Mas existem outros motivos para que as fábricas componham a técnica de Inversão de Controle.

No nosso exemplo nós informamos a AbstractFactory qual o tipo de banco de dados deve ser utilizado. Ao invés disso, a classe poderia receber essa informação a partir de um arquivo de configuração. Ou seja, por mais que alguém alegue que, com as factories, estejamos apenas transferindo o fluxo para outro lugar, o elemento “arquivo de configuração” retira o controle do fluxo da aplicação e o toma para si. O que certamente inverte o controle da aplicação – que muda em tempo de execução.

Em um framework de IoC as factories também podem exercer um papel importante. Elas sabem como criar objetos, podendo até transferir esta responsabilidade builders, que por sua vez podem adicionar decorators ao produto final. Tudo isso ficando completamente invisível para o cliente, que apenas pede seus objetos criados. Essa transparência permite que as aplicações sejam completamente alteradas em tempo de execução. Alterando apenas um parâmetro.

Uma última palavra sobre Factory e Design Patterns

Quando começamos a estudar padrões de projetos, somos levados pela crença de que devemos implementar o padrão completamente. Igual manda a receita. Ou estaremos fazendo errado. Isso nem sempre é verdade.

Os padrões servem para ajudar a solucionar problemas. E não para acrescentar complexidade desnecessária. Eu tenho certeza que você já deve ter feito algo parecido com o Factory Method, mesmo antes de conhecê-lo. Isto porque os padrões são intuitivos. O catálogo do GoF surgiu à partir de código real. Eles apenas catalogaram soluções que já existiam. E não o contrário. Desta forma, seu papel principal na mesa do/da dev é de servir como forma de inspiração e mapa para quem está perdido.

Particularmente aconselho que você, iniciante, siga o catálogo ao pé da letra. Contudo, conforme for se familiarizando com eles, implemente modificações que façam sentido. Se você tem uma classe com métodos estáticos que retornam instâncias, você pode chamá-la de fábrica sim. Se você está substituindo a sobrecarga de construtores por métodos estáticos de criação, você está implementando um método-fábrica também.

O importante é você saber o que está fazendo e não deixar o seu código virar uma salada sem sentido.

Por hoje é só. No próximo artigo vamos conhecer o padrão Observer.

Outros artigos da série

2 thoughts on “Inversão de controle Parte 2: Factory (Fábricas)

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Esse site utiliza o Akismet para reduzir spam. Aprenda como seus dados de comentários são processados.