Escribimos código flexible, utilizando SOLID

Escribimos código flexible, utilizando SOLID

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

Si te dedicas al desarrollo, probablemente hayas oído hablar de los principios SOLID. Estos permiten a los programadores escribir código limpio, bien estructurado y fácil de mantener. Es importante destacar que en programación existen varios enfoques sobre cómo realizar correctamente una tarea. Diferentes especialistas tienen distintas ideas y comprensiones del 'camino correcto', todo depende de la experiencia de cada uno. Sin embargo, las ideas proclamadas en SOLID son aceptadas prácticamente por toda la comunidad de TI. Se han convertido en el punto de partida para la aparición y desarrollo de numerosos buenos métodos de gestión del desarrollo.

Vamos a desglosar 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,000 rublos al registrarse en cualquier curso de Skillbox usando el código promocional «Habr».

¿Qué es SOLID?

Este término es un acrónimo, cada letra del término corresponde al inicio del nombre de un principio específico:

Principio de Responsabilidad Única

 
El principio de responsabilidad única (SRP) establece que cada clase o módulo en un programa debe ser responsable de una única parte de la funcionalidad de ese programa. Además, los elementos de esta responsabilidad deben estar asignados a su propia clase y no repartidos entre clases no relacionadas. El desarrollador y principal evangelista del SRP, Robert C. Martin, describe la responsabilidad como la razón del cambio. Inicialmente, propuso este término como uno de los elementos de su trabajo "Principios de diseño orientado a objetos". En el concepto se incluyó gran parte de la ley de cohesión, que fue definida anteriormente por Tom DeMarco.

Asimismo, el concepto incluyó varias nociones formuladas por David Parnas. Dos de las más importantes son la encapsulación y el ocultamiento de información. Parnas afirmó que la división de un sistema en módulos separados no debería basarse en análisis de diagramas de flujo o flujos de ejecución. Cada uno de los módulos debe contener una solución específica que proporcione la mínima información a los clientes.

Por cierto, Martin mencionó un ejemplo interesante con los altos directivos de una empresa (COO, CTO, CFO), cada uno de los cuales utiliza software específico para el negocio con diferentes objetivos. Como resultado, cualquiera de ellos puede implementar cambios en el software sin afectar los intereses de los demás directivos.

Objeto divino

Como siempre, la mejor manera de aprender sobre el SRP es verlo en acción. Echemos un vistazo a una parte del programa que NO cumple con el principio de responsabilidad única. Este es un código Ruby que describe el comportamiento y los atributos de una estación espacial.

Revise el ejemplo e intente determinar lo siguiente:
Las responsabilidades de los objetos que se declaran en la clase SpaceStation.
Aquellos que pueden estar interesados en el funcionamiento de la estación espacial.

class SpaceStation
  def initialize
    @supplies = {}
    @fuel = 0
  end
 
  def run_sensors
    puts "----- Acción del Sensor -----"
    puts "¡Ejecutando sensores!"
  end
 
  def load_supplies(type, quantity)
    puts "----- Acción de Suministro -----"
    puts "Cargando #{quantity} unidades de #{type} en el compartimento de suministros."
    
    if @supplies[type]
      @supplies[type] += quantity
    else
      @supplies[type] = quantity
    end
  end
 
  def use_supplies(type, quantity)
    puts "----- Acción de Suministro -----"
    if @supplies[type] != nil && @supplies[type] > quantity
      puts "Usando #{quantity} de #{type} del compartimento de suministros."
      @supplies[type] -= quantity
    else
      puts "Error de Suministro: Insuficiente #{type} en el compartimento de suministros."
    end
  end
 
  def report_supplies
    puts "----- Informe de Suministros -----"
    if @supplies.keys.length > 0
      @supplies.each do |type, quantity|
        puts "#{type} disponible: #{quantity} unidades"
      end
    else
      puts "El compartimento de suministros está vacío."
    end
  end
 
  def load_fuel(quantity)
    puts "----- Acción de Combustible -----"
    puts "Cargando #{quantity} unidades de combustible en el tanque."
    @fuel += quantity
  end
 
  def report_fuel
    puts "----- Informe de Combustible -----"
    puts "#{@fuel} unidades de combustible disponibles."
  end
 
  def activate_thrusters
    puts "----- Acción de Propulsores -----"
    if @fuel >= 10
      puts "Acción de propulsión exitosa."
      @fuel -= 10
    else
      puts "Error de Propulsor: Combustible insuficiente disponible."
    end
  end
