
Od prevodnika: smo smo pustili za vas o upotrebi SOLID principa u programiranju. Informacije iz članka biće korisne kako za novajlije, tako i za iskusne programere.
Ako se bavite razvojem, verovatno ste čuli za SOLID principe. Oni omogućavaju programerima da pišu čist, dobro strukturiran i lako održiv kod. Vredi napomenuti da u programiranju postoje različiti pristupi tome kako pravilno obaviti određeni zadatak. Različiti stručnjaci imaju različite ideje i razumevanja 'pravilnog puta', sve zavisi od iskustva svakog pojedinca. Ipak, ideje postavljene u SOLID-u su prihvaćene od strane gotovo svih članova IT zajednice. Postale su polazna tačka za pojavu i razvoj mnogih dobrih metoda upravljanja razvojem.
Hajde da razjasnimo šta su SOLID principi i kako nam pomažu.
Skillbox priporoča: Praktični tečaj .
Opominjamo: za vse bralce »Habra« — popust 10 000 rubljev ob vpisu na kateri koli tečaj Skillbox s promo kodo »Habr«.
Šta je SOLID?
Ovaj termin je akronim, svako slovo u terminu označava početno slovo određenog principa:
- SPrincip jedinstvene odgovornosti. Modul može imati samo jedan razlog za promenu.
- The
- Princip inverzije zavisnosti. Moduli viših nivoa ne trebaju zavisiti od modula nižih nivoa.
- (načelo razdelitve vmesnika). Programske entitete ne bi smele biti odvisne od metod, ki jih ne uporabljajo.
- (načelo inverzije odvisnosti). Moduli višjih ravni ne bi smeli biti odvisni od modulov nižjih ravni.
Načelo enotne odgovornosti
Načelo jedinstvene odgovornosti (SRP) kaže da svaka klasa ili modul u programu treba da bude odgovoran samo za jedan deo funkcionalnosti tog programa. Takođe, elementi te odgovornosti trebaju biti vezani za svoju klasu, a ne raspodeljeni po nepovezanim klasama. Razvijač i glavni zagovornik SRP-a, Robert C. Martin, opisuje odgovornost kao uzrok promena. Prvobitno je predložio ovaj termin kao jedan od elemenata svog dela „Principi objektno orijentisanog projektovanja“. U koncept su uključene mnoge karakteristike koherentnosti koje je ranije definisao Tom DeMarco.
U koncept su takođe uključene nekoliko pojmova formulisanih od strane Davida Parnas. Dva osnovna su inkapsulacija i skrivenje informacija. Parnas je tvrdio da podela sistema na odvojene module ne sme biti zasnovana na analizi blok-dijagrama ili tokova izvršenja. Bilo koji od modula treba da sadrži određeno rešenje koje pruža minimum informacija klijentima.
Usput, Martin je naveo zanimljiv primer sa višim menadžerima kompanije (COO, CTO, CFO), od kojih svaki koristi specifični softver za poslovanje sa različitom svrhom. Na kraju, bilo koji od njih može uvesti promene u softveru, ne dotičući interese drugih menadžera.
Božanski objekat
Kao obično, najbolji način da se prouči SRP je da se vidi sve u akciji. Pogledajmo deo programa koji NE odgovara načelu jedinstvene odgovornosti. Ovo je Ruby kod koji opisuje ponašanje i atribute svemirske stanice.
Pogledajte primer i pokušajte da odredite sledeće:
Obaveze onih objekata koji su proglašeni u klasi SpaceStation.
Oni koji mogu biti zainteresovani za rad svemirske stanice.
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
endNaša vesoljska postaja ni funkcionalna (mislim, da ne bom prejel klica od NASA v bližnji prihodnosti), toda tukaj je veliko za analizo.
Torej, razred SpaceStation ima več različnih odgovornosti (ali nalog). Vse jih lahko razdelimo po vrstah:
- senzorji;
- oskrba (potrošni materiali);
- gorivo;
- potisniki.
Čeprav v razredu ni dodeljenih nobenih zaposlenih, si lahko enostavno predstavljamo, kdo je za kaj odgovoren. Znanstvenik verjetno nadzira senzorje, logist pričenja z oskrbo z viri, inženir pa skrb za zaloge goriva, pilot pa nadzira potisnike.
Ali lahko rečemo, da ta program ne ustreza SRP? Ja, seveda. Ampak razred SpaceStation je tipičen "božji objekt", ki ve vse in počne vse. To je osnovni antipattern v objektno usmerjenem programiranju. Za novince so takšni objekti izjemno težki za vzdrževanje. Do sedaj je program zelo preprost, ja, a si predstavljajte, kaj se zgodi, če dodamo nove funkcionalnosti. Morda bo naši vesoljski postaji potrebna medicinska soba ali konferenčna soba. In več funkcionalnosti kot bo, bolj bo SpaceStation rasla. Ker bo ta objekt povezan z drugimi, bo vzdrževanje celotnega kompleksa postalo še bolj zapleteno. Na koncu lahko povzročimo motnje, na primer v pospeševalnikih. Če znanstvenik zahteva spremembe pri delu s senzorji, lahko to vpliva na komunikacijske sisteme postaje.
Kršenje SRP načela lahko prinese kratkoročno taktično zmago, a na koncu "izgubimo vojno", saj bo vzdrževanje takega monstruma v prihodnosti zelo težko. Najbolje je, da program razdelimo na posamezne dele kode, pri čemer vsak del odgovarja za izvedbo določene operacije. Razumejoč to, spremenimo razred SpaceStation.
Porazdelimo odgovornost
Zgornje smo določili štiri vrste operacij, ki jih nadzira razred SpaceStation. Pri refaktorizaciji bomo to imeli v mislih. Posodobljena koda bolje ustreza 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
endZnatno se poboljšala, program sada izgleda bolje. Sada je naš razred SpaceStation više poput kontejnera u kojem se iniciraju operacije za zavisne delove, uključujući set senzora, sistem za opskrbu potrošnim materijalima, rezervoar za gorivo i potisnike.
Svaka od promenljivih sada ima svoj odgovarajući razred: Sensors; SupplyHold; FuelTank; Thrusters.
V tej različici kode je nekaj pomembnih sprememb. Gre za to, da so posamezne funkcije ne le enkapsulirane v lastne razrede, ampak so organizirane na način, da postanejo predvidljive in dosledne. Skupaj združujemo podobne funkcionalne elemente, da sledimo načelu kohezivnosti. Zdaj, če bomo morali spremeniti delovanje sistema in preiti z zgoščene strukture na tabelo, preprosto uporabimo razred SupplyHold, ne da bi morali posegati v druge module. Tako, če bo uradnik, odgovoren za logistiko, kaj spremenil v svojem oddelku, ostali elementi postaje ostanejo nedotaknjeni. Pri tem razred SpaceStation niti ne bo seznanjen s spremembami.
Naši uradniki, ki delajo na vesoljski postaji, so verjetno zadovoljni s spremembami, saj lahko zahtevajo tiste, ki so zanje potrebne. Upoštevajte, da v kodi obstajajo metode, kot sta report_supplies in report_fuel, ki se nahajajo v razredih SupplyHold in FuelTank. Kaj se zgodi, če Zemlja zahteva spremembo načina oblikovanja poročil? Prisiljeni bomo spremeniti oba razreda, SupplyHold in FuelTank. Kaj pa, če bo potrebno spremeniti način dostave goriva in zalog? Verjetno bomo morali znova spremeniti vse iste razrede. To pa že krši načelo SRP. Uredimo 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 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 naloge razdeljene na dva nova razreda, FuelReporter in SupplyReporter. Oba sta podrazreda razreda Reporter. Poleg tega smo dodali primerne spremenljivke razredu SpaceStation, da bomo po potrebi inicializirali ustrezni podrazred. Zdaj, če se Zemlja odloči, da nekaj spremeni, bomo popravili podrazrede, ne pa glavnega razreda.
Seveda, nekateri razredi so še vedno odvisni drug od drugega. Tako objekt SupplyReporter deluje na SupplyHold, FuelReporter pa na FuelTank. Očitno morajo biti pospeševalniki povezani s tankom za gorivo. Ampak vse skupaj izgleda logično in uvajanje sprememb ne bo posebej zapleteno — urejanje kode enega objekta ne bo preveč vplivalo na drugega.
Tako smo ustvarili modularno kodo, kjer so naloge vsakega objekta/razreda natančno določene. Delo s takšno kodo ni problem, njeno vzdrževanje pa bo preprosta naloga. Celoten "božanski objekt" smo preoblikovali v SRP.
Skillbox priporoča:
- Двухлетний практический курс .
- Spletni tečaj .
- Практический годовой курс .
Izvor: habr.com
