Pišemo fleksibilan kod koristeći SOLID

Pišemo fleksibilan kod koristeći SOLID

Od prevodioca: objavljeno za vas članak Severina Pereza o korištenju SOLID principa u programiranju. Informacije iz članka bit će korisne i početnicima i iskusnim programerima.

Ako ste u razvoju, najvjerovatnije ste čuli za SOLID principe. Oni omogućavaju programeru da napiše čist, dobro strukturiran i lak za održavanje koda. Vrijedi napomenuti da u programiranju postoji nekoliko pristupa kako ispravno izvršiti određeni posao. Različiti stručnjaci imaju različite ideje i razumijevanje “pravog puta”; sve ovisi o iskustvu svake osobe. Međutim, ideje proklamovane u SOLID-u prihvataju gotovo svi predstavnici IT zajednice. Oni su postali polazna tačka za nastanak i razvoj mnogih dobrih praksi upravljanja razvojem.

Hajde da shvatimo šta su SOLID principi i kako nam pomažu.

Skillbox preporučuje: Praktični kurs "Mobile Developer PRO".

Podsećamo: za sve čitaoce "Habra" - popust od 10 rubalja pri upisu na bilo koji Skillbox kurs koristeći "Habr" promotivni kod.

Šta je SOLID?

Ovaj izraz je skraćenica, svako slovo pojma je početak naziva određenog principa:

  • SNačelo pojedinačne odgovornosti. Modul može imati jedan i samo jedan razlog za promjenu.
  • The Oolovka/zatvoren princip (princip otvoreno/zatvoreno). Klase i drugi elementi trebaju biti otvoreni za proširenje, ali zatvoreni za modifikacije.
  •  The Liskov Princip supstitucije (Princip zamjene Liskov). Funkcije koje koriste osnovni tip trebale bi moći koristiti podtipove osnovnog tipa, a da to ne znaju.
  • The IPrincip segregacije interfejsa  (princip razdvajanja interfejsa). Softverski entiteti ne bi trebali ovisiti o metodama koje ne koriste.
  • The DPrincip inverzije zavisnosti (princip inverzije zavisnosti). Moduli na višim nivoima ne bi trebali ovisiti o modulima na nižim nivoima.

Princip jedinstvene odgovornosti


Princip Single Responsibility Principle (SRP) navodi da svaka klasa ili modul u programu treba da bude odgovoran samo za jedan dio funkcionalnosti tog programa. Dodatno, elementi ove odgovornosti treba da budu dodijeljeni njihovoj vlastitoj klasi, a ne razbacani po nepovezanim klasama. SRP-ov programer i glavni evanđelista, Robert S. Martin, opisuje odgovornost kao razlog za promjenu. On je prvobitno predložio ovaj termin kao jedan od elemenata svog rada "Principi objektno-orijentisanog dizajna". Koncept uključuje veći dio obrasca povezivanja koji je prethodno definirao Tom DeMarco.

Koncept je takođe uključivao nekoliko koncepata koje je formulisao David Parnas. Dvije glavne su inkapsulacija i skrivanje informacija. Parnas je tvrdio da podela sistema na zasebne module ne bi trebalo da se zasniva na analizi blok dijagrama ili tokova izvršenja. Svaki od modula mora sadržavati specifično rješenje koje klijentima pruža minimum informacija.

Inače, Martin je naveo zanimljiv primjer sa višim menadžerima jedne kompanije (COO, CTO, CFO), od kojih svaki koristi određeni poslovni softver u različite svrhe. Kao rezultat toga, bilo koji od njih može implementirati promjene u softveru bez utjecaja na interese drugih menadžera.

Božanski objekt

Kao i uvijek, najbolji način da naučite SRP je da ga vidite na djelu. Pogledajmo dio programa koji NE slijedi princip jedinstvene odgovornosti. Ovo je Ruby kod koji opisuje ponašanje i atribute svemirske stanice.

Pregledajte primjer i pokušajte odrediti sljedeće:
Odgovornosti onih objekata koji su deklarisani u klasi SpaceStation.
Oni koji bi mogli 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
end

Zapravo, naša svemirska stanica je nefunkcionalna (mislim da me neće uskoro nazvati iz NASA-e), ali ovdje postoji nešto za analiziranje.

