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 Perezit në lidhje me përdorimin e 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 merret me zhvillimin, shumë mund të keni dëgjuar për parimet SOLID. Ato i japin mundësinë programuesit të shkruajë kod të pastër, të mirëstrukturizuar dhe të lehtë për t'u mbajtur. Duhet të theksohet se në programim ka disa qasje se si duhet të realizohet çdo punë. Specialistë të 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 nga praktikisht të gjithë përfaqësuesit e komunitetit IT. Ato janë bërë një pikë fillestare për shfaqjen dhe zhvillimin e shumë metodave të mira të menaxhimit të zhvillimit.

Le të merremi me atë se çfarë janë parimet SOLID dhe si na ndihmojnë.

Skillbox rekomandon: Kurs praktik «Mobil developer PRO».

Kujtojmë: për të gjithë lexuesit e «Habra» — zbritje prej 10,000 rublesh për regjistrimin në çdo kurs Skillbox me kodin promovues «Habr».

Çfarë është SOLID?

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

  • SSingle Responsibility Principle (principi i përgjegjësisë së vetme). Një modul mund të ketë një dhe vetëm një arsye për ndryshim.
  • I Open/Closed Principle (principi i hapjes/mbajtjes mbyllur). Klasa dhe elemente të tjera duhet të jenë të hapura për zgjerim, por të mbyllura për modifikim.
  • I Lskov Substitution Principle (principi i zëvendësimit të Liskovit). Funksionet që përdorin tipin bazë duhet të kenë mundësi të përdorin nën-tipat e tipit bazë, pa e ditur këtë.
  • I Interface Segregation Principle(principi i ndarjes së ndërfaqeve). Entitetet programues duhet të mos varen nga metodat që ato nuk përdorin.
  • I Dependency Inversion Principle (principi i inverzimit të varësisë). Modulët e niveleve të larta nuk duhet të varen nga modulët e niveleve të ulta.

Principi i përgjegjësisë së vetme

 
Principi i përgjegjësisë unike (SRP) thotë se çdo klasë ose moduli në program duhet të ketë përgjegjësi 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ërndahen në klasa të pa lidhura. Zhvilluesi dhe evangjelisti kryesor i SRP, Robert C. Martin, e përshkruan përgjegjësinë si shkakun e ndryshimeve. Ai fillimisht e propozoi këtë termin si një nga elementët e punës së tij "Principet e dizajnit të orientuar nga objekti". Koncepti përfshiu shumë nga rregullat e lidhshmërisë, të cilat ishin përcaktuar më parë nga Tom DeMarco.

Po ashtu, në koncept kanë hyrë disa nocione të formuluara nga David Parnas. Dy të parat janë enkapusulimi dhe fshehja e informacionit. Parnas konsideronte se ndarja e sistemit në module të veçanta nuk duhet të bazohet në analizën e diagramëve të blloqeve ose të rrjedhave të ekzekutimit. Çdo modul duhet të përmbajë një zgjidhje të caktuar që ofron minimumin e informacionit për klientët.

E kaluara, Martin sjell një shembull interesant me menaxherët e lartë të kompanisë (COO, CTO, CFO), secili prej të cilëve përdor softuer të veçantë për biznesin me qëllime të ndryshme. Në fund, çdo prej tyre mund të implementojë ndryshime në softuer, pa prekur interesat e menaxherëve të tjerë.

Objekti hyjnor

Si zakonisht, mënyra më e mirë për të mësuar SRP është ta shohësh atë në veprim. Le të shohim një pjesë të programit që NUK respekton principin e përgjegjësisë unike. Ky është një kod Ruby që përshkruan sjelljen dhe atributet e një stacioni hapësinor.

Rishikoni shembullin dhe provoni të përcaktoni:
Përgjegjësitë e objekteve që shpallën në klasën SpaceStation.
Atyre që mund të jenë të interesuar për funksionimin e stacionit hapësinor.

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

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

Pra, klasi SpaceStation ka disa përgjegjësi të ndryshme (ose detyra). Të gjitha ato mund të ndahen sipas llojit:

  • sensorë;
  • furnizimi (artikujt e nevojshëm);
  • karburant;
  • shtytësit.

Edhe pse askush nga punonjësit e stacionit nuk është përshkruar në klasë, ne mund të imagjinojmë lehtë se kush është përgjegjës për çdo gjë. Ndoshta punonjësi shkencor kontrollon sensorët, logjisti merret me furnizimin me burime, inxhinieri është përgjegjës për rezervat e karburantit, ndërsa piloti kontrollon shtytësit.

