Refatorando código PHP para Strategy Pattern

Quando falamos de Design Patterns, fica muito nebuloso entender o que podemos fazer com eles, pois o que muitos desenvolvedores não compreendem é que os padrões resolvem problemas de código ou de design já identificados ou simplesmente melhoram a manutenibilidade do projeto. Para que isso aconteça primeiro precisamos passar pelo problema, conhecer o que o padrão resolve e enfim aplicar uma refatoração.

Vamos falar neste artigo sobre o Strategy, que é um padrão comportamental, muito usado para quando temos regras de uma determinada atividade que podem conter muita lógica, ele organiza e separa o uso dessas lógicas, padronizando a usabilidade das classes de forma que novas implementações possam ser adicionadas no futuro sem muita mudança no uso de uma determinada classe de ação.

Para ilustrá-lo usaremos a refatoração, veremos o problema e aplicaremos o padrão resolvendo uma problemática específica. Para que fique menos complexo tudo será demonstrado em código e comentários, assim acredito que chegaremos a um entendimento melhor.

Problemática

Temos um sistema de gestão de logística e precisamos comunicar aos cliente quando o produto chegou na distribuidora, quando está a caminho da entrega e por fim o momento em que ele foi recebido pelo cliente.

Para tratar das mensagens que são enviadas aos clientes, foi criada uma classe Message da qual comunica por e-mail os clientes, veja abaixo a representação:

class Message
{
    // ...
    public function send(string $message)
    {
        // implementação de mensagem por email
    }
}

// ...
$message->send('Seu produto está a caminho da entrega');

Com o evoluir da aplicação foi preciso implementar um novo modelo de mensagem, o SMS, pois alguns clientes não possuíam cadastro online, e o sistema só possui cadastro simples como: o nome, telefone e endereço.

Resolvemos então criar duas classes, seguindo o principio de SOLID, single responsibility, a classe EmailMessage e a SMSMessage, pensando em futuras implementações fizemos a classe MessageManager retornar o objeto de acordo com o tipo de mensagem necessária.

class MessageManager
{
    // ...
    public function getSender()
    {
        if ($this->customer->fromOnline() && $this->customer->hasEmail()) {
            return new EmailMessage;
        }
        if ($this->customer->hasEmail() === false) {
            return new SMSMessage;
        }
        // ...
    }
}

// ...
$sender = $messageManager->getSender();
$sender->send('Seu produto está a caminho da entrega');

Mas como nada é perfeito, uma nova forma de comunicação entrou no projeto, os desenvolvedores precisam agora de um novo modo de envio, este agora via push notification, ou seja, a notificação será via aplicativo de celular, da forma atual nos forçou a ter essa implementação:

class MessageManager
{
    // ...
    public function getSender()
    {
        if ($this->customer->fromOnline() && $this->customer->hasEmail()) {
            return new EmailMessage;
        }
        if ($this->customer->hasEmail() === false) {
            return new SMSMessage;
        }
        if ($this->customer->hasCellPhone()) {
            return new PushMessage;
        }
        // ...
    }
}

// ...

Uma solução

Claramente não estamos seguindo corretamente a responsabilidade única, a classe MessageManager está com muitas regras de mensagem que se mesclam entre si, observamos que as regras de cada classe de mensagem deve estar dentro delas mesmas e o manager só deve invocar o objeto pedido no momento, pois ainda que surjam novas formas de mensagens, seria muito doloroso dar manutenção e testar esse método getSender().

Com o Strategy transformamos cada regra em sua classe, que deve ser passada pelo desenvolvedor, elas seguirão uma interface:

interface MessageSenderInterface
{
    // ...
    public function send(string $message) : bool;
    public function isValidSender() : bool;
}

// ...

Essa interface fará o contrato das estratégias para que todas sigam esses métodos, implementaremos uma das classes de mensagens adicionando Strategy no nome original:

class EmailMessageStrategy implements MessageSenderInterface
{
    // ...
    public function send(string $message) : bool
    {
        // Valida de acordo com a regra deste tipo de mensagem
        if ($this->isValidSender()) {
            return $this->sendMessage($message);
        }
        return false;
    }

    public function isValidSender() : bool
    {
        if ($this->customer->fromOnline() && $this->customer->hasEmail()) {
            return true;
        }
        return false;
    }
}
// ...

Isso deve ser feito também para as demais classes de mensagem, essa prática isola as regras/validações nas classes Strategy de mensagens. Agora faremos o MessageStrategy, que invocará o método da classe em que lhe for informado. Abaixo a implementação do padrão:

class MessageStrategy implements MessageSenderInterface
{
    // ...
    public function __construct(MessageSenderInterface $sender)
    {
        // Guardamos o objeto Message com suas regras e implementações
        $this->sender = $sender;
    }

    public function send(string $message) : bool
    {
        // Retorna a validação de acordo com a regra do objeto
        return $this->sender->send($message);
    }

    public function isValidSender() : bool
    {
        return $this->sender->isValidSender();
    }
}

// ...
// Ao criar o objeto MessageStrategy devemos informar qual o tipo de envio
$message = new MessageStrategy(new EmailMessageStrategy);

// O método invoca o send original da classes anteriormente adicionada
$message->send('Seu produto está a caminho da entrega');

Pronto! Agora a implementação é injetada no MessageStrategy podendo haver outras implementações sem precisar alterar a classe MessageStrategy e, o melhor: as regras específicas para cada modelo de mensagem estarão em suas respectivas classes.

Conclusão

A vantagem em usar esse padrão está em facilitar a organização das classes, centralizar a usabilidade e além disso também dar poder ao desenvolvedor de adicionar camadas na estratégia principal que será replicada a todas as outras estratégias que são injetadas.

Espero que tenham gostado do artigo, a ideia foi ilustrar uma forma de implementar o padrão Strategy de forma simples, lembrando que existem várias maneiras de implementá-lo e os exemplos apresentados acima são meramente uma prova de conceitos.

Até a próxima!

Deixe seu comentário