Skrive fleksibel kode ved hjelp av SOLID

Skrive fleksibel kode ved hjelp av SOLID

Fra oversetteren: publisert for deg artikkel av Severin Perez om å bruke SOLID prinsipper i programmering. Informasjonen fra artikkelen vil være nyttig for både nybegynnere og erfarne programmerere.

Hvis du er i utvikling, har du mest sannsynlig hørt om SOLID-prinsippene. De gjør det mulig for programmereren å skrive ren, godt strukturert og lett vedlikeholdbar kode. Det er verdt å merke seg at i programmering er det flere tilnærminger til hvordan man utfører en bestemt jobb riktig. Ulike spesialister har forskjellige ideer og forståelse av den "riktige veien"; alt avhenger av hver persons erfaring. Ideene som er proklamert i SOLID er imidlertid akseptert av nesten alle representanter for IT-miljøet. De ble utgangspunktet for fremveksten og utviklingen av mange gode utviklingsledelsespraksiser.

La oss forstå hva de SOLIDE prinsippene er og hvordan de hjelper oss.

Skillbox anbefaler: Praktisk kurs "Mobilutvikler PRO".

Vi minner om: for alle lesere av "Habr" - en rabatt på 10 000 rubler når du melder deg på et hvilket som helst Skillbox-kurs ved å bruke kampanjekoden "Habr".

Hva er SOLID?

Dette begrepet er en forkortelse, hver bokstav i begrepet er begynnelsen på navnet på et spesifikt prinsipp:

  • Single Ansvarsprinsipp. En modul kan ha én og bare én årsak til endring.
  • De Openn/lukket prinsipp (åpent/lukket prinsipp). Klasser og andre elementer bør være åpne for utvidelse, men stengt for endring.
  •  De Liskov Substitusjonsprinsipp (Liskov substitusjonsprinsipp). Funksjoner som bruker en basistype skal kunne bruke undertyper av basistypen uten å vite det.
  • De IGrensesnittsegregeringsprinsipp  (grensesnittseparasjonsprinsipp). Programvareenheter bør ikke være avhengige av metoder de ikke bruker.
  • De Davhengighetsinversjonsprinsipp (prinsippet om avhengighetsinversjon). Moduler på høyere nivåer bør ikke være avhengige av moduler på lavere nivåer.

Enkelt ansvarsprinsipp


Single Responsibility Principle (SRP) sier at hver klasse eller modul i et program skal være ansvarlig for kun én del av programmets funksjonalitet. I tillegg bør elementer av dette ansvaret tildeles deres egen klasse, i stedet for spredt over ikke-relaterte klasser. SRPs utvikler og sjefevangelist, Robert S. Martin, beskriver ansvarlighet som årsaken til endringen. Han foreslo opprinnelig dette begrepet som et av elementene i arbeidet hans "Principles of Object-Oriented Design". Konseptet inneholder mye av tilkoblingsmønsteret som tidligere ble definert av Tom DeMarco.

Konseptet inkluderte også flere konsepter formulert av David Parnas. De to viktigste er innkapsling og informasjonsskjuling. Parnas hevdet at oppdeling av et system i separate moduler ikke burde være basert på analyse av blokkdiagrammer eller utførelsesflyt. Enhver av modulene må inneholde en spesifikk løsning som gir et minimum av informasjon til kundene.

Martin ga forresten et interessant eksempel med seniorledere i et selskap (COO, CTO, CFO), som hver bruker spesifikk forretningsprogramvare til forskjellige formål. Som et resultat kan enhver av dem implementere endringer i programvaren uten å påvirke interessene til andre ledere.

Guddommelig objekt

Som alltid er den beste måten å lære SRP på å se den i aksjon. La oss se på en del av programmet som IKKE følger Single Responsibility-prinsippet. Dette er Ruby-kode som beskriver oppførselen og egenskapene til romstasjonen.

Se gjennom eksemplet og prøv å finne følgende:
Ansvar for de objektene som er deklarert i SpaceStation-klassen.
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} 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

Egentlig er romstasjonen vår dysfunksjonell (jeg tror ikke jeg kommer til å få en telefon fra NASA med det første), men det er noe å analysere her.

Dermed har SpaceStation-klassen flere ulike ansvarsområder (eller oppgaver). Alle kan deles inn i typer:

  • sensorer;
  • rekvisita (forbruksvarer);
  • brensel;
  • akseleratorer.

