Shkruajmë kod fleksibël duke përdorur SOLID

Shkruajmë kod fleksibël duke përdorur SOLID

Nga përkthyesi: publikuar për ju artikulli i Severin Perez rreth përdorimit të parimeve SOLID në programim. Informacioni nga artikulli do të jetë i dobishëm si për fillestarët, ashtu edhe për programuesit me përvojë.

Nëse merresh me zhvillimin, me siguri ke dëgjuar për parimet SOLID. Ato i japin mundësinë programuesit të shkruajë kod të pastër, të strukturuar mirë dhe lehtësisht të mbajtur. Duhet të theksohet se në programim ka disa qasje për mënyrën se si duhet bërë një punë e caktuar. Specialistët e ndryshëm kanë ide dhe kuptime të ndryshme për "rrugën e duhur", gjithçka varet nga përvoja e secilit. Megjithatë, idetë e shpallura në SOLID pranohen praktikisht nga të gjithë përfaqësuesit e komunitetit IT. Ato janë bërë një pikënisje për shfaqjen dhe zhvillimin e shumë metodave të mira të menaxhimit të zhvillimit.

Le të kuptojmë se çfarë janë parimet SOLID dhe si na ndihmojnë.

Skillbox rekomandon: Kurs praktik «Programues i celularëve PRO».

Kujtojmë: për të gjithë lexuesit e "Habra" — zbritje prej 10,000 rublej për regjistrimin në çdo kurs Skillbox me kodin promocional "Habr".

Çfarë është SOLID?

Ky term është një akronim, secila shkronjë e termit është fillimi i emrit të një principi të caktuar:

  • SPrincipi i Përgjegjësisë së Vetme. Një modul mund të ketë një dhe vetëm një shkak për të ndryshuar.
  • The OPrincipi i Hapjes/Mbylljes (principe i hapjes/mbylljes). Klasat dhe elementet e tjera duhet të jenë të hapura për zgjerim, por të mbyllura për modifikim.
  •  Princi Li Zëvendësimit të Liskov (principe i zëvendësimit të Liskov). Funksionet që përdorin tipin bazë duhet të kenë mundësinë të përdorin nën tipet e tipit bazë, pa e ditur këtë.
  • The IPrincipe i Ndarjes së Interfese (principe i ndarjes së interfesës). Entitetet programore nuk duhet të varen nga metodat që nuk i përdorin.
  • The DPrincipe i Inversimit të Varësive (principe i inversimit të varësive). Modulet e niveleve të larta nuk duhet të varen nga modulit e niveleve të ulëta.

Principe e Vetëm një Përgjegjësie


Principi i përgjegjësisë së vetme (SRP) thotë se çdo klasë ose modul në program duhet të jetë përgjegjës vetëm për një pjesë të funksionalitetit të këtij programi. Për më tepër, elementet e kësaj përgjegjësie duhet të jenë të lidhura me klasën e tyre, dhe jo të shpërndara në klasa të pa lidhura. Zhvilluesi dhe evangjelisti kryesor i SRP, Robert C. Martin, e përshkruan përgjegjësinë si arsyen për ndryshim. Ai fillimisht e propozoi këtë termin si një nga elementët e punës së tij "Parimet e Projektimit të Orientuar nga Objektet". Në koncept janë përfshirë shumë nga rregullat e lidhjes, të cilat ishin përkufizuar më parë nga Tom DeMarco.

Edhe në koncept janë përfshirë disa nocione të formuluara nga David Parnas. Dy të parët janë — inkapsulimi dhe fshehja e informacionit. Parnas theksonte se ndarja e sistemit në module të veçanta nuk duhet të bazohet në analizat e diagrameve ose rrjedhave të ekzekutimit. Çdo modul duhet të përmbajë një zgjidhje të caktuar, e cila ofron informacion minimal për klientët.

Për më tepër, Martini dha një shembull interesante me menaxherët e lartë të kompanisë (COO, CTO, CFO), secili prej të cilëve përdor software specifik biznesi për qëllime të ndryshme. Në fund, secili prej tyre mund të implementojë ndryshime në software pa prekur interesat e menaxherëve të tjerë.

Objekti hyjnor

Si zakonisht, mënyra më e mirë për të mësuar SRP-në është ta shihni gjithçka në veprim. Le të shohim një pjesë të programit që NUK i përmbush parimin e një përgjegjësie. Ky është kodi Ruby, që përshkruan sjelljen dhe atributet e stacionit hapësinor.

