
Del traductor: publicamos para ti 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 .
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:
- SPrincipio de Responsabilidad Única. Un módulo puede tener una y solo una razón para cambiar.
- (principio de apertura/cierre). Las clases y otros elementos deben estar abiertos a la extensión, pero cerrados a la modificación.
- (principio de sustitución de Liskov). Las funciones que utilizan un tipo base deberían poder usar los subtipos de ese tipo base sin saberlo.
- (principio de segregación de interfaces). Las entidades de software no deberían depender de métodos que no utilizan.
- (principio de inversión de dependencias). Los módulos de alto nivel no deberían depender de los módulos de bajo nivel.
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
endEn 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
endHan 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:
- Curso práctico de dos años .
- Curso en línea .
- Curso práctico de un año .
Fuente: habr.com
