Paindliku koodi kirjutamine SOLIDi abil

Paindliku koodi kirjutamine SOLIDi abil

Tõlkijalt: teie jaoks avaldatud Severin Perezi artikkel SOLID põhimõtete kasutamisest programmeerimisel. Artiklis sisalduv teave on kasulik nii algajatele kui ka kogenud programmeerijatele.

Kui olete arendusega seotud, olete tõenäoliselt kuulnud SOLIDi põhimõtetest. Need võimaldavad programmeerijal kirjutada puhast, hästi struktureeritud ja kergesti hooldatavat koodi. Väärib märkimist, et programmeerimisel on konkreetse töö korrektseks täitmiseks mitu lähenemisviisi. Erinevatel spetsialistidel on erinevad ideed ja arusaam “õigest teest”, kõik sõltub igaühe kogemusest. SOLIDis kuulutatud ideid aktsepteerivad aga peaaegu kõik IT-kogukonna esindajad. Need said paljude heade arendusjuhtimise tavade tekkimise ja arendamise lähtepunktiks.

Saame aru, mis on SOLID põhimõtted ja kuidas need meid aitavad.

Skillbox soovitab: Praktiline kursus "Mobile Developer PRO".

Tuletame meelde: kõigile "Habr" lugejatele - allahindlus 10 000 rubla, kui registreerute mis tahes Skillboxi kursusele, kasutades sooduskoodi "Habr".

Mis on SOLID?

See termin on lühend, termini iga täht on konkreetse põhimõtte nime algus:

  • Sühtne vastutuse põhimõte. Moodulil võib olla üks ja ainult üks muutmise põhjus.
  • . Opliiats/suletud põhimõte (avatud/suletud põhimõte). Klassid ja muud elemendid peaksid olema laiendamiseks avatud, kuid muutmiseks suletud.
  •  . Liskov asendusprintsiip (Liskovi asenduspõhimõte). Põhitüüpi kasutavad funktsioonid peaksid saama kasutada põhitüübi alamtüüpe seda teadmata.
  • . ILiidese eraldamise põhimõte  (liidese eraldamise põhimõte). Tarkvaraüksused ei tohiks sõltuda meetoditest, mida nad ei kasuta.
  • . Dsõltuvuse inversiooni põhimõte (sõltuvuse inversiooni põhimõte). Kõrgemate tasemete moodulid ei tohiks sõltuda madalamate tasemete moodulitest.

Ühtse vastutuse põhimõte


Ühtse vastutuse põhimõte (Single Responsibility Principle, SRP) ütleb, et programmi iga klass või moodul peaks vastutama ainult selle programmi funktsioonide ühe osa eest. Lisaks tuleks selle vastutuse elemendid määrata nende oma klassile, mitte jagada üksteisega mitteseotud klassidesse. SRP arendaja ja peaevangelist Robert S. Martin kirjeldab muutuste põhjusena vastutust. Algselt pakkus ta selle termini välja oma töö "Objektorienteeritud disaini põhimõtted" ühe elemendina. Kontseptsioon sisaldab suurt osa ühenduvusmustrist, mille oli varem määratlenud Tom DeMarco.

Kontseptsioon hõlmas ka mitmeid David Parnase sõnastatud kontseptsioone. Kaks peamist on kapseldamine ja teabe peitmine. Parnas väitis, et süsteemi jagamine eraldi mooduliteks ei tohiks põhineda plokkskeemide või täitmisvoogude analüüsil. Iga moodul peab sisaldama konkreetset lahendust, mis annab klientidele minimaalselt teavet.

Muide, Martin tõi huvitava näite ühe ettevõtte tippjuhtidega (COO, CTO, CFO), kellest igaüks kasutab konkreetset majandustarkvara erinevatel eesmärkidel. Tänu sellele saab igaüks neist tarkvaras muudatusi sisse viia, ilma et see mõjutaks teiste juhtide huve.

Jumalik objekt

Nagu alati, on parim viis SRP-i õppimiseks näha seda tegevuses. Vaatame programmi osa, mis EI järgi ühtse vastutuse põhimõtet. See on Ruby kood, mis kirjeldab kosmosejaama käitumist ja atribuute.

Vaadake näide üle ja proovige kindlaks teha järgmist.
Nende objektide kohustused, mis on deklareeritud 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} 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

Tegelikult on meie kosmosejaam düsfunktsionaalne (ma ei usu, et mulle niipea NASA helistab), kuid siin on, mida analüüsida.

Seega on SpaceStationi klassil mitmeid erinevaid kohustusi (või ülesandeid). Kõik need võib jagada tüüpideks:

  • andurid;
  • tarvikud (tarbekaubad);
  • kütus;
  • kiirendid.

