Escribir código flexible usando SOLID

Escribir código flexible usando SOLID

Desde el traductor: publicado para ti artículo de Severin Pérez sobre el uso de principios SOLID en programación. La información del artículo será útil tanto para principiantes como para programadores experimentados.

Si le gusta el desarrollo, probablemente haya oído hablar de los principios SOLID. Permiten al programador escribir código limpio, bien estructurado y de fácil mantenimiento. Vale la pena señalar que en programación existen varios enfoques sobre cómo realizar correctamente un trabajo en particular. Diferentes especialistas tienen diferentes ideas y comprensiones sobre el “camino correcto”; todo depende de la experiencia de cada persona. Sin embargo, las ideas proclamadas en SOLID son aceptadas por casi todos los representantes de la comunidad de TI. Se convirtieron en el punto de partida para el surgimiento y desarrollo de muchas buenas prácticas de gestión del desarrollo.

Entendamos qué son los principios SOLID y cómo nos ayudan.

Skillbox recomienda: Curso práctico "Desarrollador móvil PRO".

Recordamos: para todos los lectores de "Habr": un descuento de 10 rublos al inscribirse en cualquier curso de Skillbox utilizando el código promocional "Habr".

¿Qué es SÓLIDO?

Este término es una abreviatura, cada letra del término es el comienzo del nombre de un principio específico:

  • SPrincipio de Responsabilidad Única. Un módulo puede tener un solo motivo de cambio.
  • El Opluma/principio cerrado (principio abierto/cerrado). Las clases y otros elementos deben estar abiertos a la extensión, pero cerrados a la modificación.
  •  El LPrincipio de sustitución de iskov (Principio de sustitución de Liskov). Las funciones que usan un tipo base deberían poder usar subtipos del tipo base sin saberlo.
  • El IPrincipio de segregación de interfaz  (principio de separación de interfaces). Las entidades de software no deberían depender de métodos que no utilizan.
  • El DPrincipio de inversión de dependencia (principio de inversión de dependencia). Los módulos de niveles superiores no deberían depender de módulos de niveles inferiores.

Principio de responsabilidad única


El Principio de Responsabilidad Única (SRP) establece que cada clase o módulo de un programa debe ser responsable de sólo una parte de la funcionalidad de ese programa. Además, los elementos de esta responsabilidad deben asignarse a su propia clase, en lugar de distribuirse entre clases no relacionadas. El desarrollador y principal evangelista de SRP, Robert S. Martin, describe la responsabilidad como la razón del cambio. Originalmente propuso este término como uno de los elementos de su trabajo "Principios del diseño orientado a objetos". El concepto incorpora gran parte del patrón de conectividad definido previamente por Tom DeMarco.

El concepto también incluía varios conceptos formulados por David Parnas. Los dos principales son la encapsulación y la ocultación de información. Parnas argumentó que dividir un sistema en módulos separados no debería basarse en el análisis de diagramas de bloques o flujos de ejecución. Cualquiera de los módulos debe contener una solución específica que proporcione un mínimo de información a los clientes.

Por cierto, Martin dio un ejemplo interesante con altos directivos de una empresa (COO, CTO, CFO), cada uno de los cuales utiliza un software empresarial específico para diferentes propósitos. Como resultado, cualquiera de ellos puede implementar cambios en el software sin afectar los intereses de otros administradores.

Objeto divino

Como siempre, la mejor manera de aprender SRP es verlo en acción. Veamos una sección del programa que NO sigue el Principio de Responsabilidad Única. Este es el código Ruby que describe el comportamiento y los atributos de la estación espacial.

Revise el ejemplo e intente determinar lo siguiente:
Responsabilidades de aquellos objetos que se declaran en la clase SpaceStation.
Quienes puedan estar interesados ​​en el funcionamiento de la 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 realidad, nuestra estación espacial no funciona (no creo que reciba una llamada de la NASA pronto), pero hay algo que analizar aquí.

Por lo tanto, la clase SpaceStation tiene varias responsabilidades (o tareas) diferentes. Todos ellos se pueden dividir en tipos:

  • sensores;
  • suministros (consumibles);
  • combustible;
  • aceleradores.

Aunque a ninguno de los empleados de la estación se le asigna una clase, podemos imaginar fácilmente quién es responsable de qué. Lo más probable es que el científico controle los sensores, el logístico sea responsable del suministro de recursos, el ingeniero sea responsable del suministro de combustible y el piloto controle los propulsores.

