Od prevoditelja: objavljeno za vas članak Severin Perez o korištenju SOLID principa u programiranju. Informacije iz članka bit će korisne i početnicima i iskusnim programerima.
Ako se bavite razvojem, vjerojatno ste čuli za SOLID principe. Omogućuju programeru pisanje čistog, dobro strukturiranog koda koji se lako održava. Vrijedno je napomenuti da u programiranju postoji nekoliko pristupa kako pravilno izvršiti određeni posao. Različiti stručnjaci imaju različite ideje i shvaćanja "pravog puta", sve ovisi o iskustvu svake osobe. No, ideje proklamirane u SOLID-u prihvaćaju gotovo svi predstavnici IT zajednice. Oni su postali polazište za nastanak i razvoj mnogih dobrih praksi upravljanja razvojem.
Shvatimo što su principi SOLID-a i kako nam oni pomažu.
Načelo jedinstvene odgovornosti (SRP) navodi da bi svaka klasa ili modul u programu trebao biti odgovoran samo za jedan dio funkcionalnosti tog programa. Dodatno, elemente ove odgovornosti treba dodijeliti vlastitoj klasi, a ne razbacati po nepovezanim klasama. SRP-ov programer i glavni evangelizator, Robert S. Martin, opisuje odgovornost kao razlog za promjenu. Izvorno je predložio ovaj termin kao jedan od elemenata svog rada "Principi objektno orijentiranog dizajna". Koncept uključuje velik dio uzorka povezivanja koji je prethodno definirao Tom DeMarco.
Koncept je također uključivao nekoliko koncepata koje je formulirao David Parnas. Dva glavna su enkapsulacija i skrivanje informacija. Parnas je tvrdio da se podjela sustava na zasebne module ne bi trebala temeljiti na analizi blok dijagrama ili tokova izvršenja. Svaki od modula mora sadržavati određeno rješenje koje klijentima pruža minimum informacija.
Inače, Martin je naveo zanimljiv primjer sa višim menadžerima tvrtke (COO, CTO, CFO) od kojih svaki koristi određeni poslovni softver za različite namjene. Kao rezultat toga, bilo koji od njih može implementirati promjene u softver bez utjecaja na interese drugih upravitelja.
Božanski objekt
Kao i uvijek, najbolji način da naučite SRP je vidjeti ga na djelu. Pogledajmo dio programa koji NE slijedi načelo jedinstvene odgovornosti. Ovo je Ruby kod koji opisuje ponašanje i atribute svemirske stanice.
Pregledajte primjer i pokušajte utvrditi sljedeće:
Odgovornosti onih objekata koji su deklarirani u klasi SpaceStation.
Oni koji bi mogli biti zainteresirani za rad svemirske 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
Zapravo, naša svemirska postaja ne radi (ne vjerujem da će me uskoro nazvati NASA), ali ovdje ima nešto za analizirati.
Dakle, klasa SpaceStation ima nekoliko različitih odgovornosti (ili zadataka). Svi se mogu podijeliti u vrste:
senzori;
zalihe (potrošni materijal);
gorivo;
akceleratorima.
Iako nitko od zaposlenika postaje nema klasu, lako možemo zamisliti tko je za što odgovoran. Najvjerojatnije, znanstvenik kontrolira senzore, logističar je odgovoran za opskrbu resursima, inženjer je odgovoran za zalihe goriva, a pilot kontrolira pojačivače.
Možemo li reći da ovaj program nije usklađen s SRP-om? Da naravno. Ali klasa SpaceStation tipičan je "božji objekt" koji sve zna i radi sve. Ovo je glavni anti-uzorak u objektno orijentiranom programiranju. Početniku su takvi objekti izuzetno teški za održavanje. Zasad je program vrlo jednostavan, da, ali zamislite što će se dogoditi ako dodamo nove značajke. Možda će našoj svemirskoj postaji trebati medicinska stanica ili soba za sastanke. A što više funkcija bude, SpaceStation će više rasti. Pa budući da će ovaj objekt biti povezan s ostalima, servisiranje cijelog kompleksa bit će dodatno otežano. Zbog toga možemo poremetiti rad npr. akceleratora. Ako istraživač zatraži promjene na senzorima, to bi moglo utjecati na komunikacijske sustave stanice.
Kršenje načela SRP-a može donijeti kratkoročnu taktičku pobjedu, ali na kraju ćemo "izgubiti rat" i bit će vrlo teško održati takvo čudovište u budućnosti. Najbolje je podijeliti program u zasebne dijelove koda, od kojih je svaki odgovoran za izvođenje određene operacije. Razumijevajući ovo, promijenimo klasu SpaceStation.
Podijelimo odgovornost
Gore smo definirali četiri vrste operacija koje kontrolira klasa SpaceStation. Imat ćemo ih na umu prilikom refaktoriranja. 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 "----- 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
Ima puno promjena, program sada definitivno izgleda bolje. Sada je naša klasa SpaceStation postala više spremnik u kojem se pokreću operacije za zavisne dijelove, uključujući set senzora, sustav opskrbe potrošnim materijalom, spremnik goriva i pojačivače.
Za bilo koju od varijabli sada postoji odgovarajuća klasa: Senzori; SupplyHold; Spremnik za gorivo; potisnici.
Postoji nekoliko važnih promjena u ovoj verziji koda. Poanta je da pojedinačne funkcije nisu samo kapsulirane u svojim klasama, već su organizirane na takav način da postanu predvidljive i dosljedne. Grupiramo elemente slične funkcionalnosti kako bismo slijedili načelo koherentnosti. Sada, ako trebamo promijeniti način na koji sustav funkcionira, prelazeći s hash strukture na niz, samo upotrijebite klasu SupplyHold; ne moramo dirati druge module. Na taj način, ako logističar promijeni nešto u svom dijelu, ostatak postaje će ostati netaknut. U ovom slučaju klasa SpaceStation neće ni biti svjesna promjena.
Naši časnici koji rade na svemirskoj stanici vjerojatno su sretni zbog promjena jer mogu zatražiti one koje trebaju. Primijetite da kod ima metode kao što su report_supplies i report_fuel sadržane u klasama SupplyHold i FuelTank. Što bi se dogodilo kada bi Zemlja zatražila promjenu načina na koji izvještava? Obje klase, SupplyHold i FuelTank, morat će se promijeniti. Što ako trebate promijeniti način isporuke goriva i potrošnog materijala? Vjerojatno ćete morati ponovno promijeniti sve iste klase. A to je već kršenje načela SRP-a. Popravimo ovo.
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.
U ovoj posljednjoj verziji programa, odgovornosti su podijeljene u dvije nove klase, FuelReporter i SupplyReporter. Oboje su djeca razreda Reporter. Osim toga, dodali smo varijable instance klasi SpaceStation tako da se željena podklasa može inicijalizirati ako je potrebno. Sada, ako Zemlja odluči promijeniti nešto drugo, tada ćemo napraviti promjene u podklasama, a ne u glavnoj klasi.
Naravno, neki od naših razreda još uvijek ovise jedni o drugima. Dakle, objekt SupplyReporter ovisi o SupplyHold, a FuelReporter ovisi o FuelTank. Naravno, pojačivači moraju biti spojeni na spremnik goriva. Ali ovdje sve već izgleda logično, a izmjene neće biti osobito teške - uređivanje koda jednog objekta neće uvelike utjecati na drugi.
Stoga smo kreirali modularni kod u kojem su precizno definirane odgovornosti svakog od objekata/klasa. Rad s takvim kodom nije problem, njegovo održavanje bit će jednostavan zadatak. Cijeli “božanski objekt” smo pretvorili u SRP.