Scrierea unui cod flexibil folosind SOLID

Scrierea unui cod flexibil folosind SOLID

De la traducător: publicat pentru tine articol de Severin Perez despre utilizarea principiilor SOLID în programare. Informațiile din articol vor fi utile atât pentru începători, cât și pentru programatori experimentați.

Dacă sunteți în dezvoltare, cel mai probabil ați auzit de principiile SOLID. Ele permit programatorului să scrie cod curat, bine structurat și ușor de întreținut. Este demn de remarcat faptul că în programare există mai multe abordări ale modului de a efectua corect o anumită lucrare. Diferiți specialiști au idei și înțelegere diferite despre „calea cea bună”; totul depinde de experiența fiecărei persoane. Cu toate acestea, ideile proclamate în SOLID sunt acceptate de aproape toți reprezentanții comunității IT. Ele au devenit punctul de plecare pentru apariția și dezvoltarea multor bune practici de management al dezvoltării.

Să înțelegem care sunt principiile SOLID și cum ne ajută ele.

Skillbox recomandă: Curs practic „Dezvoltator mobil PRO”.

Amintim: pentru toți cititorii „Habr” - o reducere de 10 de ruble la înscrierea la orice curs Skillbox folosind codul promoțional „Habr”.

Ce este SOLID?

Acest termen este o abreviere, fiecare literă a termenului este începutul numelui unui principiu specific:

  • SPrincipiul responsabilității. Un modul poate avea un singur motiv pentru schimbare.
  • Ostilou/Principiul închis (principiul deschis/închis). Clasele și alte elemente ar trebui să fie deschise pentru extindere, dar închise pentru modificare.
  • LIskov Principiul substituirii (principiul substituției Liskov). Funcțiile care folosesc un tip de bază ar trebui să poată folosi subtipuri ale tipului de bază fără să știe.
  • IPrincipiul segregării interfeței  (principiul de separare a interfeței). Entitățile software nu ar trebui să depindă de metodele pe care nu le folosesc.
  • DPrincipiul inversării dependenței (principiul inversării dependenței). Modulele de la nivelurile superioare nu ar trebui să depindă de modulele de la nivelurile inferioare.

Principiul responsabilității unice


Principiul responsabilității unice (SRP) afirmă că fiecare clasă sau modul dintr-un program ar trebui să fie responsabilă doar pentru o parte a funcționalității acelui program. În plus, elementele acestei responsabilități ar trebui să fie atribuite propriei clase, mai degrabă decât să fie împrăștiate în clase care nu au legătură. Dezvoltatorul SRP și evanghelistul șef, Robert S. Martin, descrie responsabilitatea drept motivul schimbării. El a propus inițial acest termen ca unul dintre elementele lucrării sale „Principii ale designului orientat pe obiecte”. Conceptul încorporează o mare parte din modelul de conectivitate care a fost definit anterior de Tom DeMarco.

Conceptul a inclus și câteva concepte formulate de David Parnas. Cele două principale sunt încapsularea ș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 bloc sau a fluxurilor de execuție. Oricare dintre module trebuie să conțină o soluție specifică care să ofere un minim de informații clienților.

Apropo, Martin a dat un exemplu interesant cu manageri superiori ai unei companii (COO, CTO, CFO), fiecare dintre ei folosește software de afaceri specific în scopuri diferite. Drept urmare, oricare dintre ei poate implementa modificări în software fără a afecta interesele altor manageri.

Obiect divin

Ca întotdeauna, cel mai bun mod de a învăța SRP este să-l vezi în acțiune. Să ne uităm la o secțiune a programului care NU urmează principiul responsabilității unice. Acesta este codul Ruby care descrie comportamentul și atributele stației spațiale.

Examinați exemplul și încercați să determinați următoarele:
Responsabilitățile acelor obiecte care sunt declarate î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} 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 fapt, stația noastră spațială este disfuncțională (nu cred că voi primi un apel de la NASA în curând), dar este ceva de analizat aici.

Astfel, clasa SpaceStation are mai multe responsabilități (sau sarcini) diferite. Toate pot fi împărțite în tipuri:

  • senzori;
  • consumabile;
  • combustibil;
  • acceleratoare.

Chiar dacă niciunul dintre angajații postului nu are repartizată o clasă, ne putem imagina cu ușurință cine este responsabil pentru ce. Cel mai probabil, omul de știință controlează senzorii, logisticianul este responsabil pentru furnizarea resurselor, inginerul este responsabil pentru aprovizionarea cu combustibil, iar pilotul controlează amplificatoarele.

