Escrivim codi flexible amb SOLID

Escrivim codi flexible amb SOLID

Del traductor: publicat per a tu article de Severin Pérez sobre l'ús dels principis SOLID en la programació. La informació de l'article serà útil tant per a programadors principiants com per a programadors experimentats.

Si estàs interessat en el desenvolupament, és probable que hagis sentit parlar dels principis SOLID. Permeten al programador escriure codi net, ben estructurat i fàcil de mantenir. Val la pena assenyalar que en la programació hi ha diversos enfocaments sobre com realitzar correctament una feina determinada. Els diferents especialistes tenen diferents idees i comprensió del "camí correcte"; tot depèn de l'experiència de cada persona. Tanmateix, les idees proclamades a SOLID són acceptades per gairebé tots els representants de la comunitat informàtica. Es van convertir en el punt de partida per a l'aparició i desenvolupament de moltes bones pràctiques de gestió del desenvolupament.

Entenem quins són els principis SOLID i com ens ajuden.

Skillbox recomana: Curs pràctic "Desenvolupador mòbil PRO".

Recordem: per a tots els lectors de "Habr": un descompte de 10 rubles en inscriure's a qualsevol curs de Skillbox amb el codi promocional "Habr".

Què és SOLID?

Aquest terme és una abreviatura, cada lletra del terme és el començament del nom d'un principi específic:

  • SPrincipi de responsabilitat única. Un mòdul pot tenir un i només un motiu de canvi.
  • El Oploma/Principi tancat (principi obert/tancat). Les classes i altres elements haurien d'estar oberts per ampliar, però tancats per a modificacions.
  •  El LPrincipi de substitució d'iskov (principi de substitució de Liskov). Les funcions que utilitzen un tipus base haurien de poder utilitzar subtipus del tipus base sense saber-ho.
  • El IPrincipi de segregació de la interfície  (principi de separació de la interfície). Les entitats de programari no haurien de dependre de mètodes que no utilitzen.
  • El DPrincipi d'inversió de dependència (principi d'inversió de dependència). Els mòduls de nivells superiors no haurien de dependre dels mòduls de nivells inferiors.

Principi de responsabilitat única


El Principi de Responsabilitat Única (SRP) estableix que cada classe o mòdul d'un programa hauria de ser responsable només d'una part de la funcionalitat d'aquest programa. A més, els elements d'aquesta responsabilitat s'han d'assignar a la seva pròpia classe, en lloc de dispersar-se per classes no relacionades. El desenvolupador i evangelista en cap de SRP, Robert S. Martin, descriu la responsabilitat com la raó del canvi. Originalment va proposar aquest terme com un dels elements de la seva obra "Principis de disseny orientat a objectes". El concepte incorpora gran part del patró de connectivitat que havia definit anteriorment Tom DeMarco.

El concepte també incloïa diversos conceptes formulats per David Parnas. Els dos principals són l'encapsulació i l'amagat d'informació. Parnas va argumentar que dividir un sistema en mòduls separats no s'hauria de basar en l'anàlisi de diagrames de blocs o fluxos d'execució. Qualsevol dels mòduls ha de contenir una solució específica que proporcioni un mínim d'informació als clients.

Per cert, Martin va donar un exemple interessant amb els alts directius d'una empresa (COO, CTO, CFO), cadascun dels quals utilitza programari empresarial específic per a diferents finalitats. Com a resultat, qualsevol d'ells pot implementar canvis en el programari sense afectar els interessos d'altres gestors.

Objecte diví

Com sempre, la millor manera d'aprendre SRP és veure'l en acció. Vegem una secció del programa que NO segueix el Principi de Responsabilitat Única. Aquest és el codi Ruby que descriu el comportament i els atributs de l'estació espacial.

Revisa l'exemple i intenta determinar el següent:
Responsabilitats d'aquells objectes que es declaren a la classe SpaceStation.
Aquells que puguin estar interessats en el funcionament de l'estació 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

De fet, la nostra estació espacial és disfuncional (no crec que rebré una trucada de la NASA aviat), però hi ha alguna cosa a analitzar aquí.

Per tant, la classe SpaceStation té diverses responsabilitats (o tasques) diferents. Tots ells es poden dividir en tipus:

  • sensors;
  • subministraments (consumibles);
  • combustible;
  • acceleradors.