Shikoni shembullin dhe provoni të identifikoni të volgende:
Detyrat e atyre objekteve që shpallen në klasën SpaceStation.
Atyre që mund të kenë interes për punën e stacionit hapësinor.

klasa Stacioni Hapësinor
  def initialize
    @supplies = {}
    @fuel = 0
  end
 
  def run_sensors
    puts "----- Veprimi i Sensorëve -----"
    puts "Duke ekzekutuar sensorët!"
  end
 
  def load_supplies(type, quantity)
    puts "----- Veprimi i Furnizimit -----"
    puts "Duke ngarkuar #{quantity} njësi të #{type} në magazinën e furnizimeve."
    
    if @supplies[type]
      @supplies[type] += quantity
    else
      @supplies[type] = quantity
    end
  end
 
  def use_supplies(type, quantity)
    puts "----- Veprimi i Furnizimit -----"
    if @supplies[type] != nil && @supplies[type] > quantity
      puts "Duke përdorur #{quantity} të #{type} nga magazina e furnizimeve."
      @supplies[type] -= quantity
    else
      puts "Gabim në Furnizim: Mjaftueshëm #{type} në magazinën e furnizimeve."
    end
  end
 
  def report_supplies
    puts "----- Raporti i Furnizimeve -----"
    if @supplies.keys.length > 0
      @supplies.each do |type, quantity|
        puts "#{type} në dispozicion: #{quantity} njësi"
      end
    else
      puts "Magazina e furnizimeve është bosh."
    end
  end
 
  def load_fuel(quantity)
    puts "----- Veprimi i Karburantit -----"
    puts "Duke ngarkuar #{quantity} njësi karburant në rezervuar."
    @fuel += quantity
  end
 
  def report_fuel
    puts "----- Raporti i Karburantit -----"
    puts "#{@fuel} njësi karburant në dispozicion."
  end
 
  def activate_thrusters
    puts "----- Veprimi i Pompave -----"
    if @fuel >= 10
      puts "Veprimi i pompave ishte i suksesshëm."
      @fuel -= 10
    else
      puts "Gabim i Pompas: Mjaftueshëm karburant në dispozicion."
    end
  end
end

Pra, stacioni ynë hapësinor nuk është funksional (mendoj se nuk do marr një telefonatë nga NASA në të ardhmen e afërt), por këtu ka shumë për të analizuar.

Pra, klasa SpaceStation ka disa përgjegjësi të ndryshme (ose detyra). Të gjitha mund të shpërndahen në lloje:

  • sensorët;
  • furnizimi (materiale konsumueshëm);
  • karburanti;
  • acceleratorët.

Megjithëse askush nga punonjësit e stacionit nuk është përcaktuar në klasë, ne mund të paraqesim lehtësisht kush është përgjegjës për çfarë. Me shumë mundësi, punonjësi shkencor kontrollon sensorët, logjistika është përgjegjëse për furnizimin me ресурs, inxhinieri është përgjegjës për rezervat e karburantit dhe piloti kontrollon acceleratorët.

A mund të themi se ky program nuk i përmbush standardet e SRP? Po, sigurisht. Por klasa SpaceStation është një "objekt hyjnor" tipik, i cili di gjithçka dhe bën gjithçka. Ky është anti-shembulli kryesor në programimin e orientuar në objekt. Për një fillestar, këto objekte janë jashtëzakonisht të vështira për t'u mirëmbajtur. Deri tani, programi është shumë i thjeshtë, po, por imagjinoni se çfarë do të ndodhë nëse shtojmë funksione të reja. Ndoshta stacionit tonë kozmik i nevojitet një qendër mjekësore ose një sallë takimesh. Sa më shumë funskione të ketë, aq më shumë do të rritet SpaceStation. Po ashtu, pasi ky objekt do të lidhet me të tjerët, mirëmbajtja e të gjithës do të bëhet edhe më e komplikuar. Si rezultat, mund të prishim, për shembull, drejtorët. Nëse një kërkues shkencor kërkon ndryshime në punën me(sensorët), kjo mund të ndikojë ndjeshëm në sistemet e komunikacionit të stacionit.

