Padrões de projeto em PHP
Nesta aula, você aprenderá os padrões de projeto mais importantes em PHP: Factory, Strategy, Observer e Repository. Cada padrão será explicado com exemplos práticos, mostrando como aplicá-los para melhorar a estrutura, flexibilidade e testabilidade do seu código.
Padrões de projeto são soluções reutilizáveis para problemas comuns no desenvolvimento de software. Eles não são código pronto, mas sim guias de como estruturar classes e objetos para alcançar determinados objetivos, como flexibilidade, baixo acoplamento e facilidade de manutenção. Em PHP, esses padrões ganham ainda mais importância devido à natureza dinâmica da linguagem e ao uso frequente em frameworks como Laravel e Symfony.
Nesta aula, vamos explorar quatro padrões fundamentais: Factory, Strategy, Observer e Repository. Você verá não apenas a teoria, mas também exemplos práticos de implementação em PHP, com código comentado e boas práticas. Ao final, você será capaz de identificar quando cada padrão é útil e como aplicá-los em seus próprios projetos.
Factory
O padrão Factory é um dos padrões criacionais mais utilizados. Ele fornece uma maneira de encapsular a lógica de criação de objetos, permitindo que o código cliente não precise conhecer as classes concretas. Em vez de usar new diretamente, você delega a criação a um método fábrica, que pode retornar diferentes tipos de objetos dependendo de parâmetros ou configurações.
No PHP, o Factory é extremamente útil quando você tem hierarquias de classes ou quando a criação de um objeto envolve lógica complexa (como configuração, injeção de dependências ou validações). Um exemplo comum é uma fábrica de conexões de banco de dados, que retorna diferentes drivers (MySQL, PostgreSQL, SQLite) com base na configuração.
new MySQLConnection(),
'pgsql' => new PostgreSQLConnection(),
default => throw new InvalidArgumentException("Driver não suportado: $driver"),
};
}
}
// Uso
$conn = DatabaseFactory::create('mysql');
$conn->connect();
?>Observe que o código cliente não precisa saber qual classe concreta está sendo instanciada; basta pedir à fábrica o driver desejado. Isso facilita a manutenção, pois se um novo driver for adicionado, apenas a fábrica precisa ser alterada. Além disso, o Factory promove o princípio da responsabilidade única, separando a criação de objetos do uso deles.
Strategy
O padrão Strategy define uma família de algoritmos, encapsula cada um deles em uma classe e os torna intercambiáveis. Ele permite que o algoritmo varie independentemente dos clientes que o utilizam. Em PHP, isso é frequentemente implementado com interfaces e injeção de dependências.
Imagine um sistema de cálculo de frete que precisa suportar diferentes transportadoras (Correios, FedEx, etc.). Em vez de usar condicionais espalhadas pelo código, você pode criar uma interface ShippingStrategy e implementar cada transportadora como uma classe. O contexto (carrinho de compras) recebe a estratégia desejada e delega o cálculo.
strategy = $strategy;
}
public function calculateShipping(float $weight): float {
return $this->strategy->calculate($weight);
}
}
// Uso
$context = new ShippingContext(new CorreiosStrategy());
echo $context->calculateShipping(5); // 17.5
$context->setStrategy(new FedExStrategy());
echo $context->calculateShipping(5); // 25
?>O padrão Strategy elimina a necessidade de múltiplas condicionais e facilita a adição de novos algoritmos sem modificar o contexto. Também promove o princípio do aberto/fechado (aberto para extensão, fechado para modificação). Em aplicações reais, é comum usar Strategy para validações, ordenações, cálculos de impostos e outras operações que podem variar.
Observer
O padrão Observer estabelece uma dependência um-para-muitos entre objetos, de modo que quando um objeto (o sujeito) muda de estado, todos os seus dependentes (observadores) são notificados e atualizados automaticamente. Em PHP, isso é muito usado em eventos, como notificações de usuários, atualizações de cache ou integrações com APIs.
A implementação clássica envolve uma interface Observer com um método update(), e um sujeito que mantém uma lista de observadores e fornece métodos para anexar, desanexar e notificar. Vamos criar um exemplo simples de um sistema de eventos onde um usuário é notificado quando um novo artigo é publicado.
observers[] = $observer;
}
public function detach(Observer $observer): void {
$this->observers = array_filter($this->observers, fn($obs) => $obs !== $observer);
}
public function notify(string $event): void {
foreach ($this->observers as $observer) {
$observer->update($event);
}
}
public function publishNewArticle(string $title): void {
// Lógica para publicar o artigo...
$this->notify("Novo artigo: $title");
}
}
class UserNotifier implements Observer {
public function update(string $event): void {
echo "Notificação para usuário: $event\n";
}
}
// Uso
$blog = new Blog();
$user = new UserNotifier();
$blog->attach($user);
$blog->publishNewArticle("Entendendo Padrões de Projeto");
// Saída: Notificação para usuário: Novo artigo: Entendendo Padrões de Projeto
?>O Observer é essencial para desacoplar o sujeito dos seus observadores. O sujeito não precisa conhecer os detalhes dos observadores, apenas que eles implementam a interface. Isso permite adicionar novos observadores sem alterar o sujeito. Em PHP, o SplSubject e SplObserver do SPL (Standard PHP Library) fornecem uma implementação nativa, mas é comum criar suas próprias interfaces para maior controle.
Repository
O padrão Repository é um mediador entre a camada de domínio e a camada de mapeamento de dados (como um ORM). Ele fornece uma interface para acessar dados, escondendo a complexidade das consultas e do armazenamento. Em PHP, é amplamente utilizado em aplicações com Doctrine ou Eloquent, mas também pode ser implementado manualmente para abstrair o acesso a banco de dados.
O principal benefício do Repository é centralizar a lógica de consulta, permitindo que o restante da aplicação trabalhe com objetos de domínio sem se preocupar com a fonte de dados. Isso facilita a testabilidade, pois você pode substituir um repositório real por um mock em testes unitários.
pdo->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$id]);
$data = $stmt->fetch(PDO::FETCH_ASSOC);
return $data ? new User($data['id'], $data['name'], $data['email']) : null;
}
public function findByEmail(string $email): ?User {
// Implementação similar...
}
public function save(User $user): void {
// Inserir ou atualizar...
}
public function delete(User $user): void {
// Deletar...
}
}
?>Neste exemplo, a interface UserRepository define as operações de acesso a dados, e a implementação PDOUserRepository cuida dos detalhes de SQL. O resto da aplicação pode depender da interface, tornando o código mais flexível e testável. O padrão Repository é especialmente útil em projetos de médio e grande porte, onde a separação de responsabilidades é crucial.
Boas Práticas e Observações Finais
Ao trabalhar com padrões de projeto em PHP, lembre-se de que eles são ferramentas, não regras. Use-os quando houver um problema real a resolver, e não apenas por adotar um padrão. Sempre avalie se a complexidade extra é justificada.
Algumas boas práticas incluem: usar interfaces para definir contratos, favorecer a composição em vez de herança, e manter os padrões em pacotes separados para facilitar a manutenção. Além disso, aproveite o autoloading do Composer para carregar suas classes automaticamente.
Por fim, estude outros padrões além dos vistos aqui (como Singleton, Adapter, Decorator) e pratique com projetos reais. A compreensão profunda dos padrões de projeto é um diferencial para qualquer desenvolvedor PHP.
Referências
- PHP: Interfaces
- PHP: Classes abstratas
- PHP: SPL Interfaces (inclui SplSubject e SplObserver)
- Refactoring Guru - Padrões de Projeto
- Laravel Eloquent (exemplo de Repository)
- Doctrine ORM
- PHP: Padrões de Projeto na documentação oficial
Exercícios
- Exercício 1: Implemente um Factory para criar diferentes tipos de notificações (email, SMS, push). Cada notificação deve ter um método
send(string $message). Use uma interfaceNotificatione uma classeNotificationFactorycom um método estáticocreate(string $type).
new EmailNotification(),
'sms' => new SmsNotification(),
'push' => new PushNotification(),
default => throw new InvalidArgumentException("Tipo de notificação inválido: $type"),
};
}
}
// Teste
$notification = NotificationFactory::create('email');
$notification->send('Olá!');
?>TaxStrategy deve ter o método calculate(float $price): float. Implemente duas estratégias: ICMS (18% sobre o preço) e ISS (5% sobre o preço). Crie uma classe TaxContext que aceita uma estratégia e tem um método calculateTax(float $price).strategy->calculate($price);
}
}
// Teste
$context = new TaxContext(new ICMS());
echo $context->calculateTax(100); // 18
$context = new TaxContext(new ISS());
echo $context->calculateTax(100); // 5
?>Order) notifica observadores quando o status do pedido muda. Crie a interface Observer com o método update(string $event) e a classe Order que mantém uma lista de observadores e tem métodos attach, detach e notify. Crie um observador EmailNotifier que imprime o evento.observers[] = $observer;
}
public function detach(Observer $observer): void {
$this->observers = array_filter($this->observers, fn($obs) => $obs !== $observer);
}
public function notify(string $event): void {
foreach ($this->observers as $observer) {
$observer->update($event);
}
}
public function setStatus(string $status): void {
$this->status = $status;
$this->notify("Status alterado para: $status");
}
}
class EmailNotifier implements Observer {
public function update(string $event): void {
echo "Email enviado: $event\n";
}
}
// Teste
$order = new Order();
$notifier = new EmailNotifier();
$order->attach($notifier);
$order->setStatus('pago');
?>ProductRepository com métodos findById(int $id): ?Product e save(Product $product): void. Implemente uma versão em memória (InMemoryProductRepository) que usa um array para armazenar produtos. Demonstre o uso em um script simples.products[$id] ?? null;
}
public function save(Product $product): void {
$this->products[$product->getId()] = $product;
}
}
// Teste
$repo = new InMemoryProductRepository();
$product = new Product(1, 'Notebook', 2500.0);
$repo->save($product);
$retrieved = $repo->findById(1);
echo $retrieved->getName(); // Notebook
?>