Escribindo código flexible usando SOLID

Escribindo código flexible usando SOLID

Do tradutor: publicado para ti artigo de Severin Pérez sobre o uso de principios SOLID na programación. A información do artigo será útil tanto para principiantes como para programadores experimentados.

Se estás en desenvolvemento, o máis probable é que escoitases falar dos principios SOLID. Permiten ao programador escribir código limpo, ben estruturado e fácil de manter. Paga a pena notar que na programación hai varios enfoques sobre como realizar correctamente un traballo en particular. Distintos especialistas teñen diferentes ideas e comprensión do "camiño correcto"; todo depende da experiencia de cada persoa. Non obstante, as ideas proclamadas en SOLID son aceptadas por case todos os representantes da comunidade informática. Convertéronse no punto de partida para a aparición e desenvolvemento de moitas boas prácticas de xestión do desenvolvemento.

Entendemos cales son os principios SOLID e como nos axudan.

Skillbox recomenda: Curso práctico "Desenvolvedor móbil PRO".

Recordámolo: para todos os lectores de "Habr" - un desconto de 10 rublos ao inscribirse en calquera curso de Skillbox usando o código promocional "Habr".

Que é SOLID?

Este termo é unha abreviatura, cada letra do termo é o comezo do nome dun principio específico:

  • SPrincipio de responsabilidade individual. Un módulo pode ter un e só un motivo de cambio.
  • o OPen/Principio pechado (principio aberto/pechado). As clases e outros elementos deben estar abertos para ampliar, pero pechados para modificar.
  •  o LIskov Principio de Substitución (principio de substitución de Liskov). As funcións que usan un tipo base deberían poder usar subtipos do tipo base sen sabelo.
  • o IPrincipio de segregación de interfaces  (principio de separación da interface). As entidades de software non deben depender de métodos que non utilizan.
  • o DPrincipio de inversión de dependencia (principio de inversión de dependencia). Os módulos de niveis superiores non deben depender dos módulos de niveis inferiores.

Principio de Responsabilidade Única


O Principio de Responsabilidade Única (SRP) establece que cada clase ou módulo dun programa debe ser responsable só dunha parte da funcionalidade dese programa. Ademais, os elementos desta responsabilidade deberían asignarse á súa propia clase, en lugar de espallarse por clases non relacionadas. O desenvolvedor e evanxelista xefe de SRP, Robert S. Martin, describe a rendición de contas como a razón do cambio. Orixinalmente propuxo este termo como un dos elementos da súa obra "Principios de deseño orientado a obxectos". O concepto incorpora gran parte do patrón de conectividade que foi previamente definido por Tom DeMarco.

O concepto incluía tamén varios conceptos formulados por David Parnas. Os dous principais son o encapsulamento e a ocultación de información. Parnas argumentou que dividir un sistema en módulos separados non debería basearse na análise de diagramas de bloques ou fluxos de execución. Calquera dos módulos debe conter unha solución específica que proporcione un mínimo de información aos clientes.

Por certo, Martín puxo un exemplo interesante con altos directivos dunha empresa (COO, CTO, CFO), cada un dos cales utiliza un software empresarial específico para diferentes fins. Como resultado, calquera deles pode implementar cambios no software sen afectar os intereses doutros xestores.

Obxecto divino

Como sempre, a mellor forma de aprender SRP é velo en acción. Vexamos unha sección do programa que NON segue o Principio de Responsabilidade Única. Este é o código Ruby que describe o comportamento e os atributos da estación espacial.

Revisa o exemplo e intenta determinar o seguinte:
Responsabilidades daqueles obxectos que se declaran na clase SpaceStation.
Os que poidan estar interesados ​​no funcionamento da estación 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

En realidade, a nosa estación espacial é disfuncional (non creo que reciba unha chamada da NASA en breve), pero hai algo que analizar aquí.

Así, a clase SpaceStation ten varias responsabilidades (ou tarefas) diferentes. Todos eles pódense dividir en tipos:

  • sensores;
  • subministracións (consumibles);
  • combustible;
  • aceleradores.

Aínda que a ningún dos empregados da estación se lle asigna unha clase, podemos imaxinar facilmente quen é o responsable de que. O máis probable é que o científico controle os sensores, o loxístico encárgase de subministrar os recursos, o enxeñeiro sexa responsable do abastecemento de combustible e o piloto controle os impulsores.

