Kirjutame paindlikku koodi, kasutades SOLID-põhimõtteid

Kirjutame paindlikku koodi, kasutades SOLID-põhimõtteid

Tõlkijalt: olenud teie jaoks Severin Perese artikkel SOLID-põhimõtete rakendamisest programmeerimises. Artikli teave on kasulik nii algajatele kui ka kogenud programmeerijatele.

Kui tegelete arendusega, olete tõenäoliselt kuulnud SOLID-põhimõtetest. Need võimaldavad programmeerijal kirjutada puhtat, hästi struktureeritud ja lihtsasti hooldatavat koodi. Tuleb märkida, et programmeerimises on mitu lähenemist sellele, kuidas õigesti täita mingit ülesannet. Erinevatel spetsialistidel on erinevad ideed ja arusaamad "õigest teest", kõik oleneb igaühe kogemusest. Siiski, SOLIDis väljendatud ideed on vastuvõetavad peaaegu kõigi IT-kommuuni esindajate poolt. Need on saanud alguseks mitmete heade arendusjuhtimise meetodite tekkimisele ja arengule.

Vaatame, mis on SOLID-põhimõtted ja kuidas need meid aitavad.

Skillbox soovitab: Praktiline kursus «Mobiilne arendaja PRO».

Tuletame meelde: kõigile «Habr» lugejatele – 10 000 rubla allahindlus igale Skillboxi kursusele, kasutades sooduskoodi «Habr».

Mis on SOLID?

See termin on akronüüm, kus iga tähe algus on teatud põhimõtte nimi:

Ühe vastutuse põhimõte


Ühe vastutuse põhimõte (SRP) ütleb, et iga klass või moodul programmis peab vastutama ainult ühte osa selle programmist. Ühtlasi peavad selle vastutuse elemendid olema kinnitatud oma klassi, mitte jaotatud omavahel mitteseotud klasside vahel. Arendaja ja SRP peamine evangelist, Robert C. Martin, kirjeldab vastutust kui muutuste põhjust. Alguses ettepanek, et see termin on üks elementi tema töös "Objekti-orienteeritud disaini põhimõtted". Kontseptsioonis on palju sisse viidud seotud lahenduse seadusi, mis on varem määratlenud Tom DeMarco.

Lisaks on kontseptsiooni tulnud mitu mõistet, mille on sõnastanud David Parnas. Kaks peamist — kapseldamine ja teabe varjamine. Parnas väitis, et süsteemi jagamine eraldi mooduliteks ei tohiks põhineda vooskeemide või täitmisvoogude analüüsil. Iga moodul peaks sisaldama konkreetset lahendust, mis pakub klientidele minimaalset teavet.

Muide, Martin tõi huvitava näite ettevõtte kõrgema juhtkonna (COO, CTO, CFO) kaudu, igaühel neist on konkreetne äritarkvara, mida nad kasutavad erinevatel eesmärkidel. Seetõttu võib igaüks neist tarkvara muuta, mõjutamata teiste juhtide huve.

Jumallik objekt

Nagu tavaliselt, on parim viis SRP-d õppida, näha seda kõike töös. Vaadakem programmi lõiku, mis ei vasta ühe vastutuse põhimõttele. See on Ruby-kood, mis kirjeldab kosmosesüsteemi käitumist ja atribuute.

Vaadake näidet ja proovige kindlaks teha järgmist:
Need objektid, mille kohustused on määratletud klassis SpaceStation.
Need, kes võivad olla huvitatud kosmosejaama tööst.

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õepoolest, meie kosmosejaam ei tööta (arvan, et ei saa NASA-d peagi kõnetama), kuid siin on palju, mida analüüsida.

Nii et klassil SpaceStation on mitu erinevat vastutust (või ülesannet). Kõik need saab jagada tüüpideks:

  • andurid;
  • varustus (tarvikud);
  • kütus;
  • thrustid.

Kuigi jaama töötajad ei ole klassi määratud, saame kergesti ette kujutada, kes millise ülesande täidab. Tõenäoliselt jälgib teadlane andureid, logistika vastutab varude eest, insener vastutab kütusepeamiste eest, ja piloot kontrollib thrustide tööd.

