Kirjutame paindlikku koodi, kasutades SOLID

Kirjutame paindlikku koodi, kasutades SOLID

Tõlkijalt: avalikustatud teie jaoks Severin Peresi artikkel SOLID-põhimõtete kasutamisest programmeerimises. Artiklist saadud teave on kasulik nii algajatele kui ka kogenud programmeerijatele.

Kui tegelete arendusega, olete tõenäoliselt kuulnud SOLID-põhimõtetest. Need annavad võimaluse programmeerijale kirjutada puhtaid, hästi struktureeritud ja hõlpsasti hallatavaid koode. Oluline on märkida, et programmeerimises on mitmeid lähenemisviise, kuidas õiget tööd teha. Erinevatel spetsialistidel on erinevad ideed ja arusaamad "õigest teest", kõik sõltub igaühe kogemustest. Siiski on SOLIDis väljatud ideed aktsepteeritud praktiliselt kõikide IT-ühiskonna esindajate seas. Need on saanud lähtepunktiks paljude heade arenduse juhtimise 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 «Habra» lugejatele — 10 000 rubla soodustus, kui registreerite end Skillboxi mis tahes kursusele promokoodi «Habr» abil.

Mis on SOLID?

See termin on lühend, kus iga tähe algusosa viitab konkreetse põhimõtte nimele:

  • SSingle Responsibility Principle (ühe vastutuse põhimõte). Moodulil võib olla üks ja ainult üks põhjus muutuda.
  • The OSulgemise põhimõte (avatuse/sulgemise põhimõte). Klassid ja muud elemendid peaksid olema avatud laiendamiseks, kuid suletud muutmiseks.
  •  The LLiskovi asendamise põhimõte (Liskovi substiutsiooni põhimõte). Funktsioonid, mis kasutavad põhityypi, peaksid saama kasutada põhitypist alam tüüpe, teadmata sellest.
  • The IInterfaci jagamise põhimõte  (interfaci segregatsiooni põhimõte). Tarkvaralised olendid ei tohi sõltuda meetoditest, mida nad ei kasuta.
  • The DSõltuvuste pööramise põhimõte (sõltuvuste inversiooni põhimõte). Ülemiste tasandite moodulid ei tohiks sõltuda alumiste tasandite moodulitest.

Ühe vastutuse põhimõte


Ühe vastutuse põhimõte (SRP) ütleb, et iga klass või moodul programmis peaks vastutama ainult ühe osa selle programmi funktsionaalsusest. Samuti peab selle vastutuse elemendid olema seotud oma klassiga, mitte jagatud mittevastavate klasside vahel. Arendaja ja SRP peamine evangelist, Robert C. Martin, kirjeldab vastutust kui muutuste põhjust. Alguses pakkus ta selle termini välja ühe elemendina oma töös "Objektorienteeritud disaini põhimõtted". Kontseptsiooni kuulus palju eespool määratletud seotuse seadusest, mille määratles varem Tom DeMarco.

Kontseptsiooni kuulusid ka mitmed mõisted, mille töötas välja David Parnas. Kaks peamist neist on kapseldamine ja teabe peitmine. Parnas väitis, et süsteemi jagamine eraldi mooduliteks ei tohiks põhineda diagrammi analüüsil ega täitmisvoogudel. Iga moodul peaks sisaldama kindlat lahendust, mis annab minimaalset teavet klientidele.

Muide, Martin tõi huvitava näite ettevõtte kõrgemast juhtkonnast (COO, CTO, CFO), kellest igaühel on erinevatel põhjustel spetsiifilised äritarkvara rakendused. Seetõttu võib igaüks neist tarkvaras muudatusi teha, mõjutamata teiste juhtide huve.

Jumalik objekt

Nagu tavaliselt, on parim viis SRP-d õppida näha seda kõik toimimas. Vaadakem programmi osa, mis EI vasta üksiku vastutuse põhimõttele. See on Ruby-kood, mis kirjeldab kosmosejaama käitumist ja atribuute.

Vaadake näidet ja proovige määrata järgmist:
Need objektid, mille kohustused on määratletud SpaceStation klassis.
Need, kes võivad olla huvitatud kosmosejaamast.

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

Kuna meie kosmosejaam ei tööta (ma arvan, et NASA ei helista mulle lähitulevikus), on siin siiski midagi, mida analüüsida.

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

  • andurid;
  • varustus (tarbijamaterjalid);
  • kütus;
  • kiirendid.

Kuigi jaama töötajaid ei ole klassi määratud, on meil lihtne ette kujutada, kes mille eest vastutab. Tõenäoliselt kontrollib teadustöötaja andureid, logistika vastutab ressurside varustamise eest, insener hooldab kütusevarusid ning piloot jälgib kiirendeid.

