SOLID-ի միջոցով ճկուն կոդ գրելը

SOLID-ի միջոցով ճկուն կոդ գրելը

Թարգմանիչից. հրապարակված ձեզ համար Սեվերին Պերեսի հոդվածը SOLID սկզբունքները ծրագրավորման մեջ օգտագործելու մասին։ Հոդվածից ստացված տեղեկատվությունը օգտակար կլինի ինչպես սկսնակների, այնպես էլ փորձառու ծրագրավորողների համար:

Եթե ​​դուք զարգացման մեջ եք, ամենայն հավանականությամբ, լսել եք SOLID սկզբունքների մասին: Դրանք ծրագրավորողին հնարավորություն են տալիս գրել մաքուր, լավ կառուցվածքային և հեշտությամբ սպասարկվող կոդ: Հարկ է նշել, որ ծրագրավորման մեջ կան մի քանի մոտեցումներ, թե ինչպես ճիշտ կատարել որոշակի աշխատանք: Տարբեր մասնագետներ ունեն տարբեր պատկերացումներ և տարբեր պատկերացումներ «ճիշտ ուղու» մասին, ամեն ինչ կախված է յուրաքանչյուր մարդու փորձից: Այնուամենայնիվ, SOLID-ում հռչակված գաղափարներն ընդունվում են ՏՏ համայնքի գրեթե բոլոր ներկայացուցիչների կողմից։ Դրանք ելակետ դարձան զարգացման շատ լավ կառավարման պրակտիկաների առաջացման և զարգացման համար:

Եկեք հասկանանք, թե որոնք են SOLID սկզբունքները և ինչպես են դրանք օգնում մեզ:

Skillbox-ը խորհուրդ է տալիս. Գործնական դասընթաց «Mobile Developer PRO».

Հիշեցում. «Habr»-ի բոլոր ընթերցողների համար՝ 10 ռուբլի զեղչ «Habr» գովազդային կոդով Skillbox-ի ցանկացած դասընթացին գրանցվելիս:

Ի՞նչ է SOLID-ը:

Այս տերմինը հապավում է, տերմինի յուրաքանչյուր տառը որոշակի սկզբունքի անվան սկիզբն է.

  • Single Պատասխանատվության սկզբունք. Մոդուլը կարող է փոփոխության մեկ և միայն մեկ պատճառ ունենալ:
  • 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 դասը:

Եկեք բաշխենք պատասխանատվությունը

Վերևում մենք սահմանեցինք չորս տեսակի գործողություններ, որոնք վերահսկվում են 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 դասը նույնիսկ տեղյակ չի լինի փոփոխությունների մասին։

Տիեզերական կայանի վրա աշխատող մեր սպաները, հավանաբար, գոհ են փոփոխություններից, քանի որ նրանք կարող են պահանջել իրենց անհրաժեշտը: Ուշադրություն դարձրեք, որ կոդը ունի մեթոդներ, ինչպիսիք են report_supplies և report_fuel, որոնք պարունակվում են SupplyHold և FuelTank դասերում: Ի՞նչ կլիներ, եթե Երկիրը խնդրեր փոխել իր հաղորդումների ձևը: Երկու դասերը՝ SupplyHold և FuelTank, պետք է փոխվեն: Ի՞նչ անել, եթե Ձեզ անհրաժեշտ է փոխել վառելիքի և սպառվող նյութերի առաքման եղանակը: Հավանաբար ստիպված կլինեք նորից փոխել նույն դասերը։ Իսկ սա արդեն ՊԵԿ սկզբունքի խախտում է։ Եկեք սա շտկենք:

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-ը խորհուրդ է տալիս.

Source: www.habr.com

Добавить комментарий