Scriem cod flexibil, folosind SOLID

Scriem cod flexibil, folosind SOLID

Din partea traducătorului: am publicat pentru tine articolul lui Severin Peres despre utilizarea principiilor SOLID în programare. Informațiile din articol vor fi utile atât începătorilor, cât și programatorilor cu experiență.

Dacă te ocupi de dezvoltare, atunci cel mai probabil ai auzit despre principiile SOLID. Acestea permit programatorilor să scrie cod curat, bine structurat și ușor de întreținut. Se cuvine să menționăm că în programare există mai multe abordări referitoare la cum să îndeplinești corect o anumită sarcină. Specialiști diferiți au idei și înțelegeri diferite despre „calea corectă”, totul depinde de experiența fiecăruia. Cu toate acestea, ideile proclamate în SOLID sunt acceptate de aproape toți membrii comunității IT. Ele au devenit punctul de plecare pentru apariția și dezvoltarea multor metode bune de gestionare a dezvoltării.

Hai să ne lămurim ce sunt principiile SOLID și cum ne ajută.

Skillbox recomandă: Curs practic «Dezvoltator mobil PRO».

Vă reamintim: pentru toți cititorii „Habr” — reducere de 10.000 de ruble la înscrierea la orice curs Skillbox cu codul de promovare „Habr”.

Ce este SOLID?

Acest termen este un acronim, fiecare literă a termenului fiind începutul numelui unui principiu specific:

  • SSingle Responsibility Principle (principiul responsabilității unice). Un modul poate avea un singur motiv pentru a fi modificat.
  • The OOpen/Closed Principle (principiul deschiderii/închiderii). Clasele și alte elemente trebuie să fie deschise pentru extindere, dar închise pentru modificare.
  •  The LLiskov Substitution Principle (principiul substituției Liskov). Funcțiile care utilizează tipul de bază trebuie să poată utiliza subtipuri ale tipului de bază, fără a ști despre aceasta.
  • The IInterface Segregation Principle  (principiul segregării interfeței). Entitățile software nu ar trebui să depindă de metodele pe care nu le utilizează.
  • The DDependency Inversion Principle (principiul inversării dependențelor). Modulele de nivel superior nu ar trebui să depindă de modulele de nivel inferior.

Principiul responsabilității unice

 
Principiul single responsibility (SRP) afirmă că fiecare clasă sau modul dintr-un program ar trebui să fie responsabil doar pentru oSingură parte a funcționalității acestui program. În plus, elementele acestei responsabilități ar trebui să fie asociate cu clasa lor, nu distribuite între clase nesfârșite. Dezvoltatorul și principalul evanghelist SRP, Robert C. Martin, descrie responsabilitatea ca fiind motivul schimbărilor. La început, el a propus acest termen ca unul dintre elementele lucrării sale „Principii de proiectare orientată pe obiect”. Conceptul a inclus multe din legile coeziunii, care au fost definite anterior de Tom DeMarco.

În plus, conceptul a inclus câteva noțiuni formulate de David Parnas. Cele două principale sunt incapsularea și ascunderea informațiilor. Parnas a susținut că împărțirea unui sistem în module separate nu ar trebui să se bazeze pe analiza diagramelor de flux sau a fluxurilor de execuție. Oricare dintre module ar trebui să conțină o soluție specifică care oferă un minim de informații clienților.

Apropo, Martin a adus un exemplu interesant cu managerii de vârf ai companiei (COO, CTO, CFO), fiecare dintre ei folosind un software specific pentru afaceri cu scopuri diferite. În cele din urmă, oricare dintre ei poate efectua modificări în software fără a afecta interesele altor managers.

Obiect divin

Ca de obicei, cea mai bună modalitate de a învăța SRP este să vezi totul în acțiune. Să ne uităm la un segment de program care NU respectă principiul responsabilității unitare. Acesta este un cod Ruby care descrie comportamentul și atributele unei stații spațiale.

Consultați exemplul și încercați să determinați următoarele:
Responsabilitățile obiectelor proclamate în clasa SpaceStation.
Cei care ar putea fi interesați de funcționarea stației spațiale.

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} disponibile: #{quantity} unități"
      end
    else
      puts "Depozitul de provizii este gol."
    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} unități de combustibil disponibile."
  end
 
  def activate_thrusters
    puts "----- Thruster Action -----"
    if @fuel >= 10
      puts "Acțiune de propulsie reușită."
      @fuel -= 10
    else
      puts "Eroare la propulsie: Combustibil insuficient disponibil."
    end
  end
end

