Back-End

16 dez, 2013

Princípios de projeto orientado a objetos – LSP (Liskov substitution principle) – Parte 3

Publicidade

Hoje irei apresentar o 3º princípio do acrônimo SOLID. A definição desse princípio deve ser analisada com calma para seu correto entendimento.

LSP – Liskov substitution principle – Princípio da substituição de Liskov

Se q(x) é uma propriedade demonstrável dos objetos x de tipo T, então q(y) deve ser verdadeiro para objetos y de tipo S, onde S é um subtipo de T.

Uma definição muito utilizada é: “Classes bases devem poder ser substituídas por suas classes derivadas”.

Bem, imagine que uma empresa de dispositivos de reconhecimento de comandos de voz fechou uma parceria com uma fábrica de janelas que já possui integrada a ela uma persiana e que deseja que o dispositivo de comando de voz seja implantado em todas as suas linhas de janelas para abertura e fechamento tanto da janela quanto da persiana, e você foi contratado para projetar um software que interaja com o dispositivo, reconheça o comando de voz e execute a ação atrelada a esse comando, que foi implementada pela fábrica de janelas.

Já temos os requisitos que nossa solução deverá possuir; sendo assim, vamos analisar nosso projeto.

Modelagem
Modelagem UML

Nosso projeto do dispositivo de comando de voz já está modelado. Vamos agora ver sua implementação.

VoiceDeviceInterface

<?php

interface VoiceDeviceInterface
{
    public function recognizeCommand($command);
    public function addCommand($command, VoiceCommand $voiceCommand);
}

VoiceDevice

<?php

class VoiceDevice implements VoiceDeviceInterface
{

    /**
     * @var array VoiceCommand
     */
    protected $commands = array();

    /**
     * Método para reconhecimento do comando de voz
     * @param string $command
     */
    public function recognizeCommand($command)
    {
        $voiceCommand = $this->findCommand($command);
        $voiceCommand->execute();
    }

    /**
     * Adiciona comando de voz
     * @param string $command
     * @param VoiceCommand $voiceCommand
     */
    public function addCommand($command, VoiceCommand $voiceCommand)
    {
        $this->commands[$command] = $voiceCommand;
    }

    /**
     * Procurar por um comando existente
     * @param string $command
     * @return VoiceCommand
     * @throws InvalidArgumentException
     */
    protected function findCommand($command)
    {
        if (isset($this->commands[$command])) {
            return $this->commands[$command];
        } else {
            throw new InvalidArgumentException('Command not found');
        }
    }

}

VoiceCommand

<?php

interface VoiceCommand
{
    public function execute();
}

OpenWindowCommand

<?php

class OpenWindowCommand implements VoiceCommand
{

    /**
     * @var AbstractWindow
     */
    protected $window;

    public function __construct(AbstractWindow $window)
    {
        $this->window = $window;
    }

    public function execute()
    {
        $this->window->openWindow();
    }

}

CloseWindowCommand

<?php

class CloseWindowCommand implements VoiceCommand
{

    /**
     * @var AbstractWindow
     */
    protected $window;

    public function __construct(AbstractWindow $window)
    {
        $this->window = $window;
    }

    public function execute()
    {
        $this->window->closeWindow();
    }

}

OpenWindowShadeCommand

<?php

class OpenWindowShadeCommand implements VoiceCommand
{

    /**
     * @var AbstractWindow
     */
    protected $window;

    public function __construct(AbstractWindow $window)
    {
        $this->window = $window;
    }

    public function execute()
    {
        $this->window->openWindowShade();
    }

}

CloseWindowShadeCommand

<?php

class CloseWindowShadeCommand implements VoiceCommand
{

    /**
     * @var AbstractWindow
     */
    protected $window;

    public function __construct(AbstractWindow $window)
    {
        $this->window = $window;
    }

    public function execute()
    {
        $this->window->closeWindowShade();
    }

}

AbstractWindow

<?php

abstract class AbstractWindow
{

    const STATE_OPEN  = 'open';
    const STATE_CLOSED = 'closed';

    /**
     * @var string
     */
    private $windowState;

    /**
     * @var string
     */
    private $windowShadeState;

    public function __construct()
    {
        $this->windowState      = self::STATE_CLOSED;
        $this->windowShadeState = self::STATE_CLOSED;
    }

    public function openWindow()
    {
        if ( $this->windowIsOpen() === true ) {
            throw new WindowStateException('The window is already open');
        } else {
            $this->windowState = self::STATE_OPEN;
            echo 'Opening window...' . PHP_EOL;
        }
    }

    public function closeWindow()
    {
        if ( $this->windowIsOpen() === false ) {
            throw new WindowStateException('The window is already closed');
        } else {
            $this->windowState = self::STATE_CLOSED;
            echo 'Closing window...' . PHP_EOL;
        }
    }

    public function openWindowShade()
    {
        if ( $this->windowShadeIsOpen() === true ) {
            throw new WindowStateException('The window shade is already open');
        } else {
            $this->windowShadeState = self::STATE_OPEN;
            echo 'Opening window shade...' . PHP_EOL;
        }
    }

    public function closeWindowShade()
    {
        if ( $this->windowShadeIsOpen() === false ) {
            throw new WindowStateException('The window shade is already closed');
        } else {
            $this->windowShadeState = self::STATE_CLOSED;
            echo 'Closing window shade...' . PHP_EOL;
        }
    }

    final protected function windowIsOpen()
    {
        return $this->windowState === self::STATE_OPEN
                    ? true
                    : false;
    }

