Padrões de projeto são soluções reutilizáveis para problemas comuns no desenvolvimento de software. Eles representam práticas consolidadas pela comunidade, que ajudam a criar códigos mais organizados, flexíveis e de fácil manutenção. Nesta aula, vamos explorar quatro padrões fundamentais no contexto do PHP: Factory, Strategy, Observer e Repository. Cada um resolve um tipo específico de problema, e conhecê-los é essencial para qualquer desenvolvedor PHP que deseje escrever código profissional e escalável.

Antes de mergulharmos nos padrões, é importante entender que eles não são receitas prontas, mas sim diretrizes que devem ser adaptadas ao contexto do seu projeto. O uso exagerado de padrões pode gerar complexidade desnecessária, portanto, avalie sempre se o padrão realmente resolve um problema que você está enfrentando. Vamos começar!

Factory

O padrão Factory (Fábrica) é um padrão criacional que fornece uma interface para criar objetos em uma superclasse, mas permite que as subclasses alterem o tipo de objeto que será criado. Em termos práticos, em vez de instanciar diretamente uma classe com new, você delega a criação a um método de fábrica. Isso centraliza a lógica de criação, facilitando a manutenção e a extensão do código.

Um exemplo comum é quando temos diferentes tipos de conexão com banco de dados (MySQL, PostgreSQL, SQLite) e queremos selecionar a conexão correta com base em uma configuração. A Factory nos permite encapsular essa decisão em um único lugar, evitando espalhar condicionais pelo código.

Vamos ver um exemplo de Factory simples:

interface DatabaseConnection {
    public function connect(): void;
}

class MySQLConnection implements DatabaseConnection {
    public function connect(): void {
        echo "Conectando ao MySQL\n";
    }
}

class PostgreSQLConnection implements DatabaseConnection {
    public function connect(): void {
        echo "Conectando ao PostgreSQL\n";
    }
}

class DatabaseFactory {
    public static function create(string $type): DatabaseConnection {
        return match ($type) {
            'mysql' => new MySQLConnection(),
            'pgsql' => new PostgreSQLConnection(),
            default => throw new \InvalidArgumentException("Tipo de banco desconhecido: $type"),
        };
    }
}

// Uso
$connection = DatabaseFactory::create('mysql');
$connection->connect();

Neste exemplo, a classe DatabaseFactory centraliza a criação de conexões. Se amanhã precisarmos adicionar um novo tipo de banco, basta adicionar um novo case no match e uma nova classe que implemente a interface. O código cliente não precisa ser alterado, o que está de acordo com o princípio Open/Closed (aberto para extensão, fechado para modificação).

Strategy

O padrão Strategy é um padrão comportamental que permite definir uma família de algoritmos, colocá-los em classes separadas e torná-los intercambiáveis. Ele é útil quando você tem várias formas de realizar uma operação e quer selecionar uma delas em tempo de execução, sem usar condicionais complexas.

Um exemplo clássico é a aplicação de impostos em um e-commerce. Dependendo do país ou do tipo de produto, o cálculo do imposto varia. Em vez de preencher a classe de pedido com if/else, você define uma interface TaxStrategy e implementa cada regra em uma classe separada.

Veja o código:

interface TaxStrategy {
    public function calculate(float $price): float;
}

class BrazilTax implements TaxStrategy {
    public function calculate(float $price): float {
        return $price * 0.2; // 20% de imposto
    }
}

class USATax implements TaxStrategy {
    public function calculate(float $price): float {
        return $price * 0.1; // 10% de imposto
    }
}

class Order {
    private float $total;
    private TaxStrategy $taxStrategy;

    public function __construct(float $total, TaxStrategy $taxStrategy) {
        $this->total = $total;
        $this->taxStrategy = $taxStrategy;
    }

    public function calculateTax(): float {
        return $this->taxStrategy->calculate($this->total);
    }
}

// Uso
$order = new Order(100.0, new BrazilTax());
echo $order->calculateTax(); // 20.0

O padrão Strategy permite que o comportamento da classe Order seja alterado simplesmente trocando a estratégia passada no construtor. Isso torna o código mais testável, pois podemos testar cada estratégia isoladamente, e mais flexível, pois novas regras podem ser adicionadas sem modificar as classes existentes.

Observer

O padrão Observer é um padrão comportamental que define uma dependência de um-para-muitos entre objetos, de forma que quando um objeto muda de estado, todos os seus dependentes são notificados e atualizados automaticamente. É muito utilizado em sistemas de eventos, como notificações, logs ou atualizações de interface.

Em PHP, o SPL (Standard PHP Library) já fornece as interfaces SplSubject e SplObserver, facilitando a implementação. Vamos criar um exemplo onde um pedido, ao ter seu status alterado, notifica observadores (como um serviço de e-mail e um sistema de auditoria).

class Order implements \SplSubject {
    private string $status;
    private \SplObjectStorage $observers;

    public function __construct() {
        $this->observers = new \SplObjectStorage();
        $this->status = 'pending';
    }

    public function attach(\SplObserver $observer): void {
        $this->observers->attach($observer);
    }

