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.
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.
<?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 }
<?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 } }
<?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; } }
<?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.
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:
<?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.
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.
<?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 }
<?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; } } } }
<?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.