Princípios SOLID

Ou, o que entendi sobre eles.

Henrique Vedoveli
12 min readJan 27, 2024

Os princípios SOLID representam um conjunto de diretrizes essenciais para a organização estrutural do código, visando a sua manutenibilidade e flexibilidade. Ao implementar os conceitos do SOLID, os desenvolvedores são orientados a criar sistemas de software robustos e adaptáveis, capazes de lidar eficientemente com mudanças e evoluções no decorrer do tempo.

A ideia fundamental por trás do SOLID é estabelecer uma arquitetura de software de nível médio que seja resiliente às alterações, fácil de ser mantida e capaz de servir como alicerce para componentes reutilizáveis em uma variedade de contextos de aplicação. Esses princípios não apenas promovem a organização estrutural do código, mas também incentivam a adoção de boas práticas de engenharia de software, resultando em sistemas mais robustos e de fácil compreensão.

Os cinco princípios do SOLID são conhecidos pelas iniciais de seus nomes: SRP (Princípio da Responsabilidade Única), OCP (Princípio do Aberto/Fechado), LSP (Princípio da Substituição de Liskov), ISP (Princípio da Segregação de Interfaces) e DIP (Princípio da Inversão de Dependência). Cada um desses princípios desempenha um papel crucial na promoção de uma arquitetura de software sólida e bem projetada, permitindo que os sistemas evoluam de maneira consistente e sustentável ao longo do tempo.

Vamos agora explorar com mais profundidade cada um desses princípios, compreendendo como podem ser aplicados na prática para criar sistemas de software mais resilientes e adaptáveis.

Princípio da Responsabilidade Única (SRP)

O Princípio da Responsabilidade Única, também conhecido como SRP (Single Responsibility Principle), é um dos pilares fundamentais do SOLID, usa idea é que uma classe deve ter apenas uma razão para mudar, ou seja, deve ter apenas uma responsabilidade bem definida dentro do sistema. Isso significa que uma classe deve se concentrar em realizar uma única tarefa ou funcionalidade específica, evitando a sobrecarga de responsabilidades.

Ao utilizar o SRP é possível criar classes mais coesas e focadas, facilitando a compreensão do código, a manutenção e a reutilização em diferentes partes do sistema. Além disso, ao separar as responsabilidades de forma clara e concisa, torna-se mais simples isolar e corrigir possíveis problemas ou bugs, pois cada classe é responsável por uma parte específica do comportamento do sistema.

A aplicação eficaz do SRP requer uma análise cuidadosa do design do sistema, identificando e separando as responsabilidades de maneira lógica e coesa. Isso pode resultar em um código mais limpo, modular e resiliente, capaz de se adaptar facilmente a mudanças e evoluções no decorrer do tempo.

Resumindo

O Princípio da Responsabilidade Única (SRP) promove a coesão e a modularidade do código, contribuindo para a construção de sistemas mais robustos e de fácil manutenção.

Exemplo de uso do SRP

Vamos usar o código a seguir para exemplificar o princípio SRP.

class Funcionario:
def __init__(self, cargo, salario):
self.cargo = cargo
self.salario = salario

def calcular_pagamento(self):
if self.cargo == 'Gerente':
return self.salario * 1.5
elif self.cargo == 'Desenvolvedor':
return self.salario * 1.2
else:
return self.salario

Neste exemplo, a classe Funcionario tem a responsabilidade de armazenar informações sobre o funcionário e também de calcular o pagamento. Isso viola o SRP, pois a classe está assumindo duas responsabilidades distintas: gerenciamento de dados e lógica de negócios. Agora a seguir segue a versão refatorada com SRP.

class Funcionario:
def __init__(self, cargo, salario):
self.cargo = cargo
self.salario = salario

class CalculadoraDePagamento:
@staticmethod
def calcular_pagamento(funcionario):
if funcionario.cargo == 'Gerente':
return funcionario.salario * 1.5
elif funcionario.cargo == 'Desenvolvedor':
return funcionario.salario * 1.2
else:
return funcionario.salario

Nesta versão refatorada, a classe Funcionario agora tem apenas a responsabilidade de armazenar informações sobre o funcionário. A lógica de cálculo do pagamento foi movida para a classe CalculadoraDePagamento, que agora é responsável por realizar essa operação. Dessa forma, cada classe tem uma única responsabilidade, seguindo o princípio da Responsabilidade Única. Isso torna o código mais modular, coeso e fácil de manter e entender.

Princípio do Aberto/Fechado (OCP)

O Princípio do Aberto/Fechado, ou OCP (Open/Closed Principle), é outro pilar do SOLID que visa promover a extensibilidade e a flexibilidade do código. Este princípio estabelece que as classes e entidades de software devem estar abertas para extensão, mas fechadas para modificação.

