• Ramon Ferreira Silva

LSP: Princípio da Substituição de Liskov

Atualizado: 22 de Ago de 2019

Substituição de Liskov

O Princípio da Substituição de Liskov é o terceiro princípio, e representa a letra ‘L’ do SOLID.

Esse método mais do que qualquer outro nos diz: Programe para a interface e não para sua implementação!


Teoria


Esse princípio tem esse nome pois foi proposto por Barbara Liskov em um de sues artigos em 1988.


A teoria diz : “Dado um Tipo T, todos os seus subtipos S podem ser usados como seus substitutos sem que haja impactos no sistema.


Ou seja, caso você possua uma classe qualquer, você pode substituir suas chamadas por chamadas de quaisquer classes que herdam dela.


Mas qual a utilidade disso? Isso evita que haja muita desordem nas relações de herança em todo o sistema.


Exemplo Simples de violação da Substituição de Liskov


Dado que temos um classe Funcionário e dessa classe Herdam as classes Vendedor e Gerente, então:

class Funcionario{
    private float salario;
    private String cargo;
    private String nome;
}

class Vendedor extends Funcionario{
    private float comissao;
    
    public float getSalarioVendedor(){
        return salario + comissao;
    }
}

class Gerente extends Funcionario {
    private float bonus;
    
    public float getSalarioGerente(){
        return salario + bonus;
    }
}

Olhando essas classes não parece haver qualquer problema, cada classe possui a sua responsabilidade e não necessidade de alterar a classe cada vez que criamos um novo cargo. Porém, imagine que você possua um módulo que imprima a folha salarial, vejamos como ficaria:

class FolhaSalarial{
    private Date data;
    
    public void imprimirFolhaSalarial(List funcionarios){
        for(Funcionario f : funcionarios){
            
            if(funcionario.getCargo() == "Vendedor"){
                System.io.println(funcionario.getNome() + " ----- " + funcionario.getSalarioVendedor());
            }
            
            if(funcionario.getCargo() == "Gerente"){
                System.io.println(funcionario.getNome() + " ----- " + funcionario.getSalarioGerente());
            }
        }
    } 
}

Perceba que aqui violamos dois princípios de uma só vez: Substituição de Liskov, pois não podemos substituir Funcionário por um subtipo Gerente ou Vendedor, pois cada subtipo possui um método diferente para calcular o salário. Violando o princípio do Aberto/Fechado, dado que cada vez que criarmos um novo cargo, precisamos vir no módulo de folha de pagamento e alterá-lo.


Aplicando o Princípio da Substituição de Liskov


Para resolver o problema, basta que todos os subtipos tenha o mesmo método para calcular salário. Para isso iremos criar um método abstrato getSalario(), e todas os subtipos terão de implementá-los.

abstract class Funcionario {
    private float salario;
    private String cargo;
    private String nome;
    
    abstract float getSalario();
}

class Vendedor extends Funcionario{
    private float comissao;
    
    public float getSalario(){
        return salario + comissao;
    }
}

class Gerente extends Funcionario {
    private float bonus;
    
    public float getSalario(){
        return salario + bonus;
    }
}

class FolhaSalarial{
    private Date data;
    
    public void imprimirFolhaSalarial(List funcionarios){
        for(Funcionario f : funcionarios){
            System.io.println(funcionario.getNome() + " ----- " + funcionario.getSalario());
        }
    } 
}

Agora podemos criar quantos subtipos de funcionários desejarmos e nunca precisaremos nos preocupar com o módulo de Folha Salarial, pois ele conhece a abstração de funcionário e sabe que tipos os subtipos possuem o método getSalario(), independente de como cada um irá implementar.


Violações Sutis do Princípio da Substituição de Liskov


Não violar esse princípio é um pouco difícil e por isso devemos ficar sempre atentos. Existem inúmeras maneiras de violá-lo sem perceber, vou mostrar as duas mais comuns


Quando um Subtipo lança um exceção diferente que o Supertipo não lança.


Vejamos nosso novo módulo de de folha de pagamento:

class FolhaSalarial {
    private Date data;
    
    abstract void imprimirFolhaSalarial(List funcionarios);
}

class FolhaImpressaEmPapel extends FolhaSalaria {
    private Impressora impressora;
    
