Писање флексибилног кода користећи СОЛИД

Писање флексибилног кода користећи СОЛИД

Од преводиоца: објављено за вас чланак Северина Переза о коришћењу СОЛИД принципа у програмирању. Информације из чланка ће бити корисне и почетницима и искусним програмерима.

Ако сте у развоју, вероватно сте чули за СОЛИД принципе. Они омогућавају програмеру да напише чист, добро структуиран и лак за одржавање кода. Вреди напоменути да у програмирању постоји неколико приступа како правилно извршити одређени посао. Различити стручњаци имају различите идеје и разумевање „правог пута“; све зависи од искуства сваке особе. Међутим, идеје прокламоване у СОЛИД-у прихватају готово сви представници ИТ заједнице. Они су постали полазна основа за настанак и развој многих добрих пракси управљања развојем.

Хајде да разумемо шта су СОЛИД принципи и како нам помажу.

Скиллбок препоручује: Практични курс „Програмер за мобилне уређаје“.

Подсећамо: за све читаоце „Хабра“ – попуст од 10 рубаља при упису на било који курс Скиллбок користећи промотивни код „Хабр“.

Шта је СОЛИД?

Овај термин је скраћеница, свако слово термина је почетак назива одређеног принципа:

  • Sначело појединачне одговорности. Модул може имати један и само један разлог за промену.
  • Oоловка/затворени принцип (принцип отворено/затворено). Класе и други елементи треба да буду отворени за проширење, али затворени за модификацију.
  • Lисков Принцип замене (Принцип замене Лискова). Функције које користе основни тип би требало да могу да користе подтипове основног типа, а да то не знају.
  • IПринцип сегрегације интерфејса  (принцип раздвајања интерфејса). Софтверски ентитети не би требало да зависе од метода које не користе.
  • DПринцип инверзије зависности (принцип инверзије зависности). Модули на вишим нивоима не би требало да зависе од модула на нижим нивоима.

Принцип јединствене одговорности


Принцип јединствене одговорности (СРП) наводи да свака класа или модул у програму треба да буде одговоран само за један део функционалности тог програма. Поред тога, елементи ове одговорности треба да буду додељени њиховој сопственој класи, а не разбацани по неповезаним класама. СРП-ов програмер и главни јеванђелиста, Роберт С. Мартин, описује одговорност као разлог за промену. Он је првобитно предложио овај термин као један од елемената свог рада „Принципи објектно-оријентисаног дизајна“. Концепт укључује већи део обрасца повезивања који је претходно дефинисао Том ДеМарцо.

Концепт је такође укључивао неколико концепата које је формулисао Давид Парнас. Два главна су инкапсулација и скривање информација. Парнас је тврдио да подела система на засебне модуле не би требало да се заснива на анализи блок дијаграма или токова извршења. Сваки од модула мора да садржи специфично решење које клијентима пружа минимум информација.

Иначе, Мартин је дао занимљив пример са вишим менаџерима једне компаније (ЦОО, ЦТО, ЦФО), од којих сваки користи одређени пословни софтвер у различите сврхе. Као резултат, било који од њих може да примени промене у софтверу без утицаја на интересе других менаџера.

Божански објекат

Као и увек, најбољи начин да научите СРП је да га видите на делу. Хајде да погледамо део програма који НЕ прати принцип јединствене одговорности. Ово је Руби код који описује понашање и атрибуте свемирске станице.

Прегледајте пример и покушајте да утврдите следеће:
Одговорности оних објеката који су декларисани у класи СпацеСтатион.
Они који би могли бити заинтересовани за рад свемирске станице.

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

У ствари, наша свемирска станица је нефункционална (мислим да ме неће ускоро звати НАСА), али овде има шта да се анализира.

Дакле, класа СпацеСтатион има неколико различитих одговорности (или задатака). Сви се могу поделити на врсте:

  • сензори;
  • залихе (потрошни материјал);
  • гориво;
  • акцелератори.

Иако никоме од запослених у станици није додељена класа, лако можемо замислити ко је за шта одговоран. Највероватније, научник контролише сензоре, логистичар је одговоран за снабдевање ресурсима, инжењер је одговоран за снабдевање горивом, а пилот контролише појачиваче.

