
Do tradutor: publicamos para você sobre a utilização dos princípios SOLID na programação. As informações do artigo serão úteis tanto para iniciantes quanto para programadores experientes.
Se você trabalha com desenvolvimento, provavelmente já ouviu falar dos princípios SOLID. Eles permitem que os programadores escrevam código limpo, bem estruturado e de fácil manutenção. Vale ressaltar que na programação existem diversas abordagens sobre como realizar corretamente determinada tarefa. Diferentes profissionais têm ideias e compreensões distintas sobre o 'caminho certo', dependendo da experiência de cada um. No entanto, as ideias proclamadas pelos princípios SOLID são aceitas praticamente por todos os membros da comunidade de TI. Eles se tornaram um ponto de partida para o surgimento e desenvolvimento de múltiplos bons métodos de gerenciamento de desenvolvimento.
Vamos entender o que são os princípios SOLID e como eles nos ajudam.
A Skillbox recomenda: Curso Prático .
Lembramos: para todos os leitores do «Habr» — desconto de 10.000 rublos na inscrição em qualquer curso Skillbox com o código promocional «Habr».
O que é SOLID?
Este termo é um acrônimo, onde cada letra representa o início do nome de um determinado princípio:
- SSingle Responsibility Principle (princípio da responsabilidade única). Um módulo pode ter uma e apenas uma razão para ser modificado.
- (princípio da abertura/fechamento). Classes e outros elementos devem estar abertos para extensão, mas fechados para modificação.
- (princípio da substituição de Liskov). Funções que usam um tipo base devem ser capazes de usar subtipos desse tipo sem saber disso.
- (princípio da segregação da interface). Entidades de software não devem depender de métodos que não utilizam.
- (princípio da inversão de dependências). Módulos de nível superior não devem depender de módulos de nível inferior.
Princípio da responsabilidade única
O princípio da responsabilidade única (SRP) afirma que cada classe ou módulo em um programa deve ser responsável apenas por uma parte da funcionalidade desse programa. Além disso, os elementos dessa responsabilidade devem estar vinculados à sua classe, e não distribuídos entre classes não relacionadas. O desenvolvedor e principal defensor do SRP, Robert C. Martin, descreve a responsabilidade como a razão das mudanças. Ele inicialmente propôs esse termo como um dos elementos de seu trabalho "Princípios de Design Orientado a Objetos". O conceito incluiu muito da regularidade de coesão, que foi definida anteriormente por Tom DeMarco.
O conceito também trouxe várias ideias formuladas por David Parnas. Os dois principais são encapsulamento e ocultação de informações. Parnas argumentou que a divisão de um sistema em módulos separados não deve ser baseada em uma análise de fluxogramas ou fluxos de execução. Qualquer um dos módulos deve conter uma solução específica, que forneça o mínimo de informações aos clientes.
Aliás, Martin trouxe um exemplo interessante com os altos executivos da empresa (COO, CTO, CFO), cada um dos quais utiliza um software específico para o negócio com diferentes objetivos. No final, qualquer um deles pode implementar alterações no software sem afetar os interesses dos outros gerentes.
Objeto divino
Como de costume, a melhor maneira de entender o SRP é ver tudo em ação. Vamos olhar para um trecho de programa que NÃO está de acordo com o princípio da responsabilidade única. Este é um código Ruby que descreve o comportamento e os atributos de uma estação espacial.
Considere o exemplo e tente identificar o seguinte:
As responsabilidades dos objetos que são proclamados na classe SpaceStation.
Aqueles que podem estar interessados na operação da estação espacial.
class SpaceStation
def initialize
@supplies = {}
@fuel = 0
end
def run_sensors
puts "----- Ação do Sensor -----"
puts "Executando sensores!"
end
def load_supplies(type, quantity)
puts "----- Ação de Suprimento -----"
puts "Carregando #{quantity} unidades de #{type} no compartimento de suprimentos."
if @supplies[type]
@supplies[type] += quantity
else
@supplies[type] = quantity
end
end
def use_supplies(type, quantity)
puts "----- Ação de Suprimento -----"
if @supplies[type] != nil && @supplies[type] > quantity
puts "Usando #{quantity} de #{type} do compartimento de suprimentos."
@supplies[type] -= quantity
else
puts "Erro de Suprimento: Quantidade insuficiente de #{type} no compartimento de suprimentos."
end
end
def report_supplies
puts "----- Relatório de Suprimentos -----"
if @supplies.keys.length > 0
@supplies.each do |type, quantity|
puts "#{type} disponível: #{quantity} unidades"
end
else
puts "O compartimento de suprimentos está vazio."
end
end
def load_fuel(quantity)
puts "----- Ação de Combustível -----"
puts "Carregando #{quantity} unidades de combustível no tanque."
@fuel += quantity
end
def report_fuel
puts "----- Relatório de Combustível -----"
puts "#{@fuel} unidades de combustível disponíveis."
end
def activate_thrusters
puts "----- Ação do Propulsor -----"
if @fuel >= 10
puts "Ação de empuxo bem-sucedida."
@fuel -= 10
else
puts "Erro do Propulsor: Combustível insuficiente disponível."
end
end
endDe fato, nossa estação espacial está inativa (acho que não receberei uma ligação da NASA em um futuro próximo), mas há muito o que analisar aqui.
Assim, a classe SpaceStation tem várias responsabilidades (ou tarefas) diferentes. Todas elas podem ser divididas por tipos:
- sensores;
- suprimentos (itens de consumo);
- combustível;
- propulsores.
Embora nenhum dos funcionários da estação esteja definido na classe, podemos facilmente imaginar quem é responsável por o quê. Provavelmente, um cientista monitora os sensores, um logista cuida do suprimento de recursos, um engenheiro gerencia os estoques de combustível e um piloto controla os propulsores.
Podemos dizer que este programa não está em conformidade com o SRP? Sim, claro. Mas a classe SpaceStation é um típico "objeto divino" que sabe tudo e faz tudo. Este é o principal antipadrão na programação orientada a objetos. Para um iniciante, esses objetos são extremamente difíceis de manter. Até agora, o programa é muito simples, sim, mas imagine o que acontecerá se adicionarmos novas funcionalidades. Talvez nossa estação espacial precise de um centro médico ou uma sala de reuniões. E quanto mais funcionalidades houver, mais complexa se tornará a SpaceStation. Como esse objeto estará relacionado a outros, a manutenção de todo o complexo se tornará ainda mais difícil. No final, podemos quebrar, por exemplo, os aceleradores. Se um pesquisador solicitar mudanças no funcionamento dos sensores, isso pode impactar os sistemas de comunicação da estação.
Violar o princípio do SRP pode levar a uma vitória tática de curto prazo, mas, a longo prazo, "perderemos a guerra", e manter um monstro desses no futuro será muito complicado. É melhor dividir o programa em partes de código separadas, cada uma responsável pela execução de uma operação específica. Com isso em mente, vamos alterar a classe SpaceStation.
Distribuiremos a responsabilidade
Acima, definimos quatro tipos de operações que são controladas pela classe SpaceStation. Ao refatorar, teremos isso em mente. O código atualizado se alinha melhor com o SRP.
classe EstacaoEspacial
attr_reader :sensores, @compartimento_abastecimento, @tanque_combustivel, @propulsores
def inicializar
@compartimento_abastecimento = CompartimentoAbastecimento.new
@sensores = Sensores.new
@tanque_combustivel = TanqueCombustivel.new
@propulsores = Propulsores.new(@tanque_combustivel)
end
end
classe Sensores
def executar_sensores
puts "----- Ação do Sensor -----"
puts "Executando sensores!"
end
end
classe CompartimentoAbastecimento
attr_accessor :suprimentos
def inicializar
@suprimentos = {}
end
def carregar_suprimentos(tipo, quantidade)
puts "----- Ação de Suprimentos -----"
puts "Carregando #{quantidade} unidades de #{tipo} no compartimento de abastecimento."
if @suprimentos[tipo]
@suprimentos[tipo] += quantidade
else
@suprimentos[tipo] = quantidade
end
end
def usar_suprimentos(tipo, quantidade)
puts "----- Ação de Suprimentos -----"
if @suprimentos[tipo] != nil && @suprimentos[tipo] > quantidade
puts "Usando #{quantidade} de #{tipo} do compartimento de abastecimento."
@suprimentos[tipo] -= quantidade
else
puts "Erro de Suprimento: Quantidade insuficiente de #{tipo} no compartimento de abastecimento."
end
end
def relatar_suprimentos
puts "----- Relatório de Suprimentos -----"
if @suprimentos.keys.length > 0
@suprimentos.each do |tipo, quantidade|
puts "#{tipo} disponível: #{quantidade} unidades"
end
else
puts "O compartimento de abastecimento está vazio."
end
end
end
classe TanqueCombustivel
attr_accessor :combustivel
def inicializar
@combustivel = 0
end
def obter_niveis_combustivel
@combustivel
end
def carregar_combustivel(quantidade)
puts "----- Ação de Combustível -----"
puts "Carregando #{quantidade} unidades de combustível no tanque."
@combustivel += quantidade
end
def usar_combustivel(quantidade)
puts "----- Ação de Combustível -----"
puts "Usando #{quantidade} unidades de combustível do tanque."
@combustivel -= quantidade
end
def relatar_combustivel
puts "----- Relatório de Combustível -----"
puts "#{@combustivel} unidades de combustível disponíveis."
end
end
classe Propulsores
def inicializar(tanque_combustivel)
@tanque_combustivel_vinculado = tanque_combustivel
end
def ativar_propulsores
puts "----- Ação do Propulsor -----"
if @tanque_combustivel_vinculado.obter_niveis_combustivel >= 10
puts "Ação de propulsão bem-sucedida."
@tanque_combustivel_vinculado.usar_combustivel(10)
else
puts "Erro do Propulsor: Combustível insuficiente disponível."
end
end
endHouve muitas mudanças, o programa agora está definitivamente melhor. Agora nossa classe EstacaoEspacial se tornou, de fato, um contêiner no qual são iniciadas operações para as partes dependentes, incluindo um conjunto de sensores, um sistema de fornecimento de suprimentos, um tanque de combustível e propulsores.
Agora há uma classe correspondente para cada uma das variáveis: Sensores; CompartimentoAbastecimento; TanqueCombustivel; Propulsores.
Nesta versão do código, há várias mudanças importantes. As funções agora estão não apenas encapsuladas em suas próprias classes, mas também organizadas de maneira a serem previsíveis e consistentes. Agrupamos elementos com funcionalidades semelhantes para seguir o princípio de coesão. Agora, se precisarmos alterar o funcionamento do sistema, mudando de uma estrutura hash para um array, basta usar a classe SupplyHold, sem afetar outros módulos. Assim, se o oficial responsável pela logística fizer alguma alteração na sua seção, os demais elementos da estação permanecerão intactos. Além disso, a classe SpaceStation não terá conhecimento das modificações.
Nossos oficiais que trabalham na estação espacial provavelmente estão felizes com as mudanças, pois podem solicitar o que realmente precisam. Note que há métodos no código, como report_supplies e report_fuel, contidos nas classes SupplyHold e FuelTank. O que acontecerá se a Terra solicitar a alteração na forma de geração dos relatórios? Será necessário modificar ambas as classes, SupplyHold e FuelTank. E se precisarmos mudar a maneira de entrega de combustível e suprimentos? Provavelmente, será preciso mudar novamente todas essas classes. Isso é uma violação do princípio SRP. Vamos corrigir isso.
class EstaçãoEspacial
attr_reader :sensores, :compartimento_de_supplies, :relator_de_supplies,
:tanque_de_combustível, :relator_de_combustível, :propulsores
def initialize
@sensores = Sensores.new
@compartimento_de_supplies = CompartimentoDeSupplies.new
@relator_de_supplies = RelatorDeSupplies.new(@compartimento_de_supplies)
@tanque_de_combustível = TanqueDeCombustível.new
@relator_de_combustível = RelatorDeCombustível.new(@tanque_de_combustível)
@propulsores = Propulsores.new(@tanque_de_combustível)
end
end
class Sensores
def executar_sensores
puts "----- Ação do Sensor -----"
puts "Executando sensores!"
end
end
class CompartimentoDeSupplies
attr_accessor :supplies
attr_reader :relator
def initialize
@supplies = {}
end
def obter_supplies
@supplies
end
def carregar_supplies(tipo, quantidade)
puts "----- Ação de Supplies -----"
puts "Carregando #{quantidade} unidades de #{tipo} no compartimento de supplies."
if @supplies[tipo]
@supplies[tipo] += quantidade
else
@supplies[tipo] = quantidade
end
end
def usar_supplies(tipo, quantidade)
puts "----- Ação de Supplies -----"
if @supplies[tipo] != nil && @supplies[tipo] > quantidade
puts "Usando #{quantidade} de #{tipo} do compartimento de supplies."
@supplies[tipo] -= quantidade
else
puts "Erro de Supplies: Quantidade insuficiente de #{tipo} no compartimento de supplies."
end
end
end
class TanqueDeCombustível
attr_accessor :combustível
attr_reader :relator
def initialize
@combustível = 0
end
def obter_níveis_de_combustível
@combustível
end
def carregar_combustível(quantidade)
puts "----- Ação de Combustível -----"
puts "Carregando #{quantidade} unidades de combustível no tanque."
@combustível += quantidade
end
def usar_combustível(quantidade)
puts "----- Ação de Combustível -----"
puts "Usando #{quantidade} unidades de combustível do tanque."
@combustível -= quantidade
end
end
class Propulsores
COMBUSTÍVEL_POR_PROPULSO = 10
def initialize(tanque_de_combustível)
@tanque_de_combustível_vinculado = tanque_de_combustível
end
def ativar_propulsores
puts "----- Ação do Propulsor -----"
if @tanque_de_combustível_vinculado.obter_níveis_de_combustível >= COMBUSTÍVEL_POR_PROPULSO
puts "Ação de propulsão bem-sucedida."
@tanque_de_combustível_vinculado.usar_combustível(COMBUSTÍVEL_POR_PROPULSO)
else
puts "Erro do Propulsor: Combustível insuficiente disponível."
end
end
end
class Relator
def initialize(item, tipo)
@item_vinculado = item
@tipo = tipo
end
def relatar
puts "----- Relatório de #{@tipo.capitalize} -----"
end
end
class RelatorDeCombustível < Relator
def initialize(item)
super(item, "combustível")
end
def relatar
super
puts "#{@item_vinculado.obter_níveis_de_combustível} unidades de combustível disponíveis."
end
end
class RelatorDeSupplies 0
@item_vinculado.obter_supplies.each do |tipo, quantidade|
puts "#{tipo} disponível: #{quantidade} unidades"
end
else
puts "O compartimento de supplies está vazio."
end
end
end
iss = EstaçãoEspacial.new
iss.sensores.executar_sensores
# ----- Ação do Sensor -----
# Executando sensores!
iss.compartimento_de_supplies.usar_supplies("peças", 2)
# ----- Ação de Supplies -----
# Erro de Supplies: Quantidade insuficiente de peças no compartimento de supplies.
iss.compartimento_de_supplies.carregar_supplies("peças", 10)
# ----- Ação de Supplies -----
# Carregando 10 unidades de peças no compartimento de supplies.
iss.compartimento_de_supplies.usar_supplies("peças", 2)
# ----- Ação de Supplies -----
# Usando 2 de peças do compartimento de supplies.
iss.relator_de_supplies.relatar
# ----- Relatório de Supplies -----
# peças disponíveis: 8 unidades
iss.propulsores.ativar_propulsores
# ----- Ação do Propulsor -----
# Erro do Propulsor: Combustível insuficiente disponível.
iss.tanque_de_combustível.carregar_combustível(100)
# ----- Ação de Combustível -----
# Carregando 100 unidades de combustível no tanque.
iss.propulsores.ativar_propulsores
# ----- Ação do Propulsor -----
# Ação de propulsão bem-sucedida.
# ----- Ação de Combustível -----
# Usando 10 unidades de combustível do tanque.
iss.relator_de_combustível.relatar
# ----- Relatório de Combustível -----
# 90 unidades de combustível disponíveis.Nesta última versão do programa, as responsabilidades foram divididas em duas novas classes, FuelReporter e SupplyReporter. Ambas são subclasses da classe Reporter. Além disso, adicionamos variáveis de instância à classe SpaceStation para inicializar a subclasse necessária, se necessário. Agora, se a Terra decidir mudar mais alguma coisa, faremos as alterações nas subclasses, não na classe principal.
Claro, algumas classes ainda dependem umas das outras. Por exemplo, o objeto SupplyReporter depende de SupplyHold, enquanto FuelReporter depende de FuelTank. Naturalmente, os aceleradores devem estar relacionados ao tanque de combustível. Mas agora tudo parece lógico, e fazer mudanças não será especialmente difícil - editar o código de um objeto não afetará muito o outro.
Assim, criamos um código modular, onde as responsabilidades de cada um dos objetos/classes são claramente definidas. Trabalhar com esse código não é um problema, sua manutenção será uma tarefa simples. Transformamos todo o "objeto divino" em SRP.
A Skillbox recomenda:
- Curso Prático de Dois Anos .
- Curso online .
- Curso prático de um ano .
Fonte: habr.com
