Pisanje fleksibilnog koda koristeći SOLID

Pisanje fleksibilnog koda koristeći SOLID

Od prevodnika: smo smo pustili za vas članak Severina Pereza o upotrebi SOLID principa u programiranju. Informacije iz članka biće korisne kako za novajlije, tako i za iskusne programere.

Ako se bavite razvojem, verovatno ste čuli za SOLID principe. Oni omogućavaju programerima da pišu čist, dobro strukturiran i lako održiv kod. Vredi napomenuti da u programiranju postoje različiti pristupi tome kako pravilno obaviti određeni zadatak. Različiti stručnjaci imaju različite ideje i razumevanja 'pravilnog puta', sve zavisi od iskustva svakog pojedinca. Ipak, ideje postavljene u SOLID-u su prihvaćene od strane gotovo svih članova IT zajednice. Postale su polazna tačka za pojavu i razvoj mnogih dobrih metoda upravljanja razvojem.

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

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

Opominjamo: za vse bralce »Habra« — popust 10 000 rubljev ob vpisu na kateri koli tečaj Skillbox s promo kodo »Habr«.

Šta je SOLID?

Ovaj termin je akronim, svako slovo u terminu označava početno slovo određenog principa:

Načelo enotne odgovornosti

 
Načelo jedinstvene odgovornosti (SRP) kaže da svaka klasa ili modul u programu treba da bude odgovoran samo za jedan deo funkcionalnosti tog programa. Takođe, elementi te odgovornosti trebaju biti vezani za svoju klasu, a ne raspodeljeni po nepovezanim klasama. Razvijač i glavni zagovornik SRP-a, Robert C. Martin, opisuje odgovornost kao uzrok promena. Prvobitno je predložio ovaj termin kao jedan od elemenata svog dela „Principi objektno orijentisanog projektovanja“. U koncept su uključene mnoge karakteristike koherentnosti koje je ranije definisao Tom DeMarco.

U koncept su takođe uključene nekoliko pojmova formulisanih od strane Davida Parnas. Dva osnovna su inkapsulacija i skrivenje informacija. Parnas je tvrdio da podela sistema na odvojene module ne sme biti zasnovana na analizi blok-dijagrama ili tokova izvršenja. Bilo koji od modula treba da sadrži određeno rešenje koje pruža minimum informacija klijentima.

Usput, Martin je naveo zanimljiv primer sa višim menadžerima kompanije (COO, CTO, CFO), od kojih svaki koristi specifični softver za poslovanje sa različitom svrhom. Na kraju, bilo koji od njih može uvesti promene u softveru, ne dotičući interese drugih menadžera.

Božanski objekat

Kao obično, najbolji način da se prouči SRP je da se vidi sve u akciji. Pogledajmo deo programa koji NE odgovara načelu jedinstvene odgovornosti. Ovo je Ruby kod koji opisuje ponašanje i atribute svemirske stanice.

Pogledajte primer i pokušajte da odredite sledeće:
Obaveze onih objekata koji su proglašeni u klasi SpaceStation.
Oni koji mogu 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

Naša vesoljska postaja ni funkcionalna (mislim, da ne bom prejel klica od NASA v bližnji prihodnosti), toda tukaj je veliko za analizo.

Torej, razred SpaceStation ima več različnih odgovornosti (ali nalog). Vse jih lahko razdelimo po vrstah:

  • senzorji;
  • oskrba (potrošni materiali);
  • gorivo;
  • potisniki.

Čeprav v razredu ni dodeljenih nobenih zaposlenih, si lahko enostavno predstavljamo, kdo je za kaj odgovoren. Znanstvenik verjetno nadzira senzorje, logist pričenja z oskrbo z viri, inženir pa skrb za zaloge goriva, pilot pa nadzira potisnike.

Ali lahko rečemo, da ta program ne ustreza SRP? Ja, seveda. Ampak razred SpaceStation je tipičen "božji objekt", ki ve vse in počne vse. To je osnovni antipattern v objektno usmerjenem programiranju. Za novince so takšni objekti izjemno težki za vzdrževanje. Do sedaj je program zelo preprost, ja, a si predstavljajte, kaj se zgodi, če dodamo nove funkcionalnosti. Morda bo naši vesoljski postaji potrebna medicinska soba ali konferenčna soba. In več funkcionalnosti kot bo, bolj bo SpaceStation rasla. Ker bo ta objekt povezan z drugimi, bo vzdrževanje celotnega kompleksa postalo še bolj zapleteno. Na koncu lahko povzročimo motnje, na primer v pospeševalnikih. Če znanstvenik zahteva spremembe pri delu s senzorji, lahko to vpliva na komunikacijske sisteme postaje.

