Shkrimi i kodit fleksibël duke përdorur SOLID

Shkrimi i kodit fleksibël duke përdorur SOLID

Nga përkthyesi: botuar për ju artikull nga 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 jeni në zhvillim, me shumë mundësi keni dëgjuar për parimet SOLID. Ato i mundësojnë programuesit të shkruajë kod të pastër, të strukturuar mirë dhe lehtësisht të mirëmbajtur. Vlen të përmendet se në programim ekzistojnë disa qasje se si të kryhet saktë një punë e caktuar. Specialistë të ndryshëm kanë ide dhe kuptim të ndryshëm për "rrugën e duhur"; gjithçka varet nga përvoja e secilit person. Megjithatë, idetë e shpallura në SOLID pranohen nga pothuajse të gjithë përfaqësuesit e komunitetit të IT. Ato u bënë pikënisja për shfaqjen dhe zhvillimin e shumë praktikave të mira të menaxhimit të zhvillimit.

Le të kuptojmë se cilat janë parimet SOLID dhe si na ndihmojnë ato.

Skillbox rekomandon: Kurse praktike "Zhvilluesi i celularit PRO".

Kujtojmë: për të gjithë lexuesit e "Habr" - një zbritje prej 10 rubla kur regjistroheni në çdo kurs Skillbox duke përdorur kodin promovues "Habr".

Çfarë është SOLID?

Ky term është një shkurtim, çdo shkronjë e termit është fillimi i emrit të një parimi specifik:

  • Single Parimi i Përgjegjësisë. Një modul mund të ketë një dhe vetëm një arsye për ndryshim.
  • La Ostilolaps / Parimi i mbyllur (parimi i hapur/mbyllur). Klasat dhe elementët e tjerë duhet të jenë të hapura për zgjerim, por të mbyllura për modifikim.
  •  La LParimi i Zëvendësimit të Iskov (parimi i zëvendësimit të Liskov). Funksionet që përdorin një lloj bazë duhet të jenë në gjendje të përdorin nëntipe të llojit bazë pa e ditur atë.
  • La IParimi i ndarjes së ndërfaqes  (parimi i ndarjes së ndërfaqes). Subjektet e softuerit nuk duhet të varen nga metodat që nuk i përdorin.
  • La Dependency Parimi i Inversionit (parimi i përmbysjes së varësisë). Modulet në nivele më të larta nuk duhet të varen nga modulet në nivele më të ulëta.

Parimi i Përgjegjësisë së Vetëm


Parimi i Përgjegjësisë së Vetëm (SRP) thotë se çdo klasë ose modul në një program duhet të jetë përgjegjës vetëm për një pjesë të funksionalitetit të atij programi. Për më tepër, elementët e kësaj përgjegjësie duhet t'i caktohen klasës së tyre, në vend që të shpërndahen nëpër klasa të palidhura. Zhvilluesi dhe kryevangjelisti i SRP-së, Robert S. Martin, e përshkruan përgjegjshmërinë si arsyen e ndryshimit. Ai fillimisht e propozoi këtë term si një nga elementët e punës së tij "Parimet e dizajnit të orientuar nga objekti". Koncepti përfshin një pjesë të madhe të modelit të lidhjes që ishte përcaktuar më parë nga Tom DeMarco.

Koncepti përfshinte gjithashtu disa koncepte të formuluara nga David Parnas. Dy kryesoret janë kapsulimi dhe fshehja e informacionit. Parnas argumentoi se ndarja e një sistemi në module të veçanta nuk duhet të bazohet në analizën e diagrameve të bllokut ose flukseve të ekzekutimit. Secili prej moduleve duhet të përmbajë një zgjidhje specifike që ofron një minimum informacioni për klientët.

Nga rruga, Martin dha një shembull interesant me menaxherët e lartë të një kompanie (COO, CTO, CFO), secili prej të cilëve përdor softuer specifik biznesi për qëllime të ndryshme. Si rezultat, secili prej tyre mund të zbatojë ndryshime në softuer pa ndikuar në interesat e menaxherëve të tjerë.

Objekti hyjnor

Si gjithmonë, mënyra më e mirë për të mësuar SRP është ta shihni atë në veprim. Le të shohim një pjesë të programit që NUK ndjek Parimin e Përgjegjësisë së Vetme. Ky është kodi Ruby që përshkruan sjelljen dhe atributet e stacionit hapësinor.

Rishikoni shembullin dhe përpiquni të përcaktoni sa vijon:
Përgjegjësitë e atyre objekteve që deklarohen në klasën SpaceStation.
Ata që mund të jenë të interesuar në 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

Në fakt, stacioni ynë hapësinor është jofunksional (nuk mendoj se do të marr një telefonatë nga NASA së shpejti), por ka diçka për të analizuar këtu.

Kështu, klasa SpaceStation ka disa përgjegjësi (ose detyra) të ndryshme. Të gjithë ata mund të ndahen në lloje:

  • sensorë;
  • furnizime (harxhuese);
  • karburant;
  • përshpejtuesit.

Edhe pse asnjë nga punonjësit e stacionit nuk i është caktuar një klasë, ne mund të imagjinojmë lehtësisht se kush është përgjegjës për çfarë. Me shumë mundësi, shkencëtari kontrollon sensorët, logjistika është përgjegjës për furnizimin e burimeve, inxhinieri është përgjegjës për furnizimet me karburant dhe piloti kontrollon përforcuesit.

