
A fordítótól: megosztottunk önnek 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 .
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.
- (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.
- (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.
- (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.
- (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
endValó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
endSok 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:
- Kétéves gyakorlati tanfolyam .
- Online kurzus .
- Gyakorlati éves kurzus .
Forrás: habr.com
