Princípio de Substituição de Liskov

Princípio de Substituição de Liskov (Liskov Substitution Principle)

O Princípio de Substituição de Liskov está diretamente relacionado ao conceito de herança.

Quando atribuimos uma instância de uma classe a uma variáveis temos que criar um objeto. Isto significa que variáveis só contêm classes concretas - não abstratas.

Contudo, por causa, do polimosfismo e herança essa instancia pode herdar uma classe abstrata ou interface, o que torna o seguinte código válido. Suponhamos uma classe concreta chamada ClasseConcerta que herda de uma classe abstrata chamada ClasseAbstrata. Pelas regras de polimorfismo e herança podemos escrever:

ClasseConcerta a = new ClasseConcerta()
// ou
ClasseAbstrata b = new ClasseConcerta()

A variávels b é polimórfica, pois o seu tipo é um, mas a sua implementação é outra.

O Princípio de Substituição de Liskov dita que não apenas a classe concreta deve herdar de classe abstrata "mecanicamente", mas também compostalmente. Ou seja, se a classe abstrata define um método deve ser comportar de certa forma, a classe filha tem que seguir esse comportamento. Não seguir o comportamento leva a problemas de consistência e a bugs dificieis de encontrar e resolver.

Formalmente o Princípio de Substituição de Liskov dita que, dados um tipo T e um tipo S que herda de T, então, para qualquer afirmação verdadeira que podemos fazer sobre o comportamento de uma instância de T essa afirmação ainda tem que ser verdadeira caso seja sobre uma instância de S no seu lugar.

Em particular se podemos invocar um método de T sem causar exceções, deve ser possivel invocar o mesmo método em S também sem causar exceções.

Se a classe S seguir o Princípio de Substituição de Liskov então, podemos substitui as instâncias de T por instâncias de S com a garantia que o programa continuará consistente.

O Problema do Círculo e da Elipse

Imagine que precisa definir uma classe 'Círculo' e uma classe 'Elipse'. Qual herdaria de qual?

Por um lado, podemos pensar que todos os Cìculos são Elipses já que matematicamente todo o Círculo É-UMA Elipse. Assim, o Círculo é um subtipo de Elipse. Mas uma elipse é definda por duas distâncias (o eixo-maior e o eixo-menor) e não apenas uma como o círculo (o raio). Então para uma Elipse é possivel ter um método que estica um desses eixos por um fator numérico, tornando uma Elipse de um certo tamanho em outra de outro tamanho, mas ainda uma Elipse. Como Círculo herda de Elipse, teria que ter o mesmo método, mas esticar um Círculo torna-o uma Elipse e não um Círculo. O Círculo não deveria ter este método, pois é contra a sua essência e portanto o Círculo não pode herdar de Elipse, mesmo sendo verdade que matematicamente todo o Círculo É-UMA Elipse, em termos de programação não faz sentido.

Por outro lado, podemos pensar que todas as Elipses são Circulos que foram acrescentados de mais capacidades. Mas aqui temos o mesmo problema, ao revés. Se temos um método para produzir um Círculo com o raio aumentado por um fator, isto não pode ser aplicado para a Elipse, pois não saberiamos qual eixo aumentar.

Repare que o problema existe porque sempre queremos que a classe filha se comporte como a classe mãe. Todas as operações da classe mãe sejam possiveis para a classe filha.

Parece que não ha uma solução. E de fato, à luz do Princípio de Substituição de Liskov, não há.

Entenda que se estivermos preparados para abrir mão do comportamento consistente, então não haveria problema com qualquer umas das opções. Os métodos que não pudessem ser implementados pelo filho simplesmente lançariam exceção. Esta é uma solução sempre possível, mas uma que viola o Princípio de Substituição de Liskov.

Intuitivamente seguimos o Princípio de Substituição de Liskov quando desenhamos uma hirarquia de herança, mas se a herança tiver muitos níveis ou se os métodos não forem bem definidos pode ser que não consigamos cumprir com este princípio. O código ainda funcionará, mas o utilizador terá surpresas desagradáveis em certos cenários de uso.

Variância e Contra-variância

Quando falamos de herança e variáveis, o Princípio de Substituição de Liskov é normalmente fácil de entender e seguir. Contudo, o caso muda de figura quando falamos de tipos genéricos. Tomemos este código de exemplo em Java:

String[] names = ...
Object[] array = names; // tudo certo

List<String> names = ...
List<Object> list = names; // erro

Parece que o comportamento quando usamos array é diferente de quando usamos lista. Qual é a razão? A razão é que em java um array é um objeto covariante e List é um tipo invariante.

Uma variável do tipo List<T> so pode receber um valor do tipo List<T> com o mesmo exato T. Nem subtipos, nem supertipos de T. Como Object é um supertipo de T, não funciona.

Uma variável de array é covariante, o que signfica que uma variável do tipo array T[] pode receber um array de S[] desde que S seja subtipo de T.

Formalmente, dados B genérico, e T e S temos que B é:

  • invariante se B<T> não é subtipo a nenhum outro tipo que não B<T>

  • covariante se B<S> é um subtipo a B<T> se S herda de T. B<S> :> B<T> se S :> T

  • contra-variante se B<T> é um subtipo a B<S> se S herda de T. B<S> <: B<T> se S :> T

Esta relações existem para que o Príncipio de Substituição de Liskov seja preservado e o programa seja consistente.

Variáveis também pode ter variancia. Por exemplo, em métodos, o retorno é covariante e os parametros contravariantes. Ou seja, se um método pai define um método que devolve T uma sobre-escrita do método numa classe filha pode devolver S desde que S seja filho de T. Note que nem todas as linguagens entendem retorno covariante.

A API de Coleções

A API de coleções do Java não segue este principio completamente. Uma interface List, por exemplo, sempre tem métodos para adicionar, substituir e remover elementos, independentente se a impementação é imutável ou não. Para os casos em que a operação não é desejável ou não ha implementação, uma exceção é lançada.

Isto significa que é necessário cuidado quando sibstituimos uma implementação pou outra, já que o seu comportamento não é igual. Podemos tentar invocar um método que não tem implementação e obter uma exceção, violando Principio da Mínima Surpresa.

Esta falta do príncipio de substituição, se deve a uma falha no seguinto do Príncípio de Segregação de Interfaces

Origem Histórica

O Príncipio de Substituição de Liskov foi proposto por Barbara Liskov em 1987 no keynote de abertura de uma conferência sobre Orientação a Objetos.

O trabalho de Barbara Liskov versava sobre as regras que um sistema orientado a objetos deveria seguir e os seus achados são identicos aos publicados por Bertrand Meyer em 1988 no seu livro "Object-Oriented Software Construction" (o mesmo onde o Princípio Aberto-Fechado é definido).

Pela mão de Robert C. Martin este princípio entrou para o top 5 dos Principios de Orientação de Objetos, juntos conhecidos como os "Principios SOLID" (SOLID Principles) num trocadilho de palavras sugerindo que seguir os princípios SOLID, tornaria o código sólido, por oposição a frágil. O Princípio de Sustituição de Liskov corresponde ao L pelo seu nome em inglês: Liskov Substitution Principle.

Scroll to Top