Kas me saame öelda, et see programm ei vasta SRP-le? Jah, loomulikult. Kuid klass SpaceStation on tüüpiline "jumalik objekt", mis teab kõike ja teeb kõike. See on peamine antipattern objektorienteeritud programmeerimises. Algajate jaoks on sellised objektid äärmiselt keerulised hooldada. Praegu on programm väga lihtne, jah, kuid kujutage ette, mis juhtub, kui lisame uusi funktsioone. Võib-olla on meie kosmosejaamal vaja meditsiinituba või koosolekuruumi. Ja mida rohkem funktsioone, seda suuremaks muutub SpaceStation. Kuna see objekt on seotud teistega, siis muutub kogu kompleksi hooldamine veelgi keerulisemaks. Lõppkokkuvõttes võime häirida näiteks kiirendite toimimist. Kui teadlane soovib muudatusi sensoritega töötamisse, võib see tõepoolest mõjutada jaama side süsteeme.

SRP-printsiibi rikkumine võib anda lühiajalise taktikalise võidu, kuid lõpuks kaotame "sõja", sellise monstri teenindamine tulevikus muutub äärmiselt keeruliseks. Parim on jagada programm eraldi koodilõikudeks, millest igaühe ülesanne on teatud tegevuse täitmine. Olles sellest aru saanud, muudame klassi SpaceStation.

Jagame vastutuse

Ülalpool määratlesime neli tüüpi toiminguid, mida kontrollib klass SpaceStation. Refaktoreerimise käigus peame neid silmas pidama. 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

Muudatusi on palju, programm näeb nüüd tõeliselt parem välja. Meie SpaceStation klassist sai nüüd pigem konteiner, kus initsialiseeritakse sõltuvate osade operatsioonid, sealhulgas sensorite kogum, tarvikute varustussüsteem, kütusepaak ja kiirendid.

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

Selles koodiversioonis on mitmeid olulisi muudatusi. Esiteks, eraldi funktsioonid ei ole ainult isoleeritud oma klassidesse, vaid need on organiseeritud viisil, et need muutuvad ettearvatavateks ja järjekindlateks. Me grupeerime sarnaste funktsionaalsustega elemendid, et järgida sidususe põhimõtet. Nüüd, kui me peame süsteemi tööpõhimõtet muutma, liikudes hash-struktuurilt massiivi suunas, kasutage lihtsalt klassi SupplyHold ja teisi mooduleid pole vaja puudutada. Seega, kui logistikaülem midagi oma sektsioonis muudab, jäävad ülejäänud jaamad muutmata. Samal ajal ei ole klass SpaceStation isegi teadlik muudatustest.

Meie ohvitserid, kes töötavad kosmosejaamas, on tõenäoliselt muutustest rõõmsad, kuna nad saavad taotleda neid ressursse, mis on neile vajalikud. Pange tähele, et koodis on sellised meetodid nagu report_supplies ja report_fuel, mis kuuluvad klassidele SupplyHold ja FuelTank. Mis juhtub, kui Maa palub muuta aruandekooste viisi? Tuleb muuta mõlemat klassi, SupplyHold ja FuelTank. Aga ent kui muutub kütuse ja tarvikute tarnimise viis? Tõenäoliselt tuleb jälle muuda kõik need samad klassid. See aga rikub 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 alamharud Reporter klassist. Lisaks lisasime SpaceStation klassile eksemplari muutujad, et vajadusel õiged alamklassid initsialiseerida. Nii et kui Maa otsustab midagi veel muuta, teeme me muudatused alamklassidesse, mitte põhiklassi.

Muidugi, mõned klassid sõltuvad endiselt üksteisest. Näiteks objekt SupplyReporter sõltub SupplyHold'ist, samas kui FuelReporter sõltub FuelTank'ist. Loomulikult peavad kiirendid olema seotud kütuse mahutiga. Kuid siin näeb kõik välja loogiline ning muudatuste tegemine ei ole eriti keeruline — ühe objekti koodi muutmine ei mõjuta teist oluliselt.

Nii oleme loonud modulaarse koodi, kus iga objekti/klassi ülesanded on selgelt määratletud. Sellise koodiga töötamine ei ole probleem ja selle hooldamine on lihtne ülesanne. Kogu 'jumalik objekt' on muudetud SRP-ks.

Skillbox soovitab:

Allikas: habr.com

Osta usaldusväärne veebihosting DDoS kaitsega, VPS VDS serverid 🔥 Osta usaldusväärne veebihosting DDoS kaitsega, VPS VDS serverid | ProHoster