Թարգմանիչից. հրապարակված ձեզ համար Սեվերին Պերեսի հոդվածը SOLID սկզբունքները ծրագրավորման մեջ օգտագործելու մասին։ Հոդվածից ստացված տեղեկատվությունը օգտակար կլինի ինչպես սկսնակների, այնպես էլ փորձառու ծրագրավորողների համար:
Եթե դուք զարգացման մեջ եք, ամենայն հավանականությամբ, լսել եք SOLID սկզբունքների մասին: Դրանք ծրագրավորողին հնարավորություն են տալիս գրել մաքուր, լավ կառուցվածքային և հեշտությամբ սպասարկվող կոդ: Հարկ է նշել, որ ծրագրավորման մեջ կան մի քանի մոտեցումներ, թե ինչպես ճիշտ կատարել որոշակի աշխատանք: Տարբեր մասնագետներ ունեն տարբեր պատկերացումներ և տարբեր պատկերացումներ «ճիշտ ուղու» մասին, ամեն ինչ կախված է յուրաքանչյուր մարդու փորձից: Այնուամենայնիվ, SOLID-ում հռչակված գաղափարներն ընդունվում են ՏՏ համայնքի գրեթե բոլոր ներկայացուցիչների կողմից։ Դրանք ելակետ դարձան զարգացման շատ լավ կառավարման պրակտիկաների առաջացման և զարգացման համար:
Եկեք հասկանանք, թե որոնք են SOLID սկզբունքները և ինչպես են դրանք օգնում մեզ:
Հիշեցում.«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-ի: