Пішам гнуткі код, выкарыстоўваючы SOLID

Пішам гнуткі код, выкарыстоўваючы SOLID

Ад перакладчыка: апублікавалі для вас артыкул Севярына Перэса аб выкарыстанні прынцыпаў SOLID у праграмаванні. Інфармацыя з артыкула будзе карысная як навічкам, так і праграмістам з вопытам.

Калі вы займаецеся распрацоўкай, то хутчэй за ўсё чулі пра прынцыпы SOLID. Яны даюць магчымасць праграмісту пісаць чысты, добра структураваны і лёгка абслугоўваецца код. Варта адзначыць, што ў праграмаванні ёсць некалькі падыходаў да таго, як правільна выконваць тую ці іншую працу. У розных спецыялістаў - розныя ідэі і разуменне "правільнага шляху", усё залежыць ад вопыту кожнага. Тым не менш, ідэі, абвешчаныя ў SOLID, прымаюцца практычна ўсімі прадстаўнікамі ІТ-супольнасці. Яны сталі адпраўной кропкай для з'яўлення і развіцця мноства добрых метадаў кіравання распрацоўкай.

Давайце разбяромся з тым, што такое прынцыпы SOLID і як яны дапамагаюць нам.

Skillbox рэкамендуе: Практычны курс «Мабільны распрацоўшчык PRO».

Нагадваем: для ўсіх чытачоў "Хабра" - зніжка 10 000 рублёў пры запісе на любы курс Skillbox па промакодзе "Хабр".

Што такое SOLID?

Гэты тэрмін - абрэвіятура, кожная літара тэрміна - пачатак назвы вызначанага прынцыпу:

  • Single Responsibility Principle (прынцып адзінай адказнасці). Модуль можа мець адну і толькі адну прычыну для змены.
  • ,en Open/Closed Principle (прынцып адкрытасці/закрытасці). Класы і іншыя элементы павінны быць адчыненыя для пашырэння, але зачыненыя для мадыфікацыі.
  •  ,en Liskov Substitution Principle (прынцып падстаноўкі Ліскаў). Функцыі, якія выкарыстоўваюць базавы тып, павінны мець магчымасць выкарыстоўваць падтыпы базавага тыпу, не ведаючы пра гэта.
  • ,en Interface Segregation Principle  (прынцып падзелу інтэрфейсу). Праграмныя сутнасці не павінны залежаць ад метадаў, якія яны не выкарыстоўваюць.
  • ,en Dependency Inversion Principle (прынцып інверсіі залежнасцяў). Модулі верхніх узроўняў не павінны залежаць ад модуляў ніжніх узроўняў.

Прынцып адзінай адказнасці


Прынцып адзінай адказнасці (SRP) абвяшчае, што кожны клас або модуль у праграме павінен несці адказнасць толькі за адну частку функцыянальнасці гэтай праграмы. Акрамя таго, элементы гэтай адказнасці павінны быць замацаваны за сваім класам, а не размеркаваны па незвязаных класах. Распрацоўнік і галоўны евангеліст SRP, Роберт С. Марцін, апісвае адказнасць як прычыну перамен. Першапачаткова ён прапанаваў гэты тэрмін у якасці аднаго з элементаў сваёй працы "Прынцыпы аб'ектна-арыентаванага праектавання". У канцэпцыю ўвайшло шмат з заканамернасці складнасці, якая была вызначана раней Томам Дэмарка.

Яшчэ ў канцэпцыю ўвайшлі некалькі паняццяў, сфармуляваных Дэвідам Парнасам. Два асноўных - інкапсуляцыя і ўтойванне інфармацыі. Парнас сцвярджаў, што падзел сістэмы на асобныя модулі не павінна грунтавацца на аналізе блок-схем або плыняў выканання. Любы з модуляў павінен змяшчаць пэўнае рашэнне, якое дае мінімум інфармацыі кліентам.

Дарэчы, Марцін прыводзіў цікавы прыклад з вышэйшымі мэнэджэрамі кампаніі (COO, CTO, CFO), кожны з якіх прымяняе спецыфічнае праграмнае забеспячэнне для бізнесу з рознай мэтай. У выніку любы з іх можа ўкараняць змены ў ПЗ, не закранаючы інтарэсы іншых мэнэджэраў.

Чароўны аб'ект

Як звычайна, лепшы спосаб вывучыць SRP - гэта ўбачыць усё ў дзеянні. Давайце паглядзім на ўчастак праграмы, якая не адпавядае прынцыпу адзінай адказнасці. Гэта Ruby-код, які апісвае паводзіны і атрыбуты касмічнай станцыі.

Праглядзіце прыклад і паспрабуйце вызначыць наступнае:
Абавязкі тых аб'ектаў, якія абвешчаны ў класе SpaceStation.
Тых, хто можа быць зацікаўлены ў рабоце касмічнай станцыі.

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

Уласна, наша касмічная станцыя нефункцыянальная (думаю, што не атрымаю званок ад НАСА ў найбліжэйшай агляднай будучыні), але тут ёсць што прааналізаваць.

Так, у класа SpaceStation ёсць некалькі розных адказнасцяў (ці задач). Усе яны могуць быць разбітыя па тыпах:

  • сэнсары;
  • забеспячэнне (расходнікі);
  • паліва;
  • паскаральнікі.

