Rakstām elastīgu kodu, izmantojot SOLID

Rakstām elastīgu kodu, izmantojot SOLID

No tulkotāja: publicējusi jums Severina Peresa rakstu par SOLID principu izmantošanu programmēšanā. Informācija no raksta būs noderīga gan iesācējiem, gan pieredzējušiem programmētājiem.

Ja jūs nodarbojaties ar izstrādi, tad, visticamāk, esat dzirdējis par SOLID principiem. Tie dod iespēju programmētājam rakstīt tīru, labi strukturētu un viegli uzturamu kodu. Jāatzīmē, ka programmēšanā ir vairāki pieejas veidi, kā pareizi veikt konkrētu darbu. Dažādiem speciālistiem ir dažādas idejas un izpratne par "pareizo ceļu"; viss ir atkarīgs no katra pieredzes. Tomēr idejas, kas izvirzītas SOLID, tiek pieņemtas praktiski no visiem IT kopienas pārstāvjiem. Tās kļuvušas par pamatu daudzām labām izstrādes vadības metodēm.

Paskatīsimies, kas ir SOLID principi un kā tie mums palīdz.

Skillbox iesaka: Praktiskais kurss «Mobilais izstrādātājs PRO».

Atgādinām: visiem «Habr» lasītājiem — atlaide 10 000 rubļu reģistrējoties jebkurā Skillbox kursā ar promo kodu «Habr».

Kas ir SOLID?

Šis termins ir akronīms, katra termiņa burts ir konkrēta principa nosaukuma sākuma burts:

Vienīgās atbildības princips

 
Vienotības atbildības princips (SRP) nosaka, ka katram klasei vai modulim programmā jāuzņemas atbildība tikai par vienu funkcionalitātes daļu. Turklāt šīs atbildības elementi jāpiešķir savai klasei, nevis jāizkliedē pa saistītām klasēm. Izstrādātājs un galvenais SRP evaņģēlists Roberts C. Martins apraksta atbildību kā izmaiņu iemeslu. Viņš pirmoreiz piedāvāja šo terminu kā vienu no savām idejām darbā "Objektorientēta projektēšanas principi". Koncepcijā iekļauta arī daļa no saistības likumsakarības, ko iepriekš definējis Toms Demarko.

Koncepcijā iekļauti arī vairāki jēdzieni, ko izstrādājis Deivids Parnas. Divi galvenie ir inkapsulācija un informācijas slēpšana. Parnas apgalvoja, ka sistēmas sadalīšana atsevišķos moduļos nedrīkst balstīties uz blokshēmu vai izpildes plūsmu analīzi. Katrai no moduļiem jāietver konkrētas risinājums, kas nodrošina minimālu informāciju klientiem.

Interesanti, ka Martins minēja interesantu piemēru ar augstākajiem vadītājiem uzņēmumā (COO, CTO, CFO), katrs no kuriem izmanto specifisku programmatūru uzņēmējdarbībai ar atšķirīgiem mērķiem. Rezultātā katrs no viņiem var veikt izmaiņas programmā, neradot problēmas citiem vadītājiem.

Dievs objekts

Kā parasti, labākais veids, kā izprast SRP, ir redzēt to darbībā. Paskatīsimies uz programmas fragmentu, kas NAV saskaņā ar vienotības atbildības principu. Tā ir Ruby koda daļa, kas apraksta kosmosa stacijas uzvedību un atribūtus.

Pārlūkojiet piemēru un mēģiniet noteikt sekojošo:
To objektu pienākumi, kas pasludināti klasē SpaceStation.
Tos, kuri varētu būt ieinteresēti kosmosa stacijas darbībā.

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} available: #{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

Tātad, mūsu kosmiskā stacija pašlaik nav funkcionāla (domāju, ka tuvākajā nākotnē no NASA saņemšu zvanu), bet šeit ir ko analizēt.

Šādā veidā klasei SpaceStation ir vairākas atšķirīgas atbildības (vai uzdevumi). Visas tās var iedalīt pa veidiem:

  • sensori;
  • supply (patēriņa materiāli);
  • degviela;
  • strāvas aparāti.

Neskatoties uz to, ka neviena stacijas darbinieka loma nav noteikta klasē, mēs varam viegli iedomāties, kurš par ko atbild. Visticamāk, zinātnes darbinieks kontrolē sensorus, loģists atbild par piegādi, inženieris par degvielas rezerves, bet pilots kontrolē strāvas aparātus.