Podemos dicir que este programa non é compatible con SRP? Si, seguro. Pero a clase SpaceStation é un típico "obxecto deus" que o sabe todo e fai todo. Este é un antipatrón importante na programación orientada a obxectos. Para un principiante, estes obxectos son moi difíciles de manter. Polo de agora o programa é moi sinxelo, si, pero imaxinade o que pasará se lle engadimos novas funcións. Quizais a nosa estación espacial necesite unha estación médica ou unha sala de reunións. E cantas máis funcións haxa, máis crecerá SpaceStation. Ben, dado que esta instalación estará conectada con outras, o servizo de todo o complexo será aínda máis difícil. Como resultado, podemos interromper o funcionamento de, por exemplo, aceleradores. Se un investigador solicita cambios nos sensores, isto podería afectar moi ben aos sistemas de comunicacións da estación.

Violar o principio SRP pode dar unha vitoria táctica a curto prazo, pero ao final "perderemos a guerra" e será moi difícil manter un monstro así no futuro. É mellor dividir o programa en seccións separadas de código, cada unha das cales é responsable de realizar unha operación específica. Entendendo isto, imos cambiar a clase de SpaceStation.

Repartimos a responsabilidade

Arriba definimos catro tipos de operacións que están controladas pola clase SpaceStation. Teremos en conta á hora de refactorizar. O código actualizado coincide mellor co 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

Hai moitos cambios, o programa definitivamente parece mellor agora. Agora a nosa clase SpaceStation converteuse nun recipiente no que se inician operacións para pezas dependentes, incluíndo un conxunto de sensores, un sistema de subministración de consumibles, un depósito de combustible e refuerzos.

Para calquera das variables hai agora unha clase correspondente: Sensores; SupplyHold; FuelTank; Propulsores.

Hai varios cambios importantes nesta versión do código. A cuestión é que as funcións individuais non só están encapsuladas nas súas propias clases, senón que están organizadas de xeito que se fan previsibles e coherentes. Agrupamos elementos cunha funcionalidade similar para seguir o principio de coherencia. Agora, se necesitamos cambiar o xeito no que funciona o sistema, pasando dunha estrutura hash a unha matriz, só tes que usar a clase SupplyHold; non temos que tocar outros módulos. Deste xeito, se o responsable de loxística modifica algo na súa sección, o resto da estación permanecerá intacto. Neste caso, a clase SpaceStation nin sequera será consciente dos cambios.

Os nosos axentes que traballan na estación espacial probablemente estean satisfeitos cos cambios porque poden solicitar os que necesiten. Teña en conta que o código ten métodos como report_supplies e report_fuel contidos nas clases SupplyHold e FuelTank. Que pasaría se a Terra pedise cambiar a forma de informar? As dúas clases, SupplyHold e FuelTank, terán que cambiarse. E se precisa cambiar a forma de entrega de combustible e consumibles? Probablemente teñas que cambiar todas as mesmas clases de novo. E isto xa é unha violación do principio SRP. Imos arranxar isto.

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 versión do programa, as responsabilidades dividíronse en dúas novas clases, FuelReporter e SupplyReporter. Ambos son fillos da clase Reporteiro. Ademais, engadimos variables de instancia á clase SpaceStation para que se poida inicializar a subclase desexada se é necesario. Agora, se a Terra decide cambiar algo máis, entón faremos cambios nas subclases, e non na clase principal.

Por suposto, algunhas das nosas clases aínda dependen unhas das outras. Así, o obxecto SupplyReporter depende de SupplyHold e FuelReporter depende de FuelTank. Por suposto, os boosters deben estar conectados ao depósito de combustible. Pero aquí todo xa parece lóxico e facer cambios non será especialmente difícil: editar o código dun obxecto non afectará moito a outro.

Así, creamos un código modular onde se definen con precisión as responsabilidades de cada un dos obxectos/clases. Traballar con tal código non é un problema, mantelo será unha tarefa sinxela. Convertimos todo o "obxecto divino" en SRP.

Skillbox recomenda:

Fonte: www.habr.com

Engadir un comentario