Escrevendo código flexível usando SOLID

Escrevendo código flexível usando SOLID

Do tradutor: publicamos para você o artigo de Severin Perez 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 ‘Desenvolvedor Móvel PRO’.

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.
  • A OOpen/Closed Principle (princípio da abertura/fechamento). Classes e outros elementos devem estar abertos para extensão, mas fechados para modificação.
  •  The LLiskov Substitution Principle (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.
  • A IInterface Segregation Principle (princípio da segregação da interface). Entidades de software não devem depender de métodos que não utilizam.
  • A DDependency Inversion Principle (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
end

De 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
end

Houve 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:

Fonte: habr.com

Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS 🔥 Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS | ProHoster