Os padrões de projeto de fábrica são essenciais para gerenciar a criação de objetos em sistemas orientados a objetos. Eles promovem o baixo acoplamento e aumentam a flexibilidade, delegando a responsabilidade de instanciação a classes específicas. Este artigo explora três variações comuns: Fábrica Simples, Método de Fábrica e Fábrica Abstrata.
Fábrica Simples (Simple Factory)
O padrão Fábrica Simples envolve uma classe central (a fábrica) responsável por criar diferentes tipos de objetos com base em um parâmetro de entrada. Embora não seja um padrão GoF formal, é amplamente utilizado e serve como base para padrões mais complexos.
Vantagens:
- Desacoplamento de cliente-produto: O cliente interage apenas com a fábrica, sem precisar conhecer as classes concretas dos produtos. Isso reduz o acoplamento direto entre o cliente e os produtos específicos.
- Encapsulamento da lógica de criação: Toda a lógica de criação (condicionais, configurações, etc.) é centralizada na fábrica, tornando o código mais organizado e fácil de manter.
- Reutilização de código: A classe de fábrica pode ser reutilizada em diferentes partes de uma aplicação ou em diferentes plataformas.
Desvantagens:
- Violação do Princípio Aberto/Fechado (OCP): Ao adicionar um novo tipo de produto, a classe de fábrica existente precisa ser modificada (geralmente adicionando um novo
caseem uma instruçãoswitch). Isso viola o OCP, que prega que uma classe deve ser aberta para extensão, mas fechada para modificação. - Alto acoplamento com a fábrica: Embora desacople o cliente dos produtos concretos, ele introduz um forte acoplamento entre o cliente e a própria classe de fábrica.
Considere o exemplo de um sistema de notificações que pode enviar mensagens por e-mail, SMS ou push.
Exemplo de Código (Fábrica Simples):
using System;
// Produtos Abstratos e Concretos
public interface INotificationChannel
{
void Deliver(string recipient, string message);
}
public class EmailNotification : INotificationChannel
{
public void Deliver(string recipient, string message)
{
Console.WriteLine($"[Email] Para: {recipient} - Mensagem: {message}");
}
}
public class SmsNotification : INotificationChannel
{
public void Deliver(string recipient, string message)
{
Console.WriteLine($"[SMS] Para: {recipient} - Mensagem: {message}");
}
}
public class PushNotification : INotificationChannel
{
public void Deliver(string recipient, string message)
{
Console.WriteLine($"[Push] Para: {recipient} - Mensagem: {message}");
}
}
// A Fábrica Simples
public static class NotificationFactory
{
public static INotificationChannel CreateNotifier(string type)
{
return type.ToLower() switch
{
"email" => new EmailNotification(),
"sms" => new SmsNotification(),
"push" => new PushNotification(),
_ => throw new ArgumentException("Tipo de notificação desconhecido.")
};
}
}
// Cliente
public class ClientApp
{
public static void Run()
{
Console.WriteLine("--- Usando Fábrica Simples ---");
INotificationChannel emailNotifier = NotificationFactory.CreateNotifier("email");
emailNotifier.Deliver("john.doe@example.com", "Sua encomenda foi enviada!");
INotificationChannel smsNotifier = NotificationFactory.CreateNotifier("sms");
smsNotifier.Deliver("+123456789", "Seu código de verificação é 12345.");
INotificationChannel pushNotifier = NotificationFactory.CreateNotifier("push");
pushNotifier.Deliver("dispositivo_id_xyz", "Nova mensagem na sua caixa de entrada.");
}
}
No exemplo acima, se precisarmos adicionar um novo tipo de notificação, como uma notificação por voz, teríamos que modificar a classe NotificationFactory.
Hierarquia de Produtos e Famílias de Produtos
Antes de mergulharmos nos próximos padrões, é crucial entender dois conceitos relacionados a produtos:
- Hierarquia de Produtos (ou Nível de Produto): Refere-se a um único tipo de produto que possui diversas variações ou implementações. Por exemplo, um produto
Loggerpode ter implementações concretas comoConsoleLogger,FileLoggerouDatabaseLogger. O Método de Fábrica lida com uma única hierarquia de produtos. - Família de Produtos: Designa um conjunto de produtos relacionados que devem funcionar juntos. Por exemplo, um conjunto de interfaces de acesso a dados para diferentes entidades (
IUserDao,IDepartmentDao,IProductDao) pode ter implementações específicas para SQL Server (SqlServerUserDao,SqlServerDepartmentDao) e Oracle (OracleUserDao,OracleDepartmentDao). A Fábrica Abstrata lida com a criação de famílias inteiras de produtos.
Método de Fábrica (Factory Method)
O padrão Método de Fábrica resolve a principal desvantagem da Fábrica Simples ao delegar a criação de objetos para subclasses. Em vez de uma única classe de fábrica com lógica condicional, cada tipo de produto concreto é criado por uma subclasse de fábrica específica.
Vantagens:
- Conformidade com o OCP: Adicionar um novo tipo de produto não exige modificação nas classes existentes. Basta criar uma nova subclasse de fábrica e uma nova classe de produto.
- Desacoplamento: O cliente depende apenas da interface do criador (fábrica abstrata) e da interface do produto abstrato, não das implementações concretas.
- Flexibilidade: Permite que as subclasses decidam quais objetos instanciar, adaptando-se a diferentes contextos sem alterar o código principal.
Desvantagens:
- Complexidade: Introduz mais classes (um criador abstrato, criadores concretos, um produto abstrato, produtos concretos) em comparação com a Fábrica Simples, aumentando a complexidade inicial do projeto.
- Lida com uma única hierarquia: Cada fábrica concreta é projetada para criar um único tipo de produto em sua hierarquia. Se o objetivo é criar famílias de produtos relacionados, o padrão Fábrica Abstrata é mais adequado.
Vamos usar um exemplo de um sistema de log para ilustrar o Método de Fábrica, onde diferentes fábricas criam diferentes tipos de loggers.
Exemplo de Código (Método de Fábrica):
using System;
// Produto Abstrato
public interface IMessageLogger
{
void LogMessage(string text);
}
// Produtos Concretos
public class ConsoleLogger : IMessageLogger
{
public void LogMessage(string text)
{
Console.WriteLine($"[LOG CONSOLE] {text}");
}
}
public class FileLogger : IMessageLogger
{
public void LogMessage(string text)
{
Console.WriteLine($"[LOG ARQUIVO - simulado] {text}");
// Em um cenário real, isso escreveria em um arquivo
}
}
// Criador Abstrato (Fábrica Abstrata)
public abstract class LoggerFactoryBase
{
// O método de fábrica
public abstract IMessageLogger CreateLogger();
// Uma operação que usa o produto criado pelo método de fábrica
public void Operation(string message)
{
IMessageLogger logger = CreateLogger();
logger.LogMessage(message);
}
}
// Criadores Concretos (Fábricas Concretas)
public class ConsoleLoggerFactory : LoggerFactoryBase
{
public override IMessageLogger CreateLogger()
{
return new ConsoleLogger();
}
}
public class FileLoggerFactory : LoggerFactoryBase
{
public override IMessageLogger CreateLogger()
{
return new FileLogger();
}
}
// Cliente
public class FactoryMethodClient
{
public static void Run()
{
Console.WriteLine("\n--- Usando Método de Fábrica ---");
LoggerFactoryBase consoleCreator = new ConsoleLoggerFactory();
consoleCreator.Operation("Esta é uma mensagem de log para o console.");
LoggerFactoryBase fileCreator = new FileLoggerFactory();
fileCreator.Operation("Esta mensagem deve ir para um arquivo.");
}
}
Aqui, LoggerFactoryBase define o CreateLogger(), e as subclasses concretas (ConsoleLoggerFactory, FileLoggerFactory) implementam esse método para retornar suas respectivas instâncias de logger. O cliente interage com a LoggerFactoryBase, sem saber qual logger concreto está sendo usado.
Fábrica Abstrata (Abstract Factory)
O padrão Fábrica Abstrata fornece uma interface para criar famílias de objetos relacionados ou dependentes sem especificar suas classes concretas. Ele é útil quando um sistema deve ser independente de como seus produtos são criados, compostos e representados.
Vantagens:
- Isolamento de classes concretas: O cliente é completamente isolado das classes de produto concretas. Ele interage apenas com as interfaces da fábrica e dos produtos.
- Consistência de produtos: Garante que os produtos de uma família sejam compatíveis entre si, pois todos são criados pela mesma fábrica concreta.
- Fácil troca de famílias de produtos: É fácil trocar uma família inteira de produtos por outra (por exemplo, mudar de um banco de dados SQL Server para Oracle) alterando apenas a fábrica concreta usada.
Desvantagens:
- Complexidade aumentada: É o padrão de fábrica mais complexo, envolvendo muitas interfaces e classes para cada família de produtos e cada tipo de produto dentro da família.
- Dificuldade em estender novas famílias de produtos: Se você precsiar adicionar um novo tipo de produto a uma família existente, a interface da fábrica abstrata e todas as suas implementações concretas precisarão ser modificadas.
Um exemplo clássico é a criação de objetos de acesso a dados (DAO) para diferentes bancos de dados, onde cada banco de dados (família) terá suas próprias implementações de DAOs para diferentes entidades (usuários, departamentos, produtos).
Exemplo de Código (Fábrica Abstrata):
using System;
// Família de Produtos: Interfaces de Acesso a Dados (DAOs)
public interface IUserDataAccess
{
void AddUser(string username);
void GetUser(int id);
}
public interface IDepartmentDataAccess
{
void AddDepartment(string name);
void ListDepartments();
}
// Implementações Concretas para SQL Server
public class SqlServerUserDataAccess : IUserDataAccess
{
public void AddUser(string username) => Console.WriteLine($"[SQL Server DAO] Inserindo usuário: {username}");
public void GetUser(int id) => Console.WriteLine($"[SQL Server DAO] Recuperando usuário com ID: {id}");
}
public class SqlServerDepartmentDataAccess : IDepartmentDataAccess
{
public void AddDepartment(string name) => Console.WriteLine($"[SQL Server DAO] Adicionando departamento: {name}");
public void ListDepartments() => Console.WriteLine("[SQL Server DAO] Listando todos os departamentos.");
}
// Implementações Concretas para Oracle
public class OracleUserDataAccess : IUserDataAccess
{
public void AddUser(string username) => Console.WriteLine($"[Oracle DAO] Inserindo registro de usuário: {username}");
public void GetUser(int id) => Console.WriteLine($"[Oracle DAO] Buscando registro de usuário com ID: {id}");
}
public class OracleDepartmentDataAccess : IDepartmentDataAccess
{
public void AddDepartment(string name) => Console.WriteLine($"[Oracle DAO] Criando registro de departamento: {name}");
public void ListDepartments() => Console.WriteLine("[Oracle DAO] Exibindo todos os registros de departamento.");
}
// Fábrica Abstrata
public interface IDatabaseFactory
{
IUserDataAccess CreateUserDao();
IDepartmentDataAccess CreateDepartmentDao();
}
// Fábricas Concretas para SQL Server e Oracle
public class SqlServerFactory : IDatabaseFactory
{
public IUserDataAccess CreateUserDao() => new SqlServerUserDataAccess();
public IDepartmentDataAccess CreateDepartmentDao() => new SqlServerDepartmentDataAccess();
}
public class OracleFactory : IDatabaseFactory
{
public IUserDataAccess CreateUserDao() => new OracleUserDataAccess();
public IDepartmentDataAccess CreateDepartmentDao() => new OracleDepartmentDataAccess();
}
// Cliente
public class AbstractFactoryClient
{
public static void Run()
{
Console.WriteLine("\n--- Usando Fábrica Abstrata ---");
Console.WriteLine("Conectando-se ao SQL Server:");
IDatabaseFactory sqlDbFactory = new SqlServerFactory();
IUserDataAccess sqlUserDao = sqlDbFactory.CreateUserDao();
sqlUserDao.AddUser("João Silva");
IDepartmentDataAccess sqlDeptDao = sqlDbFactory.CreateDepartmentDao();
sqlDeptDao.ListDepartments();
Console.WriteLine("\nConectando-se ao Oracle:");
IDatabaseFactory oracleDbFactory = new OracleFactory();
IUserDataAccess oracleUserDao = oracleDbFactory.CreateUserDao();
oracleUserDao.AddUser("Maria Souza");
IDepartmentDataAccess oracleDeptDao = oracleDbFactory.CreateDepartmentDao();
oracleDeptDao.AddDepartment("Recursos Humanos");
}
}
Neste exemplo, IDatabaseFactory é a fábrica abstrata que declara métodos para criar uma família de produtos (DAOs de usuário e departamento). SqlServerFactory e OracleFactory são as fábricas concretas que implementam esses métodos para retornar DAOs específicos do SQL Server e Oracle, respectivamente. O cliente pode facilmente alternar entre SQL Server e Oracle instanciando a fábrica concreta apropriada.
Em resumo, o Método de Fábrica é ideal para criar um único tipo de objeto (mas com múltiplas implementações), enquanto a Fábrica Abstrata é usada para criar famílias de objetos relacionados.