Joustavan koodin kirjoittaminen SOLIDilla

Joustavan koodin kirjoittaminen SOLIDilla

Kääntäjältä: julkaistu sinulle Severin Perezin artikkeli SOLID-periaatteiden käytöstä ohjelmoinnissa. Artikkelin tiedot ovat hyödyllisiä sekä aloittelijoille että kokeneille ohjelmoijille.

Jos olet kiinnostunut kehityksestä, olet todennäköisesti kuullut SOLID-periaatteista. Niiden avulla ohjelmoija voi kirjoittaa puhdasta, hyvin jäsenneltyä ja helposti ylläpidettävää koodia. On syytä huomata, että ohjelmoinnissa on useita lähestymistapoja tietyn työn suorittamiseen oikein. Eri asiantuntijoilla on erilaiset ajatukset ja ymmärrys "oikeasta tiestä"; kaikki riippuu jokaisen henkilön kokemuksesta. SOLIDissa julistamat ideat hyväksyvät kuitenkin lähes kaikki IT-yhteisön edustajat. Niistä tuli lähtökohta monien hyvien kehitysjohtamiskäytäntöjen syntymiselle ja kehittymiselle.

Ymmärretään, mitä SOLID-periaatteet ovat ja miten ne auttavat meitä.

Skillbox suosittelee: Käytännön kurssi "Mobile Developer PRO".

Muistutamme sinua: kaikille "Habrin" lukijoille - 10 000 ruplan alennus ilmoittautuessaan mille tahansa Skillbox-kurssille "Habr" -tarjouskoodilla.

Mikä on SOLID?

Tämä termi on lyhenne, termin jokainen kirjain on tietyn periaatteen nimen alku:

  • Syhtenäinen vastuullisuusperiaate. Moduulilla voi olla yksi ja vain yksi syy muutokseen.
  • - Okynä/suljettu periaate (avoin/kiinni -periaate). Luokkien ja muiden elementtien tulee olla avoimia laajentamista varten, mutta suljettuja muutoksia varten.
  •  - Liskov Korvausperiaate (Liskovin korvausperiaate). Perustyyppiä käyttävien funktioiden tulee pystyä käyttämään perustyypin alatyyppejä tietämättään.
  • - IKäyttöliittymän erotteluperiaate  (rajapinnan erotteluperiaate). Ohjelmistokokonaisuudet eivät saa olla riippuvaisia ​​menetelmistä, joita ne eivät käytä.
  • - Driippuvuuden inversioperiaate (riippuvuuden inversion periaate). Korkeampien tasojen moduulit eivät saa olla riippuvaisia ​​alempien tasojen moduuleista.

Yhden vastuun periaate


Yhden vastuun periaate (Single Responsibility Principle, SRP) sanoo, että ohjelman jokaisen luokan tai moduulin tulee olla vastuussa vain yhdestä osasta kyseisen ohjelman toimintoja. Lisäksi tämän vastuun osat tulisi osoittaa omalle luokalleen, eikä hajallaan eri luokkiin. SRP:n kehittäjä ja pääevankelista Robert S. Martin kuvailee vastuullisuutta muutoksen syyksi. Hän ehdotti alun perin tätä termiä yhdeksi työnsä "Principles of Object-Oriented Design" -elementeistä. Konsepti sisältää suuren osan liitettävyyskuviosta, jonka Tom DeMarco määritteli aiemmin.

Konsepti sisälsi myös useita David Parnasin muotoilemia käsitteitä. Kaksi tärkeintä ovat kapselointi ja tiedon piilottaminen. Parnas väitti, että järjestelmän jakaminen erillisiin moduuleihin ei saisi perustua lohkokaavioiden tai suoritusvirtojen analysointiin. Jokaisen moduulin tulee sisältää erityinen ratkaisu, joka tarjoaa asiakkaille mahdollisimman vähän tietoa.

Muuten, Martin antoi mielenkiintoisen esimerkin yrityksen ylimpien johtajien (COO, CTO, CFO) kanssa, joista jokainen käyttää tiettyjä yritysohjelmistoja eri tarkoituksiin. Tämän seurauksena kuka tahansa heistä voi tehdä muutoksia ohjelmistoon vaikuttamatta muiden johtajien etuihin.

Jumalallinen esine

Kuten aina, paras tapa oppia SRP on nähdä se toiminnassa. Katsotaanpa ohjelman osaa, joka EI noudata yhden vastuun periaatetta. Tämä on Ruby-koodi, joka kuvaa avaruusaseman käyttäytymistä ja ominaisuuksia.

Tarkista esimerkki ja yritä määrittää seuraavat asiat:
Niiden objektien vastuut, jotka on ilmoitettu SpaceStation-luokassa.
Ne, jotka saattavat olla kiinnostuneita avaruusaseman toiminnasta.

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

Itse asiassa avaruusasemamme ei toimi (en usko saavani puhelua NASAlta lähiaikoina), mutta tässä on jotain analysoitavaa.

Näin ollen SpaceStation-luokassa on useita erilaisia ​​vastuita (tai tehtäviä). Kaikki ne voidaan jakaa tyyppeihin:

  • anturit;
  • tarvikkeet (kulutustarvikkeet);
  • polttoaine;
  • kiihdyttimiä.