Ei bine, stația noastră spațială nu este funcțională (cred că nu voi primi un apel de la NASA în viitorul apropiat), dar există mult de analizat aici.

Așadar, clasa SpaceStation are mai multe responsabilități (sau sarcini) diferite. Toate acestea pot fi împărțite pe tipuri:

  • senzori;
  • provizionare (consumabile);
  • combustibil;
  • propulsoare.

Deși nimeni dintre angajații stației nu este definit în clasă, ne putem imagina cu ușurință cine se ocupă de ce. Cel mai probabil, cercetătorul monitorizează senzorii, logisticianul se ocupă de aprovizionare, inginerul răspunde de stocurile de combustibil, iar pilotul controlează propulsoarele.

Putem spune că acest program nu respectă SRP? Da, desigur. Dar clasa SpaceStation este un tipic „obiect divin” care știe totul și face totul. Acesta este un model anti-exemplar principal în programarea orientată pe obiect. Pentru un novice, astfel de obiecte sunt extrem de complexe în întreținere. Deocamdată, programul este foarte simplu, da, dar imaginați-vă ce s-ar întâmpla dacă am adăuga noi funcții. Poate că stației noastre de spațiu îi va trebui o unitate medicală sau o sală de conferințe. Și cu cât vor fi mai multe funcții, cu atât SpaceStation va crește mai mult. Ei bine, având în vedere că acest obiect va fi conectat cu altele, întreținerea întregului complex va deveni și mai complexă. În cele din urmă, putem compromite funcționarea, de exemplu, a acceleratorilor. Dacă un cercetător solicită modificări în manipularea senzorilor, acest lucru ar putea afecta sistemele de comunicare ale stației.

Încălcarea principiului SRP poate aduce o victorie tactică pe termen scurt, dar în cele din urmă vom „pierde războiul”, întreținerea unui astfel de monstru în viitor va deveni destul de dificilă. Cel mai bine este să împărțim programul în segmente de cod separate, fiecare dintre ele responsabil pentru executarea unei operații specifice. Înțelegând acest lucru, să modificăm clasa SpaceStation.

Să distribuiți responsabilitatea

Mai sus, am definit patru tipuri de operațiuni care sunt controlate de clasa SpaceStation. Când efectuăm refactorizarea, le vom avea în vedere. Codul actualizat respectă mai bine 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} available: #{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

Au fost multe modificări, iar programul arată cu siguranță mai bine acum. Clasa noastră SpaceStation a devenit mai mult un container în care se inițiază operațiuni pentru componenta dependentă, inclusiv un set de senzori, un sistem de aprovizionare cu consumabile, un rezervor de combustibil și propulsoare.

Pentru fiecare dintre variabile există acum o clasă corespunzătoare: Sensors; SupplyHold; FuelTank; Thrusters.

Această versiune a codului conține câteva modificări importante. Problema este că funcțiile separate nu doar că sunt închise în propriile clase, ci sunt organizate într-un mod care le face previzibile și coerente. Grupăm elementele cu funcționalitate similară pentru a urma principiul coeziunii. Acum, dacă va trebui să modificăm principiul de funcționare al sistemului, trecând de la o structură de hash la un array, pur și simplu vom folosi clasa SupplyHold, fără a afecta celelalte module. Astfel, dacă ofițerul responsabil cu logistica face o modificare în secțiunea sa, celelalte elemente ale stației vor rămâne neafectate. În acest fel, clasa SpaceStation nu va fi nici măcar la curent cu modificările.

Ofițerii noștri care lucrează pe stația spațială sunt probabil bucuroși de aceste modificări, deoarece pot solicita ceea ce le este necesar. Rețineți că în cod există metode precum report_supplies și report_fuel, în clasele SupplyHold și FuelTank. Ce se va întâmpla dacă Pământul cere să schimbăm modul de generare a raporturilor? Va trebui să modificăm ambele clase, SupplyHold și FuelTank. Și ce se va întâmpla dacă trebuie să schimbăm modul de livrare a combustibilului și a consumabilelor? Probabil va trebui să modificăm din nou aceleași clase. Asta deja încalcă principiul SRP. Să corectăm acest lucru.

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 "----- Acțiune senzor -----"
    puts "Rularea senzorilor!"
  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 "----- Acțiune aprovizionare -----"
    puts "Încărcarea a #{quantity} unități de #{type} în depozitul de aprovizionare."
    
    if @supplies[type]
      @supplies[type] += quantity
    else
      @supplies[type] = quantity
    end
  end
 
  def use_supplies(type, quantity)
    puts "----- Acțiune aprovizionare -----"
    if @supplies[type] != nil && @supplies[type] > quantity
      puts "Folosind #{quantity} din #{type} din depozitul de aprovizionare."
      @supplies[type] -= quantity
    else
      puts "Eroare aprovizionare: #{type} insuficient în depozitul de aprovizionare."
    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 "----- Acțiune combustibil -----"
    puts "Încărcarea a #{quantity} unități de combustibil în rezervor."
    @fuel += quantity
  end
 
  def use_fuel(quantity)
    puts "----- Acțiune combustibil -----"
    puts "Folosind #{quantity} unități de combustibil din rezervor."
    @fuel -= quantity
  end
