SOLID սկզբունքները

Artak Tcharoyan
6 min readMay 7, 2020

Օբյեկտակողմնորոշված ծրագրավորումը(OOP) ծրագրային ապահովման մշակման մեջ՝ պրոյեկտների ձևավորման համար, ստեղծեց նոր մոտեցումներ։ Մասնավորապես,OOP-ն ծրագրավորողներին թույլ է տվել համատեղել ընդհանուր նպատակին կամ գործառույթներին ծառայող սուբյեկտները առանձին class-ներում, որոնք նախատեսված են ինքնուրույն խնդիրները լուծելու համար և անկախ են պրոյեկտի այլ մասերից:Այնուամենայնիվ, միայն OOP-ի օգտագործումը չի նշանակում, որ ծրագրավորողը ապահովված է վատ, դժվար կարդացվող կոդ գրելու հնարավորությունից: Ռոբերտ Մարտինը մշակել է օբյեկտակողմնորոշված ծրագրավորման և ձևավորման հինգ սկզբունքներ, որոնք կօգնեն բոլոր ցանկացողներին մշակել OOP պրոյեկտներ։ Այդ սկզբունքները կոչվում են SOLID:

Ին՞չ է SOLID-ը

Ահա ինչպես է բացվում SOLID հապավումը․
[S]. Single Responsibility Principle (Միակ պատասխանատվության սկզբունք)
[O]. Open-Closed Principle (Բաց-փակ սկզբունք)
[L]. Liskov Substitution Principle (Լիսկովի փոխարինման սկզբունք)
[I]. Interface Segregation Principle (Ինտերֆեյսի առանձնացման սկզբունք)
[D]. Dependency Inversion Principle (Կախվածության ինվերսիայի սկզբունք)

Միակ պատասխանատվության սկզբունք(Single Responsibility Principle)

Յուրաքանչյուր class պետք է լուծի միայն մեկ խնդիր։ Class-ը պետք է
պատասխանատու լինի միայն մեկ խնդրի լուծման համար։

Եթե class-ը նախատեսված է մի քանի խնդիրների լուծման համար, ապա պարզվում է, որ դրա ենթահամակարգերը,որոնք իրականացնում են այդ խնդիրների լուծումը, փողկապակցված են միմյանց հետ։Այդպիսի մի ենթահամակարգի փոփոխությունը բերում է մնացած ենթահամակարգերի փոփոխության։ Այս սկզբունքը կիրառելի է ոչ միայն class-ների, այլ նաև ծրագրային այլ կոմպոնենտների համար։

Օրինակ․

Վերը ներկայացված Car class-ը նկարագրում է ինչ որ ավտոմեքենա։ Այս class-ը խախտում է Միակ պատասխանատվության (Single Responsibility) սկզբունքը։ Single Responsibility սկզբունքի համաձայն class-ը պետք է լուծի մեկ խնդիր։ Այն ինչ մեր օրինակի class-ը լուծում է երկու խնդիր, զբաղվում է տվյալների պահպանմամբ (method save) և այդ տվյալների մանիպուլացիայով (method getName):

Ին՞չ խնդրի կարող է սա հանգեցնել։

Եթե փոխենք տվյալների բազայի հետ աշխատելու կարգը , ապա պետք է
փոփոխություններ կատարենք բոլոր այն class-ներում, որոնք աշխատում են տվյալների բազայի հետ։Այսպիսի ճարտարապետությունը ճկուն չէ․ փոփոխելով մեկ ենթահամակարգը մենք ազդում ենք մյուս ենթահամակարգերի վրա, ինչը հիշեցնում է դոմինոյի էֆեկտը։

Որպեսզի վերոնշյալ կոդը համապատասխանեցնենք միակ
պատասխանատվության (Single Responsibility) սկզբունքին, կստեղծենք ևս մեկ class, որի միակ խնդիրը կլինի բազաների հետ աշխատանքը, մասնավորապես՝ բազայում class-ի օբյեկտներ պահպանելը։

Ահա թե ինչ է ասում Սթիվ Ֆենտոնը այս մասին. «Class-ներ մշակելիս մենք պետք է ձգտենք միավորել միանման կոմպոնենտները, այսինքն այն կոմպոնենտները, որոնցում փոփոխությունները տեղի են ունենում նույն պատճառներով։ Իսկ այն կոմպոնենտները, որոնցում փոփոխությունները տեղի են ունենում տարբեր պատճառներով, պետք է փորձենք առանձնացնել»։