A mund të themi se ky program nuk përputhet me SRP? Po, sigurisht. Por klasa SpaceStation është një «objekt i hyjnishëm» tipik, i cili di gjithçka dhe bën gjithçka. Ky është anti-shablloni kryesor në programimin objektor. Për një fillestar, këto objekte janë jashtëzakonisht të vështira për t'u menaxhuar. Deri tani, programi është shumë i thjeshtë, po, por imagjinoni çfarë do ndodhte nëse shtojmë funksione të reja. Ndoshta stacioni ynë hapësinor do të ketë nevojë për një qendër mjekësore ose një dhomë takimesh. Dhe sa më shumë funksione të shtohen, aq më i madh do të bëhet SpaceStation. Duke qenë se ky objekt do të lidhjet me të tjerët, mbajtja e gjithë kompleksit do të bëhet edhe më e komplikuar. Në fund, ne mund të prishim funksionimin, për shembull, të acceleratatorëve. Nëse një shkencëtar kërkon ndryshime në funksionimin me sensorët, kjo mund të ndikojë në sistemet e komunikimit të stacionit.

Shkelja e parimit SRP mund të sjellë një fitore taktike afatshkurtër, por në përfundim ne «do ta humbim luftën», menaxhimi i një monstru të tillë në të ardhmen do të bëhet shumë i vështirë. Më mirë do të ishte të ndajmë programin në pjesë të veçanta kode, secila prej të cilave është përgjegjëse për kryerjen e një operacioni të caktuar. Duke e kuptuar këtë, le të ndryshojmë klasën SpaceStation.

Të shpërndajmë përgjegjësinë

Më lart ne përcaktuam katër lloje operacionesh, të cilat kontrollohen nga klasa SpaceStation. Gjatë ristrukturimit, ne do t'i kemi ata parasysh. Kodi i azhurnuar 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 ndodhi shumë ndryshime, programi tani duket me të vërtetë më i mirë. Tani klasa jonë SpaceStation është më shumë një enë, ku iniciohen operacionet për pjesët e varura, duke përfshirë një set sensorësh, sistemin e furnizimit, tankun e karburantit dhe thrusterët.

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. E gjithë kjo lidhet me faktin se funksionet e veçanta jo vetëm që janë inkapsuluar në klasat e tyre përkatëse, por gjithashtu janë organizuar në një mënyrë që i bën ato parashikueshmërish dhe të qëndrueshme. Ne grupojmë elementët me funksionalitet të ngjashëm për të ndjekur parimin e lidhshmërisë. Tani, nëse na nevojitet të ndryshojmë mënyrën si funksionon sistema, duke kaluar nga një strukturë hash në një array, thjesht do të përdorim klasën SupplyHold, pa e prekur modulat e tjera. Kështu, nëse oficerët përgjegjës për logjistikën bëjnë ndonjë ndryshim në seksionin e tyre, elementët e tjerë të stacionit do të mbeten të paprekur. Po ashtu, klasa SpaceStation edhe nuk do ta kuptojë ndryshimin.

Oficerët tanë që punojnë në stacionin kozmik ndoshta janë të kënaqur 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, të përfshira në klasat SupplyHold dhe FuelTank. Çfarë do të ndodhë nëse Toka kërkon të ndryshojë mënyrën e ndërtimit të raporteve? Do të nevojitet të ndryshohen të dy klasat, SupplyHold dhe FuelTank. Po nëse duam të ndryshojmë mënyrën e dërgimit të karburantit dhe materialeve të konsumit? Ndoshta do të duhet të ndryshojmë përsëri të njëjtat klasa. Dhe kjo tashmë është një shkelje e parimit SRP. Le të e zgjidhim 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 ato janë nënpjesë të klasës Reporter. Për më tepër, ne kemi shtuar variabla eksperimentale në klasën SpaceStation në mënyrë që, kur të jetë e nevojshme, të initializejmë nënklasën e duhur. Tani, nëse Toka vendos të ndryshojë diçka tjetër, do të bëjmë modifikime në nënklasa dhe jo në klasën kryesore.

Natyrisht, disa klasa ende varen nga njëra-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 ketu gjithçka duket e arsyeshme, dhe bërrja e ndryshimeve nuk do të jetë veçanërisht e vështirë — redaktimi i kodit të një objekti nuk do të ndikojë shumë në tjetrin.

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

Skillbox rekomandon:

Burimi: habr.com

Blini hosting të besueshëm për faqe interneti me mbrojtje nga DDoS, serverë VPS VDS 🔥 Blini hosting të besueshëm për faqe interneti me mbrojtje nga DDoS, serverë VPS VDS | ProHoster