Selv om ingen av stasjonens ansatte er tildelt en klasse, kan vi lett tenke oss hvem som har ansvaret for hva. Mest sannsynlig kontrollerer forskeren sensorene, logistikeren er ansvarlig for å levere ressurser, ingeniøren er ansvarlig for drivstoffforsyningen, og piloten kontrollerer boosterne.

Kan vi si at dette programmet ikke er SRP-kompatibelt? Ja sikkert. Men SpaceStation-klassen er et typisk «gudeobjekt» som vet alt og gjør alt. Dette er et viktig antimønster innen objektorientert programmering. For en nybegynner er slike gjenstander 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 en medisinsk stasjon eller et møterom. Og jo flere funksjoner det er, jo mer vil SpaceStation vokse. Vel, siden dette anlegget vil være koblet til andre, vil det å betjene hele komplekset bli enda mer komplekst. Som et resultat kan vi forstyrre driften av for eksempel akseleratorer. Dersom en forsker ber om endringer på sensorene, kan dette meget vel påvirke stasjonens kommunikasjonssystemer.

Brudd på SRP-prinsippet kan gi en kortsiktig taktisk seier, men til slutt vil vi "tape krigen", og det vil bli svært vanskelig å opprettholde et slikt monster i fremtiden. Det er best å dele programmet inn i separate kodeseksjoner, som hver er ansvarlig for å utføre en bestemt operasjon. For å forstå dette, la oss endre SpaceStation-klassen.

La oss fordele ansvaret

Ovenfor definerte vi fire typer operasjoner som kontrolleres av SpaceStation-klassen. Vi vil ha dem i bakhodet ved refaktorisering. Den oppdaterte koden samsvarer bedre med 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

Det er mange endringer, programmet ser definitivt bedre ut nå. Nå har SpaceStation-klassen vår blitt mer en container der operasjoner initieres for avhengige deler, inkludert et sett med sensorer, et forbruksmateriell, en drivstofftank og boostere.

For enhver av variablene er det nå en tilsvarende klasse: Sensorer; SupplyHold; Bensintank; Thrustere.

Det er flere viktige endringer i denne versjonen av koden. Poenget er at individuelle funksjoner ikke bare er innkapslet i egne klasser, de er organisert på en slik måte at de blir forutsigbare og konsistente. Vi grupperer elementer med lignende funksjonalitet for å følge prinsippet om sammenheng. Nå, hvis vi trenger å endre måten systemet fungerer på, ved å gå fra en hash-struktur til en array, bruker du bare SupplyHold-klassen; vi trenger ikke å berøre andre moduler. På denne måten, hvis logistikkoffiseren endrer noe i seksjonen sin, vil resten av stasjonen forbli intakt. I dette tilfellet vil SpaceStation-klassen ikke engang være klar over endringene.

Våre offiserer som jobber på romstasjonen er sannsynligvis glade for endringene fordi de kan be om de de trenger. Legg merke til at koden har metoder som report_supplies og report_fuel i SupplyHold- og FuelTank-klassene. Hva ville skje hvis Jorden ba om å endre måten den rapporterer på? Begge klassene, SupplyHold og FuelTank, må endres. Hva om du trenger å endre måten drivstoff og forbruksvarer leveres på? Du må sannsynligvis bytte alle de samme klassene igjen. Og dette er allerede et brudd på SRP-prinsippet. La oss fikse dette.

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.

I denne siste versjonen av programmet har ansvaret blitt delt inn i to nye klasser, FuelReporter og SupplyReporter. De er begge barn av Reporter-klassen. I tillegg har vi lagt til instansvariabler til SpaceStation-klassen slik at den ønskede underklassen kan initialiseres om nødvendig. Nå, hvis jorden bestemmer seg for å endre noe annet, vil vi gjøre endringer i underklassene, og ikke til hovedklassen.

Selvfølgelig er noen av våre klasser fortsatt avhengige av hverandre. Dermed avhenger SupplyReporter-objektet av SupplyHold, og FuelReporter avhenger av FuelTank. Selvsagt må boosterne kobles til drivstofftanken. Men her ser alt allerede logisk ut, og å gjøre endringer vil ikke være spesielt vanskelig - å redigere koden til ett objekt vil ikke påvirke et annet i stor grad.

Dermed har vi laget en modulær kode hvor ansvaret til hver av objektene/klassene er presist definert. Å jobbe med slik kode er ikke et problem, det vil være en enkel oppgave å vedlikeholde den. Vi har konvertert hele det "guddommelige objektet" til SRP.

Skillbox anbefaler:

Kilde: www.habr.com

Legg til en kommentar