¿Podemos decir que este programa no cumple con SRP? Si seguro. Pero la clase SpaceStation es un típico "objeto divino" que lo sabe todo y lo hace todo. Este es un antipatrón importante en la programación orientada a objetos. Para un principiante, estos objetos son extremadamente difíciles de mantener. Hasta aquí el programa es muy sencillo, sí, pero imagina lo que pasará si añadimos nuevas funciones. Quizás nuestra estación espacial necesite un puesto médico o una sala de reuniones. Y cuantas más funciones haya, más crecerá la SpaceStation. Bueno, dado que esta instalación estará conectada a otras, el mantenimiento de todo el complejo será aún más difícil. Como resultado, podemos alterar el funcionamiento de, por ejemplo, los aceleradores. Si un investigador solicita cambios en los sensores, esto podría afectar los sistemas de comunicaciones de la estación.

Violar el principio SRP puede dar una victoria táctica a corto plazo, pero al final “perderemos la guerra” y será muy difícil mantener semejante monstruo en el futuro. Es mejor dividir el programa en secciones de código separadas, cada una de las cuales es responsable de realizar una operación específica. Entendiendo esto, cambiemos la clase de SpaceStation.

Distribuyamos la responsabilidad

Arriba definimos cuatro tipos de operaciones controladas por la clase SpaceStation. Los tendremos en cuenta al refactorizar. El código actualizado coincide mejor con el 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

Hay muchos cambios, el programa definitivamente se ve mejor ahora. Ahora nuestra clase SpaceStation se ha convertido más en un contenedor en el que se inician operaciones para piezas dependientes, incluido un conjunto de sensores, un sistema de suministro de consumibles, un tanque de combustible y propulsores.

Para cualquiera de las variables ahora existe una clase correspondiente: Sensores; Retención de suministro; Depósito de combustible; Propulsores.

Hay varios cambios importantes en esta versión del código. La cuestión es que las funciones individuales no sólo están encapsuladas en sus propias clases, sino que están organizadas de tal manera que se vuelven predecibles y consistentes. Agrupamos elementos con funcionalidad similar para seguir el principio de coherencia. Ahora, si necesitamos cambiar la forma en que funciona el sistema, pasando de una estructura hash a una matriz, simplemente use la clase SupplyHold; no tenemos que tocar otros módulos. De esta forma, si el responsable de logística cambia algo en su sección, el resto de la estación permanecerá intacto. En este caso, la clase SpaceStation ni siquiera se enterará de los cambios.

Nuestros oficiales que trabajan en la estación espacial probablemente estén contentos con los cambios porque pueden solicitar los que necesiten. Observe que el código tiene métodos como report_supplies y report_fuel contenidos en las clases SupplyHold y FuelTank. ¿Qué pasaría si la Tierra pidiera cambiar la forma en que informa? Será necesario cambiar ambas clases, SupplyHold y FuelTank. ¿Qué sucede si necesita cambiar la forma en que se entregan el combustible y los consumibles? Probablemente tendrás que volver a cambiar las mismas clases. Y esto ya es una violación del principio SRP. Arreglemos esto.

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.

En esta última versión del programa, las responsabilidades se han dividido en dos nuevas clases, FuelReporter y SupplyReporter. Ambos son hijos de la clase Reportero. Además, agregamos variables de instancia a la clase SpaceStation para que la subclase deseada pueda inicializarse si es necesario. Ahora bien, si la Tierra decide cambiar algo más, entonces haremos cambios en las subclases y no en la clase principal.

Por supuesto, algunas de nuestras clases todavía dependen unas de otras. Por tanto, el objeto SupplyReporter depende de SupplyHold y FuelReporter depende de FuelTank. Por supuesto, los propulsores deben estar conectados al tanque de combustible. Pero aquí todo parece lógico y hacer cambios no será particularmente difícil: editar el código de un objeto no afectará mucho a otro.

Así, hemos creado un código modular donde se definen con precisión las responsabilidades de cada uno de los objetos/clases. Trabajar con dicho código no es un problema, mantenerlo será una tarea sencilla. Hemos convertido todo el “objeto divino” en SRP.

Skillbox recomienda:

Fuente: habr.com

Añadir un comentario