Elastīga koda rakstīŔana, izmantojot SOLID

Elastīga koda rakstīŔana, izmantojot SOLID

No tulka: publicēts jums Severina Peresa raksts par SOLID principu izmantoÅ”anu programmÄ“Å”anā. Rakstā sniegtā informācija bÅ«s noderÄ«ga gan iesācējiem, gan pieredzējuÅ”iem programmētājiem.

Ja jÅ«s interesē attÄ«stÄ«ba, jÅ«s, visticamāk, esat dzirdējuÅ”i par SOLID principiem. Tie ļauj programmētājam rakstÄ«t tÄ«ru, labi strukturētu un viegli uzturējamu kodu. Ir vērts atzÄ«mēt, ka programmÄ“Å”anā ir vairākas pieejas, kā pareizi veikt konkrētu darbu. Dažādiem speciālistiem ir dažādas idejas un izpratne par ā€œpareizo ceļuā€, tas viss ir atkarÄ«gs no katra cilvēka pieredzes. Taču SOLID sludinātās idejas pieņem gandrÄ«z visi IT kopienas pārstāvji. Tie kļuva par sākumpunktu daudzu labu attÄ«stÄ«bas pārvaldÄ«bas prakÅ”u raÅ”anās un attÄ«stÄ«bai.

Sapratīsim, kas ir SOLID principi un kā tie mums palīdz.

Skillbox iesaka: Praktiskais kurss "Mobile Developer PRO".

Atgādinām: visiem "Habr" lasītājiem - atlaide 10 000 rubļu, reģistrējoties jebkurā Skillbox kursā, izmantojot "Habr" reklāmas kodu.

Kas ir SOLID?

Šis termins ir saīsinājums, katrs termina burts ir noteikta principa nosaukuma sākums:

Vienas atbildības princips

ā€Š
Vienotās atbildÄ«bas princips (Single Responsibility Principle ā€” SRP) nosaka, ka katrai programmas klasei vai modulim ir jāatbild tikai par vienu Ŕīs programmas funkcionalitātes daļu. Turklāt Ŕīs atbildÄ«bas elementi ir jāpieŔķir savai klasei, nevis jāizkaisa pa nesaistÄ«tām klasēm. SRP izstrādātājs un galvenais evaņģēlists Roberts S. Mārtins apraksta atbildÄ«bu kā pārmaiņu iemeslu. Sākotnēji viņŔ ierosināja Å”o terminu kā vienu no sava darba "Objektorientētā dizaina principi" elementiem. Koncepcija ietver lielu daļu savienojamÄ«bas modeļa, ko iepriekÅ” definēja Toms Demarko.

Koncepcijā bija iekļauti arÄ« vairāki Deivida Parnasa formulēti jēdzieni. Divas galvenās ir iekapsulÄ“Å”ana un informācijas slēpÅ”ana. Parnas apgalvoja, ka sistēmas sadalÄ«Å”anu atseviŔķos moduļos nevajadzētu balstÄ«t uz blokshēmu vai izpildes plÅ«smu analÄ«zi. Jebkurā no moduļiem ir jābÅ«t konkrētam risinājumam, kas klientiem sniedz minimālu informāciju.

Starp citu, Martins sniedza interesantu piemēru ar uzņēmuma augstākajiem vadÄ«tājiem (COO, CTO, CFO), no kuriem katrs izmanto specifisku biznesa programmatÅ«ru dažādiem mērÄ·iem. Rezultātā jebkurÅ” no tiem var ieviest izmaiņas programmatÅ«rā, neietekmējot citu vadÄ«tāju intereses.

DieviŔķs objekts

Kā vienmēr, labākais veids, kā apgūt SRP, ir redzēt to darbībā. Apskatīsim programmas sadaļu, kurā NAV ievērots vienotas atbildības princips. Šis ir Rubīna kods, kas apraksta kosmosa stacijas uzvedību un atribūtus.

Pārskatiet piemēru un mēģiniet noteikt tālāk norādīto.
To objektu pienākumi, kuri ir deklarēti SpaceStation klasē.
Tie, kurus varētu interesēt kosmosa stacijas darbība.

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

PatiesÄ«bā mÅ«su kosmosa stacija nedarbojas (es nedomāju, ka tuvākajā laikā saņemÅ”u zvanu no NASA), taču Å”eit ir ko analizēt.

Tādējādi SpaceStation klasei ir vairāki dažādi pienākumi (vai uzdevumi). Tos visus var iedalīt tipos:

  • sensori;
  • izejmateriāli (palÄ«gmateriāli);
  • degviela;
  • paātrinātāji.

Lai arÄ« nevienam no stacijas darbiniekiem nav iedalÄ«ta klase, mēs varam viegli iedomāties, kurÅ” par ko ir atbildÄ«gs. Visticamāk, zinātnieks kontrolē sensorus, loÄ£istikas pienākums ir nodroÅ”ināt resursus, inženieris ir atbildÄ«gs par degvielas piegādēm, bet pilots kontrolē pastiprinātājus.

