PHP e o Princípio da Substituição de Liskov | by Alan Willms | Tableless | Medium
PHP e o Princípio da Substituição de Liskov
Garantindo que instâncias de uma classe possam ser substituídas por instâncias de sub-classes sem quebrar o sistema.
Esse artigo continua a série sobre os cinco princípios do SOLID aplicados ao PHP. Agora veremos o terceiro deles, que é chamado de "Princípio da Substituição de Liskov".
Esse conceito foi criado em 1974 por Barbara Liskov, que o definiu como:
Se para cada objeto O1 do tipo S existe um objeto O2 do tipo T de tal modo que para todos os programas P definidos em termos de T, o comportamento de P não muda quando O2 é substituído por O1, então S é um subtipo de T.
Essa definição é bastante técnica, mas Uncle Bob nos explica de uma forma mais simples:
Funções que usam ponteiros ou referências a classes-base devem ser capazes de usar objetos de classes derivadas sem o saber.
Ou seja, em qualquer lugar onde você usa uma instância de uma classe, você deve poder usar uma instância de uma sub-classe dela sem precisar alterar nada para que isso funcione:
Exemplos de Violações em PHP
Quando você estende o comportamento de outra classe ou implementa uma interface, um método pode quebrar o Princípio da Substituição de Liskov de diversas maneiras.
Vejamos alguns exemplos de violações:
Exigir que outro método seja chamado primeiro
A subclasse pode precisar que você informe alguma dependência ou faça alguma configuração antes que o método possa ser chamado com sucesso. Como o sistema espera que o objeto simplesmente funcione, ele não sabe que precisa configurar dependências antes de utilizá-lo.
O problema nesse exemplo é que a instância de DatabaseLogger só funciona no lugar de Logger se o valor da propriedade database for informada antes do uso.
Lançar uma exceção inesperada
Se o método na classe-mãe não lança uma exceção ou lança exceções somente de outros tipos, o código não saberá como tratá-la, e isso quebrará o sistema.
Neste segundo caso o sistema não espera uma exceção do tipo DbConnectionError, então quebrará sem tratar apropriadamente o erro.
Sobrescrever um método com um corpo vazio
Essa é uma violação mais sutil, quando o método é sobrescrito, mas não faz nada. Se a subclasse não implementa o método, será que ela é mesmo uma especialização da classe-mãe?
Se o comportamento da sub-classe muda ao ponto de um método não executar nada, é provável que a hierarquia precisa ser refatorada para tratar dois casos distintos.
Retornar um valor de tipo diferente da classe mãe
Todos os pontos em que o método é chamado no sistema, espera-se que ele retorne o mesmo tipo de dados. Se retornar um tipo inesperado, o sistema quebra.
O método autorizar() deveria respeitar o tipo de retorno da classe-mãe. A condição no final do código tem o comportamento oposto do esperado apenas porque o tipo de retorno é diferente.
Às vezes a primeira solução à qual recorremos é verificar qual é o tipo do objeto e prepará-lo ou tratá-lo de acordo:
No entanto, essa solução não é a ideal. Ao fazer isso, violamos o segundo conceito do SOLID, o Princípio do Aberto/Fechado, pois dessa forma precisaríamos alterar o código existente para introduzir um novo comportamento.
Como poderíamos resolver esse caso? Existem diversas maneiras, uma delas é fazer com que a classe receba as dependências no construtor e configure o que for necessário, desta forma o método log() sempre se comportará da maneira esperada:
Existe um pouco de polêmica quanto aos construtores violarem ou não o Princípio da Substituição de Liskov, mas perceba como essa solução é vantajosa ao utilizar um serviço de injeção de dependência.
Este princípio é um pouco mais complexo do que os dois primeiros, mas nos ajuda a evitar surpresas desagradáveis com polimorfismo.
Não existe uma solução "bala de prata" para todos os casos, pois muitas vezes é necessário mudar a forma de abstração do problema, e cada caso é diferente. No entanto, talvez essas duas sugestões possam ajudar a resolver o problema:
injetar as dependências e configurar no construtor ao invés de esperar que um setter ou um método seja chamado para preparar o objeto;
esperar interfaces ao invés de classes, assim você pode dividir melhor a sua hierarquia de classes e quem sabe implementar mais de uma interface para satisfazer todos os casos.
No próximo artigo veremos mais sobre isso, ao abordar o Princípio da Segregação de Interfaces.
The Principles of OOD, por Robert Martin.
SOLID Class Design: The Liskov Substitution Principle, por Tom Dalling
Liskov Substitution, Laracasts
Liskov Substitution Principle, Upcase
Imagem de Ryan McGuire
P.S. Por que a foto dos patos? Em algumas linguagens não existem interfaces e você pode sobrescrever métodos com argumentos diferentes do original, então utiliza-se o conceito de duck typing. Veja essa piada.