Tot i que cap dels empleats de l'estació té assignada una classe, podem imaginar fàcilment qui és el responsable de què. Molt probablement, el científic controla els sensors, el logístic és responsable del subministrament de recursos, l'enginyer és responsable del subministrament de combustible i el pilot controla els impulsors.

Podem dir que aquest programa no és compatible amb SRP? Si, es clar. Però la classe SpaceStation és un típic "objecte de Déu" que ho sap tot i ho fa tot. Aquest és un antipatró important en la programació orientada a objectes. Per a un principiant, aquests objectes són extremadament difícils de mantenir. Fins ara el programa és molt senzill, sí, però imagineu què passarà si afegim noves funcionalitats. Potser la nostra estació espacial necessitarà una estació mèdica o una sala de reunions. I com més funcions hi hagi, més creixerà SpaceStation. Bé, com que aquesta instal·lació estarà connectada amb altres, el servei de tot el complex serà encara més difícil. Com a resultat, podem interrompre el funcionament de, per exemple, acceleradors. Si un investigador sol·licita canvis als sensors, això podria afectar molt bé els sistemes de comunicacions de l'estació.

La violació del principi SRP pot donar una victòria tàctica a curt termini, però al final "perdrem la guerra" i serà molt difícil mantenir aquest monstre en el futur. El millor és dividir el programa en seccions de codi separades, cadascuna de les quals és responsable de realitzar una operació específica. Entenent això, canviem la classe SpaceStation.

Repartim la responsabilitat

Més amunt hem definit quatre tipus d'operacions que estan controlades per la classe SpaceStation. Els tindrem en compte a l'hora de refactoritzar. El codi actualitzat coincideix millor amb l'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

Hi ha molts canvis, el programa definitivament es veu millor ara. Ara la nostra classe SpaceStation s'ha convertit més en un contenidor en què s'inicien operacions per a peces dependents, com ara un conjunt de sensors, un sistema de subministrament de consumibles, un dipòsit de combustible i reforçadors.

Per a qualsevol de les variables ara hi ha una classe corresponent: Sensors; SupplyHold; Tanc de combustible; Propulsadors.

Hi ha diversos canvis importants en aquesta versió del codi. La qüestió és que les funcions individuals no només estan encapsulades en les seves pròpies classes, sinó que s'organitzen de manera que esdevinguin predictibles i coherents. Agrupem elements amb una funcionalitat similar per seguir el principi de coherència. Ara, si hem de canviar la forma en què funciona el sistema, passant d'una estructura hash a una matriu, només cal que utilitzeu la classe SupplyHold; no hem de tocar altres mòduls. D'aquesta manera, si el responsable de logística canvia alguna cosa a la seva secció, la resta de l'estació es mantindrà intacta. En aquest cas, la classe SpaceStation ni tan sols serà conscient dels canvis.

Els nostres oficials que treballen a l'estació espacial probablement estan contents amb els canvis perquè poden demanar els que necessiten. Tingueu en compte que el codi té mètodes com report_supplies i report_fuel continguts a les classes SupplyHold i FuelTank. Què passaria si la Terra demanés canviar la forma en què informa? Les dues classes, SupplyHold i FuelTank, hauran de canviar. Què passa si cal canviar la manera de lliurar el combustible i els consumibles? Probablement haureu de tornar a canviar totes les mateixes classes. I això ja és una violació del principi SRP. Arreglem això.

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 aquesta darrera versió del programa, les responsabilitats s'han dividit en dues noves classes, FuelReporter i SupplyReporter. Tots dos són fills de la classe Reporter. A més, hem afegit variables d'instància a la classe SpaceStation perquè la subclasse desitjada es pugui inicialitzar si cal. Ara, si la Terra decideix canviar alguna cosa més, farem canvis a les subclasses, i no a la classe principal.

Per descomptat, algunes de les nostres classes encara depenen les unes de les altres. Per tant, l'objecte SupplyReporter depèn de SupplyHold i FuelReporter depèn de FuelTank. Per descomptat, els boosters s'han de connectar al dipòsit de combustible. Però aquí tot ja sembla lògic i fer canvis no serà especialment difícil: editar el codi d'un objecte no afectarà gaire a un altre.

Així, hem creat un codi modular on es defineixen amb precisió les responsabilitats de cadascun dels objectes/classes. Treballar amb aquest codi no és un problema, mantenir-lo serà una tasca senzilla. Hem convertit tot l'"objecte diví" en SRP.

Skillbox recomana:

Font: www.habr.com

Afegeix comentari