Vai mēs varam teikt, ka šī programma neatbilst SRP? Jā, protams. Bet klase SpaceStation ir tipisks "dievišķais objekts", kas zina visu un dara visu. Tas ir galvenais pretparaugs objektu orientētajā programmēšanā. Jaunajiem lietotājiem šādi objekti ir ārkārtīgi grūti apkalpojami. Pašlaik programma ir ļoti vienkārša, jā, bet iedomājieties, kas notiks, ja mēs pievienosim jaunas funkcijas. Iespējams, mūsu kosmosa stacijai būs nepieciešama medicīnas telpa vai sarunu istaba. Jo vairāk funkciju būs, jo vairāk pieaugs SpaceStation. Tā kā šis objekts būs saistīts ar citiem, visa kompleksa apkalpošana kļūs vēl sarežģītāka. Galu galā mēs varam izjaukt, piemēram, paātrinātājus. Ja zinātniskais darbinieks lūgs izmaiņas darbā ar sensoriem, tas var ietekmēt stacijas sakaru sistēmas.

SRP principa pārkāpums var sniegt īslaicīgu taktisku uzvaru, bet galu galā mēs "zaudēsim karu", apkalpot šādu monsteru nākotnē kļūs diezgan grūti. Labāk ir sadalīt programmu atsevišķos koda gabalos, katrs no kuriem atbild par konkrētas operācijas izpildi. Saprotot to, mainīsim klasi SpaceStation.

Sadalam atbildību

Iepriekš mēs noteicām četrus operāciju tipus, kurus kontrolē klase SpaceStation. Pārveidojot kodu, mēs to ņemsim vērā. 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} 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

Izmaiņas ir daudz, programma tagad izskatās noteikti labāk. Tagad mūsu klase SpaceStation ir kļuvusi par konteineru, kurā tiek uzsāktas operācijas atkarīgajām daļām, tostarp sensoru komplektam, izsniegšanas sistēmai, degvielas tvertnei un paātrinātājiem.

Jebkurai no mainīgajām tagad ir atbilstoša klase: Sensors; SupplyHold; FuelTank; Thrusters.

Šajā koda versijā ir vairāki svarīgi uzlabojumi. Proti, atsevišķas funkcijas ne tikai tiek iepakotas savos klasēs, bet tās ir organizētas tā, lai būtu paredzamas un konsekventas. Mēs grupējam līdzīgas funkcionālas sastāvdaļas, lai ievērotu saiknes principu. Tagad, ja mums būs nepieciešams mainīt sistēmas darbību, pārejot no hashu struktūras uz masīvu, vienkārši izmantosim klasi SupplyHold, un tas neietekmēs citus moduļus. Tādējādi, ja loģistikas darbinieks veiks izmaiņas savā jomā, pārējie stacijas elementi paliks neskarti. Turklāt klase SpaceStation pat nezinās par izmaiņām.

Mūsu darbinieki, kas strādā kosmosa stacijā, visticamāk, ir apmierināti ar izmaiņām, jo var pieprasīt tieši tās, kas viņiem nepieciešamas. Pievērsiet uzmanību, ka kodā ir tādi metodes kā report_supplies un report_fuel, kas ir iekļautas klasēs SupplyHold un FuelTank. Kas notiks, ja Zeme lūgs mainīt ziņojumu veidošanas veidu? Būs jāmaina abi klases – SupplyHold un FuelTank. Bet kas notiks, ja būs nepieciešams mainīt degvielas un piegādes veidu? Visticamāk, būs jāmaina tie paši klases vēlreiz. Un tas jau ir SRP principa pārkāpums. Labosim 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} available: #{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 available: 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ā, pēdējā programmas versijā, pienākumi tika sadalīti divās jaunās klasēs: FuelReporter un SupplyReporter. Abas tās ir bērnu klasēm attiecībā pret Reporter klasi. Turklāt mēs pievienojām instanču mainīgos SpaceStation klasei, lai vajadzības gadījumā varētu inicializēt nepieciešamo apakšklasi. Tagad, ja Zeme nolems kaut ko vēl mainīt, izmaiņas tiks veiktas apakšklasēs, nevis pamatklasē.

Protams, daudzas klases joprojām ir atkarīgas viena no otras. Piemēram, SupplyReporter objekts ir atkarīgs no SupplyHold, bet FuelReporter ir atkarīgs no FuelTank. Protams, paātrinātājiem jābūt saistītiem ar degvielas tvertni. Taču šeit viss izskatās loģiski, un izmaiņu veikšana nebūs īpaši sarežģīta — viena objekta koda rediģēšana būtiski neietekmēs otru.

Tādējādi mēs esam izveidojuši moduļu kodu, kur katra objekta/klases pienākumi ir skaidri definēti. Strādāt ar šādu kodu nav problēma, tā apkope būs vienkārša uzdevums. Mēs visu "dievišķo objektu" pārveidojām par SRP.

Skillbox iesaka:

Avots: habr.com

Iegādājieties uzticamu tīmekļa hostingu ar DDoS aizsardzību, VPS VDS serveriem 🔥 Iegādājieties uzticamu tīmekļa hostingu ar DDoS aizsardzību, VPS VDS serveriem | ProHoster