Ծրագրային ապահովման նախագծման հիմնական սկզբունքները՝ DRY, KISS, YAGNI, SOLID

Aro Militosyan
20 min readNov 28, 2022

--

Ուղջույն բոլորին, կփորձեմ բացատրել ծրագրային ապահովման սկզբունքերի մասին, կհասկանանք ամեն մեկը իրենից ինչ է ներկայացնում և ինչի համար է նախատեսված։ Ծրագրային ապահովման ծրագրավորողները(Software programmers) օգտագործում են իրենց սեփական հապավումները, որոնցից մի քանիսը հիմնական սկզբունքներն են, որոնք ապահովում են ծրագրային ապահովման ծրագրի խորությունը և իմաստը: Մենք կդիտարկենք որոշ կարևոր հապավումներ, որոնց մասին արժե իմանալ և սովորել: Այս հոդվածը կօգնի ձեզ սովորել դրանց հիմնական հասկացությունները, կիրառությունները և հապավումների առավելությունները:

DRY principle (Don’t Repeat Yourself)

Եկեք սկսենք առաջին հերթին DRY (Don’t repeat yourself) սկզբունքից։ DRY-ը թարգմանաբար նշանակում է մի կրկնիր քեզ։ Don’t Repeat Yourself (DRY) շատ տարածված հապավում է, որն օգտագործվում է շատ ծրագրավորողների կողմից։ Copy-pasting կոդը մի քանի վայրերում նույն նախագծում ավելորդ բարդություն է առաջացնում:Debugging դառնում է ավելի դժվար, ինչպես նաև փորձարկումը (testing):

Եկեք ստեղծենք երկու ֆունկցիաներ՝ simple_arith և complex_arith հետևյալ կերպ:

def simple_arith(num1, num2, operator):
if operator == '+':
return {"result": num1 + num2}
elif operator == '-':
return {"result": num1 - num2}
elif operator == '*':
return{"result": num1 * num2}
elif operator == '/':
return {"result": num1 / num2}


def complex_arith(num1, num2, num3, operator):
if operator == '+':
return {"result": num1 + num2 + num3}
elif operator == '-':
return {"result": num1 - num2 - num3}
elif operator == '*':
return {"result": num1 * num2 * num3}
elif operator == '/':
return {"result": num1 / num2 / num3}

Ֆունկցիանները բավականին պարզ են: Դուք կարող եք շարունակել և փորձարկել դրանք՝ ավելացնելով հետևյալ տողը ձեր .py ֆայլի վերջում:

print(simple_arith(1, 2, '+'))

Ամեն ինչ լավ է, բայց եթե փորձում եք այնպիսի օպերատոր, որը չի աջակցվում, ծրագիրը խափանվում է սխալմամբ.

print(simple_arith('1', 2, '+'))
Traceback (most recent call last):
File "/home/aro/design_principles/DRY.py", line 22, in <module>
print(simple_arith('1', 2, '+'))
File "/home/aro/design_principles/DRY.py", line 3, in simple_arith
return {"result": num1 + num2}
TypeError: can only concatenate str (not "int") to str

Այս դեպքում շտկումը բավականին հեշտ է, բայց բանն այն է, որ այն պետք է պատճենել և տեղադրել simpe_arith-ում և complex_arith-ում: Այնուամենայնիվ, եթե մենք դա անենք, մենք կկրկնենք մեր կոդը: Սա վատ պրակտիկա է:

Օրինակ այսպես։

def simple_arith(num1, num2, operator):

try:
int(num1)
except ValueError:
return {'message': 'Invalid input'}

try:
int(num2)
except ValueError:
return {'message': 'Invalid input'}

if operator == '+':
return {"result": num1 + num2}
elif operator == '-':
return {"result": num1 - num2}
elif operator == '*':
return{"result": num1 * num2}
elif operator == '/':
return {"result": num1 / num2}

def complex_arith(num1, num2, num3, operator):

try:
int(num1)
except ValueError:
return {'message': 'Invalid input'}

try:
int(num2)
except ValueError:
return {'message': 'Invalid input'}

try:
int(num3)
except ValueError:
return {'message': 'Invalid input'}

if operator == '+':
return {"result": num1 + num2 + num3}
elif operator == '-':
return {"result": num1 - num2 - num3}
elif operator == '*':
return {"result": num1 * num2 * num3}
elif operator == '/':
return {"result": num1 / num2 / num3}

Ուշադրություն դարձրեք, թե որքան կոդի կրկնություն կա: Try-except բլոկը կրկնվում է 5 անգամ։

Նախքան լուծմանը անցնելը, մենք պետք է դիտարկենք Python-ում մեկ կարևոր կոնցեպտ. մասնավորապես, ավելի բարձր կարգի ֆուկցիաներ(higher-order functions) :

Ֆունկցիաների փոխանցում ֆունկցիաներին (Passing functions to functions)

Նախքան դեկորատորի ֆուկցիաներին անցնելը, մենք պետք է նախ պարզեցնենք մեկ մոտեցում: Python-ում հնարավոր է ֆունկցիան որպես արգումենտ փոխանցել մեկ այլ ֆունկցիայի։ Դա պայմանավորված է նրանով, որ Python-ի ֆունկցիաները իրականում ավելի բարձր կարգի ֆունկցիաներ են(higher-order functions):

Դիտարկենք հետևյալ կոդը:

def foo(arg):
arg()

def foo1():
print('foo1')

foo(foo1)

Ի՞նչ եք կարծում, ինչպիսի՞ն կլինի դրա արդյունքը: Arg փոփոխականն իրենից ներկայացնում է հղում դեպի foo1 ֆունկցիա, հետևբար, այն կատարում է ֆունկցիայի գործառույթ: Այսպիսով, կտպվի foo1:

Դեկորատոր ֆունկցիաներ (Decorator functions)

Decorator ֆունկցիան ոչ այլ ինչ է, քան ֆունկցիա, որն ընդունում է ֆունկցիան որպես արգումենտ: Հիմնական կառուցվածքը նման է հետևյալին։

def decorator(fun):
def wrapper(*args, **kwargs):

return fօօ(*args, **kwargs)
return wrapper