    public function detach(\SplObserver $observer): void {
        $this->observers->detach($observer);
    }

    public function notify(): void {
        foreach ($this->observers as $observer) {
            $observer->update($this);
        }
    }

    public function setStatus(string $status): void {
        $this->status = $status;
        $this->notify();
    }

    public function getStatus(): string {
        return $this->status;
    }
}

class EmailNotifier implements \SplObserver {
    public function update(\SplSubject $subject): void {
        echo "Enviando e-mail: pedido com status {$subject->getStatus()}\n";
    }
}

class AuditLogger implements \SplObserver {
    public function update(\SplSubject $subject): void {
        echo "Registrando auditoria: status alterado para {$subject->getStatus()}\n";
    }
}

// Uso
$order = new Order();
$order->attach(new EmailNotifier());
$order->attach(new AuditLogger());

$order->setStatus('paid');
// Saída:
// Enviando e-mail: pedido com status paid
// Registrando auditoria: status alterado para paid

Repare que o objeto Order não precisa conhecer os detalhes de cada observador, apenas que eles implementam a interface SplObserver. Isso desacopla o sujeito dos observadores, permitindo adicionar novos observadores sem modificar o sujeito. Esse padrão é a base de muitos sistemas de eventos em frameworks como o Laravel.

Repository

O padrão Repository é um padrão estrutural que atua como uma camada de mediação entre a lógica de negócio e o acesso a dados. Ele encapsula a lógica de consulta e persistência, fornecendo uma interface uniforme para trabalhar com entidades. Isso isola o restante da aplicação dos detalhes de implementação do banco de dados, tornando o código mais testável e fácil de mudar de persistência (ex.: de MySQL para MongoDB).

Um repositório geralmente oferece métodos como findById, findAll, save e delete. Em vez de espalhar consultas com Query Builder ou Eloquent pelo código, você centraliza tudo no repositório.

Exemplo com PDO:

class User {
    public function __construct(
        public readonly int $id,
        public readonly string $name,
        public readonly string $email
    ) {}
}

interface UserRepository {
    public function findById(int $id): ?User;
    public function findAll(): array;
    public function save(User $user): void;
}

class PDOUserRepository implements UserRepository {
    public function __construct(private \PDO $pdo) {}

    public function findById(int $id): ?User {
        $stmt = $this->pdo->prepare('SELECT * FROM users WHERE id = :id');
        $stmt->execute([':id' => $id]);
        $data = $stmt->fetch(\PDO::FETCH_ASSOC);
        return $data ? new User($data['id'], $data['name'], $data['email']) : null;
    }

    public function findAll(): array {
        $stmt = $this->pdo->query('SELECT * FROM users');
        $users = [];
        foreach ($stmt->fetchAll(\PDO::FETCH_ASSOC) as $row) {
            $users[] = new User($row['id'], $row['name'], $row['email']);
        }
        return $users;
    }

    public function save(User $user): void {
        $stmt = $this->pdo->prepare('INSERT INTO users (name, email) VALUES (:name, :email)');
        $stmt->execute([':name' => $user->name, ':email' => $user->email]);
    }
}

// Uso
$pdo = new \PDO('mysql:host=localhost;dbname=test', 'root', '');
$userRepo = new PDOUserRepository($pdo);
$user = $userRepo->findById(1);

O padrão Repository é amplamente utilizado em frameworks modernos. No Laravel, o Eloquent já oferece uma implementação de repositório, mas você pode criar seus próprios repositórios para abstrair ainda mais a camada de dados. Ele também facilita a implementação de testes, pois você pode criar repositórios falsos (fakes) que não dependem de banco de dados.

Boas Práticas e Observações Finais

Ao aplicar esses padrões, tenha em mente os princípios SOLID, especialmente o da Responsabilidade Única e o Open/Closed. Cada padrão deve ser usado para resolver um problema específico, e não por modismo. Comece com uma implementação simples e evolua conforme a necessidade.

Além disso, é importante manter a consistência na nomenclatura e na estrutura. Padrões de projeto são uma linguagem comum entre desenvolvedores; usá-los corretamente facilita a comunicação e a manutenção do código.

Por fim, estude os padrões em conjunto com os recursos da linguagem. O PHP moderno oferece muitas facilidades, como arrow functions e match, que podem simplificar a implementação de alguns padrões.

Referências