end

En realidad, nuestra estación espacial no es funcional (pienso que no recibiré una llamada de la NASA en un futuro cercano), pero hay mucho que analizar aquí.

Así que, la clase SpaceStation tiene varias responsabilidades diferentes (o tareas). Todas pueden ser clasificadas por tipos:

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

A pesar de que ninguno de los empleados de la estación está definido en la clase, podemos imaginar fácilmente quién se encarga de qué. Probablemente, un científico controla los sensores, un logista se encarga del suministro de recursos, un ingeniero vela por las reservas de combustible, y un piloto maneja los propulsores.

¿Podemos decir que este programa no cumple con el SRP? Sí, por supuesto. Pero la clase SpaceStation es un típico "objeto divino" que lo sabe todo y lo hace todo. Este es el principal anti-patrón en programación orientada a objetos. Para un principiante, tales objetos son extremadamente difíciles de mantener. Por ahora el programa es muy simple, sí, pero imagina lo que sucederá si añadimos nuevas funciones. Tal vez nuestra estación espacial necesite un centro médico o una sala de reuniones. Y cuanto más funciones haya, más crecerá SpaceStation. Dado que este objeto estará vinculado a otros, el mantenimiento de todo el complejo se volverá aún más complicado. Al final, podríamos interrumpir, por ejemplo, los aceleradores. Si un investigador solicita cambios en el funcionamiento de los sensores, esto podría afectar los sistemas de comunicación de la estación.

Violar el principio SRP puede traer una victoria táctica a corto plazo, pero al final "perderemos la guerra", ya que será muy complicado mantener a ese monstruo en el futuro. Lo mejor es dividir el programa en secciones de código separadas, cada una de las cuales es responsable de realizar una operación específica. Con esto en mente, cambiemos la clase SpaceStation.

Distribuir la responsabilidad

Anteriormente definimos cuatro tipos de operaciones que son controladas por la clase SpaceStation. Al refactorizar, tendremos esto en cuenta. El código actualizado se ajusta mejor al 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 "----- Acción del Sensor -----"
    puts "¡Ejecutando sensores!"
  end
end
 
class SupplyHold
  attr_accessor :supplies
 
  def initialize
    @supplies = {}
  end
 
  def load_supplies(type, quantity)
    puts "----- Acción de Suministros -----"
    puts "Cargando #{quantity} unidades de #{type} en el almacenamiento de suministros."
    
    if @supplies[type]
      @supplies[type] += quantity
    else
      @supplies[type] = quantity
    end
  end
 
  def use_supplies(type, quantity)
    puts "----- Acción de Suministros -----"
    if @supplies[type] != nil && @supplies[type] > quantity
      puts "Usando #{quantity} de #{type} del almacenamiento de suministros."
      @supplies[type] -= quantity
    else
      puts "Error de Suministro: Insuficiente #{type} en el almacenamiento de suministros."
    end
  end
 
  def report_supplies
    puts "----- Informe de Suministros -----"
    if @supplies.keys.length > 0
      @supplies.each do |type, quantity|
        puts "#{type} disponible: #{quantity} unidades"
      end
    else
      puts "El almacenamiento de suministros está vacío."
    end
  end
end
 
class FuelTank
  attr_accessor :fuel
 
  def initialize
    @fuel = 0
  end
 
  def get_fuel_levels
    @fuel
  end
 
  def load_fuel(quantity)
    puts "----- Acción de Combustible -----"
    puts "Cargando #{quantity} unidades de combustible en el tanque."
    @fuel += quantity
  end
 
  def use_fuel(quantity)
    puts "----- Acción de Combustible -----"
    puts "Usando #{quantity} unidades de combustible del tanque."
    @fuel -= quantity
  end
 
  def report_fuel
    puts "----- Informe de Combustible -----"
    puts "#{@fuel} unidades de combustible disponibles."
  end