end
 
class Thrusters
  FUEL_PER_THRUST = 10
 
  def initialize(fuel_tank)
    @linked_fuel_tank = fuel_tank
  end
 
  def activate_thrusters
    puts "----- Acțiune propulsie -----"
    
    if @linked_fuel_tank.get_fuel_levels >= FUEL_PER_THRUST
      puts "Acțiune de propulsie realizată cu succes."
      @linked_fuel_tank.use_fuel(FUEL_PER_THRUST)
    else
      puts "Eroare propulsie: combustibil insuficient disponibil."
    end
  end
end
 
class Reporter
  def initialize(item, type)
    @linked_item = item
    @type = type
  end
 
  def report
    puts "----- Raport #{@type.capitalize} -----"
  end
end
 
class FuelReporter < Reporter
  def initialize(item)
    super(item, "combustibil")
  end
 
  def report
    super
    puts "#{@linked_item.get_fuel_levels} unități de combustibil disponibile."
  end
end
 
class SupplyReporter  0
      @linked_item.get_supplies.each do |type, quantity|
        puts "#{type} disponibile: #{quantity} unități"
      end
    else
      puts "Depozitul de aprovizionare este gol."
    end
  end
end
 
iss = SpaceStation.new
 
iss.sensors.run_sensors
  # ----- Acțiune senzor -----
  # Rularea senzorilor!
 
iss.supply_hold.use_supplies("piese", 2)
  # ----- Acțiune aprovizionare -----
  # Eroare aprovizionare: insuficient piese în depozitul de aprovizionare.
iss.supply_hold.load_supplies("piese", 10)
  # ----- Acțiune aprovizionare -----
  # Încărcarea a 10 unități de piese în depozitul de aprovizionare.
iss.supply_hold.use_supplies("piese", 2)
  # ----- Acțiune aprovizionare -----
  # Folosind 2 din piese din depozitul de aprovizionare.
iss.supply_reporter.report
  # ----- Raport aprobat -----
  # piese disponibile: 8 unități
 
iss.thrusters.activate_thrusters
  # ----- Acțiune propulsie -----
  # Eroare propulsie: combustibil insuficient disponibil.
iss.fuel_tank.load_fuel(100)
  # ----- Acțiune combustibil -----
  # Încărcarea a 100 unități de combustibil în rezervor.
iss.thrusters.activate_thrusters
  # ----- Acțiune propulsie -----
  # Acțiune de propulsie realizată cu succes.
  # ----- Acțiune combustibil -----
  # Folosind 10 unități de combustibil din rezervor.
iss.fuel_reporter.report
  # ----- Raport combustibil -----
# 90 unități de combustibil disponibile.

În această, ultima versiune a programului, responsabilitățile au fost împărțite în două noi clase, FuelReporter și SupplyReporter. Ambele sunt subclase ale clasei Reporter. În plus, am adăugat variabile de instanță la clasa SpaceStation pentru a putea inițializa subclasa dorită dacă este necesar. Acum, dacă Pământul decide să schimbe ceva, vom aduce modificări subclaselor, nu clasei de bază.

Desigur, unele clase încă depind unele de altele. De exemplu, obiectul SupplyReporter depinde de SupplyHold, iar FuelReporter depinde de FuelTank. Firesc, acceleratoarele trebuie să fie legate de rezervorul de combustibil. Însă acum totul pare logic, iar efectuarea modificărilor nu va fi deosebit de complicată — editarea codului unui obiect nu va afecta semnificativ pe altul.

Astfel, am creat un cod modular, unde responsabilitățile fiecărui obiect/clasă sunt clar definite. Lucrul cu un astfel de cod nu este o problemă, iar întreținerea acestuia va fi o sarcină simplă. Întreaga «entitate divină» a fost transformată în SRP.

Skillbox recomandă:

Sursa: habr.com

Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS 🔥 Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS | ProHoster