Em outras palavras, uma classe deve ser projetada de modo que possa ser estendida para incluir novos comportamentos ou funcionalidades sem a necessidade de alterar o código-fonte original. Isso significa que as classes devem ser facilmente estendidas através de herança, implementação de interfaces ou composição, sem a necessidade de modificar o código existente.

Ao utilziar o OCP, é possível criar sistemas de software mais robustos e escaláveis, uma vez que a adição de novos recursos pode ser feita de forma isolada, sem impactar o funcionamento das partes já existentes do sistema.

A aplicação eficaz do Princípio do Aberto/Fechado requer uma arquitetura de software bem projetada e modular, que permita a extensão de funcionalidades de forma clara e coesa. Isso pode ser alcançado através do uso de padrões de design, como o padrão Strategy, padrão Factory ou padrão Observer, que permitem encapsular comportamentos variáveis e estender o sistema de forma flexível.

O Princípio do Aberto/Fechado promove a extensibilidade e a flexibilidade do código, permitindo que os sistemas de software evoluam de forma consistente e sustentável ao longo do tempo. Essa prática possibilita a adição de novos recursos de maneira isolada, sem interferir no funcionamento das partes já existentes do sistema. Como resultado, a base de código se torna mais modular, reutilizável e de fácil manutenção, o que impulsiona a eficiência e a agilidade no desenvolvimento de software.

Resumindo

O Princípio do Aberto/Fechado (OCP) estabelece que as entidades de software devem ser abertas para extensão, mas fechadas para modificação.

Exemplo de uso do OCP

Vamos considerar um exemplo de um sistema de processamento de pagamentos com diferentes métodos de pagamento, onde inicialmente os métodos de pagamento são tratados diretamente na classe principal.

class Pagamento:
def __init__(self, valor):
self.valor = valor

def processar_pagamento(self, metodo):
if metodo == 'cartao_credito':
self.processar_pagamento_cartao_credito()
elif metodo == 'paypal':
self.processar_pagamento_paypal()
elif metodo == 'transferencia':
self.processar_pagamento_transferencia()

def processar_pagamento_cartao_credito(self):
print(f"Processando pagamento de R${self.valor} via cartão de crédito.")

def processar_pagamento_paypal(self):
print(f"Processando pagamento de R${self.valor} via PayPal.")

def processar_pagamento_transferencia(self):
print(f"Processando pagamento de R${self.valor} via transferência bancária.")transferência bancária.")

Neste exemplo, a classe Pagamento é responsável por processar pagamentos de diferentes métodos diretamente, o que viola o princípio do OCP, pois qualquer adição ou modificação de métodos de pagamento exigiria a alteração direta desta classe.

class Pagamento:
def __init__(self, valor):
self.valor = valor

def processar_pagamento(self, metodo):
metodo.processar_pagamento(self.valor)

class MetodoPagamento:
def processar_pagamento(self, valor):
pass

class CartaoCredito(MetodoPagamento):
def processar_pagamento(self, valor):
print(f"Processando pagamento de R${valor} via cartão de crédito.")

class PayPal(MetodoPagamento):
def processar_pagamento(self, valor):
print(f"Processando pagamento de R${valor} via PayPal.")

class TransferenciaBancaria(MetodoPagamento):
def processar_pagamento(self, valor):
print(f"Processando pagamento de R${valor} via transferência bancária.")

Nesta versão refatorada, introduzimos uma hierarquia de classes onde a classe Pagamento depende apenas da abstração MetodoPagamento. Cada método de pagamento é representado por uma classe que herda de MetodoPagamento e implementa o método processar_pagamento(). Dessa forma, se novos métodos de pagamento precisarem ser adicionados, basta criar uma nova classe que herde de MetodoPagamento e implemente o método processar_pagamento(), sem a necessidade de modificar o código existente na classe Pagamento. Isso torna o sistema mais flexível, extensível e aderente ao princípio do OCP.

Princípio da Substituição de Liskov (LSP)

O Princípio da Substituição de Liskov, ou LSP (Liskov Substitution Principle), se concentra na relação entre classes e suas subclasses. Formulado por Barbara Liskov em 1987, este princípio estabelece que os objetos de uma classe derivada devem ser capazes de substituir objetos de sua classe base sem interromper o funcionamento do programa.

Em outras palavras, uma classe derivada deve ser substituível por sua classe base sem afetar a corretude do programa. Isso significa que as subclasses devem manter o mesmo comportamento que suas classes base, bem como respeitar todas as pré-condições, pós-condições e invariáveis estabelecidas pela classe base.

