Vi skriver fleksibel kode ved hjelp av SOLID

Vi skriver fleksibel kode ved hjelp av SOLID

Fra oversetteren: har publisert for deg artikkelen til Severin Perez om bruk av SOLID-prinsippene i programmering. Informasjonen i artikkelen vil vÊre nyttig for bÄde nybegynnere og erfarne programmerere.

Hvis du jobber med utvikling, har du sannsynligvis hÞrt om SOLID-prinsippene. De gir programmerere muligheten til Ä skrive ren, godt strukturert og lett vedlikeholdbar kode. Det er verdt Ä merke seg at det finnes flere tilnÊrminger til hvordan man utfÞrer arbeidet pÄ en riktig mÄte i programmering. Ulike fagfolk har forskjellige ideer og forstÄelser av "den rette veien", avhengig av hver enkelt erfaring. Likevel blir ideene uttalt i SOLID akseptert av praktisk talt alle medlemmer av IT-samfunnet. De har blitt et utgangspunkt for utvikling av mange gode metoder for prosjektledelse.

La oss se nÊrmere pÄ hva SOLID-prinsippene er og hvordan de hjelper oss.

Skillbox anbefaler: Praktisk kursus «Mobiludvikler PRO».

Vi minner om: for alle lesere av «Habr» — 10 000 rubler rabatt ved pĂ„melding til hvilket som helst Skillbox-kurs med kampanjekoden «Habr».

Hva er SOLID?

Denne termen er en akronym, der hver bokstav representerer navnet pÄ et bestemt prinsipp:

Prinsippet om enkelt ansvar

 
Prinsippet om enhetlig ansvar (SRP) sier at hver klasse eller modul i et program kun skal ha ansvar for én del av funksjonaliteten til dette programmet. Videre skal elementene av dette ansvaret knyttes til sin klasse, og ikke deles mellom urelaterte klasser. Utvikleren og hovedforkjemperen for SRP, Robert C. Martin, beskriver ansvar som Ärsaken til endring. Han introduserte opprinnelig dette begrepet som et av elementene i sitt arbeid "Prinsipper for objektorientert design". Mange av konseptene knyttet til sammenhengen ble tidligere definert av Tom DeMarco.

Flere ideer formulert av David Parnas ble ogsÄ en del av konseptet. De to viktigste er innkapsling og informasjonsbeskyttelse. Parnas hevdet at inndelingen av systemet i separate moduler ikke skulle baseres pÄ analyse av flytskjemaer eller utfÞrelsesstrÞmmer. Hver av modulene skal inneholde en spesifikk lÞsning som gir minimum informasjon til brukerne.

Forresten, Martin ga et interessant eksempel med toppledere i et selskap (COO, CTO, CFO), hver av dem bruker spesifikk programvare for virksomheten med ulike formÄl. Til slutt kan hver av dem implementere endringer i programvaren uten Ä berÞre interessene til de andre lederne.

Den guddommelige objekt

Som vanlig er den beste mÄten Ä lÊre SRP pÄ Ä se alt i aksjon. La oss se pÄ et stykke programvare som IKKE overholder prinsippet om enhetlig ansvar. Dette er Ruby-kode som beskriver atferden og egenskapene til romstasjonen.

Vennligst se gjennom eksempelet og prĂžv Ă„ identifisere fĂžlgende:
Ansvarene til de objektene som blir erklĂŠrt i klassen SpaceStation.
De som kan vĂŠre interessert i driften av romstasjonen.

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} available: #{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

Egentleg er vÄr romstasjon ikke funksjonell (jeg tror ikke jeg fÄr en telefon fra NASA i den nÊrmeste fremtid), men det er her noe Ä analysere.

SĂ„ klassen SpaceStation har flere forskjellige ansvar (eller oppgaver). Alle kan deles inn i typer:

  • sensorer;
  • forsyninger (forbruksvarer);
  • drivstoff;
  • dytter.

Selv om ingen av stasjonens ansatte er definert i klassen, kan vi enkelt forestille oss hvem som har ansvar for hva. Sannsynligvis overvÄker en forsker sensorene, en logistiker er ansvarlig for forsyningene, en ingeniÞr styrer drivstoffbeholdningen, og piloten kontrollerer dyttene.