Ինչպես տեսնում եք, ֆունկցիան (foo) փոխանցվում է որպես արգումենտ դեկորատոր ֆունկցիային։ Այնուհետև կանչվում է wrapper ֆունկցիան, որն ունի *args և **kwargs որպես արգումենտ: *Args փոփոխականը tuple է, որը պարունակում է արգումենտներ, որոնք փոխանցվել են սկզբնական foo ֆունկցիային, մինչդեռ **kwargsdictionary է, որը պարունակում է foo ֆունկցիային փոխանցված keyword արգումենտներ։

Քիչ-քիչ մոտենում ենք մեր DRY-ի սկզբունքին։

Եթե ​​ֆունկցիայի սահմանումից առաջ օգտագործենք @ նշանը, ապա դեկորատոր ֆունկցիան կփաթաթվի սահմանվող ֆունկցիայի շուրջ: Մենք կարող ենք հեռացնել լոգիկան ստուգող սխալը (error) simple_arith-ում և ավելացնել @decorator այսպես:

@decorator
def simple_arith(num1, num2, operator):

if operator == '+':
return {"result": num1 + num2}
elif operator == '-':
return {"result": num1 - num2}
elif operator == '*':
return{"result": num1 * num2}
elif operator == '/':
return {"result": num1 / num2}

Ամեն անգամ, երբ simple_arith-ը կանչվում է, սկզբում կկանչվի դեկորատորը՝ simple_arith-ը որպես արգումենտ(argument): Num1, num2, operator արգումենտները կհայտնվեն Tuple-ի տեսքով *args փոփոխականում:

Այժմ մենք կարող ենք լոգիկան ստուգող Try-except-ը դնել դեկորատորի ֆունկցիայի մեջ այսպես։

def decorator(fօօ):
def wrapper(*args, **kwargs):

nums = args[:-1]
for arg in nums:
try:
int(arg)
except ValueError:
return {'message': 'Invalid input'}

tu=tuple(map(int, nums))
tu = tu + (args[-1],)
return fօօ(*tu, **kwargs)

return wrapper

Մենք հեռացնում ենք վերջին արգումենտը (որը operator-ն է) և արդյունքը պահում ենք nums-ի մեջ։ Այնուհետև մենք ցիկլով անցնում ենք վրայով և փորձում ենք կատարել cast անել ամբողջ թվի: Եթե ​​դա ձախողվի, մենք գիտենք, որ մուտքագրումը սխալ է, և մենք կարող ենք հայտնաբերել սխալը և տեղեկացնել օգտվողին(user-ին), որ մուտքագրումը սխալ է:

Եթե ճիշտ ​​մուտքագրում է արված, մենք թվերը cast ենք անում string-ից դեպի int, պարզապես համոզվելու համար, որ ծրագիրը չի crash-վում , եթե “1”-ի նման մուտքագրում փոխանցվի, մենք օգտագործում ենք map ֆունցկիան։

tu=tuple(map(int, nums))

Այնուամենայնիվ, մենք պետք է նաև կցենք այն operator-ին, որը պարունակվում է args[-1]-ում, այսպես:

tu = tu + (args[-1],)

Երբ դա արվում է, մենք վերադարձնում ենք foo-ն՝ փոփոխված արգումենտներով:

return fօօ(*tu, **kwargs)

Այժմ այստեղ է գալիս հրաշքը: Ենթադրենք, մենք ցանկանում ենք, որ նույն տրամաբանությունը կիրառվի complex_arith ֆունկցիայի վրա։ Ի՞նչ ենք մենք անում։ Մենք պարզապես փաթաթում ենք այն @decorator-ով այսպես:

@decorator
def complex_arith(num1, num2, num3, operator):

if operator == '+':
return {"result": num1 + num2 + num3}
elif operator == '-':
return {"result": num1 - num2 - num3}
elif operator == '*':
return {"result": num1 * num2 * num3}
elif operator == '/':
return {"result": num1 / num2 / num3}

Այժմ մենք կարող ենք կանչել և ՛ simple_arith-ը, և ՛ complex_arith-ը՝ ոչ ցանկալի մուտքային տվյալներով, և երկու դեպքում էլ առաջինը կկանչվի դեկորատորը՝ մտածելով մուտքային տվյալների մասին:

Եկեք կանչենք, հետևյալ կերպ։

print(simple_arith("11",5 ,'+'))
print(complex_arith("4",5,"2" ,"*"))

Արդյունքը կլինի հետևյալը։

/usr/bin/python3.10 /home/aro/design_principles/DRY.py 
{'result': 16}
{'result': 40}

Ամփոփում

Decorator ֆունկցիաները օգտակար են DRY կոդ ստեղծելու համար, որը խուսափում է կոդի կրկնությունից: Դրանք օգտագործվում են այն դեպքերում, երբ միևնույն տրամաբանությունը պետք է կիրառվի բազմաթիվ ֆուկցիաների համար: Քանի որ օգտագործվում են *args և **kwargs տեսակները, մենք չենք սահմանափակվում միայն մեկ, երկու կամ երեք արգումենտ ունեցող ֆունկցիաներով:

KISS Principle (Keep it simple, Stupid!)

KISSKeep It Simple, Stupid-ի հապավումն է: Այս սկզբունքի էությունը ձեր կոդն ավելի պարզ դարձնելն է: Պետք է խուսափել ավելորդ բարդություններից։Պարզ կոդն ավելի հեշտ է կարդալ և հասկանալ:

Դուք պետք է հեռացնեք կրկնօրինակված կոդը, պետք է հեռացնեք ավելորդ ֆուկցիաները, չօգտագործեք ավելորդ փոփոխականներ և մեթոդներ, օգտագործեք փոփոխականների անուններ և մեթոդներ, որոնք իմաստալից են և համապատասխանում են նրանց գործողություններին։

Երբ ​​ աշխատում եք կոդի վրա, որը պարունակում է կոդի կտոր, որն անհրաժեշտ չէ կամ կարող է ավելի պարզ լինել, պետք է մտածեք այն փոփոխելու (վերամշակելու ) մասին:

Ամփոփում

Ինչ առավելություններ կտա KISS-ի սկզբունքին հետևելը.

-կկարողանաք ավելի արագ խնդիրներ լուծել,

-կկարողանաք ավելի բարձր որակի կոդ ստեղծել,

