Do tradutor: publicado para você artigo de Severin Perez sobre o uso dos princípios SOLID na programação. As informações do artigo serão úteis tanto para programadores iniciantes quanto para programadores experientes.
Se você gosta de desenvolvimento, provavelmente já ouviu falar dos princípios SOLID. Eles permitem que o programador escreva código limpo, bem estruturado e de fácil manutenção. É importante notar que na programação existem diversas abordagens sobre como executar corretamente um determinado trabalho. Diferentes especialistas têm diferentes ideias e entendimentos sobre o “caminho certo”; tudo depende da experiência de cada pessoa. No entanto, as ideias proclamadas no SOLID são aceitas por quase todos os representantes da comunidade de TI. Eles se tornaram o ponto de partida para o surgimento e desenvolvimento de muitas boas práticas de gestão do desenvolvimento.
Vamos entender o que são os princípios SOLID e como eles nos ajudam.
Lembramos:para todos os leitores de "Habr" - um desconto de 10 rublos ao se inscrever em qualquer curso Skillbox usando o código promocional "Habr".
O que é SÓLIDO?
Este termo é uma abreviatura, cada letra do termo é o início do nome de um princípio específico:
SPrincípio de Responsabilidade Único. Um módulo pode ter um e apenas um motivo para alteração.
A Ocaneta/princípio fechado (princípio aberto/fechado). Classes e outros elementos devem estar abertos para extensão, mas fechados para modificação.
A LPrincípio de Substituição de Iskov (Princípio de substituição de Liskov). Funções que usam um tipo base devem ser capazes de usar subtipos do tipo base sem saber disso.
A DPrincípio de Inversão de dependência (princípio da inversão de dependência). Módulos em níveis superiores não devem depender de módulos em níveis inferiores.
Princípio de Responsabilidade Única
O Princípio de Responsabilidade Única (SRP) afirma que cada classe ou módulo de um programa deve ser responsável por apenas uma parte da funcionalidade desse programa. Além disso, os elementos desta responsabilidade devem ser atribuídos à sua própria classe, em vez de espalhados por classes não relacionadas. O desenvolvedor e principal evangelista do SRP, Robert S. Martin, descreve a responsabilidade como o motivo da mudança. Ele propôs originalmente este termo como um dos elementos de seu trabalho "Princípios de Design Orientado a Objetos". O conceito incorpora muito do padrão de conectividade definido anteriormente por Tom DeMarco.
O conceito também incluiu vários conceitos formulados 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 deveria ser baseada na análise de diagramas de blocos 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.
A propósito, Martin deu um exemplo interessante com gestores seniores de uma empresa (COO, CTO, CFO), cada um dos quais utiliza software empresarial específico para finalidades diferentes. Como resultado, qualquer um deles pode implementar alterações no software sem afetar os interesses dos demais gestores.
Objeto divino
Como sempre, a melhor maneira de aprender o SRP é vê-lo em ação. Vejamos uma seção do programa que NÃO segue o Princípio da Responsabilidade Única. Este é o código Ruby que descreve o comportamento e os atributos da estação espacial.
Revise o exemplo e tente determinar o seguinte:
Responsabilidades dos objetos declarados na classe SpaceStation.
Aqueles que possam estar interessados no funcionamento da estação espacial.
class SpaceStation
def initialize
@supplies = {}
@fuel = 0
end
def run_sensors
puts "----- Sensor Action -----"
puts "Running sensors!"
end
def load_supplies(type, quantity)
puts "----- Supply Action -----"
puts "Loading #{quantity} units of #{type} in the supply hold."
if @supplies[type]
@supplies[type] += quantity
else
@supplies[type] = quantity
end
end
def use_supplies(type, quantity)
puts "----- Supply Action -----"
if @supplies[type] != nil && @supplies[type] > quantity
puts "Using #{quantity} of #{type} from the supply hold."
@supplies[type] -= quantity
else
puts "Supply Error: Insufficient #{type} in the supply hold."
end
end
def report_supplies
puts "----- Supply Report -----"
if @supplies.keys.length > 0
@supplies.each do |type, quantity|
puts "#{type} avalilable: #{quantity} units"
end
else
puts "Supply hold is empty."
end
end
def load_fuel(quantity)
puts "----- Fuel Action -----"
puts "Loading #{quantity} units of fuel in the tank."
@fuel += quantity
end
def report_fuel
puts "----- Fuel Report -----"
puts "#{@fuel} units of fuel available."
end
def activate_thrusters
puts "----- Thruster Action -----"
if @fuel >= 10
puts "Thrusting action successful."
@fuel -= 10
else
puts "Thruster Error: Insufficient fuel available."
end
end
end
Na verdade, nossa estação espacial é disfuncional (acho que não receberei uma ligação da NASA tão cedo), mas há algo para analisar aqui.
Assim, a classe SpaceStation tem diversas responsabilidades (ou tarefas) diferentes. Todos eles podem ser divididos em tipos:
sensores;
suprimentos (consumíveis);
combustível;
aceleradores.
Mesmo que nenhum dos funcionários da estação receba uma aula, podemos facilmente imaginar quem é responsável por quê. Muito provavelmente, o cientista controla os sensores, o logístico é responsável pelo fornecimento de recursos, o engenheiro é responsável pelo fornecimento de combustível e o piloto controla os propulsores.
Podemos dizer que este programa não é compatível com SRP? Sim, claro. Mas a classe SpaceStation é um típico “objeto divino” que sabe tudo e faz tudo. Este é um importante 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 posto médico ou de uma sala de reuniões. E quanto mais funções houver, mais a SpaceStation crescerá. Pois bem, como esta instalação estará interligada a outras, a manutenção de todo o complexo ficará ainda mais complexa. Como resultado, podemos interromper o funcionamento, por exemplo, de aceleradores. Se um pesquisador solicitar alterações nos sensores, isso poderá muito bem afetar os sistemas de comunicação da estação.
A violação do princípio do SRP pode proporcionar uma vitória táctica a curto prazo, mas no final “perderemos a guerra” e será muito difícil manter tal monstro no futuro. É melhor dividir o programa em seções separadas de código, cada uma delas responsável por executar uma operação específica. Entendendo isso, vamos mudar a classe SpaceStation.
Vamos distribuir responsabilidades
Acima definimos quatro tipos de operações que são controladas pela classe SpaceStation. Iremos mantê-los em mente ao refatorar. O código atualizado corresponde melhor ao SRP.
class SpaceStation
attr_reader :sensors, :supply_hold, :fuel_tank, :thrusters
def initialize
@supply_hold = SupplyHold.new
@sensors = Sensors.new
@fuel_tank = FuelTank.new
@thrusters = Thrusters.new(@fuel_tank)
end
end
class Sensors
def run_sensors
puts "----- Sensor Action -----"
puts "Running sensors!"
end
end
class SupplyHold
attr_accessor :supplies
def initialize
@supplies = {}
end
def load_supplies(type, quantity)
puts "----- Supply Action -----"
puts "Loading #{quantity} units of #{type} in the supply hold."
if @supplies[type]
@supplies[type] += quantity
else
@supplies[type] = quantity
end
end
def use_supplies(type, quantity)
puts "----- Supply Action -----"
if @supplies[type] != nil && @supplies[type] > quantity
puts "Using #{quantity} of #{type} from the supply hold."
@supplies[type] -= quantity
else
puts "Supply Error: Insufficient #{type} in the supply hold."
end
end
def report_supplies
puts "----- Supply Report -----"
if @supplies.keys.length > 0
@supplies.each do |type, quantity|
puts "#{type} avalilable: #{quantity} units"
end
else
puts "Supply hold is empty."
end
end
end
class FuelTank
attr_accessor :fuel
def initialize
@fuel = 0
end
def get_fuel_levels
@fuel
end
def load_fuel(quantity)
puts "----- Fuel Action -----"
puts "Loading #{quantity} units of fuel in the tank."
@fuel += quantity
end
def use_fuel(quantity)
puts "----- Fuel Action -----"
puts "Using #{quantity} units of fuel from the tank."
@fuel -= quantity
end
def report_fuel
puts "----- Fuel Report -----"
puts "#{@fuel} units of fuel available."
end
end
class Thrusters
def initialize(fuel_tank)
@linked_fuel_tank = fuel_tank
end
def activate_thrusters
puts "----- Thruster Action -----"
if @linked_fuel_tank.get_fuel_levels >= 10
puts "Thrusting action successful."
@linked_fuel_tank.use_fuel(10)
else
puts "Thruster Error: Insufficient fuel available."
end
end
end
Há muitas mudanças, o programa definitivamente parece melhor agora. Agora, nossa classe SpaceStation tornou-se mais um contêiner no qual as operações são iniciadas para peças dependentes, incluindo um conjunto de sensores, um sistema de abastecimento de consumíveis, um tanque de combustível e propulsores.
Para qualquer uma das variáveis existe agora uma classe correspondente: Sensores; Suprimento de fornecimento; Tanque de combustível; Propulsores.
Existem várias mudanças importantes nesta versão do código. A questão é que as funções individuais não são apenas encapsuladas em suas próprias classes, elas são organizadas de forma a se tornarem previsíveis e consistentes. Agrupamos elementos com funcionalidades semelhantes para seguir o princípio da coerência. Agora, se precisarmos mudar a forma como o sistema funciona, passando de uma estrutura hash para um array, basta usar a classe SupplyHold; não precisamos mexer em outros módulos. Dessa forma, caso o responsável pela logística altere alguma coisa em seu trecho, o restante da estação permanecerá intacto. Neste caso, a classe SpaceStation nem terá conhecimento das mudanças.
Nossos oficiais que trabalham na estação espacial provavelmente estão felizes com as mudanças porque podem solicitar as que precisam. Observe que o código possui métodos como report_supplies e report_fuel contidos nas classes SupplyHold e FuelTank. O que aconteceria se a Terra pedisse para mudar a forma como reporta? Ambas as classes, SupplyHold e FuelTank, precisarão ser alteradas. E se você precisar mudar a forma como o combustível e os consumíveis são entregues? Você provavelmente terá que alterar todas as mesmas classes novamente. E isso já é uma violação do princípio SRP. Vamos consertar isso.
class SpaceStation
attr_reader :sensors, :supply_hold, :supply_reporter,
:fuel_tank, :fuel_reporter, :thrusters
def initialize
@sensors = Sensors.new
@supply_hold = SupplyHold.new
@supply_reporter = SupplyReporter.new(@supply_hold)
@fuel_tank = FuelTank.new
@fuel_reporter = FuelReporter.new(@fuel_tank)
@thrusters = Thrusters.new(@fuel_tank)
end
end
class Sensors
def run_sensors
puts "----- Sensor Action -----"
puts "Running sensors!"
end
end
class SupplyHold
attr_accessor :supplies
attr_reader :reporter
def initialize
@supplies = {}
end
def get_supplies
@supplies
end
def load_supplies(type, quantity)
puts "----- Supply Action -----"
puts "Loading #{quantity} units of #{type} in the supply hold."
if @supplies[type]
@supplies[type] += quantity
else
@supplies[type] = quantity
end
end
def use_supplies(type, quantity)
puts "----- Supply Action -----"
if @supplies[type] != nil && @supplies[type] > quantity
puts "Using #{quantity} of #{type} from the supply hold."
@supplies[type] -= quantity
else
puts "Supply Error: Insufficient #{type} in the supply hold."
end
end
end
class FuelTank
attr_accessor :fuel
attr_reader :reporter
def initialize
@fuel = 0
end
def get_fuel_levels
@fuel
end
def load_fuel(quantity)
puts "----- Fuel Action -----"
puts "Loading #{quantity} units of fuel in the tank."
@fuel += quantity
end
def use_fuel(quantity)
puts "----- Fuel Action -----"
puts "Using #{quantity} units of fuel from the tank."
@fuel -= quantity
end
end
class Thrusters
FUEL_PER_THRUST = 10
def initialize(fuel_tank)
@linked_fuel_tank = fuel_tank
end
def activate_thrusters
puts "----- Thruster Action -----"
if @linked_fuel_tank.get_fuel_levels >= FUEL_PER_THRUST
puts "Thrusting action successful."
@linked_fuel_tank.use_fuel(FUEL_PER_THRUST)
else
puts "Thruster Error: Insufficient fuel available."
end
end
end
class Reporter
def initialize(item, type)
@linked_item = item
@type = type
end
def report
puts "----- #{@type.capitalize} Report -----"
end
end
class FuelReporter < Reporter
def initialize(item)
super(item, "fuel")
end
def report
super
puts "#{@linked_item.get_fuel_levels} units of fuel available."
end
end
class SupplyReporter < Reporter
def initialize(item)
super(item, "supply")
end
def report
super
if @linked_item.get_supplies.keys.length > 0
@linked_item.get_supplies.each do |type, quantity|
puts "#{type} avalilable: #{quantity} units"
end
else
puts "Supply hold is empty."
end
end
end
iss = SpaceStation.new
iss.sensors.run_sensors
# ----- Sensor Action -----
# Running sensors!
iss.supply_hold.use_supplies("parts", 2)
# ----- Supply Action -----
# Supply Error: Insufficient parts in the supply hold.
iss.supply_hold.load_supplies("parts", 10)
# ----- Supply Action -----
# Loading 10 units of parts in the supply hold.
iss.supply_hold.use_supplies("parts", 2)
# ----- Supply Action -----
# Using 2 of parts from the supply hold.
iss.supply_reporter.report
# ----- Supply Report -----
# parts avalilable: 8 units
iss.thrusters.activate_thrusters
# ----- Thruster Action -----
# Thruster Error: Insufficient fuel available.
iss.fuel_tank.load_fuel(100)
# ----- Fuel Action -----
# Loading 100 units of fuel in the tank.
iss.thrusters.activate_thrusters
# ----- Thruster Action -----
# Thrusting action successful.
# ----- Fuel Action -----
# Using 10 units of fuel from the tank.
iss.fuel_reporter.report
# ----- Fuel Report -----
# 90 units of fuel available.
Nesta última versão do programa, as responsabilidades foram divididas em duas novas classes, FuelReporter e SupplyReporter. Ambos são filhos da classe Reporter. Além disso, adicionamos variáveis de instância à classe SpaceStation para que a subclasse desejada possa ser inicializada, se necessário. Agora, se a Terra decidir mudar alguma outra coisa, faremos alterações nas subclasses, e não na classe principal.
Claro, algumas de nossas aulas ainda dependem umas das outras. Assim, o objeto SupplyReporter depende de SupplyHold e FuelReporter depende de FuelTank. Obviamente, os boosters devem estar conectados ao tanque de combustível. Mas aqui tudo já parece lógico, e fazer alterações não será particularmente difícil - editar o código de um objeto não afetará muito outro.
Assim, criamos um código modular onde as responsabilidades de cada um dos objetos/classes são definidas com precisão. Trabalhar com esse código não é problema, mantê-lo será uma tarefa simples. Convertemos todo o “objeto divino” em SRP.