Að skrifa sveigjanlegan kóða með SOLID

Að skrifa sveigjanlegan kóða með SOLID

Frá þýðanda: birt fyrir þig grein eftir Severin Perez um að nota SOLID meginreglur í forritun. Upplýsingarnar úr greininni munu nýtast bæði byrjendum og reyndum forriturum.

Ef þú ert í þróun, hefur þú líklegast heyrt um SOLID meginreglurnar. Þeir gera forritaranum kleift að skrifa hreinan, vel uppbyggðan og auðvelt að viðhalda kóða. Það er athyglisvert að í forritun eru nokkrar aðferðir við hvernig á að framkvæma tiltekið starf rétt. Mismunandi sérfræðingar hafa mismunandi hugmyndir og skilning á „réttu leiðinni“; það veltur allt á reynslu hvers og eins. Hins vegar eru hugmyndirnar sem lýst er yfir í SOLID samþykktar af næstum öllum fulltrúum upplýsingatæknisamfélagsins. Þau urðu upphafið að tilkomu og þróun margra góðra þróunarstjórnunarvenja.

Við skulum skilja hvað SOLID meginreglurnar eru og hvernig þær hjálpa okkur.

Skillbox mælir með: Verklegt námskeið "Mobile Developer PRO".

Við minnum á: fyrir alla Habr lesendur - 10 rúblur afsláttur þegar þú skráir þig á hvaða Skillbox námskeið sem er með því að nota Habr kynningarkóðann.

Hvað er SOLID?

Þetta hugtak er skammstöfun, hver stafur hugtaksins er upphafið á nafni ákveðinnar meginreglu:

  • SIngle Ábyrgðarregla. Eining getur haft eina og eina ástæðu fyrir breytingum.
  • The Openni/lokuð meginregla (opin/lokuð regla). Flokkar og aðrir þættir ættu að vera opnir til framlengingar, en lokaðir fyrir breytingar.
  •  The Liskov Skiptingarreglan (Liskov skiptireglan). Aðgerðir sem nota grunngerð ættu að geta notað undirgerðir af grunngerð án þess að vita það.
  • The IViðmótsaðgreiningarreglan  (viðmótsaðskilnaðarreglan). Hugbúnaðareiningar ættu ekki að vera háðar aðferðum sem þeir nota ekki.
  • The Dependency Inversion Principle (regla um ósjálfstæði). Einingar á hærri stigum ættu ekki að vera háðar einingum á lægri stigum.

Meginreglan um eina ábyrgð


The Single Responsibility Principle (SRP) segir að hver flokkur eða eining í forriti ætti aðeins að bera ábyrgð á einum hluta af virkni þess forrits. Auk þess ætti að úthluta þáttum þessarar ábyrgðar á þeirra eigin bekk, frekar en að dreifa þeim á óskylda bekki. Hönnuður SRP og aðalboðberi, Robert S. Martin, lýsir ábyrgð sem ástæðu breytinga. Hann lagði upphaflega til þetta hugtak sem einn af þáttunum í verki sínu "Principles of Object-oriented Design". Hugmyndin felur í sér mikið af tengimynstrinum sem áður var skilgreint af Tom DeMarco.

Hugmyndin innihélt einnig nokkur hugtök mótuð af David Parnas. Þær tvær helstu eru hjúpun og fela upplýsingar. Parnas hélt því fram að skipting kerfis í aðskildar einingar ætti ekki að byggjast á greiningu á blokkarritum eða framkvæmdarflæði. Sérhver eining verður að innihalda sérstaka lausn sem veitir viðskiptavinum lágmarksupplýsingar.

Við the vegur, Martin gaf áhugavert dæmi með æðstu stjórnendum fyrirtækis (COO, CTO, CFO), sem hver um sig notar sérstakan viðskiptahugbúnað í mismunandi tilgangi. Þar af leiðandi getur hver þeirra innleitt breytingar á hugbúnaðinum án þess að hafa áhrif á hagsmuni annarra stjórnenda.

Guðdómlegur hlutur

Eins og alltaf er besta leiðin til að læra SRP að sjá það í aðgerð. Við skulum skoða hluta áætlunarinnar sem fylgir EKKI reglunni um eina ábyrgð. Þetta er Ruby kóða sem lýsir hegðun og eiginleikum geimstöðvarinnar.

Skoðaðu dæmið og reyndu að ákvarða eftirfarandi:
Ábyrgð þessara hluta sem lýst er yfir í SpaceStation bekknum.
Þeir sem gætu haft áhuga á rekstri geimstöðvarinnar.

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

Reyndar er geimstöðin okkar óvirk (ég held að ég fái ekki símtal frá NASA í bráð), en það er eitthvað til að greina hér.

Þannig hefur SpaceStation bekkurinn nokkrar mismunandi skyldur (eða verkefni). Öllum þeim má skipta í gerðir:

  • skynjarar;
  • vistir (neysluvörur);
  • eldsneyti;
  • eldsneytisgjöf.

Jafnvel þó að enginn starfsmaður stöðvarinnar sé skipaður bekk, getum við vel ímyndað okkur hver ber ábyrgð á hverju. Líklegast stjórnar vísindamaðurinn skynjurunum, flutningafræðingurinn ber ábyrgð á því að útvega auðlindir, verkfræðingurinn ber ábyrgð á eldsneytisbirgðum og flugmaðurinn stjórnar hvatanum.