Kuigi ühelegi jaama töötajale pole klasse määratud, võime kergesti ette kujutada, kes mille eest vastutab. Tõenäoliselt juhib teadlane andureid, logistik vastutab ressursside tarnimise eest, insener vastutab kütusevarude eest ja piloot juhib võimendusi.

Kas võime öelda, et see programm ei ühildu SRP-ga? Jah muidugi. Kuid SpaceStationi klass on tüüpiline "jumalobjekt", mis teab kõike ja teeb kõike. See on objektorienteeritud programmeerimise peamine antimuster. Algajale on selliseid objekte ülimalt raske hooldada. Seni on programm väga lihtne, jah, kuid kujutage ette, mis juhtub, kui lisame uusi funktsioone. Võib-olla vajab meie kosmosejaam meditsiinipunkti või koosolekuruumi. Ja mida rohkem funktsioone on, seda rohkem SpaceStation kasvab. Kuna see rajatis ühendatakse teistega, muutub kogu kompleksi teenindamine veelgi keerukamaks. Selle tulemusena võime häirida näiteks kiirendite tööd. Kui teadlane nõuab andurite muutmist, võib see jaama sidesüsteeme väga hästi mõjutada.

SRP põhimõtte rikkumine võib küll anda lühiajalise taktikalise võidu, kuid lõpuks "kaotame sõja" ja sellise monstrumi ülalpidamine muutub tulevikus väga keeruliseks. Parim on jagada programm eraldi koodiosadeks, millest igaüks vastutab konkreetse toimingu sooritamise eest. Sellest aru saades muudame SpaceStationi klassi.

Jagame vastutust

Eespool määratlesime nelja tüüpi toiminguid, mida juhib SpaceStationi klass. Peame neid ümbertöötamisel meeles. Värskendatud kood sobib paremini SRP-ga.

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

Muutusi on palju, programm näeb nüüd kindlasti parem välja. Nüüd on meie klassist SpaceStation saanud rohkem konteiner, milles käivitatakse toimingud sõltuvate osade, sealhulgas andurite komplekti, kulumaterjalide toitesüsteemi, kütusepaagi ja võimendite jaoks.

Iga muutuja jaoks on nüüd vastav klass: Sensors; SupplyHold; Kütusepaak; Tõukurid.

Koodi selles versioonis on mitmeid olulisi muudatusi. Asi on selles, et üksikud funktsioonid ei ole mitte ainult kapseldatud oma klassidesse, vaid need on korraldatud nii, et need muutuksid etteaimatavaks ja järjepidevaks. Rühmitame sarnase funktsionaalsusega elemendid sidususe põhimõtte järgimiseks. Nüüd, kui meil on vaja süsteemi toimimist muuta, liikudes räsistruktuurilt massiivile, kasutage lihtsalt klassi SupplyHold; me ei pea teisi mooduleid puudutama. Nii jääb siis, kui logistik oma jaos midagi muudab, ülejäänud jaam puutumata. Sel juhul pole SpaceStationi klass muudatustest isegi teadlik.

Meie kosmosejaamas töötavad ohvitserid on ilmselt muudatuste üle rõõmsad, sest saavad endale vajalikke nõuda. Pange tähele, et koodil on sellised meetodid nagu report_supplies ja report_fuel, mis sisalduvad klassides SupplyHold ja FuelTank. Mis juhtuks, kui Maa paluks muuta oma aruandlusviisi? Mõlemad klassid, SupplyHold ja FuelTank, tuleb muuta. Mida teha, kui teil on vaja muuta kütuse ja kulumaterjalide tarnimise viisi? Tõenäoliselt peate kõik samad klassid uuesti vahetama. Ja see on juba SRP põhimõtte rikkumine. Teeme selle korda.

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.

Programmi viimases versioonis on kohustused jagatud kahte uude klassi: FuelReporter ja SupplyReporter. Mõlemad on Reporteri klassi lapsed. Lisaks lisasime SpaceStationi klassi eksemplarimuutujad, et vajadusel saaks soovitud alamklassi initsialiseerida. Kui nüüd Maa otsustab midagi muud muuta, siis teeme muudatusi alamklassides, mitte põhiklassis.

Muidugi sõltuvad mõned meie klassid endiselt üksteisest. Seega sõltub objekt SupplyReporter SupplyHoldist ja FuelReporter FuelTankist. Loomulikult tuleb võimendid ühendada kütusepaagiga. Kuid siin tundub kõik juba loogiline ja muudatuste tegemine pole eriti keeruline - ühe objekti koodi redigeerimine ei mõjuta teist oluliselt.

Seega oleme loonud modulaarse koodi, kus iga objekti/klassi vastutusalad on täpselt määratletud. Sellise koodiga töötamine pole probleem, selle hooldamine on lihtne ülesanne. Oleme teisendanud kogu "jumaliku objekti" SRP-ks.

Skillbox soovitab:

Allikas: www.habr.com

Lisa kommentaar