SOLID yordamida moslashuvchan kod yozish

SOLID yordamida moslashuvchan kod yozish

Tarjimondan: siz uchun chop etilgan Severin Peres maqolasi dasturlashda SOLID tamoyillaridan foydalanish haqida. Maqolada olingan ma'lumotlar yangi boshlanuvchilar uchun ham, tajribali dasturchilar uchun ham foydali bo'ladi.

Agar siz rivojlanish bilan shug'ullanayotgan bo'lsangiz, SOLID tamoyillari haqida eshitgan bo'lishingiz mumkin. Ular dasturchiga toza, yaxshi tuzilgan va oson saqlanishi mumkin bo'lgan kod yozish imkonini beradi. Shuni ta'kidlash kerakki, dasturlashda ma'lum bir ishni qanday qilib to'g'ri bajarishga bir nechta yondashuvlar mavjud. Turli mutaxassislar "to'g'ri yo'l" haqida turli xil g'oyalar va tushunchalarga ega, barchasi har bir insonning tajribasiga bog'liq. Biroq, SOLIDda e'lon qilingan g'oyalar IT hamjamiyatining deyarli barcha vakillari tomonidan qabul qilinadi. Ular rivojlanishni boshqarishning ko'plab yaxshi amaliyotlarining paydo bo'lishi va rivojlanishi uchun boshlang'ich nuqta bo'ldi.

Keling, SOLID tamoyillari nima ekanligini va ular bizga qanday yordam berishini tushunib olaylik.

Skillbox tavsiya qiladi: Amaliy kurs "Mobil dasturchi PRO".

Sizga eslatib o'tamiz: "Habr" ning barcha o'quvchilari uchun - "Habr" promo-kodidan foydalangan holda har qanday Skillbox kursiga yozilishda 10 000 rubl chegirma.

SOLID nima?

Bu atama qisqartma bo'lib, atamaning har bir harfi ma'lum bir printsip nomining boshlanishidir:

  • Syagona javobgarlik printsipi. Modul o'zgartirish uchun bitta va faqat bitta sababga ega bo'lishi mumkin.
  • The Oqalam/Yopiq printsip (ochiq/yopiq printsip). Sinflar va boshqa elementlar kengaytirish uchun ochiq bo'lishi kerak, lekin o'zgartirish uchun yopiq bo'lishi kerak.
  • β€ŠThe Liskov almashtirish printsipi (Liskov almashtirish printsipi). Baza turidan foydalanadigan funksiyalar uni bilmagan holda asosiy turdagi kichik tiplardan foydalanishi kerak.
  • The IInterfeysni ajratish printsipiβ€Š (interfeysni ajratish printsipi). Dasturiy ta'minot ob'ektlari ular ishlatmaydigan usullarga bog'liq bo'lmasligi kerak.
  • The Dependency Inversiya printsipi (qaramlikni inversiya qilish printsipi). Yuqori darajadagi modullar quyi darajadagi modullarga bog'liq bo'lmasligi kerak.

Yagona javobgarlik printsipi

β€Š
Yagona javobgarlik printsipi (SRP) dasturdagi har bir sinf yoki modul ushbu dastur funksionalligining faqat bir qismi uchun javobgar bo'lishi kerakligini ta'kidlaydi. Bundan tashqari, ushbu mas'uliyatning elementlari bir-biriga bog'liq bo'lmagan sinflarga tarqalib emas, balki o'z sinfiga tayinlanishi kerak. SRP ning ishlab chiquvchisi va bosh xushxabarchisi Robert S. Martin o'zgarishlarning sababi sifatida javobgarlikni ta'riflaydi. U dastlab ushbu atamani "Ob'ektga yo'naltirilgan dizayn tamoyillari" asarining elementlaridan biri sifatida taklif qildi. Kontseptsiya ilgari Tom DeMarko tomonidan aniqlangan ulanish sxemasining ko'p qismini o'z ichiga oladi.

