Escrevendo um código flexível usando o SOLID



De um tradutor: Severin Peres publicou um artigo sobre o uso dos princípios do SOLID na programação para você. As informações do artigo serão úteis para iniciantes e programadores com experiência.

Se você é um desenvolvedor, provavelmente já ouviu falar dos princípios do SOLID. Eles permitem que o programador escreva códigos limpos, bem estruturados e fáceis de manter. Vale a pena notar que na programação existem várias abordagens sobre como executar adequadamente esse ou aquele trabalho. Diferentes especialistas têm idéias diferentes e compreensão do "caminho certo", tudo depende da experiência de cada pessoa. No entanto, as idéias 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 o desenvolvimento de muitas boas práticas de gerenciamento de desenvolvimento.

Vamos ver o que são os princípios do SOLID e como eles nos ajudam.

A Skillbox recomenda: Curso prático "Mobile Developer PRO" .

Lembramos que: para todos os leitores de "Habr" - um desconto de 10.000 rublos ao se inscrever em qualquer curso Skillbox usando o código promocional "Habr".

O que é o SOLID?


Este termo é uma abreviação, cada letra do termo é o começo do nome de um determinado princípio:

Princípio da responsabilidade exclusiva


O princípio da responsabilidade única (SRP) afirma que cada classe ou módulo em um programa deve ser responsável por apenas uma parte da funcionalidade deste programa. Além disso, elementos dessa responsabilidade devem ser atribuídos a sua classe e não distribuídos entre classes não relacionadas. O desenvolvedor e evangelista líder do SRP, Robert S. Martin, descreve a responsabilidade como a causa da mudança. Inicialmente, ele propôs esse termo como um dos elementos de seu trabalho, “Princípios do Design Orientado a Objetos”. O conceito incluía grande parte do padrão de conectividade que foi definido anteriormente por Tom Demarco.

O conceito também incluiu vários conceitos formulados por David Parnassus. 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 um mínimo de informações aos clientes.

A propósito, Martin deu um exemplo interessante com os gerentes seniores da empresa (COO, CTO, CFO), cada um dos quais utiliza software específico para negócios com objetivos diferentes. Como resultado, qualquer um deles pode implementar alterações no software sem afetar os interesses de outros gerentes.

Objeto divino


Como sempre, a melhor maneira de aprender SRP é ver tudo em ação. Vejamos uma seção de um programa que NÃO cumpre o princípio de responsabilidade compartilhada. Este é o código Ruby que descreve o comportamento e os atributos de uma estação espacial.

Confira o exemplo e tente determinar o seguinte:
As responsabilidades desses objetos declarados na classe SpaceStation.
Aqueles que podem estar interessados ​​no trabalho 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 não funciona (acho que não receberei uma ligação da NASA em um futuro próximo), mas há algo a analisar.

Portanto, a classe SpaceStation tem várias responsabilidades (ou tarefas) diferentes. Todos eles podem ser divididos em tipos:
  • Sensores
  • fornecimento (consumíveis);
  • combustível;
  • aceleradores.

Apesar de nenhum dos funcionários da estação estar definido na classe, podemos facilmente imaginar quem é responsável por quê. Provavelmente, o cientista controla os sensores, o logístico é responsável pelo suprimento de recursos, o engenheiro é responsável pelo suprimento de combustível e o piloto controla os aceleradores.

Podemos dizer que este programa não é compatível com SRP? Sim claro. Mas a classe SpaceStation é um "objeto divino" típico que sabe tudo e faz tudo. Este é o principal anti-padrã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 novos recursos. Talvez nossa estação espacial precise de um centro médico ou de uma sala de reuniões. E quanto mais recursos houver, mais a SpaceStation crescerá. Bem, como esse objeto estará conectado com outros, a manutenção de todo o complexo se tornará ainda mais complicada. Como resultado, podemos interromper o trabalho de, por exemplo, aceleradores. Se um pesquisador solicitar alterações no trabalho com sensores, isso poderá afetar os sistemas de comunicação da estação.

A violação do princípio do SRP pode dar uma vitória tática a curto prazo, mas no final "perderemos a guerra", a manutenção de um monstro desses no futuro será muito difícil. É melhor dividir o programa em seções separadas de código, cada uma das quais é responsável por executar uma operação específica. Com isso em mente, vamos mudar a classe SpaceStation.

Compartilhar responsabilidade

Acima, identificamos quatro tipos de operações que são controladas pela classe SpaceStation. Ao refatorar, nós os manteremos em mente. 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 agora parece definitivamente melhor. Agora, nossa classe SpaceStation se tornou, em vez disso, um contêiner no qual são iniciadas operações para peças dependentes, incluindo um conjunto de sensores, um sistema de suprimento de consumíveis, um tanque de combustível e reforçadores.

Para qualquer uma das variáveis ​​agora existe uma classe correspondente: Sensores; SupplyHold; FuelTank Propulsores.

Existem várias alterações importantes nesta versão do código. O fato é que as funções individuais não são apenas encapsuladas em suas próprias classes, elas são organizadas de maneira a se tornarem previsíveis e consistentes. Agrupamos elementos de funcionalidade semelhante para seguir o princípio da conectividade. Agora, se precisarmos mudar o princípio do sistema, passando de uma estrutura de hash para uma matriz, basta usar a classe SupplyHold, não precisaremos tocar em outros módulos. Assim, se o oficial encarregado da logística mudar alguma coisa em sua seção, os elementos restantes da estação permanecerão intocados. Ao mesmo tempo, a classe SpaceStation nem estará ciente das alterações.

Nossos oficiais da estação espacial provavelmente ficarã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 acontece se a Terra pedir para alterar a maneira como os relatórios são gerados? Você precisará alterar as duas classes, SupplyHold e FuelTank. Mas e se você precisar alterar a maneira como fornece combustível e consumíveis? Talvez você precise alterar todas as mesmas classes novamente. E isso é uma violação do princípio do SRP. Vamos consertar.

 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 versão mais recente 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 inicializar a subclasse necessária, se necessário. Agora, se a Terra decidir mudar outra coisa, faremos alterações nas subclasses, e não na classe principal.

Claro, algumas aulas aqui ainda dependem uma da outra. Portanto, o objeto SupplyReporter depende do SupplyHold e o FuelReporter depende do FuelTank. Obviamente, os boosters devem ser conectados ao tanque de combustível. Mas aqui tudo parece lógico e fazer alterações não será especialmente difícil - editar o código de um objeto não afetará muito o outro.

Assim, criamos 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 é um problema; sua manutenção será uma tarefa simples. Todo o "objeto divino" que convertemos para SRP.

A Skillbox recomenda:

Source: https://habr.com/ru/post/pt442928/


All Articles