Можемо ли рећи да овај програм није усклађен са СРП? Да сигуран. Али класа СпацеСтатион је типичан „божји објекат“ који све зна и све ради. Ово је главни анти-узорак у објектно оријентисаном програмирању. За почетника, такви објекти су изузетно тешки за одржавање. До сада је програм веома једноставан, да, али замислите шта ће се десити ако додамо нове функције. Можда ће нашој свемирској станици бити потребна медицинска станица или сала за састанке. И што више функција има, више ће расти СпацеСтатион. Па, пошто ће овај објекат бити повезан са другим, сервисирање целог комплекса ће постати још теже. Као резултат тога, можемо пореметити рад, на пример, акцелератора. Ако истраживач затражи промене на сензорима, то би могло да утиче на комуникационе системе станице.

Кршење принципа СРП-а може донети краткорочну тактичку победу, али на крају ћемо „изгубити рат“ и биће веома тешко одржати такво чудовиште у будућности. Најбоље је поделити програм на засебне делове кода, од којих је сваки одговоран за обављање одређене операције. Схватајући ово, хајде да променимо класу СпацеСтатион.

Поделимо одговорност

Изнад смо дефинисали четири типа операција које контролише класа СпацеСтатион. Имаћемо их на уму приликом рефакторисања. Ажурирани код боље одговара СРП-у.

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

Много је промена, програм сада дефинитивно изгледа боље. Сада је наша класа СпацеСтатион постала више као контејнер у којем се покрећу операције за зависне делове, укључујући сет сензора, систем за снабдевање потрошним материјалом, резервоар за гориво и појачиваче.

За било коју од променљивих сада постоји одговарајућа класа: Сензори; СупплиХолд; Резервоар за гориво; Тхрустерс.

Постоји неколико важних промена у овој верзији кода. Поента је да појединачне функције нису само инкапсулиране у сопственим класама, већ су организоване на такав начин да постану предвидиве и конзистентне. Групишемо елементе са сличном функционалношћу да бисмо пратили принцип кохерентности. Сада, ако треба да променимо начин на који систем функционише, прелазећи са хеш структуре на низ, само користите класу СупплиХолд; не морамо да додирујемо друге модуле. На овај начин, ако службеник логистике нешто промени у свом делу, остатак станице ће остати нетакнут. У овом случају, класа СпацеСтатион неће ни бити свесна промена.

Наши официри који раде на свемирској станици су вероватно срећни због промена јер могу да затраже оне које су им потребне. Приметите да код има методе као што су репорт_супплиес и репорт_фуел садржане у класама СупплиХолд и ФуелТанк. Шта би се догодило када би Земља затражила да промени начин на који извештава? Обе класе, СупплиХолд и ФуелТанк, ће морати да се промене. Шта ако треба да промените начин испоруке горива и потрошног материјала? Вероватно ћете морати поново да промените све исте класе. А ово је већ кршење принципа СРП. Хајде да поправимо ово.

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.

У овој најновијој верзији програма, одговорности су подељене у две нове класе, ФуелРепортер и СупплиРепортер. Обоје су деца из класе Репортер. Поред тога, додали смо променљиве инстанце у класу СпацеСтатион тако да се жељена подкласа може иницијализовати ако је потребно. Сада, ако Земља одлучи да промени нешто друго, онда ћемо извршити промене у подкласама, а не у главној класи.

Наравно, неки од наших часова и даље зависе једни од других. Дакле, објекат СупплиРепортер зависи од СупплиХолд, а ФуелРепортер зависи од ФуелТанк-а. Наравно, појачивачи морају бити повезани са резервоаром за гориво. Али овде све већ изгледа логично, а уношење промена неће бити посебно тешко - уређивање кода једног објекта неће много утицати на други.

Тако смо креирали модуларни код где су одговорности сваког од објеката/класа прецизно дефинисане. Рад са таквим кодом није проблем, његово одржавање ће бити једноставан задатак. Претворили смо цео „божански објекат“ у СРП.

Скиллбок препоручује:

Извор: ввв.хабр.цом

Додај коментар