Kontseptsiya shuningdek, Devid Parnas tomonidan ishlab chiqilgan bir nechta tushunchalarni o'z ichiga oladi. Ikkita asosiysi - inkapsulyatsiya va ma'lumotlarni yashirish. Parnasning ta'kidlashicha, tizimni alohida modullarga bo'lish blok diagrammalari yoki bajarilish oqimlari tahliliga asoslanmasligi kerak. Modullarning har biri mijozlarga minimal ma'lumotni taqdim etadigan maxsus echimni o'z ichiga olishi kerak.

Aytgancha, Martin kompaniyaning yuqori darajali menejerlari (COO, CTO, CFO) bilan qiziqarli misol keltirdi, ularning har biri turli maqsadlarda o'ziga xos biznes dasturlardan foydalanadi. Natijada, ularning har biri boshqa menejerlarning manfaatlariga ta'sir qilmasdan dasturiy ta'minotdagi o'zgarishlarni amalga oshirishi mumkin.

Ilohiy ob'ekt

Har doimgidek, SRPni o'rganishning eng yaxshi usuli uni amalda ko'rishdir. Keling, dasturning Yagona javobgarlik tamoyiliga amal qilmaydigan qismini ko'rib chiqaylik. Bu kosmik stansiyaning xatti-harakatlari va atributlarini tavsiflovchi Ruby kodi.

Misolni ko'rib chiqing va quyidagilarni aniqlashga harakat qiling:
SpaceStation sinfida e'lon qilingan ob'ektlarning javobgarligi.
Koinot stantsiyasining ishlashiga qiziqish bildirishi mumkin bo'lganlar.

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

Aslida, bizning kosmik stansiyamiz ishlamay qolgan (menimcha, yaqin orada NASAdan qo'ng'iroq qilishim mumkin emas), lekin bu erda tahlil qilish kerak bo'lgan narsa bor.

Shunday qilib, SpaceStation klassi bir nechta turli mas'uliyatlarga (yoki vazifalarga) ega. Ularning barchasini turlarga bo'lish mumkin:

  • sensorlar;
  • materiallar (sarflanadigan materiallar);
  • yoqilg'i;
  • tezlatgichlar.

Stansiya xodimlaridan birortasiga sinf ajratilmagan boβ€˜lsa-da, kim nima uchun mas’ul ekanini bemalol tasavvur qilishimiz mumkin. Ehtimol, olim sensorlarni boshqaradi, logistik resurslarni etkazib berish uchun javobgardir, muhandis yoqilg'i ta'minoti uchun javobgardir va uchuvchi kuchaytirgichlarni boshqaradi.

Ushbu dastur SRPga mos kelmaydi deb ayta olamizmi? Ha albatta. Ammo SpaceStation klassi hamma narsani biladigan va hamma narsani qiladigan odatiy "xudo ob'ekti" dir. Bu ob'ektga yo'naltirilgan dasturlashda asosiy anti-naqshdir. Yangi boshlanuvchilar uchun bunday narsalarni saqlash juda qiyin. Hozircha dastur juda oddiy, ha, lekin yangi funksiyalarni qo'shsak nima bo'lishini tasavvur qiling. Ehtimol, bizning kosmik stantsiyamizga tibbiy punkt yoki yig'ilish xonasi kerak bo'ladi. Va qancha ko'p funktsiyalar mavjud bo'lsa, SpaceStation shunchalik ko'payadi. Xo'sh, bu ob'ekt boshqalarga ulanganligi sababli, butun majmuaga xizmat ko'rsatish yanada qiyinlashadi. Natijada, masalan, tezlatgichlarning ishlashini buzishimiz mumkin. Agar tadqiqotchi sensorlarni o'zgartirishni talab qilsa, bu stansiyaning aloqa tizimlariga juda yaxshi ta'sir qilishi mumkin.

SRP tamoyilini buzish qisqa muddatli taktik g'alabani berishi mumkin, ammo oxir-oqibat biz "urushni yo'qotamiz" va kelajakda bunday yirtqich hayvonni saqlab qolish juda qiyin bo'ladi. Dasturni kodning alohida bo'limlariga bo'lish yaxshidir, ularning har biri ma'lum bir operatsiyani bajarish uchun javobgardir. Buni tushunib, keling, SpaceStation sinfini o'zgartiraylik.

Keling, mas'uliyatni taqsimlaylik