Shkelja e parimit SRP mund të sjellë një fitore taktike të përkohshme, por në fund do të "humim luftën", pasi do të jetë shumë e vështirë të shërbesh për një monstruozitet të tillë në të ardhmen. Më mirë është të ndash programin në pjesë të veçanta kodimi, secila e cila përgjigjet për realizimin e një operacioni të caktuar. Duke e kuptuar këtë, le të modifikojmë klasën SpaceStation.

Të ndajmë përgjegjësitë

Më lart ne përcaktuam katër lloje operacionesh që kontrollohen nga klasa SpaceStation. Gjatë rifaktorizimit, do t'i kemi parasysh. Kodi i përditësuar përputhet më mirë me 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

Ka janë shumë ndryshime, programi tani duket pa dyshim më mirë. Tani klasa jonë SpaceStation është bërë më shumë një kontejner, në të cilin iniciohen operacione për pjesët e varura, duke përfshirë një grup sensorësh, sistemin e furnizimit me materiale, rezervuarin e karburantit dhe acceleratoret.

Për çdo variabël tani ka një klasë përkatëse: Sensors; SupplyHold; FuelTank; Thrusters.

Në këtë version të kodit ka disa ndryshime të rëndësishme. Çështja është që funksionet e veçanta jo vetëm që janë inkapsuluar në klasat e tyre, ato janë organizuar në një mënyrë që bëhen të parashikueshme dhe të qëndrueshme. Ne grupojmë elementë të ngjashëm sipas funksionalitetit për të ndjekur parimin e lidhshmërisë. Tani, nëse na duhet të ndryshojmë mënyrën e funksionimit të sistemit, duke kaluar nga struktura e heshit në një array, thjesht përdorni klasën SupplyHold, pa prekur modulat e tjera. Kështu, nëse oficeri përgjegjës për logjistikën bën ndonjë ndryshim në seksionin e tij, elementët e tjerë të stacionit do të mbeten të paprekur. Dheu, klasa SpaceStation nuk do të jetë as në dijeni të ndryshimeve.

Oficerët tanë që punojnë në stacionin kozmik, ndoshta janë të lumtur me ndryshimet, pasi mund të kërkojnë ato që u nevojiten atyre. Vini re se në kod ka metoda si report_supplies dhe report_fuel, që ndodhen në klasat SupplyHold dhe FuelTank. Çfarë do të ndodhte nëse Toka kërkon të ndryshojë mënyrën e formimit të raporteve? Do të nevojitej të ndryshoheshin të dy klasat, SupplyHold dhe FuelTank. Dhe nëse duhet të ndryshonim mënyrën e dorëzimit të karburantit dhe furnizimeve? Ka shumë të ngjarë që do të duhet të ndryshonim përsëri të gjitha ato të njëjtat klasa. Kjo tashmë është një shkelje e parimit SRP. Le ta rregullojmë këtë.

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.

Në këtë version të fundit të programit, detyrat janë ndarë në dy klasa të reja, FuelReporter dhe SupplyReporter. Të dyja janë nënshtesa të klasës Reporter. Për më tepër, ne kemi shtuar variabla ekzemplar në klasën SpaceStation në mënyrë që, nëse është e nevojshme, të inicializojmë nënklasën e duhur. Tani, nëse Toka vendos të ndryshojë diçka tjetër, ne do të bëjmë ndryshimet në nënklasat, jo në klasën kryesore.

Sigurisht, disa klasa ende varen një nga tjetra. kështu, objekti SupplyReporter varet nga SupplyHold, ndërsa FuelReporter varet nga FuelTank. Natyrisht, përshpejtuesit duhet të lidhen me rezervuarin e karburantit. Por këtu gjithçka duket logjike, dhe të bësh ndryshime nuk do të jetë veçanërisht e vështirë — redaktimi i kodit të një objekti nuk do të ndikojë shumë te tjetri.

Kështu, ne krijuam kod modular, ku detyrat e çdo objekti/klase janë përcaktuar qartë. Të punosh me një kod të tillë nuk është problem, mirëmbajtja do të jetë një detyrë e thjeshtë. E gjithë "objekti hyjnor" e kemi transformuar në SRP.

Skillbox rekomandon:

Burimi: habr.com

Bleni hostim të besueshëm për faqe me mbrojtje nga DDoS, serverë VPS VDS 🔥 Bleni hostim të besueshëm për faqe me mbrojtje nga DDoS, serverë VPS VDS | ProHoster