SOLID ашиглан уян хатан код бичих

SOLID ашиглан уян хатан код бичих

Орчуулагчаас: танд зориулж нийтэлсэн Северин Перезийн нийтлэл програмчлалд SOLID зарчмуудыг ашиглах тухай. Өгүүллийн мэдээлэл нь эхлэгч болон туршлагатай програмистуудад хэрэгтэй болно.

Хэрэв та хөгжлийн чиглэлээр явж байгаа бол SOLID зарчмуудын талаар сонссон байх. Эдгээр нь програмистыг цэвэр, сайн бүтэцтэй, засвар үйлчилгээ хийхэд хялбар код бичих боломжийг олгодог. Програмчлалд тодорхой ажлыг хэрхэн зөв гүйцэтгэх хэд хэдэн арга байдаг гэдгийг тэмдэглэх нь зүйтэй. Янз бүрийн мэргэжилтнүүд "зөв зам"-ын талаар өөр өөр санаа, ойлголттой байдаг бөгөөд энэ нь хүн бүрийн туршлагаас хамаарна. Гэсэн хэдий ч SOLID-д тунхагласан санааг мэдээллийн технологийн нийгэмлэгийн бараг бүх төлөөлөгчид хүлээн зөвшөөрдөг. Эдгээр нь хөгжлийн менежментийн олон сайн туршлагыг бий болгож, хөгжүүлэх эхлэлийн цэг болсон.

SOLID зарчим гэж юу болох, тэдгээр нь бидэнд хэрхэн тусалдаг болохыг ойлгоцгооё.

Skillbox зөвлөж байна: Практик курс "Мобайл хөгжүүлэгч PRO".

Бид танд сануулж байна: "Хабр" -ын бүх уншигчдад - "Habr" сурталчилгааны кодыг ашиглан Skillbox-ын аль ч курст бүртгүүлэхдээ 10 рублийн хөнгөлөлт.

SOLID гэж юу вэ?

Энэ нэр томъёо нь товчилсон үг бөгөөд нэр томъёоны үсэг бүр нь тодорхой зарчмын нэрний эхлэл юм.

  • SХариуцлагын нэг зарчим. Модульд өөрчлөлт хийх ганц шалтгаан байж болно.
  • The Oүзэг/Хаалттай зарчим (нээлттэй/хаалттай зарчим). Ангиуд болон бусад элементүүдийг өргөтгөхөд нээлттэй байх ёстой, гэхдээ өөрчлөхөд хаалттай байх ёстой.
  •  The Liskov Орлуулах зарчим (Лисковын орлуулах зарчим). Үндсэн төрлийг ашигладаг функцууд нь үндсэн төрлийн дэд төрлүүдийг өөрийн мэдэлгүйгээр ашиглах боломжтой байх ёстой.
  • The IИнтерфэйсийг тусгаарлах зарчим  (интерфейсийг салгах зарчим). Програм хангамжийн байгууллагууд нь ашигладаггүй аргуудаас хамаарах ёсгүй.
  • The 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 анги нь мэдрэгч, хэрэглээний хангамжийн систем, түлшний сав, өдөөгч зэрэг хамааралтай хэсгүүдийн үйл ажиллагааг эхлүүлдэг сав болж хувирсан.

Аль ч хувьсагчийн хувьд одоо харгалзах анги байна: Мэдрэгч; SupplyHold; Түлшний сав; Хөтлөгчүүд.

Кодын энэ хувилбарт хэд хэдэн чухал өөрчлөлт орсон байна. Гол нь бие даасан функцууд нь зөвхөн өөрийн ангиудад багтаад зогсохгүй урьдчилан таамаглаж болохуйц, тууштай байхаар зохион байгуулагдсан байдаг. Бид уялдаа холбоотой байх зарчмыг баримтлахын тулд ижил төстэй ажиллагаатай элементүүдийг бүлэглэдэг. Одоо, хэрэв бид хэш бүтцээс массив руу шилжих системийн ажлын арга барилыг өөрчлөх шаардлагатай бол SupplyHold классыг ашигла, бид бусад модулиудад хүрэх шаардлагагүй. Ингэснээр логистикийн ажилтан өөрийн хэсэгт ямар нэг зүйлийг өөрчлөх юм бол станцын үлдсэн хэсэг нь хэвээр үлдэнэ. Энэ тохиолдолд SpaceStation анги нь өөрчлөлтийн талаар мэдэхгүй байх болно.

Сансрын станцад ажиллаж байгаа манай офицерууд өөрт хэрэгтэй зүйлээ хүсэлт гаргах боломжтой учраас өөрчлөлтөд баяртай байгаа байх. Код нь SupplyHold болон FuelTank ангилалд багтсан report_supplies, report_fuel зэрэг аргуудтай болохыг анхаарна уу. Дэлхий мэдээлэх арга барилаа өөрчлөхийг хүсвэл юу болох вэ? 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 гэсэн хоёр шинэ ангилалд хуваасан. Тэд хоёулаа Сурвалжлагч ангийн хүүхдүүд. Нэмж дурдахад бид SpaceStation ангид жишээ хувьсагчдыг нэмсэн бөгөөд ингэснээр шаардлагатай бол хүссэн дэд ангиллыг эхлүүлэх боломжтой. Одоо, хэрэв Дэлхий өөр зүйлийг өөрчлөхөөр шийдсэн бол бид үндсэн ангид биш, харин дэд ангиудад өөрчлөлт оруулах болно.

Мэдээжийн хэрэг, манай зарим ангиуд бие биенээсээ хамааралтай хэвээр байна. Тиймээс SupplyReporter объект нь SupplyHold-аас, FuelReporter нь FuelTank-аас хамаардаг. Мэдээжийн хэрэг, өдөөгч нь түлшний саванд холбогдсон байх ёстой. Гэхдээ энд бүх зүйл аль хэдийн логик харагдаж байгаа бөгөөд өөрчлөлт оруулах нь тийм ч хэцүү биш байх болно - нэг объектын кодыг засварлах нь нөгөөд нь тийм ч их нөлөөлөхгүй.

Тиймээс бид объект/анги тус бүрийн үүрэг хариуцлагыг нарийн тодорхойлсон модульчлагдсан кодыг бий болгосон. Ийм кодтой ажиллах нь асуудал биш бөгөөд үүнийг хадгалах нь энгийн ажил байх болно. Бид "бурханлаг объект"-ийг бүхэлд нь SRP болгон хувиргасан.

Skillbox зөвлөж байна:

Эх сурвалж: www.habr.com

сэтгэгдэл нэмэх