Escrevendo código flexível usando SOLID

Escrevendo código flexível usando SOLID

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.

A Skillbox recomenda: curso prático "Desenvolvedor móvel PRO".

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:

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.

A Skillbox recomenda:

Fonte: habr.com

Adicionar um comentário