- Princípio da Responsabilidade Única
- Princípio do Aberto/Fechado
- Princípio de Substituição de Liskov
- Princípio da Inversão de Dependência
- Princípio da Segregação de Interface
Índice
O Princípio de Substituição de Liskov (Liskov Substitution Principle, ou LSP) é o terceiro dos cinco princípios SOLID e é fundamental para garantir a corretude e a robustez de um sistema orientado a objetos. O princípio afirma que:
“Objetos de uma subclasse devem poder substituir objetos da classe base sem alterar as propriedades corretas do programa.“
Em outras palavras, o comportamento de uma subclasse deve ser consistente com o da sua superclasse, garantindo que o sistema funcione corretamente, independentemente de qual classe esteja sendo usada.
Este princípio foi proposto por Barbara Liskov em 1987 e está diretamente relacionado à ideia de herança correta e polimorfismo. Ele nos diz que o código deve ser capaz de manipular objetos da classe base e suas subclasses de forma intercambiável, sem que o funcionamento seja comprometido.
Como Funciona o Princípio de Substituição de Liskov?
De acordo com o LSP, ao criar uma subclasse, ela deve respeitar o comportamento da classe base. Qualquer lugar do código que espera um objeto da classe base deve ser capaz de aceitar um objeto da subclasse sem causar problemas ou comportamentos inesperados.
Para cumprir esse princípio, uma subclasse deve manter as seguintes regras:
- Não Violência das Assunções da Superclasse: A subclasse não deve introduzir comportamentos que violem assunções estabelecidas pela superclasse. Por exemplo, não deve remover funcionalidades ou restringir condições que a superclasse permite.
- Compatibilidade de Contrato: O contrato (interface e comportamento) da superclasse deve ser mantido pela subclasse. Isso significa que as assinaturas dos métodos e os efeitos esperados desses métodos devem ser respeitados.
Problemas de Não Seguir o LSP
Quando o LSP não é seguido, o sistema pode apresentar:
- Comportamento Inesperado: Ao substituir um objeto da classe base por um objeto da subclasse, o sistema pode passar a se comportar de maneira incorreta ou imprevisível.
- Quebra de Contrato: A subclasse pode violar o contrato da superclasse, levando a erros quando a classe for usada em contextos onde se espera o comportamento definido pela superclasse.
- Acoplamento Excessivo: Pode haver necessidade de escrever códigos adicionais ou condicionalmente verificar o tipo de classe para evitar comportamentos indesejados, levando a um aumento no acoplamento e a uma redução na reutilização de código.
Exemplo Prático em Dart
Vamos imaginar um cenário onde temos uma classe base Bird
e duas subclasses: Sparrow
(um pardal) e Penguin
(um pinguim). A classe base tem um método chamado fly()
. No entanto, pinguins não voam, e isso cria uma violação do princípio LSP se não for corretamente modelado.
Classe que Viola o LSP:
class Bird {
void fly() {
print('Voando pelos céus!');
}
}
class Sparrow extends Bird {
// Pardal pode voar
}
class Penguin extends Bird {
void fly() {
// Pinguim não voa
throw Exception('Pinguins não podem voar!');
}
}
Nesse exemplo, a classe Penguin
viola o LSP porque não pode executar o comportamento esperado pela classe base. Se usarmos um Penguin
em um contexto onde se espera que ele voe, isso vai gerar uma exceção.
Classe Refatorada Seguindo o LSP:
Podemos resolver isso reformulando a hierarquia e segregando a funcionalidade de voo em uma interface separada:
abstract class Bird {
void eat() {
print('Comendo...');
}
}
abstract class FlyingBird extends Bird {
void fly();
}
class Sparrow extends FlyingBird {
void fly() {
print('Pardal voando pelos céus!');
}
}
class Penguin extends Bird {
// Pinguins não voam, mas ainda são aves e podem comer
}
Neste exemplo, criamos uma hierarquia mais adequada:
Bird
: Representa qualquer pássaro, com funcionalidades comuns, comoeat()
.FlyingBird
: Interface que define o comportamento de voo (fly()
). Somente pássaros que voam devem implementá-la.Sparrow
: ImplementaFlyingBird
porque pardais podem voar.Penguin
: EstendeBird
, mas não implementaFlyingBird
, porque pinguins não voam.
Com essa refatoração, podemos usar qualquer pássaro (Bird
) em contextos comuns, e apenas pássaros voadores (FlyingBird
) em contextos que exigem voo, respeitando o princípio de substituição de Liskov.
Benefícios do Princípio de Substituição de Liskov
- Código Mais Robusto: Seguir o LSP garante que substituições de classes base por subclasses não causarão comportamentos inesperados, resultando em um sistema mais estável e previsível.
- Facilidade de Manutenção: Classes que seguem o LSP evitam a necessidade de verificações condicionais para garantir o tipo de classe, reduzindo o acoplamento e simplificando o código.
- Reutilização e Flexibilidade: O código que segue o LSP é mais reutilizável, pois não existem restrições implícitas que obrigam uma análise mais detalhada sobre o comportamento das subclasses.
Resumo
O Princípio de Substituição de Liskov assegura que as subclasses possam substituir suas superclasses sem causar falhas ou comportamentos inesperados no sistema. Ele promove um design de herança bem planejado, onde cada subclasse é uma extensão válida e compatível com a classe base. Isso resulta em código mais flexível, seguro e fácil de manter, reduzindo a necessidade de verificações adicionais e garantindo que o polimorfismo seja corretamente aplicado.
Seguir o LSP leva a uma modelagem mais cuidadosa, garantindo que as hierarquias de classes sejam coerentes e que cada classe seja utilizada da maneira para a qual foi projetada. Dessa forma, o desenvolvimento de software se torna mais eficiente, menos propenso a erros e preparado para evoluir de maneira sustentável.