O PADRÃO SIMPLE FACTORY
Criar objetos geralmente é uma tarefa simples, passamos valores para o construtor e pronto, temos um objeto instanciado e pronto para uso. No entanto, algumas vezes a criação não é tão direta assim e alguma manipulação com os dados pode ser necessária. Em outros casos é preciso configurar o objeto, mesmo depois da execução do construtor.
Quando instanciar objetos não for uma tarefa tão simples, os padrões de criação oferecem boas soluções. Como vimos antes, os padrões documentados pela Gangue dos Quatro foram classificados em grupos de acordo com o tipo de problema que resolvem. Padrões que facilitam criar novas instâncias de objetos são classificados como padrões de criação.
Uma versão simples e que pode ser utilizada mais facilmente, é o Simple Factory. Nele vamos extrair a lógica que cria objeto para uma classe especializada, deixando que a manipulação dos dados não atrapalhe o restante da lógica de negócio. Alguns autores nem consideram o Simple Factory como um padrão, devido a sua simplicidade.
Exemplo de Aplicação
Na Listagem 1 temos um exemplo de código que pode se beneficiar do uso do Simple Factory. Nele, o método buscarProdutosPreferenciais recebe um usuário consulta um serviço externo para obter os produtos que o usuário costuma comprar. Ao final, os produtos são validados e filtrados para que apenas aqueles com estoque sejam retornados.
No entanto, antes de chegar na lógica que realmente faz a busca dos produtos preferenciais do usuário, é preciso configurar a chamada ao serviço externo. Essa responsabilidade extra aumenta o tamanho e a complexidade do método.
public List<Produto> buscarProdutosPreferenciais(Usuario usuario) {
ConfiguracoesServicos config = new ConfiguracoesServicoProdutosPreferenciais();
config.setRecurso(“/produtos/preferenciais/”);
config.setIdUsuario(usuario.getId());
if (getAmbienteAtual() == Ambiente.LOCAL) {
config.setEndpoint(“localhost:1234”);
config.setXid(gerarUuid());
config.setVersaoApi(“2.1”);
} else {
if (toggleApiNova.habilitado()) {
config.setVersaoApi(“2.1”);
config.setXid(gerarUuid());
} else {
config.setVersaoApi(“1.9”);
config.setXid(“”);
}
config.setEndpoint(“https://intra.” + getAmbienteAtual().toString() + “.mega\
corp.com/”);
}
ServicoRest servico = new ServicoRest(config);
List<Produto> produtos = criarProdutos(servico.executarGet())
List<Produto> produtosValidos = new ArrayList<Produto>();
for (Produto produto : produtos) {
if (produto.temEstoque()) {
produtosValidos.add(produto);
}
}
return produtosValidos;
}
Note quantas linhas apenas a criação das configurações toma. Isso acaba desviando a atenção da responsabilidade principal do método, que é buscar os produtos preferenciais do usuário.
No exemplo anterior existem dois tipos de configurações, uma para o ambiente local e outra para os demais ambientes. A configuração dos ambientes remotos precisa lidar ainda com um feature toggle, para determinar qual versão da API deve ser utilizada.
A criação de configurações locais pode ser extraída como mostrado na Listagem 2. A identificação do recurso é comum a todos as configurações, portanto pode ser definida já no construtor da fábrica, assim como o id do usuário. As demais configurações são específicas ao ambiente local, ficando portanto dentro do método criarConfiguracaoLocal.
class FabricaConfiguracaoServicos {
private ConfiguracoesServicos config;
public FabricaConfiguracaoServicoProdutosPreferenciais(String idUsuario) {
config = new ConfiguracoesServicoProdutosPreferenciais();
config.setRecurso(“/produtos/preferenciais/”);
config.setIdUsuario(idUsuario);
}
public ConfiguracoesServicos criarConfiguracaoLocal() {
config.setEndpoint(“localhost:1234”);
config.setXid(gerarUuid());
config.setVersaoApi(“2.1”);
return config;
}
}
A lógica para criar as configurações de serviços para outros ambientes depende do feature toggle. Nesse caso vamos passar a informação se o toggle está ativo e o ambiente atual como parâmetros. A implementação pode ficar como na Listagem 3.
class FabricaConfiguracaoServicos {
public ConfiguracoesServicos criarConfiguracaoRemota(boolean usarApiNova, Ambi\
ente ambiente) {
if (usarApiNova) {
config.setVersaoApi(“2.1”);
config.setXid(gerarUuid());
} else {
config.setVersaoApi(“1.9”);
config.setXid(“”);
}
config.setEndpoint(“https://intra.” + ambiente.toString() + “.megacorp.com”);
}
}
Para utilizar o Simple Factory vamos simplesmente substituir o código dentro do if pela chamada ao método fábrica apropriado. Vamos também extrair essa parte em um novo método, para facilitar a leitura, como mostrado na Listagem 4:
public List<Produto> buscarProdutosPreferenciais(Usuario usuario) {
ConfiguracoesServicos config = criarConfiguracaoDoAmbiente(usuario)
ServicoRest servico = new ServicoRest(config);
List<Produto> produtos = criarProdutos(servico.executarGet())
List<Produto> produtosValidos = new ArrayList<Produto>();
for (Produto produto : produtos) {
if (produto.temEstoque()) {
produtosValidos.add(produto);
}
}
return produtosValidos;
}
public ConfiguracoesServicos criarConfiguracaoDoAmbiente(Usuario usuario) {
FabricaConfiguracaoServicos fabrica = new FabricaConfiguracaoServicos(usuario.\
getId());
if (getAmbienteAtual() == Ambiente.LOCAL) {
return fabrica.criarConfiguracaoLocal();
} else {
return fabrica.criarConfiguracaoRemota(toggleApiNova.habilitado(), getAmbien\
teAtual());
}
}
Apesar de simples, o ganho com a separação das responsabilidades é bem grande, principalmente na legibilidade do código.
Um Pouco de Teoria
Como mencionado antes, todos os padrões ajudam o seu código a seguir os princípios de design Orientado a Objetos. No caso do Simple Factory, o maior benefício é a divisão de responsabilidades seguindo o Princípio da Responsabilidade Única.
Quem lê o código do método buscarProdutosPreferenciais pode focar apenas em entender sua lógica, sem ter que se preocupar em como as configurações são criadas. Como passamos muito mais tempo lendo código do que escrevendo, essa é uma grande vantagem.
Outro benefício da separação dessas responsabilidades é que a maneira como as configurações do serviço são criadas vai mudar menos do que lógica definida em buscarProdutosPreferenciais. Ao lidar com dependências entre classes é sempre bom que uma classe dependa de outras menos prováveis de mudar.
Os testes do método buscarProdutosPreferencias também estão validando a criação das configurações de serviços, mesmo que indiretamente. Criando uma classe especializada para isso, podemos limitar o escopo dos testes e focar na lógica de negócio, testando a criação de configurações em outros testes isolados.
O Simple Factory é um bom ponto de início para separar a criação de objetos do seu uso. Como vimos no exemplo anterior, poucas classes são criadas e a estrutura do padrão é bem simples. Se o seu contexto permite isolar a maneira como objetos são criados e você tiver que lidar apenas com um tipo de objeto, o Simple Factory é uma excelente maneira de resolver o problema.
Quando Não Usar
Apesar de simples, existem situações onde utilizar o padrão Simple Factory não ajuda muito. Um sinal bem claro de que o padrão não está sendo efetivo é quando a classe fábrica começa a crescer e ter vários métodos para criar os mesmos produtos de maneiras diferentes. Essa talvez seja uma boa hora para aplicar outros padrões fábrica.
Nas seções seguintes vamos comparar como evoluir do Simple Factory para o Factory Method ou para o Builder, dependendo de qual a necessidade do seu contexto. Não vamos entrar em detalhes sobre estes padrões, mas outros recursos com mais detalhes serão indicados ao final do artigo (veja a seção de Referências Externas).
Evoluindo o Simple Factory para Factory Method
Se existem vários métodos que podem ser agrupados com suas próprias lógicas para criar objetos, provavelmente nomeados com um sufixo ou prefixo em comum, talvez seja melhor utilizar o Factory Method e separar esses grupos em outras classes fábrica.
Voltando ao exemplo anterior, imagine que será necessário criar configurações para outros serviços. Ao invés de colocar tudo em um única classe, é melhor criar fábricas especializadas em construir as configuração de cada serviço. Refatorar o Simple Factory para o Factory Method vai ajudar a evitar que a classe fábrica cresça.
Com o Factory Method, continuaremos criando objetos do tipo ConfiguracoesServicos, mas as regras deles serão divididas em classes separadas. Essas classes fábricas seguem uma mesma interface, garantindo que elas possam ser trocadas facilmente. Ao fim, evitamos ter uma classe fábrica com muitas responsabilidades e, como elas são intercambiáveis, conseguimos ter flexibilidade no código que as utiliza.
Evoluindo o Simple Factory para Builder
Se ao rever os métodos você perceber que existe pouca variação na lógica entre eles, apenas mudam-se os valores que são atribuídos ou quais atributos são utilizados, será necessário criar muitos métodos fábrica, com muita duplicação entre si.
No exemplo anterior, imagine que várias configurações de serviço devem ser criadas, mas a diferença entre os métodos é o valor de alguns atributos. Nesse caso podemos utilizar o Builder para expor a configuração dos atributos mas esconder a criação dos objetos.
Com o Builder, ao invés de definirmos o processo de como o objeto será construído, oferecemos métodos para que a classe cliente consiga configurar, de maneira simples, o produto final. Assim evitamos criar vários métodos fábrica para cada possível cenário, sem perder a separação da responsabilidade de criação.