Rugalmas kódot írunk a SOLID elvek figyelembevételével

Rugalmas kódot írunk a SOLID elvek figyelembevételével

A fordítótól: megosztottunk önnek Severin Pérez cikkét a SOLID elvek programozásban való alkalmazásáról. A cikkből származó információk hasznosak lesznek mind a kezdők, mind a tapasztalt programozók számára.

Ha fejlesztéssel foglalkozik, valószínűleg már hallott a SOLID elvekről. Ezek lehetővé teszik a programozók számára, hogy tiszta, jól struktúrált és könnyen karbantartható kódot írjanak. Érdemes megjegyezni, hogy a programozásban számos megközelítés létezik arra, hogyan kell helyesen végezni a különböző feladatokat. Különböző szakembereknek – eltérő ötleteik és elképzeléseik vannak arról, mi a "helyes út", ami minden esetben az egyén tapasztalatától függ. Mindazonáltal a SOLID-ban megfogalmazott ötleteket az IT-közösség szinte minden képviselője elfogadja. Ezek alapot adtak számos jó fejlesztési menedzsment módszer megjelenésének és fejlődésének.

Nézzük meg, mik is a SOLID elvek és hogyan segítenek bennünket.

A Skillbox ajánlja: Gyakorlati kurzus «Mobilfejlesztő PRO».

Emlékeztetjük: minden «Habr» olvasónak 10 000 rubeles kedvezmény jár bármely Skillbox kurzus jelentkezésekor a «Habr» promóciós kód felhasználásával.

Mi az a SOLID?

Ez a kifejezés egy betűszó, amelynek minden betűje egy adott elv nevét kezdi:

  • SSingle Responsibility Principle (egyetlen felelősség elve). Egy modulnak csak egy és csak egy oka lehet a megváltoztatására.
  • A OOpen/Closed Principle (megnyitás/zárás elve). A osztályok és egyéb elemek nyitva kell álljanak a bővítés előtt, de zárva kell legyenek a módosítás előtt.
  •  A LLiskov Substitution Principle (Liskov helyettesítési elve). Azok a funkciók, amelyek az alap típust használják, képesek kell legyenek alap típustól származó altípusokat használni anélkül, hogy tudomásuk lenne róla.
  • A IInterface Segregation Principle (interfész szétválasztás elve). A programozási entitásoknak nem szabad függeniük azoktól a módszerektől, amelyeket nem használnak.
  • A DDependency Inversion Principle (függőségek megfordításának elve). A magas szintű moduloknak nem szabad függeniük az alacsony szintű moduloktól.

Egységes felelősség elve

 
A felelősség egységes elve (SRP) azt állítja, hogy minden osztály vagy modul a programban csak egy funkciós részért felelős. Továbbá, e felelősség elemeit annak az osztálynak kell fenntartania, amelyhez tartozik, nem pedig különböző, egymástól független osztályok között kell elosztania. A fejlesztő és az SRP fő evangelizálója, Robert C. Martin a felelősséget a változás okának tekinti. Ezt a kifejezést eredetileg a „Objektum-orientált tervezési elvek” című munkájának egyik elemeként javasolta. A koncepció a korábban Tom DeMarco által definiált kohéziós törvények számos aspektusát magában foglalja.

A koncepcióba számos fogalom is bekerült, amelyeket David Parnas fogalmazott meg. Két alapvető a kapszulázás és az információelrejtés. Parnas állította, hogy a rendszert külön modulokra való felosztása nem alapulhat blokkdiagramok vagy végrehajtási folyamatok elemzésén. Bármely modulnak tartalmaznia kell egy adott megoldást, amely minimális információt biztosít az ügyfelek számára.

Egyébként Martin egy érdekes példát említett a vállalat felsővezetőiről (COO, CTO, CFO), akik mindannyian különböző célokra használnak specifikus üzleti szoftvereket. Ennek eredményeként bármelyikük bevezethet változtatásokat a szoftverben anélkül, hogy a többi vezető érdekeit érintené.

Isteni objektum

Mint általában, a legjobb módja az SRP tanulmányozásának, ha mindent akcióban látunk. Nézzünk meg egy olyan programrészletet, amely NEM felel meg az egységes felelősség elvének. Ez egy Ruby-kód, amely a űrállomás viselkedését és attribútumait írja le.

Nézze meg a példát, és próbálja meg meghatározni a következőket:
A SpaceStation osztályban deklarált objektumok felelősségeit.
Azokat, akiket érdekelhet a űrállomás működése.

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} available: #{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

Valójában az űrállomásunk nem működik (gondolom, hogy nem fogok telefonhívást kapni a NASA-tól a közeljövőben), de van itt mit elemezni.

