Od prevajalca: objavljeno za vas članek Severina Pereza o uporabi principov SOLID v programiranju. Informacije iz članka bodo koristne tako za začetnike kot za izkušene programerje.
Če se ukvarjate z razvojem, ste verjetno že slišali za načela SOLID. Programerju omogočajo pisanje čiste, dobro strukturirane kode, ki jo je enostavno vzdrževati. Omeniti velja, da v programiranju obstaja več pristopov, kako pravilno opraviti določeno delo. Različni strokovnjaki imajo različne predstave in razumevanje »prave poti«, vse je odvisno od izkušenj posameznika. Vendar pa ideje, ki jih razglaša SOLID, sprejemajo skoraj vsi predstavniki IT skupnosti. Postale so izhodišče za nastanek in razvoj številnih dobrih praks vodenja razvoja.
Razumejmo, kaj so načela SOLID in kako nam pomagajo.
Spomnimo:za vse bralce "Habr" - popust v višini 10 rubljev ob vpisu v kateri koli tečaj Skillbox s promocijsko kodo "Habr".
Kaj je SOLID?
Ta izraz je okrajšava, vsaka črka izraza je začetek imena določenega načela:
SNačelo odgovornosti. Modul ima lahko en in samo en razlog za spremembo.
O Opero/zaprto načelo (princip odprto/zaprto). Razredi in drugi elementi morajo biti odprti za razširitev, vendar zaprti za spreminjanje.
O Liskov substitucijski princip (načelo zamenjave po Liskovu). Funkcije, ki uporabljajo osnovni tip, bi morale imeti možnost uporabljati podtipe osnovnega tipa, ne da bi to vedele.
O INačelo ločevanja vmesnika (princip ločevanja vmesnikov). Entitete programske opreme ne bi smele biti odvisne od metod, ki jih ne uporabljajo.
O DNačelo inverzije odvisnosti (načelo inverzije odvisnosti). Moduli na višjih ravneh ne bi smeli biti odvisni od modulov na nižjih ravneh.
Načelo ene same odgovornosti
Načelo enotne odgovornosti (SRP) navaja, da mora biti vsak razred ali modul v programu odgovoren samo za en del funkcionalnosti tega programa. Poleg tega je treba elemente te odgovornosti dodeliti njihovemu lastnemu razredu, namesto da bi bili razpršeni po nepovezanih razredih. Razvijalec in glavni evangelizator SRP, Robert S. Martin, opisuje odgovornost kot razlog za spremembo. Ta izraz je prvotno predlagal kot enega od elementov svojega dela "Načela objektno usmerjenega oblikovanja". Koncept vključuje velik del vzorca povezljivosti, ki ga je predhodno opredelil Tom DeMarco.
Koncept je vključeval tudi več konceptov, ki jih je oblikoval David Parnas. Dva glavna sta enkapsulacija in skrivanje informacij. Parnas je trdil, da razdelitev sistema na ločene module ne bi smela temeljiti na analizi blokovnih diagramov ali tokov izvajanja. Vsak od modulov mora vsebovati specifično rešitev, ki strankam zagotavlja minimalne informacije.
Mimogrede, Martin je navedel zanimiv primer z višjimi menedžerji podjetja (COO, CTO, CFO), od katerih vsak uporablja določeno poslovno programsko opremo za različne namene. Posledično lahko kateri koli od njih izvede spremembe v programski opremi, ne da bi to vplivalo na interese drugih upravljavcev.
Božanski predmet
Kot vedno je najboljši način za učenje SRP ta, da ga vidite v akciji. Poglejmo del programa, ki NE sledi načelu enotne odgovornosti. To je koda Ruby, ki opisuje vedenje in lastnosti vesoljske postaje.
Preglejte primer in poskusite ugotoviti naslednje:
Odgovornosti tistih objektov, ki so deklarirani v razredu SpaceStation.
Tisti, ki bi jih morda zanimalo delovanje vesoljske postaje.
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
Pravzaprav naša vesoljska postaja ne deluje (mislim, da me NASA ne bo kmalu poklicala), vendar je tukaj nekaj za analizirati.
Tako ima razred SpaceStation več različnih odgovornosti (ali nalog). Vse jih lahko razdelimo na vrste:
senzorji;
zaloge (potrošni material);
gorivo;
pospeševalci.
Čeprav nihče od zaposlenih na postaji nima razreda, si zlahka predstavljamo, kdo je za kaj odgovoren. Najverjetneje znanstvenik nadzoruje senzorje, logist je odgovoren za oskrbo z viri, inženir je odgovoren za oskrbo z gorivom, pilot pa nadzoruje pospeševalnike.
Ali lahko rečemo, da ta program ni skladen s SRP? Ja seveda. Toda razred SpaceStation je tipičen "božji objekt", ki vse ve in naredi vse. To je glavni anti-vzorec v objektno usmerjenem programiranju. Za začetnika so takšni objekti izjemno težki za vzdrževanje. Zaenkrat je program zelo preprost, ja, ampak predstavljajte si, kaj se bo zgodilo, če dodamo nove funkcije. Morda bo naša vesoljska postaja potrebovala medicinsko postajo ali sejno sobo. In več ko bo funkcij, bolj bo SpaceStation rasel. No, ker bo ta objekt povezan z drugimi, bo servisiranje celotnega kompleksa še težje. Posledično lahko zmotimo delovanje na primer pospeševalnikov. Če raziskovalec zahteva spremembe na senzorjih, bi to lahko zelo vplivalo na komunikacijske sisteme postaje.
Kršitev načela SRP lahko prinese kratkoročno taktično zmago, a na koncu bomo »izgubili vojno« in takšno pošast bo v prihodnosti zelo težko vzdrževati. Najbolje je, da program razdelite na ločene dele kode, od katerih je vsak odgovoren za izvajanje določene operacije. Če to razumemo, spremenimo razred SpaceStation.
Razdelimo odgovornost
Zgoraj smo definirali štiri vrste operacij, ki jih nadzoruje razred SpaceStation. Pri refaktoriranju jih bomo upoštevali. Posodobljena koda se bolje ujema s 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
Sprememb je veliko, program zdaj zagotovo izgleda bolje. Zdaj je naš razred SpaceStation postal bolj vsebnik, v katerem se začnejo operacije za odvisne dele, vključno z nizom senzorjev, sistemom za oskrbo s potrošnim materialom, rezervoarjem za gorivo in ojačevalniki.
Za vsako od spremenljivk je zdaj ustrezen razred: Senzorji; SupplyHold; Rezervoar za gorivo; Potisni motorji.
V tej različici kode je več pomembnih sprememb. Bistvo je v tem, da posamezne funkcije niso samo enkapsulirane v svoje razrede, temveč so organizirane tako, da postanejo predvidljive in konsistentne. Združujemo elemente s podobno funkcionalnostjo, da sledimo načelu skladnosti. Zdaj, če moramo spremeniti način delovanja sistema in se premakniti iz zgoščene strukture v matriko, preprosto uporabimo razred SupplyHold; ni se nam treba dotikati drugih modulov. Na ta način, če logistik kaj spremeni v svojem delu, ostane preostali del postaje nedotaknjen. V tem primeru se razred SpaceStation sploh ne bo zavedal sprememb.
Naši uradniki, ki delajo na vesoljski postaji, so verjetno veseli sprememb, saj lahko zahtevajo tiste, ki jih potrebujejo. Upoštevajte, da ima koda metode, kot sta report_supplies in report_fuel, vsebovane v razredih SupplyHold in FuelTank. Kaj bi se zgodilo, če bi Zemlja zahtevala spremembo načina poročanja? Spremeniti bo treba oba razreda, SupplyHold in FuelTank. Kaj pa, če morate spremeniti način dostave goriva in potrošnega materiala? Verjetno boste morali znova zamenjati vse iste razrede. In to je že kršitev načela SRP. Popravimo to.
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.
V tej zadnji različici programa so bile odgovornosti razdeljene v dva nova razreda, FuelReporter in SupplyReporter. Oba sta otroka razreda Reporter. Poleg tega smo razredu SpaceStation dodali spremenljivke primerka, tako da je mogoče želeni podrazred po potrebi inicializirati. Zdaj, če se Zemlja odloči spremeniti nekaj drugega, bomo naredili spremembe v podrazredih in ne v glavnem razredu.
Seveda so nekateri naši razredi še vedno odvisni drug od drugega. Tako je predmet SupplyReporter odvisen od SupplyHold, FuelReporter pa od FuelTank. Seveda morajo biti ojačevalniki priključeni na rezervoar za gorivo. Toda tukaj je vse že videti logično in spreminjanje ne bo posebej težko - urejanje kode enega predmeta ne bo močno vplivalo na drugega.
Tako smo ustvarili modularno kodo, kjer so natančno določene odgovornosti vsakega od objektov/razredov. Delo s takšno kodo ni problem, njeno vzdrževanje bo preprosto opravilo. Celoten »božanski objekt« smo pretvorili v SRP.