Scrivemu codice flessibile cù SOLID

Scrivemu codice flessibile cù SOLID

Da u traduttore: publicatu per voi articulu di Severin Perez circa l'usu di i principii SOLID in a prugrammazione. L'infurmazioni da l'articulu seranu utili à i principianti è i programatori sperimentati.

Sè vo site in u sviluppu, probabilmente avete intesu parlà di i principii SOLID. Permettenu à u programatore di scrive un codice pulito, ben strutturatu è faciule da mantene. Hè da nutà chì in a prugrammazione ci sò parechji approcci à cumu fà currettamente un travagliu particulari. Diversi spezialisti anu idee diverse è capiscenu a "strada ghjusta"; tuttu dipende di l'esperienza di ogni persona. Tuttavia, l'idee proclamate in SOLID sò accettate da quasi tutti i rapprisentanti di a cumunità IT. Sò diventati u puntu di partenza per l'emergenza è u sviluppu di parechje pratiche di gestione di u sviluppu bè.

Capemu ciò chì sò i principii SOLID è cumu ci aiutanu.

Skillbox consiglia: Corso praticu "Sviluppatore Mobile PRO".

Ramintemu: per tutti i lettori di "Habr" - un scontu di 10 000 rubles quandu si iscrizzione in ogni cursu Skillbox cù u codice promozionale "Habr".

Cosa hè SOLID?

Stu termu hè una abbreviazione, ogni lettera di u termu hè u principiu di u nome di un principiu specificu:

  • SPrincipiu di rispunsabilità. Un modulu pò avè una è una sola ragione per cambià.
  • lu OPenna / Principiu Chiusu (principiu apertu / chjusu). E classi è altri elementi devenu esse aperti per estensione, ma chjusi per mudificazione.
  •  lu LIskov Principiu di Sustituzione (principiu di sustituzione di Liskov). E funzioni chì utilizanu un tipu di basa duveranu esse capaci di utilizà sottotipi di u tipu di basa senza sapè.
  • lu IPrincipiu di Segregazione di l'interfaccia  (principiu di separazione di l'interfaccia). L'entità software ùn deve micca dipende di i metudi chì ùn anu micca utilizatu.
  • lu DPrincipiu d'inversione di dipendenza (principiu di l'inversione di dependenza). I moduli à i livelli più alti ùn deve micca dipende di i moduli à i livelli più bassi.

Principiu di Responsabilità Unicu


U Principiu di Responsabilità Unicu (SRP) dice chì ogni classa o modulu in un prugramma deve esse rispunsevuli di una sola parte di e funziunalità di quellu prugramma. Inoltre, elementi di sta rispunsabilità deve esse attribuiti à a so propria classa, piuttostu cà spargugliati in classi senza relazione. U sviluppatore di SRP è u capu evangelista, Robert S. Martin, descrive a responsabilità cum'è u mutivu di u cambiamentu. Hè prupostu urigginariamenti stu terminu cum'è unu di l'elementi di u so travagliu "Principi di u Design Orientatu à l'Ughjettu". U cuncettu incorpora assai di u mudellu di cunnessione chì era definitu prima da Tom DeMarco.

U cuncettu include ancu parechji cuncetti formulati da David Parnas. I dui principali sò l'incapsulazione è l'ocultazione di l'infurmazioni. Parnas hà sustinutu chì a divisione di un sistema in moduli separati ùn deve esse basatu annantu à l'analisi di i diagrammi di blocchi o di i flussi di esecuzione. Qualchidunu di i moduli deve cuntene una suluzione specifica chì furnisce un minimu di informazioni à i clienti.

Per via, Martin hà datu un esempiu interessante cù i dirigenti di l'impresa (COO, CTO, CFO), ognunu di quale usa un software di cummerciale specificu per diversi scopi. In u risultatu, qualsiasi di elli ponu implementà cambiamenti in u software senza affettà l'interessi di l'altri gestori.

Ughjettu divinu

Cum'è sempre, u megliu modu per amparà SRP hè di vede in azzione. Fighjemu una sezione di u prugramma chì ùn seguita micca u Principiu di Responsabilità Unicu. Questu hè u codice Ruby chì descrive u cumpurtamentu è l'attributi di a stazione spaziale.

Revisate l'esempiu è pruvate à determinà i seguenti:
Responsabilità di quelli oggetti chì sò dichjarati in a classa SpaceStation.
Quelli chì ponu esse interessatu in u funziunamentu di a stazione spaziale.

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

In verità, a nostra stazione spaziale hè disfunzionale (ùn pensu micca chì riceveraghju una chjamata da a NASA prestu), ma ci hè qualcosa da analizà quì.

Cusì, a classa SpaceStation hà parechje rispunsabilità (o compiti). Tutti ponu esse divisu in tipi:

  • sensori;
  • forniture (consumables);
  • carburante;
  • acceleratori.