Kas me saame öelda, et see programm ei vasta SRP-le? Jah, kindlasti. Kuid klass SpaceStation on tüüpiline „jumalik objekt“, mis teab kõigest ja teeb kõike. See on peamine antipattern objektorienteeritud programmeerimises. Algajatele on sellised objektid äärmiselt keerulised hooldada. Praegu on programm väga lihtne, jah, aga kujutage ette, mis juhtub, kui lisame uusi funktsioone. Võib-olla vajab meie kosmosejaam meditsiinipunkti või koosolekuruumi. Ja mida rohkem funktsioone lisatakse, seda keerulisemaks muutub SpaceStation. Kuna see objekt on seotud teiste objektidega, muutub kogu kompleksi hooldamine veelgi keerulisemaks. Lõpuks võime rikku hakata, näiteks, kiirendites. Kui teadlane küsib muudatusi anduritelt töötamisel, võib see kindlasti mõjutada jaama side süsteeme.

SRP-printsiibi rikkumine võib anda lühiajalise taktikalise võidu, kuid lõpuks „kaotame sõja“, kuna sellise koletise hooldamine tulevikus muutub üsna keeruliseks. Parim on jagada programm eraldi koodilõikudeks, millest igaüks vastutab konkreetse operatsiooni täitmise eest. Seda mõistes muudame klassi SpaceStation.

Jagame vastutust

Ülal oleme määratlenud neli tüüpi operatsiooni, mida kontrollib klass SpaceStation. Refaktoorimise käigus võtame need arvesse. Uuendatud kood vastab paremini SRP-le.

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

Muutusi on palju, programm näeb nüüd tõeliselt parem välja. Meie SpaceStation klass on nüüd pigem konteiner, kus algatatakse tegevusi sõltuvate osade jaoks, sealhulgas andurite komplekt, tarnehalduse süsteem, kütusepaak ja thrusterid.

Iga muutuja jaoks on nüüd vastav klass: Sensors; SupplyHold; FuelTank; Thrusters.

Selles koodiversioonis on mitmeid olulisi muudatusi. Esiteks on eraldi funktsioonid mitte ainult kapseldatud oma klassidesse, vaid need on ka korraldatud viisil, mis muudab need etteennustatavaks ja järjepidevaks. Me grupeerime funktsionaalselt sarnased elemendid, et järgida seotuse põhimõtet. Nüüd, kui me peame süsteemi toimimise printsiipi muutma, liikudes hash-struktuurilt massiivi, saame lihtsalt kasutada klassi SupplyHold, puudutamata teisi mooduleid. Nii et kui logistikaülem muudab midagi oma sektsioonis, jääb ülejäänud jaam puutumata. Klass SpaceStation ei saa sellest muutusest isegi teadlikuks.

Meie ohvitserid, kes töötavad kosmosejaamas, on tõenäoliselt muudatustest rõõmsad, kuna nad saavad taotleda just neid, mis neile vajalikud. Pange tähele, et koodis on sellised meetodid nagu report_supplies ja report_fuel, mis sisalduvad klassides SupplyHold ja FuelTank. Mis juhtub, kui Maa palub muuta aruannete koostamise viisi? Mõlemat klassi, SupplyHold ja FuelTank, tuleb muuta. Aga mis siis, kui tuleb muuta kütuse ja tarvikute tarnimise viisi? Ilmselt tuleb siis jälle muuta kõiki neid samu klasse. See rikub juba SRP-printsiipi. Parandame selle.

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.

Selles, viimases versioonis on ülesanded jagatud kaheks uueks klassiks, FuelReporter ja SupplyReporter. Mõlemad on tütreklassid Reporter klassi suhtes. Lisaks oleme lisanud eksemplarimuutujad SpaceStation klassi, et vajadusel vajalik allklass initsialiseerida. Nüüd, kui Maa otsustab midagi veel muuta, teeme parandused allklassidesse, mitte põhiklassi.

Loomulikult sõltuvad mõned klassid endiselt üksteisest. Näiteks sõltub SupplyReporter SupplyHold objektist ja FuelReporter sõltub FuelTank objektist. Muidugi peavad kiirendid olema seotud kütusepaagiga. Kuid nüüd paistab kõik loogilisena ning muudatuste tegemine ei ole eriti keeruline — ühe objekti koodi redigeerimine ei mõjuta teist liiga palju.

Seega oleme loonud modulaarse koodi, kus iga objekti/kliendi kohustused on täpselt määratletud. Sellise koodiga töötamine ei ole probleem ning selle hooldamine on lihtne ülesanne. Kogu "jumalik objekt" oleme muutnud SRP-ks.

Skillbox soovitab:

Allikas: habr.com

Osta usaldusväärne hostimine veebilehtede jaoks DDoS-i kaitsega, VPS VDS serverid 🔥 Osta usaldusväärne hostimine veebilehtede jaoks DDoS-i kaitsega, VPS VDS serverid | ProHoster