Tehát a SpaceStation osztálynak több különböző felelőssége (vagy feladata) van. Ezek mind különböző típusokra bonthatók:

  • érzékelők;
  • ellátás (fogyóeszközök);
  • üzemanyag;
  • tömkedők.

Bár a stáb tagjai nincsenek meghatározva az osztályban, könnyedén elképzelhetjük, ki miért felelős. Valószínűleg a tudós irányítja az érzékelőket, a logisztikus felel az erőforrások ellátásáért, a mérnök az üzemanyagtartalékokért, míg a pilóta a tömkedőket irányítja.

Mondhatjuk, hogy ez a program nem felel meg az SRP-nek? Igen, természetesen. De a SpaceStation osztály egy tipikus "isteni objektum", amely mindent tud és mindent megcsinál. Ez az alapvető ellensémablon az objektum-orientált programozásban. A kezdők számára az ilyen objektumok rendkívül bonyolultak a karbantartás szempontjából. Jelenleg a program nagyon egyszerű, igen, de képzeljük el, mi történik, ha új funkciókat adunk hozzá. Lehet, hogy a űrállomásnak szüksége lesz orvosi rendelőre vagy konferenciateremre. És minél több funkciót adunk hozzá, annál bonyolultabbá válik a SpaceStation. Mivel ez az objektum másokkal is összekapcsolódik, az egész komplexum karbantartása még nehezebb lesz. Végül előfordulhat, hogy megszegjük a például a gyorsítók működését. Ha a tudományos munkatárs módosításokat kér a szenzorokkal való munkához, az nyilvánvalóan befolyásolhatja az űrállomás kommunikációs rendszereit.

Az SRP-elv megsértése rövid távú taktikai győzelmet hozhat, de végül "elveszítjük a háborút", mivel az ilyen szörnyeteg karbantartása a jövőben nagyon nehéz lesz. A legjobb, ha a programot különálló kódblokkra bontjuk, mindegyik a bizonyos műveletek végrehajtásáért felel. Ezt megértve, módosítsuk a SpaceStation osztályt.

Odaítéljük a felelősséget

Fent négy olyan műveletet határoztunk meg, amelyeket a SpaceStation osztály irányít. A refaktorálás során ezt szem előtt tartjuk. A frissített kód jobban megfelel az SRP-nek.

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 "----- Szenzor Akció -----"
    puts "Szenzorok futnak!"
  end
end
 
class SupplyHold
  attr_accessor :supplies
 
  def initialize
    @supplies = {}
  end
 
  def load_supplies(type, quantity)
    puts "----- Beszerzési Akció -----"
    puts "Betöltés: #{quantity} egység #{type} a készletbe."
    
    if @supplies[type]
      @supplies[type] += quantity
    else
      @supplies[type] = quantity
    end
  end
 
  def use_supplies(type, quantity)
    puts "----- Beszerzési Akció -----"
    if @supplies[type] != nil && @supplies[type] > quantity
      puts "#{quantity} #{type} felhasználása a készletből."
      @supplies[type] -= quantity
    else
      puts "Készlet Hiba: Insufficient #{type} in the supply hold."
    end
  end
 
  def report_supplies
    puts "----- Készletjelentés -----"
    if @supplies.keys.length > 0
      @supplies.each do |type, quantity|
        puts "#{type} elérhető: #{quantity} egység"
      end
    else
      puts "A készlet üres."
    end
  end
end
 
class FuelTank
  attr_accessor :fuel
 
  def initialize
    @fuel = 0
  end
 
  def get_fuel_levels
    @fuel
  end
 
  def load_fuel(quantity)
    puts "----- Tüzelőanyag Akció -----"
    puts "Betöltés: #{quantity} egység tüzelőanyag a tankba."
    @fuel += quantity
  end
 
  def use_fuel(quantity)
    puts "----- Tüzelőanyag Akció -----"
    puts "#{quantity} egység tüzelőanyag felhasználása a tankból."
    @fuel -= quantity
  end
 
  def report_fuel
    puts "----- Tüzelőanyag Jelentés -----"
    puts "#{@fuel} egység tüzelőanyag elérhető."
  end
end
 
class Thrusters
  def initialize(fuel_tank)
    @linked_fuel_tank = fuel_tank
  end
 
  def activate_thrusters
    puts "----- Hajtómű Akció -----"
    if @linked_fuel_tank.get_fuel_levels >= 10
      puts "Hajtóerő művelet sikeres."
      @linked_fuel_tank.use_fuel(10)
    else
      puts "Hajtómű Hiba: Insufficient fuel available."
    end
  end