Kan vi si at dette programmet ikke overholder SRP? Ja, selvfÞlgelig. Men klassen SpaceStation er en typisk "guddommelig objekt" som vet alt og gjÞr alt. Dette er en hoved anti-mall i objektorientert programmering. For nybegynnere er slike objekter ekstremt vanskelige Ä vedlikeholde. SÄ langt er programmet veldig enkelt, ja, men forestill deg hva som vil skje hvis vi legger til nye funksjoner. Kanskje romstasjonen vÄr trenger et medisinsenter eller et mÞterom. Jo flere funksjoner, desto mer vil SpaceStation vokse. Og siden dette objektet vil vÊre knyttet til andre, vil vedlikeholdet av hele komplekset bli enda mere komplisert. Til slutt kan vi forstyrre for eksempel akseleratorene. Hvis en forsker ber om endringer i arbeidet med sensorene, kan dette pÄvirke stasjonens kommunikasjonsystemer.

Brudd pÄ SRP-prinsippet kan gi en kortsiktig taktisk seier, men til slutt vil vi "tape krigen", vedlikehold av et slikt monster i fremtiden vil bli ganske vanskelig. Det beste er Ä dele programmet opp i separate kodebiter, hver med ansvar for Ä utfÞre en spesifikk operasjon. Med dette i tankene, la oss endre klassen SpaceStation.

Fordel ansvar

Ovenfor har vi definert fire typer operasjoner som kontrolleres av klassen SpaceStation. NÄr vi refaktoriserer, vil vi ha dem i bakhodet. Den oppdaterte koden overholder SRP bedre.

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

Det er mange endringer, programmet ser nÄ definitivt bedre ut. NÄ har klassen SpaceStation blitt mer som en beholder der operasjoner for avhengige komponenter initieres, inkludert en rekke sensorer, system for levering av forbruksvarer, drivstofftank og thrustere.

For hver av variablene er det nÄ en tilsvarende klasse: Sensors; SupplyHold; FuelTank; Thrusters.

I denne versjonen av koden er det flere viktige endringer. Faktisk er separate funksjoner ikke bare inkapslet i sine egne klasser, men de er organisert pÄ en mÄte som gjÞr dem forutsigbare og sammenhengende. Vi grupperer elementer med lik funksjonalitet for Ä fÞlge prinsippet om sammenheng. NÄ, hvis vi trenger Ä endre systemets arbeidsprinsipp ved Ä gÄ fra en hash-struktur til en matrise, kan vi enkelt bruke klassen SupplyHold, og det vil ikke vÊre nÞdvendig Ä berÞre andre moduler. Dermed, hvis offiseren som har ansvar for logistikken endrer noe i sin seksjon, vil de andre elementene pÄ stasjonen forbli urÞrte. Klassen SpaceStation vil til og med ikke vÊre klar over endringene.

VÄre offiserer som jobber pÄ romstasjonen er sannsynligvis glade for endringene, siden de kan be om det som er nÞdvendig for dem. VÊr oppmerksom pÄ at det i koden finnes metoder som report_supplies og report_fuel, som er inneholdt i klassene SupplyHold og FuelTank. Hva skjer hvis Jorden ber om Ä endre mÄten rapportene dannes pÄ? Det vil vÊre nÞdvendig Ä endre begge klassene, SupplyHold og FuelTank. Hva om vi ogsÄ mÄ endre mÄten drivstoff og forbruksvarer leveres pÄ? Vi mÄ sannsynligvis endre alle de samme klassene igjen. Og dette bryter SRP-prinsippet. La oss fikse det.

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.

I denne, den siste versjonen av programmet, er oppgavene delt opp i to nye klasser, FuelReporter og SupplyReporter. Begge er underklasser av Reporter-klassen. I tillegg har vi lagt til instansvariabler i klassen SpaceStation for Ă„ initialisere den nĂždvendige underklassen om nĂždvendig. NĂ„, hvis Jorden bestemmer seg for Ă„ endre noe annet, vil vi gjĂžre endringer i underklassene og ikke i hovedklassen.

SelvfĂžlgelig er noen klasser fortsatt avhengige av hverandre. For eksempel er objektet SupplyReporter avhengig av SupplyHold, mens FuelReporter er avhengig av FuelTank. SelvfĂžlgelig mĂ„ akseleratorene vĂŠre knyttet til drivstofftanken. Men nĂ„ er alt logisk, og endringer vil ikke vĂŠre spesielt vanskelige — redigering av kode for ett objekt vil ikke pĂ„virke et annet i stor grad.

Dermed har vi skapt modulĂŠr kode der oppgavene til hvert av objektene/klassene er nĂžyaktig definert. Å jobbe med slik kode er ikke et problem, vedlikeholdet vil vĂŠre en enkel oppgave. Hele den «guddommelige objektet» er konvertert til SRP.

Skillbox anbefaler:

Kilde: habr.com

KjĂžp pĂ„litelig hosting for nettsteder med DDoS-beskyttelse, VPS VDS-servere đŸ”„ KjĂžp pĂ„litelig hosting for nettsteder med DDoS-beskyttelse, VPS VDS-servere | ProHoster