Dakle, klasa SpaceStation ima nekoliko različitih odgovornosti (ili zadataka). Svi se mogu podijeliti na vrste:

  • senzori;
  • zalihe (potrošni materijal);
  • gorivo;
  • akceleratori.

Iako nikome od zaposlenih u stanici nije određena klasa, lako možemo zamisliti ko je za šta odgovoran. Najvjerovatnije, naučnik kontrolira senzore, logističar je odgovoran za snabdijevanje 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 sa SRP? Da naravno. Ali klasa SpaceStation je tipičan "božji objekat" koji sve zna i sve radi. Ovo je glavni anti-uzorak u objektno orijentiranom programiranju. Za početnika, takve objekte je izuzetno teško održavati. Do sada je program vrlo jednostavan, da, ali zamislite šta će se dogoditi ako dodamo nove funkcije. Možda će našoj svemirskoj stanici biti potrebna medicinska stanica ili soba za sastanke. I što više funkcija ima, SpaceStation će više rasti. Pa, budući da će ovaj objekat biti povezan sa drugim, servisiranje čitavog kompleksa će postati još teže. Kao rezultat toga, možemo poremetiti rad, na primjer, akceleratora. Ako istraživač zatraži promjene na senzorima, to bi vrlo dobro moglo utjecati na komunikacijske sisteme stanice.

Kršenje principa SRP-a može donijeti kratkoročnu taktičku pobjedu, ali na kraju ćemo “izgubiti rat”, a takvo čudovište će biti vrlo teško održati 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

Iznad smo definirali četiri tipa 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

Mnogo je promjena, program sada definitivno izgleda bolje. Sada je naša SpaceStation klasa postala više kao kontejner u kojem se pokreću operacije za zavisne dijelove, uključujući set senzora, potrošni sistem za opskrbu, rezervoar za gorivo i pojačivače.

Za bilo koju od varijabli sada postoji odgovarajuća klasa: Senzori; SupplyHold; Rezervoar za gorivo; Thrusters.

Postoji nekoliko važnih promjena u ovoj verziji koda. Poenta je da pojedinačne funkcije nisu samo inkapsulirane u svojim klasama, već su i organizirane na takav način da postanu predvidljive i konzistentne. Grupiramo elemente sa sličnom funkcionalnošću kako bismo pratili princip koherentnosti. Sada, ako trebamo promijeniti način na koji sistem radi, prelazeći sa strukture heš na niz, samo upotrijebite klasu SupplyHold; ne moramo dirati druge module. Na ovaj način, ako službenik logistike nešto promijeni u svom dijelu, ostatak stanice će ostati netaknut. U ovom slučaju, klasa SpaceStation neće ni biti svjesna promjena.

Naši službenici koji rade na svemirskoj stanici su vjerovatno sretni zbog promjena jer mogu zatražiti one koje im trebaju. Obratite pažnju da kod ima metode kao što su report_supplies i report_fuel sadržane u klasama SupplyHold i FuelTank. Šta bi se dogodilo kada bi Zemlja zatražila da promijeni način na koji izvještava? Obje klase, SupplyHold i FuelTank, će se morati promijeniti. Šta ako trebate promijeniti način isporuke goriva i potrošnog materijala? Vjerovatno ćete morati ponovo promijeniti sve iste klase. A to je već kršenje principa SRP. Hajde da 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 najnovijoj verziji programa, odgovornosti su podijeljene u dvije nove klase, FuelReporter i SupplyReporter. Oboje su deca iz klase Reporter. Dodatno, 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, onda ćemo napraviti promjene u podklasama, a ne u glavnoj klasi.

Naravno, neki od naših časova i dalje zavise jedni od drugih. Dakle, SupplyReporter objekat zavisi od SupplyHold, a FuelReporter zavisi od FuelTank-a. Naravno, pojačivači moraju biti povezani na rezervoar za gorivo. Ali ovdje sve već izgleda logično, a unošenje promjena neće biti posebno teško - uređivanje koda jednog objekta neće uvelike utjecati na drugi.

Tako smo kreirali modularni kod u kojem su odgovornosti svakog od objekata/klasa precizno definirane. Rad s takvim kodom nije problem, njegovo održavanje će biti jednostavan zadatak. Cijeli “božanski objekt” smo pretvorili u SRP.

Skillbox preporučuje:

izvor: www.habr.com

Dodajte komentar