end
 
class Thrusters
  def initialize(fuel_tank)
    @linked_fuel_tank = fuel_tank
  end
 
  def activate_thrusters
    puts "----- Acción de Propulsores -----"
    if @linked_fuel_tank.get_fuel_levels >= 10
      puts "Acción de impulso exitosa."
      @linked_fuel_tank.use_fuel(10)
    else
      puts "Error de Propulsores: Combustible insuficiente disponible."
    end
  end
end

Han habido muchos cambios, el programa ahora se ve definitivamente mejor. Ahora nuestra clase SpaceStation se ha convertido, más bien, en un contenedor que inicia operaciones para partes dependientes, incluyendo un conjunto de sensores, un sistema de suministro de materiales, un tanque de combustible y propulsores.

Para cualquiera de las variables ahora hay una clase correspondiente: Sensors; SupplyHold; FuelTank; Thrusters.

En esta versión del código hay varios cambios importantes. El hecho es que las funciones individuales no solo están encapsuladas en sus propias clases, sino que están organizadas de tal manera que se vuelven predecibles y coherentes. Agrupamos elementos con funcionalidad similar para seguir el principio de cohesión. Ahora, si necesitamos modificar la forma en que funciona el sistema, pasando de una estructura de hash a un array, simplemente utilizamos la clase SupplyHold; no será necesario afectar otros módulos. Así, si el oficial encargado de logística modifica algo en su sección, los demás elementos de la estación permanecerán intactos. Además, la clase SpaceStation ni siquiera será consciente de los cambios.

Nuestros oficiales que trabajan en la estación espacial probablemente están contentos con los cambios, ya que pueden solicitar lo que realmente necesitan. Cabe destacar que en el código existen métodos como report_supplies y report_fuel que se encuentran en las clases SupplyHold y FuelTank. ¿Qué sucede si la Tierra solicita modificar la forma en que se generan los informes? Será necesario cambiar ambas clases, SupplyHold y FuelTank. ¿Y si se necesita modificar la forma de entrega de combustible y suministros? Probablemente habrá que modificar de nuevo todas esas mismas clases. Y eso ya sería una violación del principio SRP. Vamos a solucionarlo.

class EstaciónEspacial
  attr_reader :sensores, :suministro_almacen, :reportero_suministro,
              :tanque_combustible, :reportero_combustible, :cohetes

  def initialize
    @sensores = Sensores.new
    @suministro_almacen = SuministroAlmacen.new
    @reportero_suministro = ReporteroSuministro.new(@suministro_almacen)
    @tanque_combustible = TanqueCombustible.new
    @reportero_combustible = ReporteroCombustible.new(@tanque_combustible)
    @cohetes = Cohetes.new(@tanque_combustible)
  end
end

class Sensores
  def run_sensores
    puts "----- Acción de Sensor -----"
    puts "¡Ejecutando sensores!"
  end
end

class SuministroAlmacen
  attr_accessor :suministros
  attr_reader :reportero

  def initialize
    @suministros = {}
  end

  def get_suministros
    @suministros
  end

  def load_suministros(tipo, cantidad)
    puts "----- Acción de Suministro -----"
    puts "Cargando #{cantidad} unidades de #{tipo} en el almacén de suministros."

    if @suministros[tipo]
      @suministros[tipo] += cantidad
    else
      @suministros[tipo] = cantidad
    end
  end

  def use_suministros(tipo, cantidad)
    puts "----- Acción de Suministro -----"
    if @suministros[tipo] != nil && @suministros[tipo] > cantidad
      puts "Usando #{cantidad} de #{tipo} del almacén de suministros."
      @suministros[tipo] -= cantidad
    else
      puts "Error de Suministro: Suministros insuficientes de #{tipo} en el almacén."
    end
  end
end

