L - Princípio de Substituição de Liskov

L - Princípio de Substituição de Liskov

Em inglês, Liskov Substitution Principle.

Um pouco de história

Esse princípio vem do notável trabalho de Bárbara Liskov, cientista da computação, que foi pioneira em diversas contribuições para a ciência da computação e computação distribuída.

Barbara foi reconhecida em 2008 com o Turing Award, a mais alta distinção em ciência da computação, dada à ela “por contribuições para fundamentos práticos e teóricos de linguagem de programação e design de sistemas, especialmente relacionados à abstração de dados, tolerância a falhas e computação distribuída”. traduzido de A. M. Turing Award.

No vídeo abaixo, Bárbara fala para o A. M. Turing Award sobre a origem e o nome desse princípio:

O princípio

Basicamente, o Princípio de Substituição de Liskov fala sobre tipagem comportamental. Ou seja, quando temos uma classe principal e uma sub-classe que extende da principal (herança), a sub-classe deve se comportar da mesma forma que a classe principal, podendo substituí-la sem que a execução do sistema seja comprometida.

Comportamento

Vejamos o seguinte exemplo, onde esse princípio é quebrado:

open class ValidadorDeTexto {
    open val limiteDeCaracteres = 5

    fun ehValido(texto: String): Boolean {
        return texto.length < limiteDeCaracteres
    }
}

fun main() {
    val validador: ValidadorDeTexto = ValidadorDeTexto()

    val nome = "Carlos"

    if (validador.ehValido(nome)) {
        println("Nome valido")
    } else {
        println("Nome invalido")
    }
}
Nome invalido

No código acima, temos a classe ValidadorDeTexto, que tem por objetivo validar o tamanho do texto através da função ehValido(texto: String): Boolean , que retorna True se o texto enviado tiver tamanho menor do que o definido na variável limiteDeCaracteres. Essa classe é uma open class, o que significa que podemos utilizá-la diretamente ou criar novas classes que estendem dela.

Nesse primeiro exemplo, fizemos uso da classe ValidadorDeTexto diretamente passando o texto Carlos para ser validado. O resultado é Nome invalido, já que o texto possui 6 caracteres e ultrapassa o limite de 5 caracteres definido pela variável limiteDeCaracteres.

Agora, imagine que temos um cenário onde precisamos criar um validador específico para nomes. Esse novo validador deve permitir nomes de até 10 caracteres.

Já que a classe ValidadorDeTexto é uma open class, podemos criar o novo validador herdando dela e aproveitar a implementação da função já existente. Ou seja, a classe ValidadorDeNome, que iremos criar, será uma sub-classe de ValidadorDeTexto.

Vejamos abaixo como ficaria:

open class ValidadorDeTexto {
    open val limiteDeCaracteres = 5

    fun ehValido(texto: String): Boolean {
        return texto.length < limiteDeCaracteres
    }
}

class ValidadorDeNome : ValidadorDeTexto() {
    override val limiteDeCaracteres = 10
}

fun main() {
    val validador = ValidadorDeNome()

    val nome = "Carlos"

    if (validador.ehValido(nome)) {
        println("Nome valido")
    } else {
        println("Nome invalido")
    }
}
Nome valido

Do ponto de vista de código, essa é uma solução válida e não gera problemas de compilação ou execução. Porém, o Princípio de Substituição de Liskov é quebrado pois, quando substituímos a classe ValidadorDeTexto pela sua sub-classe ValidadorDeNome, o comportamento do sistema é alterado. Ou seja, o mesmo texto que antes era inválido, passou a ser válido após a mudança.

Esse efeito é perigoso pelo fato de que a mudança na validação não é explícita e só é visível analisando a implementação, o que não pode ser aceito. Se esses validadores estivessem em uma biblioteca externa, é provável que não seja possível ter acesso à implementação. Assim, não seria possível avaliar o comportamento sem que o sistema seja executado ou através de testes unitários.

Exceptions

Esse princípio não trata apenas de comportamentos e retornos. É importante tomar cuidado também com o lançamento de exceções pelas sub-classes, onde novas exceções não podem ser lançadas pelas funções das sub-classes, a não ser que essas exceções sejam subtipos das exceções já lançadas pelas funções da classe principal.

Referências

  • Arquitetura Limpa: O Guia do Artesão para Estrutura e Design de Software. Martin, Robert C ; traduzido por Samantha Batista. Rio de Janeiro. Alta Books, 2018.