Getum við sagt að þetta forrit sé ekki SRP samhæft? Já að sjálfsögðu. En SpaceStation bekkurinn er dæmigerður „guðshlutur“ sem veit allt og gerir allt. Þetta er mikil andstæðingur-mynstur í hlutbundinni forritun. Fyrir byrjendur er mjög erfitt að viðhalda slíkum hlutum. Hingað til er forritið mjög einfalt, já, en ímyndaðu þér hvað mun gerast ef við bætum við nýjum eiginleikum. Kannski mun geimstöðin okkar þurfa læknastöð eða fundarherbergi. Og því fleiri aðgerðir sem eru, því meira mun SpaceStation stækka. Jæja, þar sem þessi aðstaða verður tengd öðrum, verður þjónusta við alla samstæðuna enn flóknari. Fyrir vikið getum við truflað virkni til dæmis hraða. Ef rannsakandi óskar eftir breytingum á skynjurum gæti það mjög vel haft áhrif á fjarskiptakerfi stöðvarinnar.

Brot á SRP meginreglunni getur gefið skammtíma taktískan sigur, en á endanum munum við „tapa stríðinu“ og það verður mjög erfitt að viðhalda slíku skrímsli í framtíðinni. Það er best að skipta forritinu í aðskilda hluta kóða, sem hver um sig ber ábyrgð á að framkvæma ákveðna aðgerð. Til að skilja þetta, skulum við breyta SpaceStation bekknum.

Dreifum ábyrgðinni

Hér að ofan skilgreindum við fjórar tegundir aðgerða sem stjórnað er af SpaceStation bekknum. Við munum hafa þau í huga við endurnýjun. Uppfærði kóðinn passar betur við 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

Það eru miklar breytingar, prógrammið lítur örugglega betur út núna. Nú er SpaceStation flokkurinn okkar orðinn meira gámur þar sem aðgerðir eru hafnar fyrir háða hluta, þar á meðal skynjara, neyslubúnað, eldsneytistank og örvunartæki.

Fyrir hverja breytu er nú samsvarandi flokkur: Skynjarar; SupplyHold; Eldsneytistankur; Þrýstivélar.

Það eru nokkrar mikilvægar breytingar á þessari útgáfu kóðans. Málið er að einstakar aðgerðir eru ekki aðeins bundnar inn í eigin flokka, þær eru skipulagðar á þann hátt að þær verða fyrirsjáanlegar og samkvæmar. Við flokkum þætti með svipaða virkni til að fylgja meginreglunni um samræmi. Nú, ef við þurfum að breyta því hvernig kerfið virkar, fara úr kjötkássabyggingu yfir í fylki, notaðu bara SupplyHold flokkinn; við þurfum ekki að snerta aðrar einingar. Þannig, ef flutningafulltrúinn breytir einhverju í hlutanum sínum, mun restin af stöðinni haldast ósnortinn. Í þessu tilviki mun SpaceStation bekkurinn ekki einu sinni vita af breytingunum.

Yfirmenn okkar sem vinna við geimstöðina eru líklega ánægðir með breytingarnar því þeir geta óskað eftir þeim sem þeir þurfa. Taktu eftir að kóðinn hefur aðferðir eins og report_supplies og report_fuel sem eru í SupplyHold og FuelTank flokkunum. Hvað myndi gerast ef jörðin myndi biðja um að breyta því hvernig hún tilkynnir? Breyta þarf báðum flokkum, SupplyHold og FuelTank. Hvað ef þú þarft að breyta því hvernig eldsneyti og rekstrarvörur eru afhentar? Þú verður líklega að skipta um alla sömu flokka aftur. Og þetta er nú þegar brot á SRP meginreglunni. Við skulum laga þetta.

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.

Í þessari nýjustu útgáfu af forritinu hefur ábyrgðinni verið skipt í tvo nýja flokka, FuelReporter og SupplyReporter. Þau eru bæði börn í fréttamannabekknum. Að auki bættum við tilviksbreytum við SpaceStation flokkinn svo hægt sé að frumstilla þann undirflokk sem óskað er eftir ef þörf krefur. Nú, ef jörðin ákveður að breyta einhverju öðru, þá munum við gera breytingar á undirflokkunum, en ekki á aðalflokknum.

Auðvitað eru sumir bekkirnir okkar enn háðir hver öðrum. Þannig fer SupplyReporter hluturinn eftir SupplyHold og FuelReporter fer eftir FuelTank. Að sjálfsögðu verða hvatarnir að vera tengdir við eldsneytistankinn. En hér lítur allt nú þegar rökrétt út og það verður ekki sérstaklega erfitt að gera breytingar - að breyta kóða eins hlutar mun ekki hafa mikil áhrif á annan.

Þannig höfum við búið til einingakóða þar sem ábyrgð hvers hlutar/flokka er nákvæmlega skilgreind. Það er ekki vandamál að vinna með slíkan kóða, það er einfalt verkefni að viðhalda honum. Við höfum breytt öllu „guðlega hlutnum“ í SRP.

Skillbox mælir með:

Heimild: www.habr.com

Bæta við athugasemd