Yuqorida biz SpaceStation klassi tomonidan boshqariladigan to'rt turdagi operatsiyalarni aniqladik. Refaktoring paytida biz ularni yodda tutamiz. Yangilangan kod SRP ga yaxshiroq mos keladi.

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

Juda ko'p o'zgarishlar bor, dastur, albatta, hozir yaxshiroq ko'rinadi. Endi bizning SpaceStation sinfimiz ko'proq konteynerga aylandi, unda bog'liq qismlar, jumladan, sensorlar to'plami, sarflanadigan ta'minot tizimi, yonilg'i baki va kuchaytirgichlar uchun operatsiyalar boshlanadi.

Har qanday o'zgaruvchi uchun endi tegishli sinf mavjud: Sensorlar; SupplyHold; FuelTank; Qo'zg'atuvchilar.

Kodning ushbu versiyasida bir nechta muhim o'zgarishlar mavjud. Gap shundaki, individual funktsiyalar nafaqat o'z sinflarida qamrab olinadi, balki ular bashorat qilinadigan va izchil bo'ladigan tarzda tashkil etilgan. Uyg'unlik tamoyiliga amal qilish uchun biz o'xshash funksionallikka ega elementlarni guruhlaymiz. Endi, agar biz tizimning ishlash usulini o'zgartirishimiz kerak bo'lsa, xesh tuzilmasidan massivga o'tishimiz kerak bo'lsa, shunchaki SupplyHold sinfidan foydalaning; biz boshqa modullarga tegmasligimiz kerak. Shunday qilib, agar logistika xodimi o'z bo'limida biror narsani o'zgartirsa, stantsiyaning qolgan qismi saqlanib qoladi. Bunday holda, SpaceStation sinfi o'zgarishlardan hatto xabardor bo'lmaydi.

Koinot stantsiyasida ishlayotgan ofitserlarimiz o'zgarishlardan xursand bo'lishlari mumkin, chunki ular o'zlariga kerakli narsalarni so'rashlari mumkin. E'tibor bering, kodda SupplyHold va FuelTank sinflarida mavjud report_supplies va report_fuel kabi usullar mavjud. Agar Yer hisobot berish usulini o'zgartirishni so'rasa nima bo'ladi? Ikkala sinf, SupplyHold va FuelTank, o'zgartirilishi kerak bo'ladi. Yoqilg'i va sarf materiallarini etkazib berish usulini o'zgartirish kerak bo'lsa-chi? Ehtimol, siz yana bir xil sinflarni o'zgartirishingiz kerak bo'ladi. Va bu allaqachon SRP printsipining buzilishi. Keling, buni tuzataylik.

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.

Dasturning ushbu so'nggi versiyasida mas'uliyat ikki yangi sinfga, FuelReporter va SupplyReporterga bo'lingan. Ularning ikkalasi ham β€œReporter” sinfining farzandlari. Bundan tashqari, biz SpaceStation sinfiga misol o'zgaruvchilarini qo'shdik, shunda kerak bo'lganda kerakli pastki sinfni ishga tushirish mumkin. Endi, agar Yer boshqa narsani o'zgartirishga qaror qilsa, biz asosiy sinfga emas, balki kichik sinflarga o'zgartirish kiritamiz.

Albatta, ba'zi sinflarimiz hali ham bir-biriga bog'liq. Shunday qilib, SupplyReporter obyekti SupplyHold-ga, FuelReporter esa FuelTank-ga bog'liq. Albatta, kuchaytirgichlar yonilg'i bakiga ulangan bo'lishi kerak. Ammo bu erda hamma narsa mantiqiy ko'rinadi va o'zgartirishlar kiritish unchalik qiyin bo'lmaydi - bir ob'ektning kodini tahrirlash boshqasiga katta ta'sir qilmaydi.

Shunday qilib, biz modulli kodni yaratdik, unda har bir ob'ekt/sinfning mas'uliyati aniq belgilangan. Bunday kod bilan ishlash muammo emas, uni saqlab qolish oddiy vazifa bo'ladi. Biz butun "ilohiy ob'ektni" SRP ga aylantirdik.

Skillbox tavsiya qiladi:

Manba: www.habr.com

a Izoh qo'shish