Vai mēs varam teikt, ka Ŕī programma nav saderÄ«ga ar SRP? Jā, protams. Bet SpaceStation klase ir tipisks "dieva objekts", kas zina visu un dara visu. Å is ir galvenais objektorientētās programmÄ“Å”anas pretmodelis. Iesācējam Ŕādus objektus ir ārkārtÄ«gi grÅ«ti uzturēt. Pagaidām programma ir ļoti vienkārÅ”a, jā, taču iedomājieties, kas notiks, ja pievienosim jaunas funkcijas. VarbÅ«t mÅ«su kosmosa stacijai bÅ«s nepiecieÅ”ama medicÄ«nas stacija vai sanāksmju telpa. Un jo vairāk bÅ«s funkciju, jo vairāk SpaceStation pieaugs. Tā kā Ŕī iekārta bÅ«s savienota ar citām, visa kompleksa apkalpoÅ”ana kļūs vēl grÅ«tāka. Rezultātā varam traucēt, piemēram, akseleratoru darbÄ«bu. Ja pētnieks pieprasa izmaiņas sensoros, tas var ļoti labi ietekmēt stacijas sakaru sistēmas.

SRP principa pārkāpÅ”ana var dot Ä«stermiņa taktisku uzvaru, taču galu galā mēs ā€œzaudēsim karuā€, un turpmāk Ŕādu briesmoni uzturēt bÅ«s ļoti grÅ«ti. Vislabāk ir sadalÄ«t programmu atseviŔķās koda sadaļās, no kurām katra ir atbildÄ«ga par konkrētas darbÄ«bas veikÅ”anu. To saprotot, mainÄ«sim SpaceStation klasi.

Sadalīsim atbildību

IepriekÅ” mēs definējām četrus operāciju veidus, kurus kontrolē SpaceStation klase. Mēs tos paturēsim prātā, veicot pārstrukturÄ“Å”anu. Atjauninātais kods labāk atbilst 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

Ir daudz izmaiņu, programma tagad noteikti izskatās labāk. Tagad mūsu SpaceStation klase ir kļuvusi vairāk par konteineru, kurā tiek uzsāktas darbības ar atkarīgām daļām, tostarp sensoru komplektu, patērējamo materiālu padeves sistēmu, degvielas tvertni un pastiprinātājiem.

Jebkuram no mainÄ«gajiem tagad ir atbilstoÅ”a klase: Sensors; SupplyHold; Degvielas tvertne; Dzinēji.

Å ajā koda versijā ir vairākas svarÄ«gas izmaiņas. Lieta ir tāda, ka atseviŔķas funkcijas ir ne tikai iekapsulētas savās klasēs, tās ir organizētas tā, lai tās kļūtu paredzamas un konsekventas. Mēs grupējam elementus ar lÄ«dzÄ«gu funkcionalitāti, lai ievērotu saskaņotÄ«bas principu. Tagad, ja mums ir jāmaina sistēmas darbÄ«bas veids, pārejot no hash struktÅ«ras uz masÄ«vu, vienkārÅ”i izmantojiet SupplyHold klasi; mums nav jāpieskaras citiem moduļiem. Tādā veidā, ja loÄ£istikas virsnieks kaut ko mainÄ«s savā sekcijā, pārējā stacija paliks neskarta. Å ajā gadÄ«jumā SpaceStation klase pat neuzzinās par izmaiņām.

MÅ«su virsnieki, kas strādā kosmosa stacijā, droÅ”i vien ir priecÄ«gi par izmaiņām, jo ā€‹ā€‹viņi var pieprasÄ«t sev nepiecieÅ”amās. Ņemiet vērā, ka kodam ir tādas metodes kā report_supplies un report_fuel, kas ietvertas klasēs SupplyHold un FuelTank. Kas notiktu, ja Zeme lÅ«gtu mainÄ«t veidu, kā tā ziņo? Abas klases, SupplyHold un FuelTank, bÅ«s jāmaina. Ko darÄ«t, ja jāmaina degvielas un palÄ«gmateriālu piegādes veids? Visticamāk, jums atkal bÅ«s jāmaina visas tās paÅ”as klases. Un tas jau ir SRP principa pārkāpums. Labosim Å”o.

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.

Å ajā jaunākajā programmas versijā pienākumi ir sadalÄ«ti divās jaunās klasēs ā€” FuelReporter un SupplyReporter. Viņi abi ir Reportieru klases bērni. Turklāt SpaceStation klasei pievienojām instanču mainÄ«gos, lai vajadzÄ«bas gadÄ«jumā varētu inicializēt vajadzÄ«go apakÅ”klasi. Tagad, ja Zeme nolems mainÄ«t kaut ko citu, tad mēs veiksim izmaiņas apakÅ”klasēs, nevis galvenajā klasē.

Protams, dažas no mÅ«su klasēm joprojām ir atkarÄ«gas viena no otras. Tādējādi objekts SupplyReporter ir atkarÄ«gs no SupplyHold, un FuelReporter ir atkarÄ«gs no FuelTank. Protams, pastiprinātāji ir jāpievieno degvielas tvertnei. Bet Å”eit jau viss izskatās loÄ£iski, un izmaiņu veikÅ”ana nebÅ«s Ä«paÅ”i sarežģīta ā€“ viena objekta koda rediģēŔana citu Ä«paÅ”i neietekmēs.

Tādējādi esam izveidojuÅ”i modulāru kodu, kurā ir precÄ«zi definēti katra objekta/klases pienākumi. Darbs ar Ŕādu kodu nav problēma, tā uzturÄ“Å”ana bÅ«s vienkārÅ”s uzdevums. Mēs esam pārveidojuÅ”i visu "dieviŔķo objektu" par SRP.

Skillbox iesaka:

Avots: www.habr.com

Pievieno komentāru