    public void imprimirFolhaSalarial(List funcionarios) throws ImpressoraOfflineException {
        
        if(this.impressora.isOffline()){
            throw new ImpressoraOfflineException("A impressora esta offline");
        } else {
            for(Funcionario f : funcionarios){
                this.impressora.imprimirLinha(funcionario.getNome() + " ----- " + funcionario.getSalario());
            }
        }
    } 
}

class FolhaImpressaEmTela extends FolhaSalarial{
    public void imprimirFolhaSalarial(List funcionarios){
        for(Funcionario f : funcionarios){
            System.io.println(funcionario.getNome() + " ----- " + funcionario.getSalario());
        }
    } 
}

Nesse exemplo, cada parte se substituirmos o modulo FolhaSalarial pelo seu subtipo FolhaImpressaEmPapel, temos que adicionar um novo tratamento de exceção, ou seja, precisamos alterar o sistema. Caso a exceção ImpressoraOfflineException seja um exceção não verificada, temos um cenário ainda pior, pois o sistema pode quebrar sem que ao menos possamos nos precaver.


Um maneira de não violar o princípio é se os subtipos lançarem exceções que são subtipos das Exceções lançadas pelo supertipo. Vamos ver como fica:

class FolhaSalarial{
    private Date data;
    
    abstract void imprimirFolhaSalarial(List funcionarios) throws IOException;
}

class FolhaImpressaEmPapel extends FolhaSalaria {
    private Impressora impressora;
    
    public void imprimirFolhaSalarial(List funcionarios) throws ImpressoraOfflineException {
        
        if(this.impressora.isOffline()){
            throw new ImpressoraOfflineException("A impressora esta offline");
        } else {
            for(Funcionario f : funcionarios){
                this.impressora.imprimirLinha(funcionario.getNome() + " ----- " + funcionario.getSalario());
            }
        }
    } 
}

class FolhaImpressaEmTela extends FolhaSalarial{
    public void imprimirFolhaSalarial(List funcionarios) throws IOException{
        for(Funcionario f : funcionarios){
            System.io.println(funcionario.getNome() + " ----- " + funcionario.getSalario());
        }
    } 
}

Agora ImpressoraOfflineException sendo um subtipo de IOException não há problema algum, pois já estaremos esperando por esse tipo de Exceção, e trataremos caso ocorra, sem que haja modificações em que utilizar o nosso módulo FolhaSalarial.

Perceba também que FolhaImpressaEmTela nunca lançará nenhuma exceção, também não é um problema, pois isso será transparente para quem utilizar o módulo.


Clássico caso do Quadrado


O exemplo do quadrado é o mais utilizado na literatura, neste exemplo temos duas classes : Retangulo, que é o supertipo e o quadrado que é o subtipo.

class Retangulo {

    private int altura;
    private int largura;

    public void setAltura(int altura){
        this.altura = altura
    }
    
    public void setLargura(int largura){
        this.largura = largura;
    }
    
    public final int getArea(){
        return altura * largura;
    }
}

class Quadrado extends Retangulo{
    public void setAltura(int altura){
        this.altura = altura;
        this.largura = altura;
    }
    
    public void setLargura(int largura){
        this.altura = largura;
        this.largura = largura;
    }
}

Como sabemos, o quadrado é um retângulo que possui altura e largura iguais, sendo assim, quando alteramos a altura, precisamos alterar a largura também, e vice-versa.

Agora quando vamos usar a nossa classe Quadrado, temos:

Retangulo q1 = new Quadrado();
q1.setAltura(5);
q1.setLargura(10);

if(q1.getArea() == 50){
    //comportamento esperado
} else {
    //comportamento inesperado
}

No código acima o comportamento esperado para um Retângulo seria multiplicar a altura = 5 pela largura = 10, resultando na área  50. Mas ao invés disso, temos altura  = largura = 10, sendo assim, 10 multiplicado por 10 que resulta na área 100. É muito fácil comprovar como isso pode ser errado, basta que termos algum teste unitário para a classe retângulo, que ele logo falharia.


Conclusão


O princípio da Substituição de Liskov veio para garantir que não criaremos nenhum design estranho enquanto usamos a herança. A herança é um mecanismo muito poderoso da Orientação a Objetos, mas potencialmente pode criar muita confusão e comportamentos estranhos e difíceis  de serem detectados. Seguindo esse princípio os comportamentos ficam mais previsíveis ao longo da cadeia de  heranças.


#CódigoLimpo #SOLID #PadrõesdeProjeto #BoasPráticas #Liskov #ArquiteturadeSoftware

21 visualizações