Vaikka kenellekään aseman työntekijöistä ei ole määrätty luokkaa, voimme helposti kuvitella, kuka on vastuussa mistäkin. Todennäköisimmin tiedemies ohjaa antureita, logistiikka vastaa resurssien toimittamisesta, insinööri vastaa polttoaineen hankinnasta ja lentäjä ohjaa tehosteita.

Voimmeko sanoa, että tämä ohjelma ei ole SRP-yhteensopiva? Toki. Mutta SpaceStation-luokka on tyypillinen "jumalaobjekti", joka tietää kaiken ja tekee kaiken. Tämä on merkittävä anti-malli olio-ohjelmoinnissa. Aloittelijalle tällaisia ​​esineitä on erittäin vaikea ylläpitää. Toistaiseksi ohjelma on hyvin yksinkertainen, kyllä, mutta kuvittele mitä tapahtuu, jos lisäämme uusia ominaisuuksia. Ehkä avaruusasemamme tarvitsee lääkäriaseman tai kokoushuoneen. Ja mitä enemmän toimintoja on, sitä enemmän SpaceStation kasvaa. No, koska tämä laitos yhdistetään muihin, koko kompleksin huolto tulee entistä monimutkaisemmaksi. Tämän seurauksena voimme häiritä esimerkiksi kiihdytinten toimintaa. Jos tutkija pyytää muutoksia antureisiin, tämä voi hyvinkin vaikuttaa aseman viestintäjärjestelmiin.

SRP-periaatteen rikkominen voi antaa lyhyen aikavälin taktisen voiton, mutta lopulta "häviämme sodan", ja tällaisen hirviön ylläpitäminen tulee olemaan erittäin vaikeaa tulevaisuudessa. On parasta jakaa ohjelma erillisiin koodin osiin, joista jokainen on vastuussa tietyn toiminnon suorittamisesta. Ymmärtääksemme tämän, muutetaan SpaceStation-luokkaa.

Jaetaan vastuuta

Yllä määritimme neljä toimintotyyppiä, joita SpaceStation-luokka ohjaa. Pidämme ne mielessä, kun refaktoroimme. Päivitetty koodi vastaa paremmin SRP:tä.

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

Muutoksia on paljon, ohjelma näyttää nyt ehdottomasti paremmalta. Nyt SpaceStation-luokistamme on tullut enemmän kontti, jossa käynnistetään riippuvien osien toiminnot, mukaan lukien joukko antureita, kulutustavarajärjestelmä, polttoainesäiliö ja tehostimet.

Jokaiselle muuttujalle on nyt vastaava luokka: Sensors; SupplyHold; Polttoainetankki; Puskurit.

Tässä koodiversiossa on useita tärkeitä muutoksia. Asia on siinä, että yksittäiset toiminnot eivät ole vain kapseloituja omiin luokkiinsa, vaan ne on järjestetty siten, että niistä tulee ennustettavia ja johdonmukaisia. Ryhmittelemme elementtejä, joilla on samanlainen toiminnallisuus, johdonmukaisuusperiaatteen mukaisesti. Jos meidän on nyt muutettava järjestelmän toimintatapaa siirtymällä hash-rakenteesta taulukkoon, käytä vain SupplyHold-luokkaa; meidän ei tarvitse koskea muihin moduuleihin. Näin jos logistiikkaupseeri muuttaa jotain osastollaan, muu asema säilyy ennallaan. Tässä tapauksessa SpaceStation-luokka ei edes ole tietoinen muutoksista.

Avaruusasemalla työskentelevät upseerimme ovat luultavasti iloisia muutoksista, koska he voivat pyytää tarvitsemansa. Huomaa, että koodissa on SupplyHold- ja FuelTank-luokissa olevia menetelmiä, kuten report_supplies ja report_fuel. Mitä tapahtuisi, jos Maa pyytää muuttamaan tapaa, jolla se raportoi? Molemmat luokat, SupplyHold ja FuelTank, on vaihdettava. Entä jos sinun on muutettava tapaa, jolla polttoaine ja kulutustarvikkeet toimitetaan? Sinun on luultavasti vaihdettava kaikki samat luokat uudelleen. Ja tämä on jo SRP-periaatteen vastaista. Korjataan tämä.

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.

Tässä ohjelman uusimmassa versiossa vastuut on jaettu kahteen uuteen luokkaan, FuelReporter ja SupplyReporter. He ovat molemmat Reporter-luokan lapsia. Lisäksi SpaceStation-luokkaan lisäsimme ilmentymämuuttujia, jotta haluttu alaluokka voidaan tarvittaessa alustaa. Nyt, jos maapallo päättää muuttaa jotain muuta, teemme muutoksia alaluokkiin, emme pääluokkaan.

Tietysti jotkut luokistamme ovat edelleen riippuvaisia ​​toisistaan. Siten SupplyReporter-objekti riippuu SupplyHoldista ja FuelReporter riippuu FuelTankista. Tietysti tehostimet on kytkettävä polttoainesäiliöön. Mutta täällä kaikki näyttää jo loogiselta, ja muutosten tekeminen ei ole erityisen vaikeaa - yhden objektin koodin muokkaaminen ei vaikuta suuresti toiseen.

Näin ollen olemme luoneet modulaarisen koodin, jossa kunkin objektin/luokan vastuut on määritelty tarkasti. Työskentely tällaisen koodin kanssa ei ole ongelma, sen ylläpitäminen on yksinkertainen tehtävä. Olemme muuntaneet koko "jumalan esineen" SRP:ksi.

Skillbox suosittelee:

Lähde: will.com

Lisää kommentti