Միակ պատասխանատվության(Single Responsibility) սկզբունքի ճիշտ
կիրառումը հանգեցնում է մոդուլի ներսում տարրերի միացման բարձր աստիճանի, այսինքն ` այն փաստին, որ դրա ներսում լուծելի առաջադրանքները լավ են համապատասխանում դրա հիմնական նպատակին:

Բաց-փակ սկզբունք(Open-Closed Principle)

Ծրագրային սուբյեկտները (դասեր, մոդուլներ, ֆունկցիաներ) պետք է բաց լինեն ընդլայնման համար, փակ լինեն փոփոխությունների համար։
Շարունակենք աշխատանքը Car class-ի հետ։

Մենք ունենք մի քանի օբյեկտ,որոնք պատկանում են Car class-ին, և ուզում ենք պարզել,թե ինչ գույն ունեն դրանցից յուրաքանչյուրը։ Ենթադրենք մենք լուծում ենք այս խնդիրը getColor ֆունկցիայի միջոցով։

Այս մոտեցման ամենամեծ խնդիրը այն է, որ մեքենայի գույն որոշելու համար
ֆունկցիան վերլուծում է կոնկրետ օբյեկտներ։ getColor ֆունկցիան չի
համապատասխանում Բաց-փակ (Open-Closed) սկզբունքին, քանի որ, օրինակ, երբ նոր մեքենաներ ավելանան, դրանց գույնը որոշելու համար մենք պետք է փոփոխություն կատարենք ֆունկցիայի մեջ։ Օրինակ ավելացնենք նոր մեքենա։

Դրանից հետո մենք ստիպված կլինենք փոխել getColor ֆունկցիայի կոդը:

Ինչպես տեսնում ենք, զանգվածին նոր էլեմենտ ավելացնելիս մենք ստիպված ենք ավելացնել getColor ֆունկցիայի կոդը։ Սա շատ պարզ օրինակ է, սակայն նմանատիպ կառուցվածքով պրոյեկտներում անընդհատ պետք է ընդլայնել ֆունկցիաները։

Ինչպե՞ս համապատասխանեցնել getColor ֆունկցիան Բաց-փակ (Open-Closed)սկզբունքին։

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

Նկատենք, որ Car class-ը ունի getColor աբստրակտ(վիրտուալ) մեթոդ։ Այսպիսի մոտեցման դեպքում պետք է, որ առանձին մեքենաներ նկարագրող class-ները ընդլայնեն getColor մեթոդը և նկարագրեն այն։ Արդյունքում մեքենա նկարագրող յուրաքանչյուր class կունենա իր getColor մեթոդը և բավական է կանչել getColor մեթոդը զանգվածի յուրաքանչյուր էլեմենտի համար։

Լիսկովի փոխարինման սկզբունք
(Liskov Substitution Principle)

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

Եթե պարզվում է, որ class-ի տեսակը ստուգվում է կոդով, ապա փոխարինման սկզբունքը խախտվում է: Դիտարկենք այս սկզբունքի կիրառումը ՝ «Car» class-ի հետ վերադառնալով օրինակին: Մենք կգրենք մի ֆունկցիա, որը պետք է տեղեկատվություն տա մեքենայի դռների քանակի վերաբերյալ։

Այս ֆունկցիան խախտում է Լիսկովի փոխարինման (Liskov Substitution)
սկզբունքը (ինչպես նաև Բաց-փակ (Open-Closed) սկզբունքը)։ Այս կոդը պետք է իմանա բոլոր օբյեկտների տիպերի մասին և կախված տիպից կանչի
համապատասխան ֆունկցիան։ Արդյունքում նոր մեքենա ավելացնելիս պետք է ավելացնենք ֆունկցիայի կոդը։ Որպեսզի ֆունկցիան չխախտի փոխարինման սկզբունքը, վերափոխենք այն ըստ Սթիվ Ֆենտոնի կողմից ձևակերպված պահանջների։ Եթե մեթոդները ընդունում կամ վերադարձնում են որպես պարամետր, ինչ որ class-ի տիպի օբյեկտ, ապա դրանք պետք է ընդունեն և վերադարձնեն նաև այդ class-ից ժառանգված class-ների տիպի օբյեկտներ։ Փորձենք ձևափոխել carDoorCount ֆունկցիան։

Այժմ այս ֆունկցիային չի հետաքրքրում դրան փոխանցված օբյեկտների տեսակը։ Նա ուղակի կանչում է օբյեկտների doorCount ֆունկցիան: Այն ամենը, ինչը մեր ֆունկցիան պետք է իմանա տիպերի մասին դա այն է , որ բոլոր տիպերը պետք է լինեն Car տիպի կամ ժառանգված լինեն Car-ից և Car class-ում պետք է ունենանք doorCount մեթոդը, իսկ դրա ժառանգ class-ները պետք է նկարագրեն այս մեթոդը։

Ինտերֆեյսի առանձնացման սկզբունք (Interface Segregation Principle)

Ռոբերտ Ս. Մարտինը սահմանել է այս սկզբունքը հետևյալ կերպ.
Ծրագրային ապահովման սուբյեկտները չպետք է կախված լինեն այն
մեթոդներից, որոնք նրանք չեն օգտագործում:

Ինտերֆեյսի տարանջատման սկզբունքը նշանակում է, որ չափազանց «հաստ» ինտերֆեյսները պետք է բաժանվեն փոքր և ավելի սպեցիֆիկ ինտերֆեյսների, այնպես, որ փոքր ինտերֆեյսերի ծրագրային սուբյեկտները իմանան միայն այն մեթոդների մասին, որոնց հետ նրանք պետք է աշխատեն: Արդյունքում, ինտերֆեյսի մեթոդը փոխելիս, ծրագրային սուբյեկտները, որոնք չեն օգտագործում այս մեթոդը, չպետք է փոխվեն:

Օրինակ ունենք ինտերֆեյս, որը նկարագրում է օնլայն խանութի ապրանքները։ Ենթադրենք մեր ապրանքները կարող են ունենալ պրոմոկոդ, զեղչ, ունեն ինչ որ գին, չափ և այլն։

Դիտարկենք հետևյալ ինտերֆեյսը.

Այս ինտերֆեյսը վատ է նրանով, որ իր մեջ պարունակում է չափազանց շատ
մեթոդներ։ Ինչ պետք է անենք, եթե մեր ապրանքների class-ը չի կարող ունենալ զեղչ կամ պրոմոկոդ։ Այսպիսով, որպեսզի չօգտագործվող մեթոդները ստիպված չլինենք նկարագրել class-ում ավելի լավ է բաժանենք մեր ինտերֆեյսը ավելի փոքր ինտերֆեյսների։ Այդ դեպքում կոնկրետ class-ը կիրականացնի այն ինտերֆեյսները, որոնք իրեն անհրաժեշտ են։

Կախվածության ինվերսիայի սկզբունք(Dependency Inversion Principle)

Կախվածության առարկան պետք է լինի աբստրակցիա, այլ ոչ թե կոնկրետ ինչ-որ բան:
1. Վերին մակարդակի մոդուլները չպետք է կախված լինեն ցածր մակարդակի
մոդուլներից: Երկուսն էլ պետք է կախված լինեն աբստրակցիաներից:
2. Աբստրակցիաները չպետք է կախված լինեն դետալներից: Դետալները պետք է կախված լինեն աբստրակցիաներից:

Ծրագրերի մշակման գործընթացում կա մի պահ, երբ ֆունկցիոնալը դադարում է տեղավորվել նույն մոդուլի մեջ: Երբ դա տեղի է ունենում, մենք պետք է լուծենք մոդուլային կախվածության խնդիրը: Արդյունքում, օրինակ, կարող է պարզվել, որ բարձր մակարդակի կոմպոնենտները կախված են ցածր մակարդակի կոմպոնենտներից:

Օրինակ․Դիտարկենք գնորդի կողմից պատվերի վճարումը՝

Ամեն ինչ թվում է բավականին տրամաբանական և օրինաչափ: Բայց կա մեկ
խնդիր` Customer class-ը կախված է OrderProcessor class-ից (ավելին, բաց -փակ սկզբունքը չի բավարարվում): Որպեսզի ազատվենք կոնկրետ class-ից
կախվածությունից, անհրաժեշտ է, որ Customer class-ը կախված լինի աբստրակցիայից։ Այսինքն OrderProcessorInterface ինտերֆեյսից։

Այսպիսով, Customer class-ը այժմ կախված է միայն աբստրակցիայից, իսկ
կոնկրետ իրականացումը, այսինքն մանրամասները նրա համար այնքան էլ կարևոր չեն:

--

--