Ancu s'è nimu di l'impiegati di a stazione sò assignati una classa, pudemu facilmente imagine quale hè rispunsevule per ciò chì. Probabilmente, u scientist cuntrola i sensori, u logisticu hè rispunsevule per furnisce i risorse, l'ingegnere hè rispunsevuli di u suministru di carburante, è u pilotu cuntrolla i boosters.

Pudemu dì chì stu prugramma ùn hè micca conforme à SRP? Iè, sicuru. Ma a classa SpaceStation hè un "ughjettu di Diu" tipicu chì sapi tuttu è face tuttu. Questu hè un grande anti-pattern in a prugrammazione orientata à l'ughjettu. Per un principiante, tali oggetti sò assai difficiuli di mantene. Finu a ora, u prugramma hè assai simplice, sì, ma imaginate ciò chì succede si aghjunghjenu novi funziunalità. Forse a nostra stazione spaziale avarà bisognu di una stazione medica o di una sala di riunioni. E più funzioni ci sò, più SpaceStation cresce. Ebbè, postu chì sta facilità serà cunnessa cù l'altri, u serviziu di tuttu u cumplessu diventerà ancu più difficiule. In u risultatu, pudemu disturbà u funziunamentu di, per esempiu, acceleratori. Se un ricercatore dumanda cambiamenti à i sensori, questu puderia influenzà assai bè i sistemi di cumunicazione di a stazione.

A violazione di u principiu di SRP pò dà una vittoria tattica à cortu termine, ma à a fine "perderemu a guerra", è diventerà assai difficiuli di mantene un tali mostru in u futuru. Hè megliu di dividisce u prugramma in sezzioni separati di codice, ognuna di quale hè rispunsevuli di fà una operazione specifica. Capiscendu questu, cambiemu a classa SpaceStation.

Distribuemu a rispunsabilità

Sopra avemu definitu quattru tipi di operazioni chì sò cuntrullati da a classa SpaceStation. Avemu da tene à mente quandu refactoring. U codice aghjurnatu currisponde megliu à u 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

Ci sò assai cambiamenti, u prugramma hè definitu megliu avà. Avà a nostra classa SpaceStation hè diventata più di un cuntainer in quale l'operazioni sò iniziate per e parte dipendente, cumpresu un set di sensori, un sistema di fornitura di cunsumu, un tank di carburante è boosters.

Per qualsiasi di e variàbili ci hè avà una classa currispundente: Sensors; SupplyHold; FuelTank; I propulsori.

Ci hè parechje cambiamenti impurtanti in questa versione di u codice. U puntu hè chì e funzioni individuali ùn sò micca solu incapsulate in i so classi, sò urganizati in modu chì diventanu prevedibile è coherente. Raggruppemu elementi cù funziunalità simili per seguità u principiu di coherenza. Avà, s'ellu ci vole à cambià u modu di travagliu di u sistema, passendu da una struttura di hash à un array, basta aduprà a classa SupplyHold; ùn avemu micca toccu altri moduli. Questu modu, se l'uffiziale di logistica cambia qualcosa in a so sezione, u restu di a stazione ferma intactu. In questu casu, a classa SpaceStation ùn serà ancu cunzignata di i cambiamenti.

I nostri ufficiali chì travaglianu in a stazione spaziale sò prubabilmente felici di i cambiamenti perchè ponu dumandà quelli chì anu bisognu. Avete chì u codice hà metudi cum'è report_supplies è report_fuel cuntenuti in e classi SupplyHold è FuelTank. Chì succederebbe se a Terra dumandassi di cambià a manera di raporta ? E duie classi, SupplyHold è FuelTank, anu da esse cambiate. E s'è avete bisognu di cambià a manera di furnisce u carburante è i consumabili? Probabilmente avete da cambià tutte e stesse classi di novu. È questu hè digià una violazione di u principiu SRP. Fixemu questu.

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.

In questa ultima versione di u prugramma, i rispunsabilità sò stati divisi in duie classi novi, FuelReporter è SupplyReporter. Sò tramindui figlioli di a classa Reporter. Inoltre, avemu aghjustatu variabili di istanza à a classa SpaceStation in modu chì a subclasse desiderata pò esse inizializzata se ne necessariu. Avà, se a Terra decide di cambià qualcosa altru, allora faremu cambiamenti à i subclassi, è micca à a classa principale.

Di sicuru, alcune di e nostre classi dipendenu sempre l'una di l'altru. Cusì, l'ughjettu SupplyReporter dipende di SupplyHold, è FuelReporter dipende di FuelTank. Di sicuru, i boosters deve esse cunnessi à u tank di carburante. Ma quì tuttu pare digià logicu, è fà cambiamenti ùn serà micca particularmente difficiule - edità u codice di un ughjettu ùn avarà micca assai un altru.

Cusì, avemu creatu un codice modulare induve e rispunsabilità di ognuna di l'uggetti / classi sò precisamente definite. U travagliu cù tali codice ùn hè micca un prublema, mantenenu serà un compitu simplice. Avemu cunvertitu tuttu u "ughjettu divinu" in SRP.

Skillbox consiglia:

Source: www.habr.com

Add a comment