Нягледзячы на ​​тое, што ніхто з супрацоўнікаў станцыі не вызначаны ў класе, мы можам з лёгкасцю ўявіць, хто за што адказвае. Хутчэй за ўсё, навуковы працаўнік кантралюе сэнсары, лагіст адказвае за забеспячэнне рэсурсамі, інжынер адказвае за запасы паліва, а пілот кантралюе паскаральнікі.

Ці можам мы сказаць, што гэтая праграма не адпавядае SRP? Так, вядома. Але клас SpaceStation з'яўляецца тыповым "боскім аб'ектам", які ведае пра ўсё і робіць усё. Гэта асноўны антышаблон у аб'ектна-арыентаваным праграмаванні. Для пачаткоўца такія аб'екты надзвычай складаныя ў абслугоўванні. Пакуль што праграма вельмі простая, так, але ўявіце, што адбудзецца, калі мы дадамо новыя функцыі. Магчыма, нашай касмічнай станцыі спатрэбіцца медпункт ці перагаворны пакой. І чым больш будзе функцый, тым мацней вырасце SpaceStation. Ну а паколькі гэты аб'ект будзе злучаны з іншымі, то абслугоўванне ўсяго комплексу стане яшчэ больш складаным. У выніку мы можам парушыць працу, напрыклад, паскаральнікаў. Калі навуковы супрацоўнік запытае змен у працы з сэнсарамі, тое гэта цалкам можа паўплываць на сістэмы сувязі станцыі.

Парушэнне SRP-прынцыпу можа даць кароткачасовую тактычную перамогу, але ў выніку мы "прайграем вайну", абслугоўваць такога монстра ў будучыні стане вельмі няпроста. Лепш за ўсё падзяліць праграму на асобныя ўчасткі кода, кожны з якіх адказвае за выкананне вызначанай аперацыі. Разумеючы гэта, давайце зменім клас SpaceStation.

Размяркуем адказнасць

Вышэй мы вызначылі чатыры тыпу аперацый, якія кантралююцца класам SpaceStation. Пры рэфактарынгу мы будзем мець іх на ўвазе. Абноўлены код лепш адпавядае 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

Змен шмат, праграма зараз выглядае вызначана лепш. Цяпер наш клас SpaceStation стаў, хутчэй, кантэйнерам, у якім ініцыююцца аперацыі для залежных частак, уключаючы набор сэнсараў, сістэму падачы расходных матэрыялаў, паліўны бак, паскаральнікі.

Для любой з зменных зараз ёсць адпаведны клас: Sensors; SupplyHold; FuelTank; Thrusters.

У гэтай версіі кода ёсць некалькі важных змен. Справа ў тым, што асобныя функцыі не толькі інкапсуляваная ва ўласныя класы, яны арганізаваны такім чынам, каб стаць прадказальнымі і паслядоўнымі. Мы групуем падобныя па функцыянальнасці элементы для прытрымлівання прынцыпу складнасці. Цяпер, калі нам спатрэбіцца змяніць прынцып працы сістэмы, перайшоўшы з хэш-структуры на масіў, проста скарыстайцеся класам SupplyHold, закранаць іншыя модулі не давядзецца. Такім чынам, калі афіцэр, адказны за лагістыку, нешта зменіць у сваёй секцыі, астатнія элементы станцыі застануцца некранутымі. Пры гэтым клас SpaceStation нават не будзе ў курсе змен.

Нашы афіцэры, якія працуюць на касмічнай станцыі, верагодна, рады зменам, паколькі могуць запытваць тыя, якія неабходны менавіта ім. Звярніце ўвагу, што ў кодзе ёсць такія метады, як report_supplies і report_fuel, якія змяшчаюцца ў класах SupplyHold і FuelTank. Што здарыцца, калі Зямля папросіць змяніць спосаб фармавання справаздач? Неабходна будзе змяніць абодва класа, SupplyHold і FuelTank. А што, калі трэба будзе змяніць спосаб дастаўкі паліва і расходнікаў? Верагодна, давядзецца зноў змяніць усе тыя ж класы. А гэта ўжо парушэнне SRP-прынцыпу. Давайце гэта выправім.

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.

У гэтай, апошняй версіі праграмы абавязкі былі разбітыя на два новыя класы, FuelReporter і SupplyReporter. Яны абодва з'яўляюцца даччынымі ў адносінах да класа Reporter. Акрамя таго, мы дадалі асобныя зменныя да класа SpaceStation з тым, каб пры неабходнасці ініцыялізаваць патрэбны падклас. Цяпер, калі Зямля вырашыць памяняць яшчэ нешта, то мы занясем праўкі ў падкласы, а не ў асноўны клас.

Вядома, некаторыя класы ў нас да гэтага часу залежаць адзін ад аднаго. Так, аб'ект SupplyReporter залежыць ад SupplyHold, а FuelReporter залежыць ад FuelTank. Само сабой, паскаральнікі павінны быць злучаны з паліўным бакам. Але тут ужо ўсё выглядае лагічным, а занясенне змен не будзе асабліва складаным - рэдагаванне кода аднаго аб'екта не занадта паўплывае на іншы.

Такім чынам, мы стварылі модульны код, дзе абавязкі кожнага з аб'ектаў/класаў дакладна вызначаны. Працаваць з такім кодам - ​​не праблема, яго абслугоўванне будзе простай задачай. Увесь "боскі аб'ект" мы пераўтварылі ў SRP.

Skillbox рэкамендуе:

Крыніца: habr.com

Дадаць каментар