A aplicação eficaz do Princípio da Substituição de Liskov requer uma compreensão sólida da hierarquia de classes e uma cuidadosa consideração das relações de herança. Os desenvolvedores devem garantir que as subclasses estendam a funcionalidade das classes base de maneira consistente e sem introduzir comportamentos inesperados.

Resumindo

O Princípio da Substituição de Liskov (LSP) enfatiza a importância da consistência e coerência na hierarquia de classes, promovendo a interoperabilidade e facilitando a manutenção do código ao longo do tempo.

Exemplo de uso do LSP

Vamos usar o princípio LSP nesse código a seguir.

class Retangulo:
def __init__(self, altura, largura):
self.altura = altura
self.largura = largura

def set_altura(self, altura):
self.altura = altura

def set_largura(self, largura):
self.largura = largura

def area(self):
return self.altura * self.largura

class Quadrado(Retangulo):
def __init__(self, lado):
self.altura = lado
self.largura = lado

def set_altura(self, altura):
self.altura = altura
self.largura = altura

def set_largura(self, largura):
self.largura = largura
self.altura = largura

Neste exemplo, temos uma classe Retangulo com métodos para definir altura e largura, bem como calcular a área. A classe Quadrado herda de Retangulo e redefine os métodos de definição de altura e largura para garantir que sempre sejam iguais, assumindo assim que um quadrado é apenas um tipo especial de retângulo. A seguir temos a versão refatorada usando o princípio LSP.

class Forma:
def area(self):
pass

class Retangulo(Forma):
def __init__(self, altura, largura):
self.altura = altura
self.largura = largura

def set_altura(self, altura):
self.altura = altura

def set_largura(self, largura):
self.largura = largura

def area(self):
return self.altura * self.largura

class Quadrado(Forma):
def __init__(self, lado):
self.lado = lado

def set_lado(self, lado):
self.lado = lado

def area(self):
return self.lado ** 2

Nesta versão refatorada, temos uma classe abstrata Forma que define o método area(). As classes Retangulo e Quadrado herdam de Forma e implementam o método area() de acordo com a geometria específica de cada forma. Agora, a relação de herança entre Quadrado e Retangulo respeita o LSP, pois um quadrado não modifica o comportamento esperado de um retângulo. Isso garante que objetos de Quadrado possam ser substituídos por objetos de Retangulo sem introduzir comportamentos inesperados, conforme exigido pelo princípio de Liskov.

Princípio da Segregação de Interfaces (ISP)

O Princípio da Segregação de Interfaces (ISP) visa orientar a criação de sistemas de software mais flexíveis e modulares. Este princípio enfatiza a importância de dividir interfaces grandes e coesas em interfaces menores e mais específicas, cada uma direcionada às necessidades específicas dos clientes que a utilizam.

A ideia central por trás do ISP é garantir que as interfaces sejam projetadas de maneira apropriada para cada cliente que as consome. Isso implica que os clientes não devem ser forçados a depender de métodos que não utilizam. Em outras palavras, as interfaces devem oferecer apenas os métodos relevantes para cada cliente, evitando assim dependências desnecessárias e acoplamento excessivo.

Quando interfaces grandes são segregadas em interfaces menores e mais específicas, isso ajuda a evitar a poluição da interface com métodos que não são relevantes para todos os clientes. Dessa forma, cada cliente pode depender apenas das partes da interface que são pertinentes ao seu contexto de uso, resultando em um sistema mais coeso e modular.

A aplicação eficaz do ISP requer uma análise cuidadosa das necessidades dos clientes e uma divisão adequada das interfaces em unidades coesas e coesas. Isso pode envolver a criação de interfaces mais especializadas e a implementação de múltiplas interfaces por classe, conforme necessário, para atender às diferentes necessidades dos clientes.

De modo que o ISP promove a criação de interfaces coesas e específicas, reduzindo o acoplamento e tornando o código mais flexível e fácil de manter. Ao dividir interfaces em unidades mais pequenas e específicas, os sistemas de software se tornam mais adaptáveis às mudanças e mais robustos em face de novos requisitos e evoluções no ambiente de desenvolvimento.

Resumindo

O Princípio da Segregação de Interfaces (ISP) propõe que interfaces grandes e genéricas devem ser divididas em interfaces menores e mais específicas, direcionadas às necessidades dos clientes individuais. Isso promove um baixo acoplamento entre componentes do sistema, evitando que os clientes dependam de funcionalidades que não utilizam.

Exemplo de uso do ISP

A seguir temos um exemplo de código para ser utilizado o princípio ISP.

class Animal:
def comer(self):
pass

def voar(self):
pass

def nadar(self):
pass

class Pato(Animal):
def comer(self):
print("O pato está comendo.")

def voar(self):
print("O pato está voando.")

def nadar(self):
print("O pato está nadando.")