A mund të themi se ky program nuk është në përputhje me SRP? Po sigurisht. Por klasa SpaceStation është një "objekt perëndi" tipik që di gjithçka dhe bën gjithçka. Ky është një anti-model i madh në programimin e orientuar drejt objektit. Për një fillestar, objekte të tilla 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ë veçori të reja. Ndoshta stacioni ynë hapësinor do të ketë nevojë për një stacion mjekësor ose një dhomë takimesh. Dhe sa më shumë funksione të ketë, aq më shumë SpaceStation do të rritet. Epo, duke qenë se ky objekt do të lidhet me të tjerët, shërbimi i të gjithë kompleksit do të bëhet edhe më i vështirë. Si rezultat, ne mund të prishim funksionimin, për shembull, të përshpejtuesve. Nëse një studiues kërkon ndryshime në sensorë, kjo mund të ndikojë shumë mirë në sistemet e komunikimit të stacionit.

Shkelja e parimit të SRP mund të japë një fitore taktike afatshkurtër, por në fund ne do të "humbim luftën" dhe do të bëhet shumë e vështirë të mbajmë një përbindësh të tillë në të ardhmen. Është mirë që programi të ndahet në seksione të veçanta të kodit, secila prej të cilave është përgjegjëse për kryerjen e një operacioni specifik. Duke e kuptuar këtë, le të ndryshojmë klasën SpaceStation.

Le të shpërndajmë përgjegjësinë

Më sipër përcaktuam katër lloje operacionesh që kontrollohen nga klasa SpaceStation. Ne do t'i mbajmë ato parasysh gjatë rifaktorimit. 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} 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

Ka shumë ndryshime, programi duket më mirë tani. Tani klasa jonë e Stacionit Hapësinor është shndërruar më shumë në një kontejner në të cilin nisen operacionet për pjesët e varura, duke përfshirë një grup sensorësh, një sistem furnizimi harxhues, një rezervuar karburanti dhe përforcues.

Për cilindo nga variablat ekziston tani një klasë përkatëse: Sensorët; SupplyHold; Rezervuar karburanti; Thrusters.

Ka disa ndryshime të rëndësishme në këtë version të kodit. Çështja është se funksionet individuale jo vetëm që janë të përmbledhura në klasat e tyre, por ato janë të organizuara në atë mënyrë që të bëhen të parashikueshme dhe të qëndrueshme. Ne grupojmë elementë me funksionalitet të ngjashëm për të ndjekur parimin e koherencës. Tani, nëse duhet të ndryshojmë mënyrën e funksionimit të sistemit, duke kaluar nga një strukturë hash në një grup, thjesht përdorim klasën SupplyHold; ne nuk kemi pse të prekim module të tjera. Në këtë mënyrë, nëse oficeri i logjistikës ndryshon diçka në seksionin e tij, pjesa tjetër e stacionit do të mbetet e paprekur. Në këtë rast, klasa SpaceStation as nuk do të jetë në dijeni të ndryshimeve.

Oficerët tanë që punojnë në stacionin hapësinor janë ndoshta të lumtur për ndryshimet sepse mund të kërkojnë ato që u nevojiten. Vini re se kodi ka metoda të tilla si report_supplies dhe report_fuel të përfshira në klasat SupplyHold dhe FuelTank. Çfarë do të ndodhte nëse Toka do të kërkonte të ndryshonte mënyrën e raportimit? Të dyja klasat, SupplyHold dhe FuelTank, do të duhet të ndryshohen. Po sikur t'ju duhet të ndryshoni mënyrën e dorëzimit të karburantit dhe materialeve harxhuese? Ndoshta do t'ju duhet të ndryshoni të gjitha të njëjtat klasa përsëri. Dhe kjo tashmë është një shkelje e parimit të 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 < 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.

Në këtë version të fundit të programit, përgjegjësitë janë ndarë në dy klasa të reja, FuelReporter dhe SupplyReporter. Të dy janë fëmijë të klasës Reporter. Përveç kësaj, ne shtuam variabla të shembullit në klasën SpaceStation në mënyrë që nënklasa e dëshiruar të mund të inicializohet nëse është e nevojshme. Tani, nëse Toka vendos të ndryshojë diçka tjetër, atëherë ne do të bëjmë ndryshime në nënklasat, dhe jo në klasën kryesore.

Sigurisht, disa nga klasat tona varen ende nga njëra-tjetra. Kështu, objekti SupplyReporter varet nga SupplyHold, dhe FuelReporter varet nga FuelTank. Natyrisht, përforcuesit duhet të lidhen me rezervuarin e karburantit. Por këtu gjithçka tashmë duket logjike, dhe bërja e ndryshimeve nuk do të jetë veçanërisht e vështirë - redaktimi i kodit të një objekti nuk do të ndikojë shumë në një tjetër.

Kështu, ne kemi krijuar një kod modular ku përcaktohen saktësisht përgjegjësitë e secilit prej objekteve/klasave. Puna me një kod të tillë nuk është problem, mirëmbajtja e tij do të jetë një detyrë e thjeshtë. Ne e kemi konvertuar të gjithë "objektin hyjnor" në SRP.

Skillbox rekomandon:

Burimi: www.habr.com

Shto një koment