class TanqueCombustible
  attr_accessor :combustible
  attr_reader :reportero

  def initialize
    @combustible = 0
  end

  def get_niveles_combustible
    @combustible
  end

  def load_combustible(cantidad)
    puts "----- Acción de Combustible -----"
    puts "Cargando #{cantidad} unidades de combustible en el tanque."
    @combustible += cantidad
  end

  def use_combustible(cantidad)
    puts "----- Acción de Combustible -----"
    puts "Usando #{cantidad} unidades de combustible del tanque."
    @combustible -= cantidad
  end
end

class Cohetes
  COMBUSTIBLE_POR_TURBO = 10

  def initialize(tanque_combustible)
    @tanque_combustible_vinculado = tanque_combustible
  end

  def activate_cohetes
    puts "----- Acción de Cohete -----"

    if @tanque_combustible_vinculado.get_niveles_combustible >= COMBUSTIBLE_POR_TURBO
      puts "Acción de empuje exitosa."
      @tanque_combustible_vinculado.use_combustible(COMBUSTIBLE_POR_TURBO)
    else
      puts "Error de Cohete: Combustible insuficiente disponible."
    end
  end
end

class Reportero
  def initialize(item, tipo)
    @item_vinculado = item
    @tipo = tipo
  end

  def report
    puts "----- Informe de #{@tipo.capitalize} -----"
  end
end

class ReporteroCombustible < Reportero
  def initialize(item)
    super(item, "combustible")
  end

  def report
    super
    puts "#{@item_vinculado.get_niveles_combustible} unidades de combustible disponibles."
  end
end

class ReporteroSuministro  0
      @item_vinculado.get_suministros.each do |tipo, cantidad|
        puts "#{tipo} disponibles: #{cantidad} unidades"
      end
    else
      puts "El almacén de suministros está vacío."
    end
  end
end

iss = EstaciónEspacial.new

iss.sensores.run_sensores
  # ----- Acción de Sensor -----
  # ¡Ejecutando sensores!

iss.suministro_almacen.use_suministros("piezas", 2)
  # ----- Acción de Suministro -----
  # Error de Suministro: Suministros insuficientes de piezas en el almacén.
iss.suministro_almacen.load_suministros("piezas", 10)
  # ----- Acción de Suministro -----
  # Cargando 10 unidades de piezas en el almacén.
iss.suministro_almacen.use_suministros("piezas", 2)
  # ----- Acción de Suministro -----
  # Usando 2 de piezas del almacén.
iss.reportero_suministro.report
  # ----- Informe de Suministro -----
  # piezas disponibles: 8 unidades

iss.cohetes.activate_cohetes
  # ----- Acción de Cohete -----
  # Error de Cohete: Combustible insuficiente disponible.
iss.tanque_combustible.load_combustible(100)
  # ----- Acción de Combustible -----
  # Cargando 100 unidades de combustible en el tanque.
iss.cohetes.activate_cohetes
  # ----- Acción de Cohete -----
  # Acción de empuje exitosa.
  # ----- Acción de Combustible -----
  # Usando 10 unidades de combustible del tanque.
iss.reportero_combustible.report
  # ----- Informe de Combustible -----
# 90 unidades de combustible disponibles.

En esta última versión del programa, las responsabilidades se han dividido en dos nuevas clases, FuelReporter y SupplyReporter. Ambas son subclases de la clase Reporter. Además, hemos añadido variables de instancia a la clase SpaceStation para poder inicializar la subclase adecuada cuando sea necesario. Ahora, si la Tierra decide cambiar algo más, haremos las modificaciones en las subclases, no en la clase base.

Por supuesto, todavía hay algunas clases que dependen unas de otras. Así, el objeto SupplyReporter depende de SupplyHold, y FuelReporter depende de FuelTank. Por supuesto, los impulsores deben estar relacionados con el tanque de combustible. Pero aquí todo parece lógico, y hacer cambios no será especialmente complicado: editar el código de un objeto no afectará demasiado a otro.

Así, hemos creado código modular, donde las responsabilidades de cada uno de los objetos/clases están claramente definidas. Trabajar con este código no es un problema, su mantenimiento será una tarea sencilla. Hemos transformado todo el "objeto divino" en SRP.

Skillbox recomienda:

Fuente: habr.com

Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS 🔥 Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS | ProHoster