Біз SOLID көмегімен икемді код жазамыз

Біз SOLID көмегімен икемді код жазамыз

Аудармашыдан: сіз үшін жарияланған Северин Перестің мақаласы бағдарламалауда SOLID принциптерін қолдану туралы. Мақаладағы ақпарат жаңадан бастаушыларға да, тәжірибелі бағдарламашыларға да пайдалы болады.

Егер сіз әзірлеумен айналыссаңыз, сіз SOLID принциптері туралы естіген боларсыз. Олар бағдарламашыға таза, жақсы құрылымдалған және оңай қызмет көрсететін кодты жазуға мүмкіндік береді. Айта кету керек, бағдарламалауда белгілі бір жұмысты дұрыс орындаудың бірнеше тәсілдері бар. Әртүрлі мамандардың «дұрыс жол» туралы әртүрлі идеялары мен түсінігі бар, бәрі әр адамның тәжірибесіне байланысты. Дегенмен, SOLID-те жарияланған идеяларды IT қауымдастығының барлық дерлік өкілдері қабылдайды. Олар дамуды басқарудың көптеген жақсы тәжірибелерінің пайда болуы мен дамуының бастапқы нүктесі болды.

SOLID принциптерінің не екенін және олардың бізге қалай көмектесетінін түсінейік.

Skillbox ұсынады: Практикалық курс «Мобильді әзірлеуші ​​PRO».

Біз еске саламыз: «Хабрдың» барлық оқырмандары үшін - «Habr» жарнамалық кодын пайдаланып кез келген Skillbox курсына жазылу кезінде 10 000 рубль көлемінде жеңілдік.

SOLID дегеніміз не?

Бұл термин аббревиатура, терминнің әрбір әрпі белгілі бір принцип атауының басы болып табылады:

  • SЖауапкершілік принципі. Модульде өзгертудің бір ғана себебі болуы мүмкін.
  • The Oқалам/Жабық принцип (ашық/жабық принцип). Сыныптар және басқа элементтер кеңейту үшін ашық, бірақ өзгерту үшін жабық болуы керек.
  •  The LИсков ауыстыру принципі (Лисков ауыстыру принципі). Негізгі типті пайдаланатын функциялар негізгі түрдің ішкі түрлерін білмей пайдалана алуы керек.
  • The IИнтерфейсті бөлу принципі  (интерфейсті бөлу принципі). Бағдарламалық құрал нысандары олар пайдаланбайтын әдістерге тәуелді болмауы керек.
  • The Dтәуелділік Инверсия принципі (тәуелділік инверсия принципі). Жоғары деңгейлердегі модульдер төменгі деңгейдегі модульдерге тәуелді болмауы керек.

Бірыңғай жауапкершілік қағидасы


Бірыңғай жауаптылық қағидасы (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

Шын мәнінде, біздің ғарыш станциямыз жұмыс істемейді (мен NASA-дан жақын арада қоңырау соғады деп ойламаймын), бірақ мұнда талдау керек нәрсе бар.

Осылайша, 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; FuelTank; Итергіштер.

Кодтың осы нұсқасында бірнеше маңызды өзгерістер бар. Мәселе мынада: жеке функциялар тек өз сыныптарында инкапсуляцияланбайды, олар болжамды және бірізді болатындай етіп ұйымдастырылады. Біз үйлесімділік принципін сақтау үшін ұқсас функционалдығы бар элементтерді топтастырамыз. Енді жүйенің жұмыс істеу тәсілін өзгерту керек болса, хэш құрылымынан массивке ауысу керек болса, жай ғана 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

пікір қалдыру