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.
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 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.