-կոդերի բազան ավելի ճկուն կլինի, ավելի հեշտ կլինի ընդլայնել, փոփոխել կամ վերափոխել, երբ նոր պահանջներ ի հայտ գան,

Դարձրեք ամեն ինչ հնարավորինս պարզ, բայց ՝ ոչ ավելի հեշտ։

YAGNI Principle( You Aren’t Gonna Need It)

YAGNI-ին հայտնի է որպես «Դու դրա կարիքը չունես» սկզբունք, որը վերցված է eXtreme Programming-ից ՝ պնդում է, որ չպետք է նախօրոք ֆունկցիոնալություն ստեղծեք, ավելի ճիշտ՝ քանի դեռ դրա կարիքը չկա:

Այս սկզբունքը նման է KISS սկզբունքին, երբ երկուսն էլ նպատակ ունեն ավելի պարզ լուծման: Նրանց միջև տարբերությունն այն է, որ YAGNI-ն կենտրոնանում է ավելորդ ֆունկցիոնալությունն ու տրամաբանությունը հեռացնելու վրա, իսկ KISS-ը կենտրոնանում է բարդության վրա:

Ամփոփում

Միշտ իրականացրեք այնպիսի բաներ, երբ դրանք իրականում ձեզ անհրաժեշտ են, մի իրականացրեք այնպիսի բաներ, երբ պարզապես կանխատեսում եք կամ մտածում եք, որ դրանք ձեզ պետք կգան:

S.O.L.I.D Principle

Վերջապես հասանք SOLID-ի սկզբունքներին։ SOLID սկզբունքները հինգ տարբեր ծրագրային նախագծման սկզբունքների միաձուլում է, ինչպես նշված է ստորև։

1) Single Responsibility Principle (SRP)
2) Open/Closed Principle (OCP)
3) Liskov Substitution Principle (LSP)
4) Interface Segregation Principle (ISP)
5) Dependency Inversion Principle (DIP)

Այս գաղափարը առաջ է քաշել Ռոբերտ Մարտինը, որը հայտնի է նաև որպես «Քեռի Բոբ»:

SOLID սկզբունքի կիրառման նպատակները հետևյալն են.

-կոդը դարձնենք ավելի հասկանալի,

-հեշտացնում է կոդի բազմակի օգտագործումը,

-հեշտացնում է թեստավորումը,

-ճկուն է սցենարի փոփոխությունները հարմարցնելու համար,

Single-responsibility principle

