Neste caso, a classe Pedido
tem duas responsabilidades: processar o pedido e gerar a fatura. Isso viola o SRP, porque são duas tarefas distintas.
class Pedido: def __init__(self, detalhes_pedido): self.detalhes_pedido = detalhes_pedido
def processar_pedido(self): # Lógica para processar o pedido pass
class Fatura: def __init__(self, detalhes_pedido): self.detalhes_pedido = detalhes_pedido
def gerar_fatura(self): # Lógica para gerar a fatura pass
Aqui, as responsabilidades são separadas em duas classes distintas, cada uma com uma responsabilidade única. A classe Pedido
lida com o processamento do pedido, enquanto a classe Fatura
lida com a geração da fatura. Isso torna o sistema mais fácil de manter e estender.
Princípio Aberto-Fechado:
O Princípio Aberto-Fechado (OCP - Open-Closed Principle) é outro dos cinco princípios SOLID de design e programação orientada a objetos. Ele afirma que uma classe deve ser aberta para extensão, mas fechada para modificação.
Esse princípio busca garantir que as mudanças no sistema não afetem diretamente o comportamento de classes já existentes, o que facilita a manutenção e a evolução do software sem comprometer o código previamente desenvolvido e testado.
A aplicação do OCP é frequentemente feita através do uso de herança ou interfaces/abstrações, o que permite que classes derivadas ou implementações adicionais possam estender o comportamento de uma classe base sem a necessidade de alterar o código original.
Vamos imaginar um sistema que calcula o pagamento de diferentes tipos de empregados (por exemplo, empregados com salário fixo e empregados com pagamento por comissão).
class CalculadoraPagamento: def calcular_pagamento(self, empregado): if isinstance(empregado, EmpregadoFixo): return empregado.salario elif isinstance(empregado, EmpregadoComissao): return empregado.salario_base + empregado.comissao else: raise ValueError("Tipo de empregado desconhecido")
Neste exemplo, a classe CalculadoraPagamento
tem que ser modificada sempre que um novo tipo de empregado for adicionado, violando o OCP.
from abc import ABC, abstractmethod
class Empregado(ABC): @abstractmethod def calcular_pagamento(self): pass
class EmpregadoFixo(Empregado): def __init__(self, salario): self.salario = salario
def calcular_pagamento(self): return self.salario
class EmpregadoComissao(Empregado): def __init__(self, salario_base, comissao): self.salario_base = salario_base self.comissao = comissao
def calcular_pagamento(self): return self.salario_base + self.comissao
class CalculadoraPagamento: def calcular_pagamento(self, empregado: Empregado): return empregado.calcular_pagamento()
Agora, a classe CalculadoraPagamento
não precisa ser modificada para lidar com novos tipos de empregados. Se quisermos adicionar um novo tipo de empregado (por exemplo, um empregado com pagamento por horas extras), basta criar uma nova classe que implemente a interface Empregado
e definir o método calcular_pagamento
. O código original continua inalterado, respeitando o OCP.
O Princípio Aberto-Fechado ajuda a criar sistemas mais robustos, que são facilmente extensíveis e menos propensos a falhas quando novas funcionalidades são adicionadas. Ele promove a ideia de escrever código que possa ser “extendido” de forma segura, sem modificar a base do código existente.
Princípio da substituição de Liskov:
O Princípio de Substituição de Liskov (LSP - Liskov Substitution Principle) é um dos cinco princípios SOLID de design orientado a objetos. Ele afirma que objetos de uma classe derivada devem ser substituíveis por objetos de sua classe base sem alterar o comportamento correto do programa.
Em outras palavras, o princípio sugere que se uma classe A é uma subclasse de uma classe B, então, no contexto de um programa, pode-se substituir qualquer instância de B por uma instância de A sem que isso cause problemas ou altere o comportamento esperado do sistema.
Quando uma subclasse herda de uma classe base, ela deve garantir que o comportamento de todas as operações que são “herdadas” da classe base continue funcionando de maneira adequada e sem surpresas. Se isso não acontecer, o código que espera um objeto da classe base pode falhar ao utilizar um objeto da subclasse, violando o LSP.
Vamos ilustrar o LSP com um exemplo simples.
Imagine um sistema que tenha uma classe Ave
, que tem um método voar
. A partir de Ave
, você cria a classe Pinguim
, que não pode voar. Se tentarmos aplicar o princípio de substituição de Liskov, podemos ter problemas.
class Ave: def voar(self): print("A ave está voando")
class Pinguim(Ave): def voar(self): raise Exception("Pinguins não podem voar!")
Aqui, a classe Pinguim
herda de Ave
e substitui o método voar
com um comportamento inadequado (lançar uma exceção). Se substituirmos um objeto de tipo Ave
por um objeto de tipo Pinguim
, o código quebraria, já que a operação voar
não pode ser realizada para um pinguim, violando o LSP.
Para aderir ao LSP, é preciso garantir que todas as subclasses possam ser usadas no lugar de suas classes base de maneira segura. A solução seria evitar a herança direta entre Pinguim
e Ave
, já que nem todas as aves podem voar. Em vez disso, podemos criar uma hierarquia que reflita melhor o comportamento de cada tipo de ave.
from abc import ABC, abstractmethod
class Ave(ABC): @abstractmethod def comportamento(self): pass
class AveQueVoa(Ave): def comportamento(self): print("A ave está voando")
class Pinguim(Ave): def comportamento(self): print("O pinguim está nadando")
Agora, tanto a classe AveQueVoa
quanto a classe Pinguim
herdam de Ave
, mas implementam comportamentos distintos sem violar o LSP. Assim, podemos substituir um objeto de Ave
por um objeto de AveQueVoa
ou Pinguim
sem problemas, já que cada classe tem um comportamento bem definido e consistente com a sua natureza.
O Princípio de Substituição de Liskov é fundamental para garantir que o uso de herança seja seguro e não cause problemas inesperados quando substituímos uma classe base por uma classe derivada. Quando o LSP é seguido corretamente, podemos garantir que o código seja mais robusto, reutilizável e menos propenso a falhas.
Princípio da Segregação da Interface:
O Princípio da Segregação de Interface (ISP - Interface Segregation Principle) é um dos princípios SOLID de design orientado a objetos. Ele afirma que uma classe não deve ser forçada a implementar interfaces que ela não usa.
O ISP sugere que, em vez de criar interfaces grandes e genéricas, é melhor criar várias interfaces menores, cada uma com um conjunto de métodos específicos que atendem a uma necessidade bem definida. Isso evita que as classes sejam forçadas a implementar métodos que não fazem sentido para elas, o que torna o código mais coeso e flexível.
Em resumo, a ideia é dividir interfaces grandes e genéricas em interfaces mais específicas e coesas, para que as classes implementem apenas os métodos que realmente necessitam. Isso ajuda a manter o código limpo, fácil de entender e de modificar, e reduz o acoplamento entre os componentes.
Imagine que temos uma interface Animal
que define métodos para vários comportamentos de animais, incluindo voar
, nadar
e andar
. Se você tem um Pinguim
, que não pode voar, ele seria forçado a implementar o método voar
, o que não faz sentido.
class Animal: def voar(self): pass
def nadar(self): pass
def andar(self): pass
class Pinguim(Animal): def voar(self): raise Exception("Pinguins não podem voar!")
def nadar(self): print("Pinguim está nadando!")
def andar(self): print("Pinguim está andando!")
Aqui, a classe Pinguim
é forçada a implementar o método voar
, o que viola o ISP, pois o comportamento de voar não faz sentido para um pinguim.
A solução seria dividir a interface Animal
em várias interfaces menores, para que cada tipo de animal implemente apenas os métodos que são relevantes para ele.
from abc import ABC, abstractmethod
class Nadador(ABC): @abstractmethod def nadar(self): pass
class Andador(ABC): @abstractmethod def andar(self): pass
class Voador(ABC): @abstractmethod def voar(self): pass
class Pinguim(Nadador, Andador): def nadar(self): print("Pinguim está nadando!")
def andar(self): print("Pinguim está andando!")
class Passaro(Voador, Andador): def voar(self): print("Pássaro está voando!")
def andar(self): print("Pássaro está andando!")
Agora, a interface foi dividida em três interfaces menores: Nadador
, Andador
e Voador
. O Pinguim
implementa apenas as interfaces Nadador
e Andador
, sem ser forçado a implementar o método voar
. Já o Pássaro
implementa Voador
e Andador
, que são os comportamentos que fazem sentido para ele.
O Princípio da Segregação de Interface visa evitar a criação de interfaces grandes e complexas, promovendo a criação de interfaces menores e específicas para cada tipo de cliente. Isso resulta em um código mais flexível, reutilizável, fácil de entender e de manter. A adoção desse princípio contribui para a construção de sistemas mais coesos e com menor acoplamento.
Princípio da inversão da dependência:
O Princípio da Inversão de Dependência (DIP - Dependency Inversion Principle) é o último dos cinco princípios SOLID de design e programação orientada a objetos. Esse princípio afirma que:
Em outras palavras, a ideia central do DIP é que, em vez de os módulos de alto nível (que implementam a lógica principal do sistema) dependerem diretamente de módulos de baixo nível (que implementam detalhes específicos ou implementações concretas), ambos devem depender de abstrações, como interfaces ou classes abstratas. Além disso, as abstrações não devem conhecer os detalhes, mas sim depender deles.
Esse princípio busca desacoplar o código, permitindo maior flexibilidade, facilidade de manutenção e maior capacidade de extensão, sem a necessidade de modificar o código existente sempre que novas implementações ou funcionalidades forem necessárias.
Vamos imaginar um cenário onde temos uma classe Pedido
que depende diretamente de uma classe EmailService
para enviar notificações.
class EmailService: def enviar_email(self, destinatario, mensagem): # Código para enviar um e-mail print(f"Enviando e-mail para {destinatario}: {mensagem}")
class Pedido: def __init__(self): self.email_service = EmailService()
def processar(self): # Processar pedido print("Pedido processado!")
Neste exemplo, a classe Pedido
depende diretamente da implementação concreta de EmailService
. Se quisermos alterar a maneira como enviamos os e-mails (por exemplo, usando um serviço de SMS em vez de e-mail), precisaríamos alterar a classe Pedido
, o que violaria o DIP e tornaria o código difícil de manter.
A solução para isso é criar uma abstração, como uma interface, que descreva o comportamento desejado, e fazer com que a classe Pedido
dependa dessa abstração, em vez de uma implementação concreta.
from abc import ABC, abstractmethod
# Abstraçãoclass Notificador(ABC): @abstractmethod def notificar(self, destinatario, mensagem): pass
# Implementação concreta de Emailclass EmailService(Notificador): def notificar(self, destinatario, mensagem): print(f"Enviando e-mail para {destinatario}: {mensagem}")
# Implementação concreta de SMSclass SMSService(Notificador): def notificar(self, destinatario, mensagem): print(f"Enviando SMS para {destinatario}: {mensagem}")
# Classe de alto nívelclass Pedido: def __init__(self, notificador: Notificador): self.notificador = notificador
def processar(self): print("Pedido processado!")
Agora, a classe Pedido
não depende mais diretamente de EmailService
, mas sim da abstração Notificador
. Dessa forma, podemos passar qualquer tipo de notificador (como EmailService
ou SMSService
) para a classe Pedido
sem precisar modificar a classe Pedido
quando mudamos a implementação do serviço de notificação.
PushNotificationService
) sem alterar a classe Pedido
.Pedido
depende de uma abstração e não de uma implementação concreta, fica mais fácil de testar. Podemos mockar ou usar implementações alternativas de Notificador
nos testes sem afetar o comportamento da classe Pedido
.O Princípio da Inversão de Dependência é fundamental para criar sistemas que sejam flexíveis e fáceis de evoluir. Ele nos ensina a depender de abstrações em vez de implementações concretas, o que resulta em código mais desacoplado, modular e de fácil manutenção. Ao seguir o DIP, evitamos que mudanças em detalhes de implementação afetem a lógica de alto nível, tornando o sistema mais robusto e preparado para mudanças futuras.