Pisanje prilagodljive kode z uporabo SOLID-a

Pisanje prilagodljive kode z uporabo SOLID-a

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.

Skillbox priporoča: Praktični tečaj "Mobilni razvijalec PRO".

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.

Skillbox priporoča:

Vir: www.habr.com

Dodaj komentar