
Od prevoditelja: objavili smo vam o korištenju SOLID principa u programiranju. Informacije iz članka bit će korisne kako za novake, tako i za iskusne programere.
Ako se bavite razvojem, vjerojatno ste čuli za SOLID principe. Oni omogućavaju programerima pisanje čistog, dobro strukturiranog i lako održivog koda. Važno je napomenuti da u programiranju postoji nekoliko pristupa kako pravilno izvršiti neki zadatak. Različiti stručnjaci imaju različite ideje i tumačenja „ispravnog puta“, a sve zavisi od iskustva svakog pojedinca. Ipak, ideje koje su proklamirane u SOLID-u prihvaćaju gotovo svi članovi IT zajednice. Postali su polazišna točka za pojavu i razvoj mnogih dobrih metoda upravljanja razvojem.
Pogledajmo što su SOLID principi i kako nam pomažu.
Skillbox preporučuje: Praktični tečaj .
Podsjećamo: za sve čitatelje "Habra" — popust od 10 000 rubalja prilikom upisa na bilo koji tečaj Skillbox uz promo kod "Habr".
Što su SOLID?
Ovaj pojam je akronim, svako slovo označava početno slovo određenog principa:
- SPrincip jednog odgovornosti. Modul može imati samo jedan razlog za promjenu.
- (princip otvorenosti/zaklopenosti). Klase i drugi elementi trebaju biti otvoreni za proširenje, ali zatvoreni za modifikaciju.
- Princip razdvajanja sučelja. Softverske jedinice ne bi trebale ovisiti o metodama koje ne koriste.
- Princip inverzije ovisnosti
- Princpi jedinstvene odgovornosti
Princip jedinstvene odgovornosti
Načelo jedinstvene odgovornosti (SRP) tvrdi da bi svaki razred ili modul u programu trebao biti odgovoran samo za jedan dio funkcionalnosti tog programa. Također, elementi te odgovornosti trebaju biti vezani za svoj razred, a ne raspoređeni po nepovezanim razredima. Razvijač i glavni zagovornik SRP-a, Robert C. Martin, opisuje odgovornost kao uzrok promjena. Prvotno je ovaj termin predložio kao jedan od elemenata svog rada 'Načela objektno orijentiranog dizajna'. U koncept su uključene mnoge značajke kohezije, koje je ranije definirao Tom DeMarco.
U koncept su također uključeni pojmovi koje je formulirao David Parnas. Dva osnovna su inkapsulacija i skrivanje informacija. Parnas je tvrdio da razdvajanje sustava na odvojene module ne bi trebalo biti temeljeno na analizi blok-dijagrama ili tokova izvršenja. Svaki od modula trebao bi sadržavati određeno rješenje koje pruža minimalne informacije klijentima.
Usput, Martin je naveo zanimljiv primjer s višim menadžerima tvrtke (COO, CTO, CFO), od kojih svaki koristi specifičan poslovni softver s različitim ciljevima. Na kraju, svatko od njih može implementirati promjene u softver bez utjecaja na interese drugih menadžera.
Božanski objekt
Kao i obično, najbolji način da se prouči SRP je vidjeti ga u akciji. Pogledajmo dio programa koji NE odgovara načelu jedinstvene odgovornosti. Ovo je Ruby kod koji opisuje ponašanje i atribute svemirske stanice.
Pogledajte primjer i pokušajte odrediti sljedeće:
Obveze onih objekata koji su proglašeni u razredu SpaceStation.
Onih koji bi mogli biti zainteresirani 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} 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
endNaša svemirska stanica nije funkcionalna (mislim da neću dobiti poziv od NASA-e u skoroj budućnosti), ali ovdje ima što za analizirati.
Dakle, klasa SpaceStation ima nekoliko različitih odgovornosti (ili zadatka). Sve se njih može razvrstati po tipovima:
- senzori;
- opskrba (potrošni materijal);
- gorivo;
- potisnici.
Iako nitko od osoblja stanice nije definiran u klasi, možemo lako zamisliti tko za što odgovara. Vjerojatno znanstvenik kontrolira senzore, logističar je odgovoran za opskrbu resursima, inženjer upravlja zalihama goriva, a pilot kontrolira potisnike.
Možemo li reći da ova programa ne ispunjava SRP? Da, naravno. No, klasa SpaceStation je tipični „božanski objekt“ koji zna sve i sve obavlja. To je osnovni antišablon u objektno-orijentiranom programiranju. Za novake, takvi objekti su izuzetno složeni za održavanje. Program je trenutno vrlo jednostavan, da, ali zamislite što će se dogoditi kada dodamo nove funkcije. Možda će našoj svemirskoj stanici trebati medicinska stanica ili soba za sastanke. A što više funkcija dodamo, to će SpaceStation postati složenija. A budući da će ovaj objekt biti povezan s drugima, održavanje cijelog kompleksa postat će još složenije. Na kraju, mogli bismo poremetiti rad, primjerice, ubrzivača. Ako znanstvenik zatraži promjene u radu sa senzorima, to bi moglo utjecati na komunikacijske sustave stanice.
Kršenje SRP principa može donijeti kratkoročnu taktičku pobjedu, ali na kraju ćemo „izgubiti rat”, održavanje takvog čudovišta u budućnosti postat će vrlo otežano. Najbolje je program podijeliti na odvojene dijelove koda, od kojih svaki odgovara za izvršenje određene operacije. Shvaćajući to, promijenimo klasu SpaceStation.
Raspostavljanje odgovornosti
Gore smo definirali četiri vrste operacija koje kontrolira klasa SpaceStation. Prilikom refaktoringa imat ćemo ih na umu. Ažurirani kod bolje odgovara SRP-u.
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 "----- Akcija senzora -----"
puts "Pokretanje senzora!"
end
end
class SupplyHold
attr_accessor :supplies
def initialize
@supplies = {}
end
def load_supplies(type, quantity)
puts "----- Akcija opskrbe -----"
puts "Učitavanje #{quantity} jedinica #{type} u skladište."
if @supplies[type]
@supplies[type] += quantity
else
@supplies[type] = quantity
end
end
def use_supplies(type, quantity)
puts "----- Akcija opskrbe -----"
if @supplies[type] != nil && @supplies[type] > quantity
puts "Koristenje #{quantity} jedinica #{type} iz skladišta."
@supplies[type] -= quantity
else
puts "Greška u opskrbi: Nedovoljno #{type} u skladištu."
end
end
def report_supplies
puts "----- Izvještaj o opskrbi -----"
if @supplies.keys.length > 0
@supplies.each do |type, quantity|
puts "#{type} dostupno: #{quantity} jedinica"
end
else
puts "Skladište je prazno."
end
end
end
class FuelTank
attr_accessor :fuel
def initialize
@fuel = 0
end
def get_fuel_levels
@fuel
end
def load_fuel(quantity)
puts "----- Akcija goriva -----"
puts "Učitavanje #{quantity} jedinica goriva u rezervoar."
@fuel += quantity
end
def use_fuel(quantity)
puts "----- Akcija goriva -----"
puts "Koristenje #{quantity} jedinica goriva iz rezervoara."
@fuel -= quantity
end
def report_fuel
puts "----- Izvještaj o gorivu -----"
puts "#{@fuel} jedinica goriva dostupno."
end
end
class Thrusters
def initialize(fuel_tank)
@linked_fuel_tank = fuel_tank
end
def activate_thrusters
puts "----- Akcija potisnika -----"
if @linked_fuel_tank.get_fuel_levels >= 10
puts "Akcija potisnika uspješna."
@linked_fuel_tank.use_fuel(10)
else
puts "Greška potisnika: Nedovoljno goriva."
end
end
endBilo je mnogo promjena, program sada izgleda definitivno bolje. Naš razred SpaceStation sada je u suštini kontejner u kojem se iniciraju operacije za ovisne dijelove, uključujući skup senzora, sustav opskrbe, rezervoar za gorivo, i potisnike.
Svaka od varijabli sada ima odgovarajući razred: Sensors; SupplyHold; FuelTank; Thrusters.
U ovoj verziji koda postoji nekoliko važnih promjena. Naime, posebne funkcije nisu samo enkapsulirane unutar vlastitih klasa, već su organizirane na način da postanu predvidljive i dosljedne. Grupiramo slične elemente po funkcionalnosti da bismo slijedili princip kohezije. Sada, ako budemo trebali promijeniti način rada sustava, prelazeći s heksadodekadske strukture na niz, jednostavno ćemo koristiti klasu SupplyHold, a da pritom ne diramo druge module. Tako će, ako član osoblja zadužen za logistiku napravi neku promjenu u svom odjelu, ostali elementi stanice ostati netaknuti. Pri tome klasa SpaceStation neće ni biti svjesna promjena.
Naši službenici koji rade na svemirskoj stanici vjerojatno su zadovoljni promjenama jer mogu tražiti ono što im je zaista potrebno. Imajte na umu da u kodu postoje metode poput report_supplies i report_fuel, smještene u klasama SupplyHold i FuelTank. Što će se dogoditi ako Zemlja zatraži promjenu načina oblikovanja izvještaja? Trebat će promijeniti obje klase, SupplyHold i FuelTank. A što ako će biti potrebno promijeniti način isporuke goriva i potrepština? Vjerojatno će ponovno biti potrebno promijeniti sve iste klase. A to je već kršenje SRP principa. Popravimo to.
klasa SvemirskaStanica
attr_reader :senzori, @supply_hold, @supply_reporter,
:rezervoar_goriva, :fuel_reporter, :potisnici
def initialize
@senzori = Senzori.new
@supply_hold = Zalihe.new
@supply_reporter = ReporterZaliha.new(@supply_hold)
@rezervoar_goriva = RezervoarGoriva.new
@fuel_reporter = ReporterGoriva.new(@rezervoar_goriva)
@potisnici = Potisnici.new(@rezervoar_goriva)
end
end
klasa Senzori
def run_senzori
puts "----- Rad Senzora -----"
puts "Pokretanje senzora!"
end
end
klasa Zalihe
attr_accessor :zalihe
attr_reader :reporter
def initialize
@zalihe = {}
end
def get_zalihe
@zalihe
end
def load_zalihe(vrsta, kolicina)
puts "----- Akcija Zaliha -----"
puts "Učitavanje #{kolicina} jedinica #{vrsta} u skladište."
if @zalihe[vrsta]
@zalihe[vrsta] += kolicina
else
@zalihe[vrsta] = kolicina
end
end
def use_zalihe(vrsta, kolicina)
puts "----- Akcija Zaliha -----"
if @zalihe[vrsta] != nil && @zalihe[vrsta] > kolicina
puts "Korištenje #{kolicina} od #{vrsta} iz skladišta."
@zalihe[vrsta] -= kolicina
else
puts "Greška u Zaliha: Nedovoljno #{vrsta} u skladištu."
end
end
end
klasa RezervoarGoriva
attr_accessor :gorivo
attr_reader :reporter
def initialize
@gorivo = 0
end
def get_nivo_goriva
@gorivo
end
def load_gorivo(kolicina)
puts "----- Akcija Goriva -----"
puts "Učitavanje #{kolicina} jedinica goriva u rezervoar."
@gorivo += kolicina
end
def use_gorivo(kolicina)
puts "----- Akcija Goriva -----"
puts "Korištenje #{kolicina} jedinica goriva iz rezervoara."
@gorivo -= kolicina
end
end
klasa Potisnici
FUEL_PER_THRUST = 10
def initialize(rezervoar_goriva)
@povezani_rezervoar_goriva = rezervoar_goriva
end
def activate_potisnike
puts "----- Akcija Potisnika -----"
if @povezani_rezervoar_goriva.get_nivo_goriva >= FUEL_PER_THRUST
puts "Akcija potiska uspješna."
@povezani_rezervoar_goriva.use_gorivo(FUEL_PER_THRUST)
else
puts "Greška Potisnika: Nedovoljno goriva dostupno."
end
end
end
klasa Reporter
def initialize(item, vrsta)
@povezani_item = item
@vrsta = vrsta
end
def report
puts "----- #{@vrsta.capitalize} Izvještaj -----"
end
end
klasa ReporterGoriva < Reporter
def initialize(item)
super(item, "gorivo")
end
def report
super
puts "#{@povezani_item.get_nivo_goriva} jedinica goriva dostupno."
end
end
klasa ReporterZaliha 0
@povezani_item.get_zalihe.each do |vrsta, kolicina|
puts "#{vrsta} dostupno: #{kolicina} jedinica"
end
else
puts "Skladište je prazno."
end
end
end
iss = SvemirskaStanica.new
iss.senzori.run_senzori
# ----- Rad Senzora -----
# Pokretanje senzora!
iss.supply_hold.use_zalihe("dijelovi", 2)
# ----- Akcija Zaliha -----
# Greška u Zaliha: Nedovoljno dijelova u skladištu.
iss.supply_hold.load_zalihe("dijelovi", 10)
# ----- Akcija Zaliha -----
# Učitavanje 10 jedinica dijelova u skladište.
iss.supply_hold.use_zalihe("dijelovi", 2)
# ----- Akcija Zaliha -----
# Korištenje 2 od dijelova iz skladišta.
iss.supply_reporter.report
# ----- Izvještaj o Zaliha -----
# dijelovi dostupni: 8 jedinica
iss.potisnici.activate_potisnike
# ----- Akcija Potisnika -----
# Greška Potisnika: Nedovoljno goriva dostupno.
iss.rezervoar_goriva.load_gorivo(100)
# ----- Akcija Goriva -----
# Učitavanje 100 jedinica goriva u rezervoar.
iss.potisnici.activate_potisnike
# ----- Akcija Potisnika -----
# Akcija potiska uspješna.
# ----- Akcija Goriva -----
# Korištenje 10 jedinica goriva iz rezervoara.
iss.fuel_reporter.report
# ----- Izvještaj o Gorivu -----
# 90 jedinica goriva dostupno.U ovoj, posljednjoj verziji programa, obaveze su podijeljene u dvije nove klase, FuelReporter i SupplyReporter. Obje su podklase klase Reporter. Osim toga, dodali smo primjerne varijable klasi SpaceStation kako bismo, ako je potrebno, inicijalizirali odgovarajuću podklasu. Sada, ako Zemlja odluči promijeniti nešto drugo, napravit ćemo izmjene u podklasama, a ne u osnovnoj klasi.
Naravno, neki razredi još uvijek ovise jedni o drugima. Tako, objekt SupplyReporter ovisi o SupplyHold, dok FuelReporter ovisi o FuelTanku. Naravno, pojačivači moraju biti povezani s rezervoarom goriva. No, ovdje sve izgleda logično, a unošenje izmjena neće biti osobito teško — uređivanje koda jednog objekta neće previše utjecati na drugi.
Na taj način, stvorili smo modularni kod gdje su obaveze svakog od objekata/klasa točno definirane. Raditi s takvim kodom nije problem, njegovo održavanje bit će jednostavan zadatak. Cijeli "božanski objekt" preoblikovali smo u SRP.
Skillbox preporučuje:
- Dvoipogodišnji praktični tečaj .
- Online tečaj .
- Praktični godišnji tečaj .
Izvor: habr.com
