Back-End

2 mai, 2013

Princípios de Projeto Orientado a Objetos – OCP (Open/Closed Principle) – Parte 2

Publicidade

No artigo anterior, falei sobre o primeiro princípio do acrônimo SOLID,SRP – Single Responsibility Principle (Princípio da Responsabilidade Única), onde toda classe ou método deve ter uma e apenas uma responsabilidade e ilustrei com um estudo de caso. Irei apresentar, hoje, outro estudo; mas agora para o princípio OCP  – Open/Closed Principle (Princípio do Aberto e Fechado).

Você pode se perguntar: “Mais que raios é esse tal de OCP? Se está aberto, não está fechado e se está fechado não está aberto, não é?”. Calma, tudo isso pode parecer muito confuso, mais na verdade é mais fácil do que você imagina. Vamos lá!

Definição: OCP – Open/Closed Principle – Princípio do Aberto e Fechado

Classes (módulos, funções, etc) devem ser abertas para extensões, mas fechadas para modificações.

Esse princípio utiliza o famoso conceito de Polimorfismo da Orientação a Objetos, onde a utilização de classes abstratas como referência possam representar vários tipos de classes concretas de forma homogênea, ou seja, sem que seja necessário tratar cada classe concreta de forma diferente.

Toda classe deve ser capaz de ser estendida, mas não pode ser modificada caso já esteja em produção (com exceção para correções de bugs). Vamos criar um cenário, como foi feito no artigo anterior – lembrando que o objetivo é ilustrar de forma objetiva como aplicar o princípio OCP, deixando de lado (nesse momento) padrões de codificação, nomenclatura, etc.

Vamos imaginar que lhe foi solicitado a criação de um componente de envio de notificações via e-mail e SMS e ele será integrado a um sistema de cobrança que outra equipe está desenvolvendo. Eles irão utilizar seu componente para notificar clientes inadimplentes.

Bom, já sabemos o que nosso componente deverá fazer, mas antes de sair codificando, vamos montar a modelagem preliminar.

Obs.: Reforçando que a solução aqui apresentada é apenas para ilustrar como aplicar o principio OCP, a implementação pode ser feita da maneira que você desejar, nos seus padrões, etc, desde que não viole o princípio apresentado.

Modelo Preliminar 1


diagrama1

Pronto, já temos a modelagem do componente de notificações! Como você já aprendeu que com o SRP cada classe deve ter uma e apenas uma responsabilidade, separamos todo o componente em cinco classes: NotificationAbstract, EmailNotification, SMSNotification, SendQueueNotifications e TriggerNotifications. Vou explicar o que cada uma faz e iremos “codar”.

  • NotificationAbstract: Classe abstrata de notificações;
  • EmailNotification: Classe que estende de NotificationAbstract e implementa no método sendEmail() toda lógica para envio do e-mail;
  • SMSNotification: Classe que estende de NotificationAbstract e implementa no método sendSms() toda lógica para envio do SMS;
  • SendQueueNotifications: Classe responsável por enfileirar as notificações para serem disparadas;
  • TriggerNotifications: Classe responsável por disparar as notificações que foram adicionadas em SendQueueNotifications.
Classe NotificationAbstract
<?php

abstract class NotificationAbstract
{
    protected $from;
    protected $to;
    protected $subject; 
	protected $notification;

    public function __construct($from, $to, $subject, $notification)
    {
        $params = array(
            'from'         => $from,
            'to'           => $to,
            'subject'      => $subject,
            'notification' => $notification
        );
        foreach ($params as $label => $param)
        {
            if (is_null($param))
            {
                throw new InvalidArgumentException("O parâmetro '{$label} 'não pode ser nulo!");
            }

            // Seta propriedades
            $method = 'set' . ucfirst($label);
            $this->$method($param);
        }
    }

    // Aqui irão os métodos gets e sets das propriedades
    // from, to, subject e notification
}
Classe EmailNotification e SMSNotification
<?php

