Pisanje fleksibilnog koda pomoću SOLID-a

Pisanje fleksibilnog koda pomoću SOLID-a

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.

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

Podsjećamo: za sve čitatelje "Habra" - popust od 10 000 rubalja pri upisu na bilo koji tečaj Skillbox koristeći promotivni kod "Habr".

Što je SOLID?

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

Načelo jedinstvene odgovornosti


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.

Skillbox preporučuje:

Izvor: www.habr.com

Dodajte komentar