Putem spune că acest program nu este compatibil SRP? Da sigur. Dar clasa SpaceStation este un „obiect zeu” tipic care știe totul și face totul. Acesta este un anti-model major în programarea orientată pe obiecte. Pentru un incepator, astfel de obiecte sunt extrem de greu de intretinut. Până acum programul este foarte simplu, da, dar imaginați-vă ce se va întâmpla dacă adăugăm noi funcții. Poate că stația noastră spațială va avea nevoie de o stație medicală sau de o sală de ședințe. Și cu cât există mai multe funcții, cu atât SpaceStation va crește mai mult. Ei bine, deoarece această facilitate va fi conectată la altele, deservirea întregului complex va deveni și mai complexă. Drept urmare, putem perturba funcționarea, de exemplu, a acceleratoarelor. Dacă un cercetător solicită modificări ale senzorilor, acest lucru ar putea afecta foarte bine sistemele de comunicații ale stației.

Încălcarea principiului SRP poate da o victorie tactică pe termen scurt, dar în final vom „pierde războiul” și va deveni foarte dificil să menținem un astfel de monstru în viitor. Cel mai bine este să împărțiți programul în secțiuni separate de cod, fiecare dintre ele fiind responsabilă pentru efectuarea unei operații specifice. Înțelegând acest lucru, să schimbăm clasa SpaceStation.

Să distribuim responsabilitatea

Mai sus am definit patru tipuri de operațiuni care sunt controlate de clasa SpaceStation. Le vom ține cont atunci când refactorizăm. Codul actualizat se potrivește mai bine cu 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

Sunt multe schimbări, programul cu siguranță arată mai bine acum. Acum, clasa noastră SpaceStation a devenit mai mult un container în care sunt inițiate operațiuni pentru părți dependente, inclusiv un set de senzori, un sistem de alimentare cu consumabile, un rezervor de combustibil și boosters.

Pentru oricare dintre variabile există acum o clasă corespunzătoare: Senzori; SupplyHold; Rezervor de combustibil; Propulsoare.

Există mai multe modificări importante în această versiune a codului. Ideea este că funcțiile individuale nu sunt doar încapsulate în propriile lor clase, ci sunt organizate în așa fel încât să devină previzibile și consistente. Grupăm elemente cu funcționalitate similară pentru a urma principiul coerenței. Acum, dacă trebuie să schimbăm modul în care funcționează sistemul, trecând de la o structură hash la o matrice, trebuie doar să folosim clasa SupplyHold; nu trebuie să atingem alte module. Astfel, dacă ofițerul de logistică schimbă ceva în secția sa, restul stației va rămâne intact. În acest caz, clasa SpaceStation nici măcar nu va fi conștientă de modificări.

Ofițerii noștri care lucrează la stația spațială sunt probabil mulțumiți de schimbări, deoarece le pot solicita pe cele de care au nevoie. Observați că codul are metode precum report_supplies și report_fuel conținute în clasele SupplyHold și FuelTank. Ce s-ar întâmpla dacă Pământul ar cere să schimbe modul în care raportează? Ambele clase, SupplyHold și FuelTank, vor trebui schimbate. Ce se întâmplă dacă trebuie să schimbați modul de livrare a combustibilului și a consumabilelor? Probabil va trebui să schimbați din nou aceleași clase. Și aceasta este deja o încălcare a principiului SRP. Să reparăm asta.

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.

În această ultimă versiune a programului, responsabilitățile au fost împărțite în două clase noi, FuelReporter și SupplyReporter. Ambii sunt copii ai clasei Reporter. În plus, am adăugat variabile de instanță la clasa SpaceStation, astfel încât subclasa dorită să poată fi inițializată dacă este necesar. Acum, dacă Pământul decide să schimbe altceva, atunci vom face modificări la subclase, și nu la clasa principală.

Desigur, unele dintre clasele noastre încă depind unele de altele. Astfel, obiectul SupplyReporter depinde de SupplyHold, iar FuelReporter depinde de FuelTank. Desigur, booster-urile trebuie conectate la rezervorul de combustibil. Dar aici totul pare deja logic, iar efectuarea modificărilor nu va fi deosebit de dificilă - editarea codului unui obiect nu va afecta foarte mult altul.

Astfel, am creat un cod modular în care responsabilitățile fiecăruia dintre obiecte/clase sunt definite cu precizie. Lucrul cu un astfel de cod nu este o problemă, menținerea acestuia va fi o sarcină simplă. Am convertit întregul „obiect divin” în SRP.

Skillbox recomandă:

Sursa: www.habr.com

Adauga un comentariu