Kršenje SRP načela lahko prinese kratkoročno taktično zmago, a na koncu "izgubimo vojno", saj bo vzdrževanje takega monstruma v prihodnosti zelo težko. Najbolje je, da program razdelimo na posamezne dele kode, pri čemer vsak del odgovarja za izvedbo določene operacije. Razumejoč to, spremenimo razred SpaceStation.

Porazdelimo odgovornost

Zgornje smo določili štiri vrste operacij, ki jih nadzira razred SpaceStation. Pri refaktorizaciji bomo to imeli v mislih. Posodobljena koda bolje ustreza 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} available: #{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

Znatno se poboljšala, program sada izgleda bolje. Sada je naš razred SpaceStation više poput kontejnera u kojem se iniciraju operacije za zavisne delove, uključujući set senzora, sistem za opskrbu potrošnim materijalima, rezervoar za gorivo i potisnike.

Svaka od promenljivih sada ima svoj odgovarajući razred: Sensors; SupplyHold; FuelTank; Thrusters.

V tej različici kode je nekaj pomembnih sprememb. Gre za to, da so posamezne funkcije ne le enkapsulirane v lastne razrede, ampak so organizirane na način, da postanejo predvidljive in dosledne. Skupaj združujemo podobne funkcionalne elemente, da sledimo načelu kohezivnosti. Zdaj, če bomo morali spremeniti delovanje sistema in preiti z zgoščene strukture na tabelo, preprosto uporabimo razred SupplyHold, ne da bi morali posegati v druge module. Tako, če bo uradnik, odgovoren za logistiko, kaj spremenil v svojem oddelku, ostali elementi postaje ostanejo nedotaknjeni. Pri tem razred SpaceStation niti ne bo seznanjen s spremembami.

Naši uradniki, ki delajo na vesoljski postaji, so verjetno zadovoljni s spremembami, saj lahko zahtevajo tiste, ki so zanje potrebne. Upoštevajte, da v kodi obstajajo metode, kot sta report_supplies in report_fuel, ki se nahajajo v razredih SupplyHold in FuelTank. Kaj se zgodi, če Zemlja zahteva spremembo načina oblikovanja poročil? Prisiljeni bomo spremeniti oba razreda, SupplyHold in FuelTank. Kaj pa, če bo potrebno spremeniti način dostave goriva in zalog? Verjetno bomo morali znova spremeniti vse iste razrede. To pa že krši načelo SRP. Uredimo 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  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 naloge razdeljene na dva nova razreda, FuelReporter in SupplyReporter. Oba sta podrazreda razreda Reporter. Poleg tega smo dodali primerne spremenljivke razredu SpaceStation, da bomo po potrebi inicializirali ustrezni podrazred. Zdaj, če se Zemlja odloči, da nekaj spremeni, bomo popravili podrazrede, ne pa glavnega razreda.

Seveda, nekateri razredi so še vedno odvisni drug od drugega. Tako objekt SupplyReporter deluje na SupplyHold, FuelReporter pa na FuelTank. Očitno morajo biti pospeševalniki povezani s tankom za gorivo. Ampak vse skupaj izgleda logično in uvajanje sprememb ne bo posebej zapleteno — urejanje kode enega objekta ne bo preveč vplivalo na drugega.

Tako smo ustvarili modularno kodo, kjer so naloge vsakega objekta/razreda natančno določene. Delo s takšno kodo ni problem, njeno vzdrževanje pa bo preprosta naloga. Celoten "božanski objekt" smo preoblikovali v SRP.

Skillbox priporoča:

Izvor: habr.com

Kupite zanesljivo gostovanje za spletne strani z zaščito pred DDoS, VPS VDS strežniki 🔥 Kupite zanesljivo gostovanje za spletne strani z zaščito pred DDoS, VPS VDS strežniki | ProHoster