Modo fácil dos Princípios SOLID
SOLID é um acrônimo para cinco princípios do POO (Programação Orientada a Objetos), que tem como objetivo de facilitar o desenvolvimento de software, tornando mais fácil a manutenção e o entendimento do código.
A Programação Orientada a Objetos pode ser definida por quatro pilares Herança, encapsulamento, abstração e polimorfismo, um dos principais objetivos é facilitar o entendimento e a modelagem de software complexo através da organização do código em unidades lógicas que representam entidades ou conceitos.
A Abstração é o processo de ocultar detalhes complexos e mostrar apenas as características essenciais de um objeto ou sistema. Por exemplo, uma classe “Carro” pode incluir atributos como "marca”, "modelo” e “cor”, e métodos como "ligar” e "acelerar”, sem necessariamente detalhar os mecanismo interno para realizar essas funções.
O Encapsulamento é a habilidade de esconder as características intrínsecas (características ou qualidades que fazem parte da natureza ou essência de algo ou alguém) de um dado objeto de outros objetos. É frequentemente implementado usando modificadores de acesso, como "privado”, "público” e "protegido”, que definem a visibilidade desses membros da classe. Ele fortalece a segurança do código e evita que o estado do objeto seja modificado inadvertidamente.
Um exemplo simples de público, protegido e privado é quando se tem uma classe banco, o teu nome é acessível por todos, o ID da tua conta é protegido, mas outras pessoas conseguem ver, e a tua senha é privada, transformando isso em código ficaria assim:
class Bank:
def __init__(self):
# Público
self.name = 'Álex P Carvalho'
# Protegido
self._account = '123456-7'
# Privado
self.__password = '987654321'
bank = Bank()
print(bank.name)
print(bank._account)
print(bank.__password)
Rodando esse código, ele irá pegar o nome e a conta e surgirá um erro quando ele tentar pegar a senha.
encapsulamento.py" Álex P Carvalho 123456-7 Traceback (most recent call last): File "/Users/user/Documents/SOLID/encapsulamento.py", line 20, in <module> print(bank. password) AttributeError: 'Bank' object has no attribute '_ password'. Did you mean: 'get_password'?
Como o atributo está no modo privado, o python altera o nome interno aplicando o name mangling, isso significa que o atributo não pode ser acessado diretamente fora da classe. Uma forma de acessar a senha é criando um método público para retornar o valor de __password.
class Bank:
def __init__(self):
# Público
self.name = 'Álex P Carvalho'
# Protegido
self._account = '123456-7'
# Privado
self.__password = '987654321'
def get_password(self):
return self.__password
bank = Bank()
print(bank.name)
print(bank._account)
print(bank.get_password())
Rodando dessa maneira ela retorna tudo como o esperado.
encapsulamento.py" Álex P Carvalho 123456-7 987654321
A Herança permite a criação de classes derivadas (subclasses) que herdam atributos e métodos de uma classe base (superclasse). Por exemplo, uma classe "Veículos” pode ser especializada em subclasses como "Carro”, "Moto” e "caminhão”, cada um herdando atributos e métodos comuns da classe "Veículos”, enquanto adiciona seus próprios atributos e métodos específicos.
# Superclasse
class Veiculo:
def __init__(self, marca, modelo):
self.marca = marca
self.modelo = modelo
def descrever(self):
return f"Veículo {self.marca} {self.modelo}"
# Subclasse
class Carro(Veiculo):
def __init__(self, marca, modelo, portas):
super().__init__(marca, modelo) # Chama o construtor da superclasse
self.portas = portas
def descrever(self): # Sobrescrita do método
return f"Carro {self.marca} {self.modelo} com {self.portas} portas"
# Subclasse
class Moto(Veiculo):
def __init__(self, marca, modelo, cilindradas):
super().__init__(marca, modelo)
self.cilindradas = cilindradas
def descrever(self):
return f"Moto {self.marca} {self.modelo} com {self.cilindradas}cc"
# Subclasse
class Caminhao(Veiculo):
def __init__(self, marca, modelo, capacidade):
super().__init__(marca, modelo)
self.capacidade = capacidade
def descrever(self):
return f"Caminhão {self.marca} {self.modelo} com capacidade de {self.capacidade} toneladas"
# Criando objetos das subclasses
carro = Carro("BMW", "X6", 4)
moto = Moto("Honda", "CBR 600", 600)
caminhao = Caminhao("Volvo", "FH16", 25)
print(carro.descrever())
print(moto.descrever())
print(caminhao.descrever())
O Polimorfismo é a propriedade de duas ou mais classes derivadas de uma mesma superclasse responderem a mesma mensagem, mas cada subclasse responde de uma forma diferente da outra classe derivada
Os princípios solid facilitam a manutenção, pois as classes implementam apenas métodos que realmente precisam, com classes com apenas uma funcionalidade.
O entendimento do código é importante pois facilita a colaboração entre os membros da equipe, ou se é um serviço onde se desenvolve com apenas uma pessoa, caso tenhas que dar manutenção futura no código, vai conseguir entender perfeitamente o que cada função faz, gerando assim menos bugs e melhor entendimento.
A importância de reduzir o acoplamento é o gerenciamento do sistema, pois se estiver classes fortemente acopladas e precisarmos fazer alteração em uma das classes, precisamos alterar o código das outras classes também.
O acoplamento se refere ao nível de dependência entre módulos de um sistema. Um baixo acoplamento é desejável, pois torna a aplicação mais flexível, reusável e organizada. Isso permite substituir partes do código sem grandes impactos, facilitando manutenção e evolução do software.
Já um alto acoplamento cria dependências fortes entre os módulos, tornando mudanças mais complexas e arriscadas. Quando um módulo altamente acoplado é alterado, outros módulos também precisam ser modificados, aumentando a complexidade do sistema.
Um exemplo de acoplamento:
// utils.ts
const formatZipCode = (zipcode: string): string => {};
const formatPhoneNumber = (phoneNumber: string): string => {};
//shipping.ts
import { formatZipCode } from "./utils.ts";
const calculateShipping = (fromZipCode: string, toZipcode: string): number => {
fromZipCode = formatZipcode(fromZipCode)
toZipCode = formatZipCode(toZipCode)
};
//contact.ts
import { formatZipCode } from "./util.ts"
const addressFromZipCode = (zipcode: string) => {
zipcode = formatZipCode(zipcode)
};
Neste exemplo se o formato do CEP pra cálculo do frete (calculateShipping) precisasse ser diferente, ao modificar o formatZipCode no arquivo utils.ts, a outra função será alterada e pode ser que pare de funcionar.
Este é um exemplo chamado de Common Coupling que ocorre quando vários componentes compartilham um estado global.
S - Single Responsiblity Principle (Princípio da responsabilidade única)
O princípio SRP diz que uma classe, um componente, entidade ou uma função deve ser especializada em um único assunto.
Por exemplo: em vez de ter um funcionário em no restaurante que serve para atender, cozinhar, limpar e ainda fazer a parte o financeiro, contrata mais funcionários cada um para uma função especifica.
Com entidades independentes e isoladas você consegue reaproveitar o código mais facilmente, refatorar melhor o código, facilita a criação de teste automatizados e gerar menos bugs.
Esse código em ruby é um exemplo onde uma função tem 3 responsabilidades.
- Buscar clientes no Banco de dados
- Filtrar clientes ativos
- Enviar emails
def email_clients(clients)
clients.each do |client|
client_record = DB.find(client)
email(client) if client_record.active?
end
end
Aplicando o SRP nós conseguimos isolar as responsabilidades para cada função.
def email_clients(clients)
active_clients = active_clients(clients)
active_clients.each { |client| email(client)}
end
def active_clients(clients)
clients.select { |client | is_client_active?(client) }
end
def is_client_active(client)
client_record = DB.find(client)
client_record.active?
end
O SRP nos da uma fácil manutenção no código, se quisermos mudar a regra de ativação do cliente, só alteramos is_client_active?
, e uma legibilidade melhor do código
O - Open/closed principle (Princípio do Aberto/Fechado)
O OCP diz que classes, entidades ou funções devem estar abertas para extensão e fechadas para modificação.
Um exemplo básico de OCP é um navegador de internet, por exemplo o google chrome, quando se instala extensões no navegador, não é necessário ir no binário do navegador para que a extensão funcione é só instalar que a extensão adiciona novas funcionalidades sem modificar o código do navegador.
Este exemplo abaixo é um exemplo errado de código, pois se quisessemos adicionar outro método de notificação, teria que modificar o código que ja está pronto.
class NotificationSender
def send_email(message)
end
def send_whatsapp(message)
end
def send_sms(message)
end
end
A maneira correta de resolver é "separar o comportamento extensível por trás de uma interface e inverter as dependências” isso significa criar uma abstração que define um conjunto de regras, e cada implementação especifica apenas segue esse padrão. Isso permite flexibilidade, facilita a manutenção e evita que partes do sistema fiquem acopladas a detalhes específicos, tornando o código mais modular e reutilizável
class NotificationSender
def send(message)
raise NotImplementedError, "Subclasses must implement the 'send' method"
end
end
class EmailNotification < NotificationSender
def send(message)
end
end
class WhatsAppNotification < NotificationSender
def send(message)
end
end
class SMSNotification < NotificationSender
def send(message)
end
end
class Notification
def send(notification_sender, message)
notification_sender.send(message)
end
end
Com o Principio Aberto/Fechado podemos implementar novas classes sem modificar o código existente. Ou seja, cada classe está fechada para modificação mas podem ter diversas outras notificações sem precisar alterar o código existente.
L - Liskov Substitution Principle (Princípio da substituição de Liskov)
O LSP diz que objetos podem ser substituídos por seus subtipos sem que isso afete a execução correta do programa.
Respeitar o Princípio de Liskov, força o seu programa a ter as abstrações no nível certo e ser mais consistente, sugere que você não faça subclasses terem um comportamento não esperado, no sentido de um método fundamental existir em uma altura da árvore de herança, e depois esse mesmo método gerar uma exceção numa altura mais abaixo.
O código abaixo mostra como uma subclasse pode substituir uma classe base sem lançar uma exceção inesperada.
class A
def get_name
puts 'Meu nome é A'
end
end
class B < A
def get_name
puts 'Meu nome é B'
end
end
objeto1 = A.new
objeto2 = B.new
def print_name(objeto)
objeto.get_name
end
print_name(objeto1) # Meu nome é A
print_name(objeto2) # Meu nome é B
A classe B extende a classe A e o método get_name
, foi instanciado 2 objetos, cada um de um tipo e ao imprimir ambos os objetos a partir da função print_name
que recebe o objeto do tipo A cada um chama a sua própria função get_name
. Está sendo passado como parâmetro tanto a classe base como a subclasse e o código segue funcionando como esperado.
I - Interface Segregation Principle (Princípio da Segregação da Interface)
O ISP diz que uma classe não deve ser forçada a implementar interface e métodos que não iram utilizar, esse principio diz que é melhor criar interfaces mais especificas ao invés de ter uma única interface genérica.
Por exemplo: se cria interface Animal, onde se tem Dorme, Come, Voa.
O pássaro, consegue dormir, comer e voar.
O elefante consegue dormir e comer, mas não consegue voar
module Animal
def sleep
raise NotImplementedError, "Subclasses must implement the 'sleep' method"
end
def eat
raise NotImplementedError, "Subclasses must implement the 'eat' method"
end
def fly
raise NotImplementedError, "Subclasses must implement the 'fly' method"
end
end
class Bird
include Animal
def sleep
puts "Bird sleeping..."
end
def eat
puts "Bird eating..."
end
def fly
puts "Bird flying..."
end
end
class Elephant
include Animal
def sleep
puts "Elephant sleeping..."
end
def eat
puts "Elephant eating..."
end
end
Rodando esse código no terminal gerou um erro, pois a não foi implementado o método voar na classe elefante.
(3.13.1) > ruby isp.rb Bird sleeping... Bird eating... Bird flying... Elephant sleeping... Elephant eating... isp.rb:12:in
fly': Subclasses must implement the 'fly' method (NotImplementedError)
from isp.rb:56:in <main>' (3.13.1) > I
Após refatorar o código, foi criado interfaces fragmentadas para que cada classe faça o que ela pode fazer, evitando erro de herança desnecessários.
module Animal
def sleep
raise NotImplementedError, "Subclasses must implement the 'sleep' method"
end
def eat
raise NotImplementedError, "Subclasses must implement the 'eat' method"
end
end
module FlyingAnimal
include Animal
def fly
raise NotImplementedError, "Subclasses must implement the 'fly' method"
end
end
class Bird
include FlyingAnimal
def sleep
puts "Bird sleeping..."
end
def eat
puts "Bird eating..."
end
def fly
puts "Bird flying..."
end
end
class Elephant
include Animal
def sleep
puts "Elephant sleeping..."
end
def eat
puts "Elephant eating..."
end
end
(3.13.1) > ruby isp.rb Bird sleeping... Bird eating... Bird flying... Elephant sleeping... Elephant eating... (3.13.1) >
D - Dependency Inversion Principle (Princípio de Inversão de Dependência)
De acordo com Uncle Bob, esse princípio pode ser definido da seguinte forma:
Módulos de alto nível não devem depender de módulos de baixo nível. Ambos devem depender da abstração.
Abstrações não devem depender de detalhes. Detalhes devem depender de abstrações.
Você só precisa aplicar consequentemente os princípios Open/Closed e Liskov Substitution à sua base de código. Depois de fazer isso, suas classes também obedecem ao DIP. Isso permite que você altere componentes de nível superior e inferior sem afetar nenhuma outra classe, desde que você não altere nenhuma abstração de interface.
require_relative 'mysql_connection'
class PasswordReminder
def initialize
@db_connection = MySQLConnection.new
end
end
Neste pequeno trecho de código temos um alto nível de acoplamento, pois a classe PasswordReminder
tem a responsabilidade de criar uma instância da classe MySQLConnection
.
Criamos uma interface DBConnectionInterface
e refatoramos o código para que os módulos de alto e baixo nível dependam da abstração proposta pela interface que acabamos de criar, com isso a classe PasswordReminder
não tem a mínima ideia de qual banco de dados a aplicação irá utilizar.
module DBConnectionInterface
def connect
raise NotImplementedError, "Subclasses must implement the 'connect' method"
end
end
class MySQLConnection
include DBConnectionInterface
def connect
puts "Conectando ao MySQL..."
end
end
class OracleConnection
include DBConnectionInterface
def connect
puts "Conectando ao Oracle..."
end
end
class PasswordReminder
def initialize(db_connection)
@db_connection = db_connection
end
def remind
@db_connection.connect
puts "Lembrete de senha enviado!"
end
end
Conclusão
Os princípios foram apresentados pela primeira vez pelo Engenheiro de Software Robert C. Martin (Uncle Bob) nos anos 2000, Design Principles and Design Patterns
, a sigla SOLID foi inventada por volta de 2004 por Michael Feathers.
O Entendimento dos princípios SOLID é uma forma de aprimorar a qualidade e a eficiência do nosso software, construindo uma base sólida e deixando nosso código escalável e flexível. Facilitando a implementação de novos requisitos e a simplicidade da manutenção do sistema, promovendo um código mais legível e compreensível, facilitando a colaboração entre os membros da equipe, reduzindo as chances de erro e entregando um software com mais qualidade.
Referências
https://www.youtube.com/watch?v=6SfrO3D4dHM
https://stackify.com/dependency-inversion-principle/
https://blog.grancursosonline.com.br/pilares-da-poo/
https://en.wikipedia.org/wiki/SOLID
https://staff.cs.utu.fi/~jounsmed/doos_06/material/DesignPrinciplesAndPatterns.pdf
https://medium.com/mercos-engineering/acoplamento-de-software-db9d2ba264fd