S-ը բացվում և թարգմանվում է որպես մեկ պատասխանատվության սկզբունք (single-responsibility principle

Class-ը փոխելու համար երբեք չպետք է լինի մեկից ավելի պատճառ: Այսինքն՝ յուրաքանչյուր class պետք է ունենա միայն մեկ պատասխանատվություն:Հիմնականում class-ը պետք է ունենա միայն մեկ նպատակ, եթե ​​class-ի ֆունկցիոնալությունը պետք է փոխվի, դա անելու մեկ պատճառ պետք է լինի:

Code

Պատկերացնենք, որ ավտոսրահը վաճառում է 3 տեսակի ավտոմեքենա։ Օգտագործողը կարող է:

Պահանջել մեքենա

Փորձարկել մեքենան

Գնել մեքենա

Կոդը հետևյալն է։

class Car:
prices={'BMW': 100000, 'Audi': 200000, 'Mercedes': 300000}
def __init__(self, name):

if name not in self.prices:
print("Sorry, we don't have this car")
return

self.name = name
self.price = self.prices[name]

def testDrive(self):
print("Driving {}".format(self.name))

def buy(self, cash):
if cash < self.price:
print("Sorry, you don't have enough money")
else:
print("Buying {}".format(self.name))

if __name__ == '__main__':
car = Car('BMW')
car.buy(100000)

Եթե ​​պահանջվող մեքենան հասանելի չէ, օգտվողը սխալ է ստանում: Վերջապես, օգտատերը կարող է փորձարկել մեքենան վարել և նաև գնել այն: Մեքենան հնարավոր է գնել միայն ամբողջ գումարն առաջարկելով։

Եթե ​​նկատում եք, Car class-ը ունի բազմաթիվ պարտականություններ, այն պետք է կարգավորի մեքենայի մասին տեղեկատվությունը, ինչպես նաև մեքենա գնելու ֆինանսական ծախսերը: Այսպիսով, այն խախտում է միասնական պատասխանատվության սկզբունքը (single-responsibility principle

Հիմա եկեք պատկերացնենք, որ ավտոսրահը ցանկանում է երկու փոփոխություն կատարել:

Սկսում է հաճախորդներին առաջարկել վճարել ապառիկ՝ նախապես ամբողջ գումարի փոխարեն:

Եվ փոխել մեքենայի գինը

Այժմ մեքենաների class-ը փոխելու երկու պատճառ կա: Պետք է լինի միայն մեկ պատճառ, որպեսզի class-ը փոխվի:

Մենք կարող ենք հեշտությամբ շտկել դա՝ ստեղծելով առանձին class, որը կոչվում է Financials և իրականացնելով վերը թվարկված փոփոխությունները։

class Car:
prices={'BMW':200000, 'Audi': 200000, 'Mercedes': 300000}
def __init__(self, name):
if name not in self.prices:
print("Sorry, we don't have this car")
return

self.name = name
self.price = self.prices[name]

def testDrive(self):

print("Driving {}".format(self.name))

class Finances:
def buy(car, cash):

if cash == car.price:
print("Buying {}".format(car.name))
elif cash > car.price/3:
print("Buying {} on installments".format(car.name))
else:
print("Sorry, you don't have enough money")

if __name__ == '__main__':
car = Car('BMW')
Finances.buy(car, 100000)

Նկատենք, որ class-երից յուրաքանչյուրը՝ Car- ը և Financials- ը, փոխվելու միայն մեկ պատճառ ունեն:

Ամփոփում

Փորձենք հասկանալ, արդյո՞ք խնդիր ունենք նույն class-ում մեքենաների գործարքի և ֆինանսական ֆունկցիաների համատեղության մեջ:Այնուամենայնիվ, ավելի հեշտ է աշխատել փոքր մասերի բաժանված կոդերի հետ։Նկատենք, որ կոդն այժմ ավելի թույլ է կապակցված, քան նախկինում, քանի որ այն բաժանել ենք երկու class-ների: Class-ներից յուրաքանչյուրն այժմ ունի հստակ սահմանված խնդիր։ Car class- ը զբաղվում է միայն մեքենայով, իսկ Financials class-ը՝ միայն ֆինանսական հարցերով:

The Open-Closed principle

Ծրագրային ապահովումն(Software) կարող է անընդհատ փոփոխվել՝ երբեք ամբողջական չէ, գրեթե միշտ ինչ-որ բան կա շտկելու կամ բարելավելու:

Այսպիսով, հարց է առաջանում, թե ինչպե՞ս ենք մենք ստեղծում software, որը հարմարեցնում է փոփոխությունները և միաժամանակ կայուն է:

Բաց-փակ(Open-Closed) սկզբունքը այս հարցին պատասխանելու միջոց է։

Բաց-փակ սկզբունքը որոշ առումներով բավականին կապված է single responsibility սկզբունքի հետ: Այս սկզբունքի էությունը հետևյալն է.

Software-ի բաղադրիչները(components) (class-ները և ֆունկցիանները) պետք է բաց լինեն ընդլայնման համար, բայց փակ լինեն փոփոխության համար:

Գաղափարը բավականին պարզ է։ Ձեր կոդի կոնկրետ class-ի տրամաբանությունը չպետք է փոխվի: Ավելի շուտ, եթե լրացուցիչ հատկանիշ պետք է ներդրվի, կոդը փոփոխելու փոխարեն պետք է ընդլայնվի։

CODE

Պատկերացրեք, որ մենք ունենք հետևյալ կոդը։

class Car:
def __init__(self, name, price):
self.name = name
self.price = price

def buy(self, cash):
if cash == self.price:
print("Buying {}".format(self.name))
else:
print("Sorry, you don't have enough money")

def buy_with_discount(self, cash):
if int(0.8*self.price) == int(cash):
print("Buying {} with 20%% discount".format(self.name))

if __name__ == '__main__':
car = Car('BMW', 100000)
car.buy_with_discount(80000)

Մենք օգտագործում ենք նույն թեման, ինչ նախորդ սկզբունքի մեջ: Հնարավոր է գնել մեքենա buy ֆունկցիայով և հնարավոր է նաև գնել 20% զեղչով buy_with_discount ֆունկցիայով։

Buy_with_discount ֆունկցիային տրամադրվող cash գումարը պետք է լինի մեքենայի գնի ուղիղ 80%-ը, որպեսզի այն գնվի:

Հիմա, ենթադրենք, որ ավտոդիլերը ցանկանում է մի քանի զեղչեր առաջարկել դիլերական ընկերության հավատարիմ անդամներին, VIP մարդկանց կամ ծանոթ և բարեկամ մարդկանց համար:😃

Ինչպես տեսնում եք, բիզնեսի կարիքները փոխվել են և պահանջվում է փոխել կոդը:

Հարցմանը բավարարելու համար մենք կարող ենք պարզապես գրել մեկ այլ if buy_with_discount ֆունկցիայի համար, որը 30% զեղչով կհաշվի։

if int(0.7*self.price) == int(cash):
print("Buying {} with 30%% discount".format(self.name))

Բայց սա սխալ մոտեցում է բաց-փակ սկզբունքի համար։ Որովհետև Car class-ի տրամաբանությունը չպետք է փոխվի։ Փոխարենը պետք է ընդլայնել։ OOP-ում (Օբյեկտ կողմնորոշված ծրագրավորում) ֆունկցիոնալությունը ընդլայնելու ընդհանուր եղանակը պոլիմորֆիզմի (polymorphism) օգտագործումն է, նաև հայտնի է որպես ժառանգականություն (inheritance):

Տեսնենք, թե ինչպես կարող ենք օգտագործել ժառանգությունը (inheritance) Python-ում այս խնդիրը ճիշտ լուծելու համար։

Հետևյալ կոդը դիտարկեքն։

class Car:
def __init__(self, name, price):
self.name = name
self.price = price

def buy(self, cash):
if cash == self.price:
print("Buying {}".format(self.name))
else:
print("Sorry, you don't have enough money")

class Discount():
def __init__(self, discount):
self.discount=discount

def buy_with_discount(self, car):
print("Buying {} with {}% discount".format(car.name, self.discount))

class Discount20(Discount):
def __init__(self):
super().__init__(20)

def buy_with_discount(self, cash, car):
if int(0.8*car.price) == int(cash):
return super().buy_with_discount(car)

class Discount30(Discount):
def __init__(self):
super().__init__(30)

def buy_with_discount(self, cash, car):
if int(0.7*car.price) == int(cash):
return super().buy_with_discount(car)

if __name__ == '__main__':
car = Car('BMW', 100000)
Discount20().buy_with_discount(80000, car)
Discount30().buy_with_discount(70000, car)

Առաջին բանը, որ մենք անում ենք, զեղչային տրամաբանությունն առանձնացնելն է սովորական գնման տրամաբանությունից՝ ստեղծելով Discount class: Այնուհետև մենք սահմանում ենք buy_with_discount ֆունկցիան, որը պարզապես տպում է, որ մեքենան գնվել է։

Հաջորդիվ պետք է իրականացնենք 20% զեղչի տրամաբանությունը։ Բայց հիշեք, որ մեզ չի թույլատրվում փոփոխել Discount class-ը սկզբունքի համաձայն: Միակ բանը, որ մենք կարող ենք անել, դա ընդլայնելն է։ Ինչպե՞ս ենք այն ընդլայնում: Ստեղծելով նոր class և ունենալով նոր class-ի ժառանգություն Discount class-ից:

Մենք սահմանում ենք Discount20 նոր class և այն ժառանգում Discount class-ից՝ որպես արգումենտ class-ի սահմանման մեջ փոխանցում ենք Discount-ը:

Ամենահետաքրքիրը այն է, որ, ենթադրենք, ուզում ենք առաջարկել 30% զեղչ: Ինչպե՞ս ենք մենք դրան հասնում: Մենք պարզապես ստեղծում ենք նոր class, որը կոչվում է Discount30 և կրկնում ենք կոդը՝ փոխելով միայն արժեքները 20-ից 30 և 0,8-ից 0,7:

Կոդն այժմ կարող է աշխատացնել, և կտեսնեք, որ երկու մեքենա ենք գնել: Մեկը 70% զեղչով, մյուսը՝ 80% զեղչով։

Ամփոփում

Հասկացանք որ բաց-փակ սկզբունքը պետք օգտագործենք այնպես որ չփոփոխենք կոդը այլ պետք է միայն ընդլայնենք ։

The Liskov Substitution Principle

Նախքան այս սկզբունքի մեջ խորանալը և այն, թե ինչպես է այն համապատասխանում վերջին երկու սկզբունքներին։

Եկեք նայենք սահմանմանը:

Լիսկովի փոխարինման սկզբունքը հետևյալն է անհրաժեշտ է, որ ժառանգ class-ները կարողանան փոխարինել ծնող class- ներին։ Այս սկզբունքի նպատակը կայանում է նրանում, որ ժառանգ class-ները կարող են օգտագործվել ծնող class-ների փոխարեն, որոնցից նրանք ձևավորվում են՝ առանց խափանելու (crash-վելու ) ծրագիրը։

Ասենք, որ ունես ենթադաս(subclass), որը ժառանգվում է բազային class-ից (Base class): Ենթադրենք, դուք որոշակի փոփոխություններ եք կատարում ենթադասում(subclass-ում), ինչպես օրինակ՝ արգումենտների, արժեքների տեսակների և, հնարավոր է, նաև վերադարձի տեսակի փոփոխություն: Այս դեպքում դուք կխախտեք LSP սկզբունքը, քանի որ սուպերդասի(superclass) նմուշը(instance) ենթադասի(subclass) նմուշով(instance) փոխարինելը կհանգեցնի սկզբունքի փոփոխության: Կոդը, որը կախված է բազային class-ից(Base class), կարող է ակնկալել str-ի տիպի վերադարձվող, օրինակ, բայց երբ դուք փոխարինում եք նմուշը ենթադասով(subclass-ով ), այն վերադարձնում է int: Սա կարող է հանգեցնել կոդի խափանման(crash-վելուն) կամ այլ սխալների:

Ընդհանուր առմամբ, ենթադասում(subclass-ում) ֆունկցիայի պարամետրերը և վերադարձի տեսակը պետք է անփոփոխ լինեն:

Code

Եկեք նայենք մի պարզ օրինակի։

class Car:
def __init__(self, name):
self.name = name
self.gears = ["N", "1", "2", "3", "4", "5", "6", "R"]
self.speed = 0
self.gear = "N"

def changeGear(self, gear):
if (gear in self.gears):
self.gear = gear
print("Car %s is in gear %s" % (self.name, self.gear))

def accelerate(self):
if (self.gear == "N"):
print("Error: Car %s is in gear N" % self.name)
else:
self.speed += 1
print("Car %s is accelerating" % self.name)

class SportsCar(Car):
def __init__(self, name):
super().__init__(name)
self.turbos = [2, 3]

def accelerate(self, turbo):
if (self.gear == "N"):
print("Error: Car %s is in gear N" % self.name)
else:
if (turbo in self.turbos):
self.speed += turbo
print("Car %s is accelerating with turbo %d" % (self.name, turbo))

if __name__ == '__main__':

car = Car('BMW')
car.changeGear("1")
car.accelerate()


autoCar = SportsCar('Audi')
autoCar.changeGear("1")
autoCar.accelerate()

Car class-ը սահմանում է ավտոմեքենա: Մենք սկզբնարժեքավորում ենք այն անունով(name), շարժակների քանակով(gears), արագությամբ(speed) և փոխանցման(gear) ընթացիկ դիրքով: ChangeGear ֆունկցիան թույլ է տալիս փոխել փոխանցումները, մինչդեռ accelerate ֆունկցիան ավելացնում է մեքենայի արագությունը 1-ով: Եթե փոխանցումը գտնվում է չեզոք դիրքում N, մենք չենք փոխում արագությունը, փոխարենը տեղեկացնում ենք օգտագործողին:

Ենթադրենք, որ մենք ցանկանում ենք մոդելավորել նաև SportsCar: Մենք այն ժառանգում ենք base classCar-ից: Սկզբնարժեքավորման ժամանակ մենք նաև սահմանում ենք turbos փոփոխական, որը ցուցակ (list ) է, որը ցույց է տալիս, թե ինչ turbo մակարդակներ է աջակցում մեքենան:

Վարքագծի (behavior) փոփոխության պատճառով մենք պետք է սահմանենք accelerate նոր ֆունկցիա, որն ընդունում է turbo-ն որպես պարամետր: Արագությունը 1-ի փոխարեն ավելանում է turbo քանակով, որպեսզի սպորտային մեքենան կարողանա ավելի արագ ընթանալ:

Խնդիր է, եթե ​​մենք փորձենք փոխարինել Car-ի նմուշը(instance) SportsCar-ի նմուշով, դա հանգեցնելու է կոդի քրաշվելուն(crash), քանի որ SportsCar-ում accelerate-ը ակնկալում է turbo արգումենտ:

LSP-ի սկզբունքի համաձայն վերը նշվածի փոխարինումը չպետք է սխալի հանգեցնի, և կոդը պետք է գործի նորմալ:

Դիտարկենք երկու լուծման ճանապարհ։

Լուծում 1

Պարզ լուծումը կլինի turbos փոփոխականը ֆիքսված արժեք ունեցող turbo փոփոխականով փոխարինելը: Եվ accelerate ֆունկցիանից հեռացնելով turbo պարամետրը այսպես”

class Car:
def __init__(self, name):
self.name = name
self.gears = ["N", "1", "2", "3", "4", "5", "6", "R"]
self.speed = 0
self.gear = "N"

def changeGear(self, gear):
if (gear in self.gears):
self.gear = gear
print("Car %s is in gear %s" % (self.name, self.gear))

def accelerate(self):
if (self.gear == "N"):
print("Error: Car %s is in gear N" % self.name)
else:
self.speed += 1
print("Car %s is accelerating" % self.name)

class SportsCar(Car):
def __init__(self, name):
super().__init__(name)
self.turbo = 2
#Այս կետում փոխարինեցինք turbos-ը ֆիքսված արժեք ունեցող turbo փոփոխականով

def accelerate(self):
if (self.gear == "N"):
print("Error: Car %s is in gear N" % self.name)
else:
self.speed += self.turbo
print("Car %s is accelerating with turbo %d" % (self.name, self.turbo))

if __name__ == '__main__':

car = Car('BMW')
car.changeGear("1")
car.accelerate()

car = SportsCar('BMW')
car.changeGear("1")
car.accelerate()

Ինչպես տեսնում եք, ֆունկցիայի արգումենտներն այժմ ուղղակիորեն նույն են ինչ base class-ի ֆունկցիայի արգումենտները: Սա նշանակում է, որ մենք կարող ենք փոխարինել Car class կոդը __main__ ֆունկցիայի մեջ SportsCar-ով՝ առանց կոդի քրաշվելու (crash):

Այնուամենայնիվ, եթե նկատում եք, որ այս դեպքում մենք ունենք ֆիքսված turbo արժեք, իսկ եթե մենք ուզում ենք, որ օգտվողը սահմանի turbo արժեքը, ինչպես դա հնարավոր էր նախկինում: Պահանջվում է օգտագործել abstract class:

Լուծում 2 Abstract class

Խնդիրն այն է, թե ինչպես ենք մենք մոդելավորել մեքենաները։ Որպեսզի վերը նշված կոդը համապատասխանի LSP-ին և ունենա տուրբո ընտրության ֆունկցիոնալությունը, մենք պետք է ստեղծենք վերացական(abstract ) բազային(base) classCar:

Այնուհետև մենք ստեղծում ենք երկու առանձին ենթադասեր(subclass), որոնք ժառանգում են դրանից՝ SportsCar և RegularCar: Ստորև դիագրամը ցույց է տալիս։

Կոդով կլինի հետևյալը։

from abc import ABC, abstractmethod

class Car(ABC):
@abstractmethod
def __init__(self, name):
self.name = name
self.gears = ["N", "1", "2", "3", "4", "5", "6", "R"]
self.speed = 0
self.gear = "N"

def changeGear(self, gear):
if (gear in self.gears):
self.gear = gear
print("Car %s is in gear %s" % (self.name, self.gear))

def accelerate(self):
if (self.gear == "N"):
print("Error: Car %s is in gear N" % self.name)
else:
self.speed += 1
print("Car %s is accelerating" % self.name)

class RegularCar(Car):
def __init__(self, name):
super().__init__(name)

class SportsCar(Car):
def __init__(self, name):
super().__init__(name)
self.turbos = [2, 3]

def turboAccelerate(self, turbo):
if (self.gear == "N"):
print("Error: Car %s is in gear N" % self.name)
else:
if (turbo in self.turbos):
self.speed += turbo
print("Car %s is accelerating with turbo %d" % (self.name, turbo))

if __name__ == '__main__':

car = RegularCar('BMW')
car.changeGear("1")
car.accelerate()

autoCar = SportsCar('Audi')
autoCar.changeGear("1")
autoCar.turboAccelerate(2)

Մենք Car-ը վերածում ենք աբստրակտ (abstract) class-ի՝ օգտագործելով ABC մոդուլը: __init__ ֆունկցիային վերագրվում է @abstractmethod՝ աբստրակտ ֆունկցիա արտահայտելու համար։

Սա երաշխավորում է, որ Car օբյեկտը չի կարող ստեղծվել: Կարող են ստեղծվել միայն այն class-ներից, որոնք ժառանգում են Car-ից:

Մենք ստեղծում ենք երկու class, որոնք ժառանգում են Car-ից:

RegularCar, որը ցույց է տալիս սովորական մեքենա առանց turbo:

SportsCar, որը ցույց է տալիս սպորտային մեքենա turbo-ներով:

RegularCar-ը լրացուցիչ փոփոխություններ չունի: SportsCar class-ում մենք կարող ենք կատարել մեր ուզած փոփոխությունները: Մենք սահմանում ենք turbo-ների զանգված և turboAccelerate ֆունկցիան, որը պարունակում է լրացուցիչ turbo պարամետր:

Այս դեպքում SportsCar-ում ֆունկցիայի պարամետրը փոխելը չի ​​խախտում LSP-ն: Ինչո՞ւ, քանի որ Car-ի բազային(base) class-ը վերացական (abstract) է, ուստի այն չի կարող ստեղծված լինել: Եթե ​​այն հնարավոր չէ ստեղծել, այն չի կարող փոխարինված լինել subclass-ներում:

Ամփոփում

Մենք իմացանք Լիսկովի փոխարինման սկզբունք ինչի է համար է նախատեսված: Մենք տեսանք սկզբունքը խախտող օրինակ և այն լուծել երկու եղանակ։ LSP-ն օգնում է կոդը դարձնել ճկուն և կանխում է խնդիրները, երբ հին կոդը ընդլայնվում է նոր կոդով: LSP-ն ապահովում է, որ կոդի վարքագիծը(behavior) մնում է նույնը ինչպես նոր, այնպես էլ հին կոդերի հիմքերում, ինչի արդյունքում կոդն ավելի քիչ է քրաշվում:

The Interface Segregation Principle

Այս գրառման մեջ մենք կուսումնասիրենք SOLID հապավման I-ին, որը ներկայացնում է ինտերֆեյսի առանձնացման սկզբունքը:

Հաճախորդներին չպետք է ստիպել կախվածության մեջ լինել այն մեթոդներից, որոնք նրանք չեն օգտագործում: Աղբյուրը. Agile Software Development; Robert C. Martin

Սկզբունքը նման է single responsibility սկզբունքին այն առումով, որ class-ները պետք է իրականացնեն միայն այնպիսի մեթոդներ, որոնք նրանք իրականում օգտագործում են:

Պատկերացրեք, որ մենք ունենք ենթադաս(subclass), որն իրականացնում է բոլոր մեթոդները բազային(base) class-ում: Բայց մի կոնկրետ մեթոդի համար ֆունկցիայի մարմինը բացառություն է առաջացնում, քանի որ մենք չենք ուզում, որ այդ մեթոդն ընդհանրապես կանչելի լինի: Այդ մեկ կոնկրետ մեթոդը հակասում է class-ի վարքագծի(behavior) սահմանմանը և տեխնիկապես չպետք է կիրառվի: Սա կլինի ISP-ի սկզբունքի խախտում :

Դուք կարող եք մտածել, թե ինչու ենթադասը(subclass-ը) պետք է իրականացնի այն մեթոդը, որն իրեն անհրաժեշտ չէ: Տեսեք, այս սկզբունքը ինտերֆեյսների մասին է: Երբ ինտերֆեյսը ժառանգվում է, բոլոր գոյություն ունեցող մեթոդները պետք է իրականացվեն:

Մինչ հաջորդ թեմային անցնելը, եկեք մի փոքր անրադառնանք interfaces-ին և duck typing -ին։

Interfaces, abstract classes and duck typing in Python

Ինտերֆեյսները ծրագրային(software) կառուցվածք են, որոնք սահմանում և պարտադրում են, թե ինչ մեթոդներ պետք է ունենա class-ի օբյեկտը: Python-ում չկա ինտերֆեյս class:

Python-ում ինտերֆեյս ստեղծելու համար օգտագործվում է duck typing, այսինքն, օգտագործում ենք abstract class-ը ինտերֆեյսը ներկայացնելու համար։ Այլ կերպ ասած, եթե սահմանած class-ն իրեն պահում է ինտերֆեյսի նման,ապա կարող ենք օգտագործել այնպես, ինչպես ինտերֆեյսը :

Հիմնական կետն այստեղ այն է, որ մենք վերաբերվում ենք մեր աբստրակտ (abstract ) class-ին որպես ինտերֆեյսի (ունենալով դատարկ մեթոդի մարմիններ և մեթոդները աբստրակտ (abstract ) հայտարարելով):

Code

Ինտերֆեյսը կարելի է համարել լիովին աբստրակտ (abstract ) class։ Հետևաբար, մենք աբստրակտ(abstract) class-ի բոլոր մեթոդները նշում ենք @abstractmethod-ով:

Դիտարկենք հետևյալ կոդը։


from abc import ABC, abstractmethod


class Car(ABC):
@abstractmethod
def __init__(self, name):
"""Please implement intialization of a car"""

@abstractmethod
def accelerate(self):
"""Please implement accelerating of a car"""

@abstractmethod
def turboAccelerate(self, turbo):
"""Please implement turboaccelerating of a car"""


class RegularCar(Car):
def __init__(self, name):
self.name = name
self.speed = 0

def accelerate(self):
self.speed += 1
print(f"Car {self.name} is accelerating" )

def turboAccelerate(self, turbo):
raise Exception("Regular car has no turbo")


class SportsCar(Car):
def __init__(self, name):
self.name = name
self.speed = 0

def accelerate(self):
self.speed += 1
print(f"Car {self.name} is accelerating")

def turboAccelerate(self, turbo):
self.speed += turbo
print(f"Car {self.name} is accelerating with turbo {turbo}")


if __name__ == '__main__':
car = RegularCar('BMW')
car.accelerate()

autoCar = SportsCar('Audi')
autoCar.turboAccelerate(2)

### Այս տողը exception կառաջացնի
### Սա ISP-ի խախտում է, քանի որ դասը պարունակում է չօգտագործվող ֆունկցիա
car.turboAccelerate(2)

Մենք սահմանում ենք աբստրակտ(abstract) հիմնական(base) classCar: Կրկին սա կարելի է համարել ինտերֆեյս, քանի որ բոլոր երեք մեթոդները սահմանվում են որպես աբստրակտ(abstract):

Հաջորդ քայլը, մենք ստեղծում ենք երկու ենթադաս (subclassRegularCar և SportsCar: Այս երկու class-ները ժառանգում են Car-ում սահմանված մեթոդները: Այնուամենայնիվ, նկատեք, որ RegularCar-ը չունի turbo, այնպես որ մենք բացառություն ենք անում, եթե turboAccelerate-ի կանչը կատարվում է RegularCar օբյեկտի վրա:

Մենք կարող ենք ստուգել կոդը __main__ ֆունկցիայի մեջ և տեսնել, որ turboAccelerate-ի կանչն իսկապես exception է առաջացնում: Մնացած ամեն ինչ աշխատում է այնպես, ինչպես սպասվում էր:

Այժմ դուք կարող եք մտածել, թե ինչու սահմանել turboAccelerate ֆունկցիան RegularCar-ի համար, եթե այն, այնուամենայնիվ, չպետք է օգտագործվի:

Սա է ISP-ի սկզբունքը։ Մենք չպետք է ստիպենք RegularCar-ին ներդնել turboAccelerate, եթե այն չի ապահովվում:

Բացի այդ, երկրորդ խնդիրը կոդերի պահպանման խնդիրն է: Պատկերացրեք, որ մենք պետք է ինչ-որ բան փոխենք turboAccelerate ֆունկցիայի մեջ: Ենթադրենք, մենք պետք է լրացուցիչ պարամետր փոխանցենք ֆունկցիային:Եթե ​​մենք այս փոփոխությունը կատարենք ինտերֆեյսի մեջ, պետք է փոփոխություն կատարենք նաև RegularCar-ում, թեև ֆունկցիան ուղղակի exception է գցում: Այսինքն՝ մենք ստիպված ենք անօգուտ փոփոխություններ կատարել միայն կոդը կոմպիլյացիայի ենթարկելու համար։

Փորձենք ուղղել կոդը։

Լուծում


from abc import ABC, abstractmethod


class NoTurbo(ABC):
@abstractmethod
def __init__(self, name):
"""Please implement intialization of a car"""

@abstractmethod
def accelerate(self):
"""Please implement accelerating of a car"""


class WithTurbo(NoTurbo):
@abstractmethod
def turboAccelerate(self):
"""Please implement turboaccelerating of a car"""


class RegularCar(NoTurbo):
def __init__(self, name):
self.name = name
self.speed = 0

def accelerate(self):
self.speed += 1
print(f"Car {self.name} is accelerating")


class SportsCar(WithTurbo):
def __init__(self, name):
self.name = name
self.speed = 0

def accelerate(self):
self.speed += 1
print(f"Car {self.name} is accelerating")

def turboAccelerate(self, turbo):
self.speed += turbo
print(f"Car {self.name} is accelerating with turbo {turbo}")


if __name__ == '__main__':
car = RegularCar('BMW')
car.accelerate()

autoCar = SportsCar('Audi')
autoCar.turboAccelerate(2)

Այսպիսով. Մեր նպատակն էր հեռացնել turboAccelerateRegularCar-ից: Բայց մենք չենք կարող դա անել, Code քանի որ ինտերֆեյսը պարունակում է մեթոդ, և ինտերֆեյսի բոլոր մեթոդները պետք է իրականացվեն ենթադասում(subclass):

Հետեւաբար, լուծումը երկու ինտերֆեյս ստեղծելն է: Մեկը կոչվում է NoTurbo, իսկ մյուսը՝ WithTurbo: Մենք պարզապես turboAccelerate-ը դարձնում ենք ֆունկցիա WithTurbo-ում և ոչ NoTurbo-ում: Սովորական մեքենան օգտագործում է NoTurbo, մինչդեռ տուրբո մեքենան օգտագործում է WithTurbo:

Խնդիրը լուծվեց…

Ինչպես տեսնում եք հիմա, RegularCar-ը և SportsCar-ը չեն իրականացնում որևէ անօգուտ ֆունկցիա, որը նրանց պետք չի գալու:

Ամփոփում

Այս գրառման մեջ դուք իմացաք ինտերֆեյսի առանձնացման սկզբունքի մասին: Հիմնականում սկզբունքն ասում է, որ class-ին չպետք է ստիպել իրականացնել ֆունկցիաններ, որոնք նա չի օգտագործում:

Dependency inversion principle

Մենք վերջապես հասանք սկզբունքներից վերջինին։ 5-րդ սկզբունքը կոչվում է կախվածության ինվերսիայի սկզբունք: Սահմանումը ունի երկու մաս…

Վերին մակարդակի մոդուլները չպետք է կախված լինեն ցածր մակարդակի մոդուլներից։ Երկուսն էլ պետք է կախված լինեն աբստրակտցիաներից։

Աբստրակցիաները չպետք է կախված լինեն դետալներից։ Դետալները պետք է կախված լինեն աբստրակցիաներից

Կոդի և կոնկրետ օրինակի ժամանակն է։

Code

Դիտարկենք հետևյալ օրինակը, որտեղ մենք մոդելավորում ենք ռոբոտ:

class Apple:
def eat(self):
print(f"Eating Apple. Transferring {5} units of energy to brain...")

class Robot:
def get_energy(self):
apple = Apple()
apple.eat()

if __name__ == '__main__':
robot = Robot()
robot.get_energy()

Robot class-ը ունի միայն մեկ ֆունկցիա՝ get_energy: Մենք իրականացնում ենք Apple-ը, որից ռոբոտը կարող է էներգիա ստանալ:

Հիմա ինչ-որ պահի ռոբոտը կհոգնի խնձոր ուտելուց։ Այսպիսով, մենք ավելացնում ենք Chocolate class- ը՝ ռոբոտին ուտելու ավելի շատ տարբերակներ առաջարկելու համար:

class Apple:
def eat(self):
print(f"Eating Apple. Transferring {5} units of energy to brain...")

class Chocolate:
def eat(self):
print(f"Eating Chocolate. Transferring {10} units of energy to brain...")

class Robot:
def get_energy(self, eatable: str):
if eatable == "Apple":
apple = Apple()
apple.eat()
elif eatable == "Chocolate":
chocolate = Chocolate()
chocolate.eat()

if __name__ == '__main__':
robot = Robot()
robot.get_energy("Apple")

Այնուամենայնիվ, նկատեք, թե ինչ է տեղի ունենում get_energy մեթոդի հետ: Որպես պարամետր պետք է փոխանցենք մի տող, որը ցույց է տալիս, թե ինչ պետք է ուտի ռոբոտը։Օրինակ, եթե Chocolate-ում eat մեթոդին լրացուցիչ պարամետր ներմուծենք, get_energy կոդը կխախտվի։Այս խնդիրները առաջանում են ամուր միացման և ուժեղ կախվածությունների պատճառով և սա կախվածության ինվերսիայի սկզբունքի խախտում է:

Նայենք հետևյալ դիագրամը։

3

Լուծում

Այս խնդիրը լուծելու համար մենք պետք է ներդնենք աբստրակտ մեթոդը՝ փոփոխելով UML դիագրամը հետևյալ կերպ։

Իսկ կոդը կունենա այսպիսի տեսք։

from abc import ABC, abstractmethod

class Eatable(ABC):
@abstractmethod
def eat(self):
return NotImplemented

class Apple(Eatable):
def eat(self):
print(f"Eating Apple. Transferring {5} units of energy to brain...")

class Chocolate(Eatable):
def eat(self):
print(f"Eating Chocolate. Transferring {10} units of energy to brain...")

class Robot:
def get_energy(self, eatable: Eatable):
eatable.eat()

if __name__ == '__main__':
robot = Robot()
robot.get_energy(Apple())

Մենք ստեղծում ենք Eatable ինտերֆեյս, որն իրականացվում է ինչպես Apple-ի, այնպես էլ Chocolate-ի կողմից:

Մենք փոխում ենք get_energy-ի մեթոդի արգումենտը այնպես, որ այն ակնկալում է Eatable տեսակի արգումենտ str-ի փոխարեն:Բոլոր ուտելիք class-ները իրականացնում են Eatable ինտերֆեյս, վստահ ենք, որ կոդը չի քրաշվի (crash), եթե փոխվեն Chocolate- ը կամ Apple-ը:

Ամփոփում

Այսպիսով ուսումնասիրեցինք կախվածության ինվերսիայի սկզբունքը: Սկզբունքն, ըստ էության, ասում է, որ ավելի բարձր մակարդակի մոդուլները չպետք է կախված լինեն ավելի ցածր մակարդակի մոդուլներից: Փոխարենը երկուսն էլ պետք է կախված լինեն աբստրակտությունից: Մենք պետք է ավելի բարձր մակարդակի մոդուլներ ստեղծենք՝ անկախ ցածր մակարդակի մոդուլներում իրականացման առանձնահատկություններից։

Մենք վերջապես հասանք ավարտին!

Հուսով եմ, որ ձեզ դուր եկավ այս հոդվածը։

Շնորհակալություն ընթերցելու համար։

#SOLID #KISS #YAGNI #DRY #Design #Principles

--

--