end

Sok változás történt, a program most határozottan jobban néz ki. Most már a SpaceStation osztályunk inkább egy konténer, amelyben műveletek iniciálódnak a függő részekhez, beleértve a szenzorkészletet, az ellátás rendszerét, a tüzelőanyag-tartályt és a hajtóműveket.

Minden változóhoz most van egy megfelelő osztály: Sensors; SupplyHold; FuelTank; Thrusters.

Ez a kódverzió számos fontos változást hozott. Az a lényeg, hogy az egyes funkciók nemcsak saját osztályaikba vannak kapszulázva, hanem úgy is vannak megszervezve, hogy előrejelzhetőek és következetesek legyenek. A hasonló funkciójú elemeket csoportokba rendezzük, hogy betartsuk a koherencia elvét. Így most, ha meg kell változtatnunk a rendszer működési elvét, például hash-struktúráról tömbre váltanánk, egyszerűen használhatjuk a SupplyHold osztályt, anélkül hogy más modulokat kellene érintenünk. Tehát, ha a logisztikáért felelős tiszt ezt megváltoztatja a saját szekciójában, a többi állomás elem érintetlen marad. Ilyen módon a SpaceStation osztály még csak nem is tud a változásokról.

Űrállomáson dolgozó tisztjeink valószínűleg örülnek a változásoknak, mivel kérheti azokat, amik számukra szükségesek. Vegye figyelembe, hogy a kódban olyan metódusok találhatóak, mint a report_supplies és a report_fuel, melyek a SupplyHold és a FuelTank osztályokban vannak. Mi történik, ha a Föld megkérdőjelezi a jelentések kialakítási módját? Mindkét osztályt, a SupplyHold és a FuelTank osztályokat módosítani kell. És mi van akkor, ha a benzin és az ellátmány szállítási módját kell megváltoztatni? Valószínűleg újra az összes ugyanazon osztályt kell módosítani. Ez már megsérti az SRP elvet. Javítsuk ki ezt.

class űrállomás
  attr_reader :érzékelők, :készlet_tároló, :készlet_jelentő,
              :üzemanyag_tartály, :üzemanyag_jelentő, :hajtóművek
 
  def initialize
    @érzékelők = Érzékelők.new
    @készlet_tároló = KészletTároló.new
    @készlet_jelentő = KészletJelentő.new(@készlet_tároló)
    @üzemanyag_tartály = ÜzemanyagTartály.new
    @üzemanyag_jelentő = ÜzemanyagJelentő.new(@üzemanyag_tartály)
    @hajtóművek = Hajtóművek.new(@üzemanyag_tartály)
  end
end
 
class Érzékelők
  def futtatás_érzékelők
    puts "----- Érzékelő Művelet -----"
    puts "Érzékelők futtatása!"
  end
end
 
class KészletTároló
  attr_accessor :készletek
  attr_reader :jelentő
 
  def initialize
    @készletek = {}
  end
 
  def get_készletek
    @készletek
  end
 
  def load_készletek(típus, mennyiség)
    puts "----- Készlet Művelet -----"
    puts "Raktározás #{mennyiség} egységnyi #{típus}-t a készlet tárolóban."
    
    if @készletek[típus]
      @készletek[típus] += mennyiség
    else
      @készletek[típus] = mennyiség
    end
  end
 
  def use_készletek(típus, mennyiség)
    puts "----- Készlet Művelet -----"
    if @készletek[típus] != nil && @készletek[típus] > mennyiség
      puts "Használva #{mennyiség} egységnyi #{típus} a készlet tárolóból."
      @készletek[típus] -= mennyiség
    else
      puts "Készlet Hiba: Elégtelen #{típus} a készlet tárolóban."
    end
  end
end
 
class ÜzemanyagTartály
  attr_accessor :üzemanyag
  attr_reader :jelentő
 
  def initialize
    @üzemanyag = 0
  end
 
  def get_üzemanyag_szintek
    @üzemanyag
  end
 
  def load_üzemanyag(mennyiség)
    puts "----- Üzemanyag Művelet -----"
    puts "Raktározás #{mennyiség} egységnyi üzemanyag a tartályban."
    @üzemanyag += mennyiség
  end
 
  def use_üzemanyag(mennyiség)
    puts "----- Üzemanyag Művelet -----"
    puts "Használva #{mennyiség} egységnyi üzemanyag a tartályból."
    @üzemanyag -= mennyiség
  end
end
 