class EmailNotification extends NotificationAbstract
{
    public function sendEmail()
    {
        // Código de implementação de envio de E-mail
    }
}

class SMSNotification extends NotificationAbstract
{
    public function sendSms()
    {
        // Código de implementação de envio de SMS
    }
}
Classe SendQueueNotifications
<?php

class SendQueueNotifications
{
    /**
     * Notificações
     * @var NotificationAbstract[] Notificações
     */
    private $notifications = array();

    public function addNotification(NotificationAbstract $notification)
    {
        array_push($this->notifications, $notification);
    }

    public function getCountNotifications()
    {
        return count($this->notifications);
    }

    public function getNotifications()
    {
        return $this->notifications;
    }
}
Classe TriggerNotifications
<?php

class TriggerNotifications
{
    /**
     * Dispara as notificações da fila
     * @param SendQueueNotifications $queue
     * @throws Exception
     */
    public function sendNotifications(SendQueueNotifications $queue)
    {
        if ($queue->getCountNotifications() == 0)
        {
            throw new Exception('Não existem notificações para serem disparadas');
        }

        foreach ($queue->getNotifications() as $notification)
        {
            try
            {
                if ($notification instanceof EmailNotification)
                {
                    $notification->sendEmail();
                }
                elseif ($notification instanceof SMSNotification)
                {
                    $notification->sendSMS();
                }
            }
            catch (Exception $e)
            {
                throw $e;
            }
        }
    }
}

Pronto, terminamos a implementação do modelo que arquitetamos e já podemos disponibilizar nosso componente para a outra equipe implementar. Não vou comentar a implementação feita aqui (por enquanto).

A outra equipe implementou o componente de notificação, tudo está funcionando em perfeita ordem, mas nesse meio tempo um novo executivo (sem noção) assumiu o posto de Diretor Financeiro e uma de suas exigências foi que os clientes inadimplentes sejam notificados pelo Facebook e Twitter além do e-mail e SMS (já pensou se a moda pega?! Ainda bem que não é permitido por lei!).

Bom, você agora sabe que houve uma adição de requisito no componente que você desenvolveu, mais como você utilizou o princípio SRP para arquitetar sua solução, pra implementar esse novo requisito é moleza. Vejamos o projeto preliminar com a adição do requisito.

Modelo Preliminar 2


diagrama2

 

Nas classe FacebookNotification e TwitterNotification, você irá implementar os respectivos métodos sendFacebook() e sendTwitter() igual  as classes EmailNotification e SMSNotification e deverá ainda alterar o método sendNotifications() da classe TriggerNotifications pra que seja possível disparar as notificações das novas notificações que você acabou de adicionar. O código, então, ficaria assim:

Classe TriggerNotifications (1° refatoração)
<?php

class TriggerNotifications
{
    /**
     * Dispara as notificações da fila
     * @param SendQueueNotifications $queue
     * @throws Exception
     */
    public function sendNotifications(SendQueueNotifications $queue)
    {
        if ($queue->getCountNotifications() == 0)
        {
            throw new Exception('Não existem notificações para serem disparadas');
        }

        foreach ($queue->getNotifications() as $notification)
        {
            try
            {
                if ($notification instanceof EmailNotification)
                {
                    $notification->sendEmail();
                }
                elseif ($notification instanceof SMSNotification)
                {
                    $notification->sendSMS();
                }
                elseif ($notification instanceof FacebookNotification)
                {
                    $notification->sendFacebook();
                }
                elseif ($notification instanceof TwitterNotification)
                {
                    $notification->sendTwitter();
                }
            }
            catch (Exception $e)
            {
                throw $e;
            }
        }
    }
}

Pronto, terminamos! Mais você deve estar se perguntando: “E cade o tal principio OCP? Onde ele se aplica?”. Bom, já sabemos que o princípio OCP tem a definição de “Aberta para extensão, mas fechadas para modificações”. Onde você acha que violamos o princípio?