class Cachorro(Animal):
def comer(self):
print("O cachorro está comendo.")

def voar(self):
print("O cachorro não pode voar.")

def nadar(self):
print("O cachorro está nadando.")

Neste exemplo, temos uma classe Animal que define métodos para comer, voar e nadar. As classes Pato e Cachorro herdam de Animal e implementam esses métodos. No entanto, como nem todos os animais podem voar, isso viola o ISP, pois a interface Animal está sendo usada de forma genérica para todos os tipos de animais, incluindo aqueles que não voam.

class Animal:
def comer(self):
pass

class Ave:
def voar(self):
pass

class Nadador:
def nadar(self):
pass

class Pato(Animal, Ave, Nadador):
def comer(self):
print("O pato está comendo.")

def voar(self):
print("O pato está voando.")

def nadar(self):
print("O pato está nadando.")

class Cachorro(Animal, Nadador):
def comer(self):
print("O cachorro está comendo.")

def nadar(self):
print("O cachorro está nadando.")

Nesta versão refatorada, dividimos a interface Animal em interfaces mais específicas: Ave e Nadador. As classes Pato e Cachorro agora implementam apenas as interfaces relevantes para elas. Isso segue o ISP, garantindo que cada classe dependa apenas das interfaces que são pertinentes ao seu contexto de uso, evitando assim dependências desnecessárias e acoplamento excessivo.

Princípio da Inversão de Dependência (DIP)

O Princípio da Inversão de Dependência, conhecido como DIP (Dependency Inversion Principle), é um dos pilares do SOLID que visa promover a flexibilidade e a manutenibilidade do código. Este princípio estabelece que módulos de alto nível não devem depender de módulos de baixo nível, mas sim de abstrações. Além disso, ele afirma que detalhes de implementação devem depender de abstrações, não o contrário.

O DIP sugere que as classes de alto nível devem depender de abstrações, enquanto as classes de baixo nível devem implementar essas abstrações. Isso promove um acoplamento mais fraco entre os componentes do sistema, facilitando a modificação e a extensão do código sem afetar outras partes do sistema.

A aplicação eficaz do DIP envolve a criação de interfaces e abstrações que definem os contratos entre os módulos do sistema. Essas abstrações permitem que os módulos de alto nível interajam com os módulos de baixo nível sem conhecer detalhes de implementação específicos. Em vez disso, eles dependem apenas das interfaces, o que torna o sistema mais flexível e adaptável a mudanças.

Além disso, o DIP promove a reutilização de código e o encapsulamento de detalhes de implementação, tornando o código mais modular e coeso. Ao seguir este princípio, os desenvolvedores podem escrever sistemas de software mais flexíveis, que são mais fáceis de entender, manter e estender ao longo do tempo.

Resumindo

O Princípio da Inversão de Dependência promove a criação de sistemas de software com acoplamento fraco entre os componentes, facilitando a modificação e a extensão do código sem afetar outras partes do sistema. Ao depender de abstrações em vez de implementações concretas, os sistemas se tornam mais flexíveis, reutilizáveis e adaptáveis a mudanças.

Exemplo de uso do DIP

class Lampada:
def ligar(self):
print("Lâmpada acesa.")

def desligar(self):
print("Lâmpada apagada.")

class Interruptor:
def __init__(self):
self.lampada = Lampada()

def pressionar(self):
if self.lampada.estado == 'ligado':
self.lampada.desligar()
else:
self.lampada.ligar()

Neste exemplo, a classe Interruptor depende diretamente da classe Lampada. Isso viola o Princípio da Inversão de Dependência, porque Interruptor está fortemente acoplado a uma implementação específica de Lampada. A seguir está uma versão refatorada utilizando o princípio DIP.

class DispositivoComutavel:
def ligar(self):
pass

def desligar(self):
pass

class Lampada(DispositivoComutavel):
def ligar(self):
print("Lâmpada acesa.")

def desligar(self):
print("Lâmpada apagada.")

class Interruptor:
def __init__(self, dispositivo):
self.dispositivo = dispositivo

def pressionar(self):
if self.dispositivo.estado == 'ligado':
self.dispositivo.desligar()
else:
self.dispositivo.ligar()

Nesta versão refatorada, introduzimos uma interface DispositivoComutavel que define os métodos ligar() e desligar(). A classe Lampada implementa essa interface. A classe Interruptor agora depende de DispositivoComutavel em vez de Lampada, seguindo assim o DIP. Isso permite que Interruptor seja associado a qualquer dispositivo que implemente a interface DispositivoComutavel, tornando-o mais flexível e desacoplado de implementações específicas.

--

--

Henrique Vedoveli

Maters Student in Computer Science and Machine Learning Engineer🦜