class Hajtóművek
  ÜZEMANYAG_EGY_HAJSÁSHOZ = 10
 
  def initialize(üzemanyag_tartály)
    @kapcsolt_üzemanyag_tartály = üzemanyag_tartály
  end
 
  def aktiválás_hajtóművek
    puts "----- Hajtómű Művelet -----"
    
    if @kapcsolt_üzemanyag_tartály.get_üzemanyag_szintek >= ÜZEMANYAG_EGY_HAJSÁSHOZ
      puts "Hajtási művelet sikeres."
      @kapcsolt_üzemanyag_tartály.use_üzemanyag(ÜZEMANYAG_EGY_HAJSÁSHOZ)
    else
      puts "Hajtómű Hiba: Elégtelen üzemanyag áll rendelkezésre."
    end
  end
end
 
class Jelentő
  def initialize(tárgy, típus)
    @kapcsolt_tárgy = tárgy
    @típus = típus
  end
 
  def report
    puts "----- #{@típus.capitalize} Jelentés -----"
  end
end
 
class ÜzemanyagJelentő < Jelentő
  def initialize(tárgy)
    super(tárgy, "üzemanyag")
  end
 
  def report
    super
    puts "#{@kapcsolt_tárgy.get_üzemanyag_szintek} egységnyi üzemanyag áll rendelkezésre."
  end
end
 
class KészletJelentő  0
      @kapcsolt_tárgy.get_készletek.each do |típus, mennyiség|
        puts "#{típus} elérhető: #{mennyiség} egység"
      end
    else
      puts "A készlet tároló üres."
    end
  end
end
 
iss = űrállomás.new
 
iss.érzékelők.futtatás_érzékelők
  # ----- Érzékelő Művelet -----
  # Érzékelők futtatása!
 
iss.készlet_tároló.use_készletek("alkatrészek", 2)
  # ----- Készlet Művelet -----
  # Készlet Hiba: Elégtelen alkatrész a készlet tárolóban.
iss.készlet_tároló.load_készletek("alkatrészek", 10)
  # ----- Készlet Művelet -----
  # Raktározás 10 egységnyi alkatrész-t a készlet tárolóban.
iss.készlet_tároló.use_készletek("alkatrészek", 2)
  # ----- Készlet Művelet -----
  # Használva 2 egységnyi alkatrész a készlet tárolóból.
iss.készlet_jelentő.report
  # ----- Készlet Jelentés -----
  # alkatrészek elérhetők: 8 egység
 
iss.hajtóművek.aktiválás_hajtóművek
  # ----- Hajtómű Művelet -----
  # Hajtómű Hiba: Elégtelen üzemanyag áll rendelkezésre.
iss.üzemanyag_tartály.load_üzemanyag(100)
  # ----- Üzemanyag Művelet -----
  # Raktározás 100 egységnyi üzemanyag a tartályban.
iss.hajtóművek.aktiválás_hajtóművek
  # ----- Hajtómű Művelet -----
  # Hajtási művelet sikeres.
  # ----- Üzemanyag Művelet -----
  # Használva 10 egységnyi üzemanyag a tartályból.
iss.üzemanyag_jelentő.report
  # ----- Üzemanyag Jelentés -----
# 90 egységnyi üzemanyag áll rendelkezésre.

A program legújabb verziójában a feladatok két új osztályra, a FuelReporter-re és a SupplyReporter-re lettek felosztva. Mindkettő a Reporter osztály gyermekeként működik. Ezen kívül példányváltozókat adtunk a SpaceStation osztályhoz, hogy szükség esetén inicializálni tudjuk a megfelelő alosztályt. Most, ha a Föld valamit meg akar változtatni, a módosításokat az alosztályokban végezzük el, nem pedig az alap osztályban.

Természetesen néhány osztályunk még mindig függ egymástól. Például a SupplyReporter objektum a SupplyHold-tól függ, míg a FuelReporter a FuelTank-ra támaszkodik. Nyilvánvaló, hogy a gyorsítóknak is kapcsolódniuk kell a üzemanyagtartályhoz. De ezúttal minden logikusnak tűnik, és a módosítások sem lesznek különösebben bonyolultak — egy objektum kódjának szerkesztése nem befolyásolja lényegesen a másikat.

Így moduláris kódot hoztunk létre, ahol minden objektum/óra feladatainak pontosan meghatározottak. Egy ilyen kód használata nem probléma, karbantartása egyszerű feladat lesz. Az egész „isteni objektumot” SRP-ra alakítottuk.

A Skillbox ajánlja:

Forrás: habr.com

Megbízható hosting vásárlása weboldalak számára, DDoS védelemmel, VPS VDS szerverek. 🔥 Megbízható webtárhely vásárlása DDoS védelemmel, VPS VDS szerverek | ProHoster