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.

Alan Willms
Tableless

--

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.

Possíveis Soluções

À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.

Conclusão

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.

Referências

* * *

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.

--

--

Alan Willms
Tableless

Software development nerd. In 💙 with Ruby, PHP, JavaScript, Crystal, and other techy stuff.