Exercícios

  1. Crie uma classe ShapeFactory que retorne objetos de formas geométricas (Círculo, Quadrado, Triângulo) com base em uma string. Cada forma deve ter um método area().
  2. ✓ Resposta:
    interface Shape { public function area(): float; }
    class Circle implements Shape {
        public function __construct(private float $radius) {}
        public function area(): float { return pi() * $this->radius ** 2; }
    }
    class Square implements Shape {
        public function __construct(private float $side) {}
        public function area(): float { return $this->side ** 2; }
    }
    class Triangle implements Shape {
        public function __construct(private float $base, private float $height) {}
        public function area(): float { return ($this->base * $this->height) / 2; }
    }
    class ShapeFactory {
        public static function create(string $type, array $params): Shape {
            return match ($type) {
                'circle' => new Circle($params['radius']),
                'square' => new Square($params['side']),
                'triangle' => new Triangle($params['base'], $params['height']),
                default => throw new \InvalidArgumentException("Forma desconhecida: $type"),
            };
        }
    }
    
  3. Implemente o padrão Strategy para um sistema de pagamento que aceita Pix, Cartão de Crédito e Boleto. Cada estratégia deve ter um método pay(float $amount).
  4. ✓ Resposta:
    interface PaymentStrategy {
        public function pay(float $amount): void;
    }
    class PixPayment implements PaymentStrategy {
        public function pay(float $amount): void { echo "Pagando R$ {$amount} via Pix\n"; }
    }
    class CreditCardPayment implements PaymentStrategy {
        public function pay(float $amount): void { echo "Pagando R$ {$amount} via Cartão de Crédito\n"; }
    }
    class BoletoPayment implements PaymentStrategy {
        public function pay(float $amount): void { echo "Pagando R$ {$amount} via Boleto\n"; }
    }
    class PaymentContext {
        public function __construct(private PaymentStrategy $strategy) {}
        public function setStrategy(PaymentStrategy $strategy): void { $this->strategy = $strategy; }
        public function pay(float $amount): void { $this->strategy->pay($amount); }
    }
    
  5. Usando as interfaces SPL, crie um sistema onde um Stock notifica observadores quando o estoque de um produto fica baixo.
  6. ✓ Resposta:
    class Stock implements \SplSubject {
        private \SplObjectStorage $observers;
        private int $quantity;
        public function __construct(private string $product) {
            $this->observers = new \SplObjectStorage();
            $this->quantity = 0;
        }
        public function attach(\SplObserver $observer): void { $this->observers->attach($observer); }
        public function detach(\SplObserver $observer): void { $this->observers->detach($observer); }
        public function notify(): void {
            foreach ($this->observers as $observer) { $observer->update($this); }
        }
        public function setQuantity(int $quantity): void {
            $this->quantity = $quantity;
            if ($quantity < 10) { $this->notify(); }
        }
        public function getProduct(): string { return $this->product; }
        public function getQuantity(): int { return $this->quantity; }
    }
    class LowStockAlert implements \SplObserver {
        public function update(\SplSubject $subject): void {
            echo "Alerta: estoque baixo para {$subject->getProduct()} ({$subject->getQuantity()} restantes)\n";
        }
    }
    
  7. Crie um repositório para a entidade Product (id, nome, preço) usando PDO. Implemente os métodos findById, findAll e save.
  8. ✓ Resposta:
    class Product {
        public function __construct(
            public readonly ?int $id,
            public readonly string $name,
            public readonly float $price
        ) {}
    }
    interface ProductRepository {
        public function findById(int $id): ?Product;
        public function findAll(): array;
        public function save(Product $product): void;
    }
    class PDOProductRepository implements ProductRepository {
        public function __construct(private \PDO $pdo) {}
        public function findById(int $id): ?Product {
            $stmt = $this->pdo->prepare('SELECT * FROM products WHERE id = :id');
            $stmt->execute([':id' => $id]);
            $row = $stmt->fetch(\PDO::FETCH_ASSOC);
            return $row ? new Product($row['id'], $row['name'], $row['price']) : null;
        }
        public function findAll(): array {
            $stmt = $this->pdo->query('SELECT * FROM products');
            $products = [];
            foreach ($stmt->fetchAll(\PDO::FETCH_ASSOC) as $row) {
                $products[] = new Product($row['id'], $row['name'], $row['price']);
            }
            return $products;
        }
        public function save(Product $product): void {
            if ($product->id === null) {
                $stmt = $this->pdo->prepare('INSERT INTO products (name, price) VALUES (:name, :price)');
                $stmt->execute([':name' => $product->name, ':price' => $product->price]);
            } else {
                $stmt = $this->pdo->prepare('UPDATE products SET name = :name, price = :price WHERE id = :id');
                $stmt->execute([':name' => $product->name, ':price' => $product->price, ':id' => $product->id]);
            }
        }
    }
    
  9. Refatore o código abaixo para usar um Factory para criar diferentes tipos de notificação (Email, SMS, Push). O código atual usa if/else.
  10. ✓ Resposta:
    interface Notification {
        public function send(string $message): void;
    }
    class EmailNotification implements Notification {
        public function send(string $message): void { echo "Enviando e-mail: $message\n"; }
    }
    class SMSNotification implements Notification {
        public function send(string $message): void { echo "Enviando SMS: $message\n"; }
    }
    class PushNotification implements Notification {
        public function send(string $message): void { echo "Enviando push: $message\n"; }
    }
    class NotificationFactory {
        public static function create(string $type): Notification {
            return match ($type) {
                'email' => new EmailNotification(),
                'sms' => new SMSNotification(),
                'push' => new PushNotification(),
                default => throw new \InvalidArgumentException("Tipo de notificação desconhecido: $type"),
            };
        }
    }
    // Uso: $notification = NotificationFactory::create('email'); $notification->send('Olá');