Quem respondeu na classe TriggerNotifications, meus parabéns! Veja esses if/elseif no método sendNotification(), será que não existe uma melhor forma de implementarmos isso? E a melhor forma não é alterar de if/elseif para switch.

Veja que a classe está aberta para extensões. Você pode estender ela e reimplementar o método sendNotification(), por exemplo. Porém, ela NÃO está fechada para modificações, pois dessa forma sempre que precisarmos adicionar um novo modo de notificação, teremos que alterar o método sendNotification() adicionando mais um if/else ou switch.

Vamos remodelar essa solução e aplicarmos finalmente o OCP.

Modelo Preliminar 3


diagrama3

Esse é o modelo final do nosso componente.

Alteramos a classe NotificationAbstract adicionando um método abstrato chamado send(), e nas classes filhas (EmailNotificaton, SMSNotification, FacebookNotification e TwitterNotification) alteramos os métodos send<etc…>  para send() implementando da nossa classe abstrata NotificationAbstract. Agora precisamos refatorar o método sendNotifications()  da classe TriggerNotification, vamos ver as classes refatoradas.

Classe NotificationAbstract (1° refatoração)
<?php

abstract class NotificationAbstract
{
    protected $from;
    protected $to;
    protected $subject;
    protected $notification;

    /**
     * Método abstrato para envio da notificação
     */
    abstract public function send();

    /**
     * Método construtor
     * @param string $from
     * @param string $to
     * @param string $subject
     * @param string $notification
     * @throws InvalidArgumentException
     */
    public function __construct($from, $to, $subject, $notification)
    {
        $params = array(
            'from'         => $from,
            'to'           => $to,
            'subject'      => $subject,
            'notification' => $notification
        );
        foreach ($params as $label => $param)
        {
            if (is_null($param))
            {
                throw new InvalidArgumentException("O parâmetro '{$label} 'não pode ser nulo!");
            }

            // Seta propriedades
            $method = 'set' . ucfirst($label);
            $this->$method($param);
        }
    }

    // Aqui irão os métodos gets e sets das propriedades
    // from, to, subject e notification
}
Classe TriggerNotifications (2° refatoração)
<?php

class TriggerNotifications
{
    /**
     * Dispara as notificações da fila
     * @param SendQueueNotifications $queue
     * @throws Exception
     */
    public function sendNotifications(SendQueueNotifications $queue)
    {
        if ($queue->getCountNotifications() == 0)
        {
            throw new Exception('Não existem notificações para serem disparadas');
        }

        foreach ($queue->getNotifications() as $notification)
        {
            try
            {
                $notification->send();
            }
            catch (Exception $e)
            {
                throw $e;
            }
        }
    }
}
Classes EmailNotificaton, SMSNotification, FacebookNotification e TwitterNotification (1° refatoração)
<?php

class EmailNotification extends NotificationAbstract
{
    public function send()
    {
        // Código de implementação de envio de E-mail
    }
}

class SMSNotification extends NotificationAbstract
{
    public function send()
    {
        // Código de implementação de envio de SMS
    }
}

class FacebookNotification extends NotificationAbstract
{
    public function send()
    {
        // Código de implementação de envio para o Facebook
    }
}

class TwitterNotification extends NotificationAbstract
{
    public function send()
    {
        // Código de implementação de envio para o Twitter
    }
}

Perceberam que a utilização dos dois primeiros Princípios de Projeto SRP e OCP tornou a manutenção do nosso código muito mais flexível tanto para manutenção, quanto para extensão? Conseguiram entender a representação de vários tipos de forma homogênea que citei no inicio do artigo?

Tentei deixar o artigo o mais curto e objetivo possível pra não cansar a leitura, mais espero que tenha ficado claro.

O próximo artigo será sobre LSP – Liskov substitution principle.

Referências