    final protected function windowShadeIsOpen()
    {
        return $this->windowShadeState === self::STATE_OPEN
                    ? true
                    : false;
    }

}

WindowStateException

<?php

class WindowStateException extends RuntimeException {}

AbstractVoiceDeviceFactory

<?php

abstract class AbstractVoiceDeviceFactory
{
    abstract protected function createVoiceDevice(AbstractWindow $window);

    public function create(AbstractWindow $window)
    {
        return $this->createVoiceDevice($window);
    }
}

VoiceDeviceFactory

<?php

class VoiceDeviceFactory extends AbstractVoiceDeviceFactory
{
    /**
     * Cria um objeto do tipo VoiceDeviceInterface
     * @param \AbstractWindow $window
     * @return \VoiceDeviceInterface
     */
    protected function createVoiceDevice(\AbstractWindow $window)
    {
        $openWindowCommand  = new OpenWindowCommand($window);
        $closeWindowCommand = new CloseWindowCommand($window);
        $openWindowShadeCommand = new OpenWindowShadeCommand($window);
        $closeWindowShadeCommand = new CloseWindowShadeCommand($window);

        $voiceDevice = new VoiceDevice();
        $voiceDevice->addCommand('abrir janela', $openWindowCommand);
        $voiceDevice->addCommand('fechar janela', $closeWindowCommand);
        $voiceDevice->addCommand('abrir persiana', $openWindowShadeCommand);
        $voiceDevice->addCommand('fechar persiana', $closeWindowShadeCommand);

        return $voiceDevice;
    }

}

 

Já temos nosso projeto implementado e agora vamos analisar nosso diagrama UML e o código implementado. Preste bem atenção: estamos seguindo dois princípios de projeto e utilizando dois design patterns. Conseguiu identificar? Estamos seguindo o princípio SRP e OCP e os design patterns Command e FactoryMethod.

A classe VoiceDevice, que implementa a interface VoiceDeviceInterface, possui uma única responsabilidade que é referente ao comando de voz. Veja também que ela está aberta para extensão, mas fechada para alterações. Dessa forma, podemos adicionar novos comandos sem alterar sua estrutura. O objeto VoiceDevice é criado a partir da fábrica de objetos VoiceDeviceFactory, que é uma implementação da classe abstrata AbstractVoiceDeviceFactory para criação de objetos do tipo VoiceDeviceInterface.

As classes OpenWindowCommand, CloseWindowCommand, etc, implementam a interface VoiceCommandInterface,  que por sua vez são responsáveis por encapsular uma solicitação como um objeto, que nesse caso, são objetos do tipo AbstractWindow (classe base para implementação dos diversos tipos de janelas).

A finalidade desse artigo não é abordar design patterns, mas os princípios de projetos são bases fundamentais dos design patterns.

Muito bom, mas e cadê o LSP? Vamos ver a execução desse projeto em um client utilizando a classe BusWindow que é uma subclasse de AbstractWindow.

ClientTest

<?php

try {
    $factory = new VoiceDeviceFactory();
    $voiceDevice = $factory->create(new BusWindow());

    $voiceDevice->recognizeCommand('abrir persiana');  // Opening window shade...
    $voiceDevice->recognizeCommand('abrir janela');    // Opening window...
    $voiceDevice->recognizeCommand('fechar janela');   // Closing window...
    $voiceDevice->recognizeCommand('fechar persiana'); // Closing window shade...
} catch (WindowStateException $we) {
    echo sprintf('Sorry... %s', $we->getMessage()) . PHP_EOL;
} catch (Exception $e) {
    echo sprintf('Ops! %s', $e->getMessage()) . PHP_EOL;
}

Excelente! Funcionou como previsto! Mas vamos supor que esse dispositivo também tenha que ser implementado em janelas para aviões. Como seria essa implementação?

Você pode pensar: “Simples, basta criar uma subclasse de AbstractWindow  chamada AirplaneWindow e pronto!”. Hum… Então vamos ver o código e sua execução em um client agora com AirplaneWindow.

ClientTest2

<?php

try {
    $factory = new VoiceDeviceFactory();
    $voiceDevice = $factory->create(new AirplaneWindow());

    $voiceDevice->recognizeCommand('abrir persiana');  // Opening window shade...
    $voiceDevice->recognizeCommand('abrir janela');    // Ops! # You cannot open the window
    $voiceDevice->recognizeCommand('fechar janela');   // Ops! # You cannot close the window
    $voiceDevice->recognizeCommand('fechar persiana'); // Closing window shade...
} catch (WindowStateException $we) {
    echo sprintf('Sorry... %s', $we->getMessage()) . PHP_EOL;
} catch (Exception $e) {
    echo sprintf('Ops! %s', $e->getMessage()) . PHP_EOL;
}

Implementação de AirplaneWindow

<?php

class AirplaneWindow extends AbstractWindow
{
    public function openWindow()
    {
        throw new RuntimeException('# You cannot open the window');
    }

    public function closeWindow()
    {
        throw new RuntimeException('# You cannot close the window');
    }
}

Veja como se comportou o comando “abrir janela”! Acabamos de violar o princípio LSP, pois a classe derivada AirplaneWindow não pode ser substituída por sua classe base AbstractWindow. Para aviões não é possível utilizar os métodos openWindow() e closeWindow(), gerando assim um comportamento inesperado. Seu projeto deve ser analisado novamente para solucionar esse problema na hierarquia de AbstractWindow.

Bem, eu darei a solução para esse problema no próximo artigo, no qual falarei sobre o princípio ISP – Interface segregation principle – Princípio da segregação de interface.

Referências: