اصول SOLID که هر توسعه دهنده باید بداند

اصول SOLID که هر توسعه دهنده باید بداند

برنامه نویسی شی گرا طرح جدیدی را در توسعه نرم افزار به ارمغان آورده.
این توسعه دهندگان را قادر می سازد داده ها را با همان هدف / عملکرد در یک کلاس ترکیب کنند تا صرف نظر از کل برنامه ، تنها هدف را در آنجا انجام دهند.
اما ، این برنامه نویسی شی گرا از برنامه های گیج کننده یا غیرقابل نگهداری جلوگیری نمی کند.
به همین ترتیب ، پنج دستورالعمل توسط Robert C. Martin تدوین شد. این پنج راهنما / اصل ایجاد برنامه های خواندنی و قابل نگهداری را برای توسعه دهندگان آسان کرده است.
این پنج اصل را اصول S.O.L.I.D می نامیدند (نام اختصاری آن Michael Feathers بود).
S: اصل مسئولیت منفرد (Single Responsibility Principle)
O: اصل بسته (Open-Closed Principle)
L: اصل تعویض لیسکوف (Liskov Substitution Principle)
I: اصل تفکیک رابط (Interface Segregation Principle)
D: اصل وارونگی وابستگی (Dependency Inversion Principle)
در زیر به طور مفصل در مورد آنها بحث خواهیم کرد.
توجه: بسیاری از مثالهای این مقاله ممکن است به عنوان مورد واقعی کافی نباشند یا در برنامه های دنیای واقعی قابل استفاده نباشند. همه چیز به طراحی و مورد استفاده شما بستگی دارد. مهمترین چیز درک و دانستن چگونگی اعمال / پیروی از اصول است.
نکته: از ابزارهایی مانند Bit (GitHub) برای اشتراک و استفاده مجدد از مولفه ها (و ماژول های کوچک) به راحتی در پروژه ها و برنامه ها استفاده کنید. همچنین به شما و تیمتان کمک می کند تا در وقت صرفه جویی کنید ، هماهنگ باشید و با هم سریعتر بسازید. رایگان است ، امتحان کنید.

Single Responsibility Principle

“… شما یک کار داشتید” – لوکی به اسکورج در ثور: راگناروک
یک کلاس باید فقط یک کار داشته باشد.
یک کلاس باید فقط مسئول یک چیز باشد. اگر یک کلاس بیش از یک مسئولیت داشته باشد ، بهم پیوسته می شود. تغییر به یک مسئولیت منجر به تغییر مسئولیت دیگر می شود.
توجه: این اصل نه تنها برای کلاسها بلکه برای اجزای نرم افزار و ریز سرویس ها نیز کاربرد دارد.
به عنوان مثال ، این طرح را در نظر بگیرید:


class Animal {
constructor(name: string){ }
getAnimalName() { }
saveAnimal(a: Animal) { }
}

کلاس Animal اصل SRP را نقض می کند.
چگونه SRP را نقض می کند؟
SRP اظهار می دارد که کلاسها باید یک مسئولیت داشته باشند ، در اینجا می توانیم دو مسئولیت را ترسیم کنیم: مدیریت پایگاه داده حیوانات و مدیریت خواص حیوانات. سازنده و getAnimalName ویژگی های Animal را مدیریت می کنند در حالی که saveAnimal ذخیره سازی Animal را در یک پایگاه داده مدیریت می کند.
این طراحی چگونه در آینده باعث بروز موضوعات خواهد شد؟
اگر برنامه به گونه ای تغییر کند که بر عملکردهای مدیریت پایگاه داده تأثیر بگذارد. برای جبران تغییرات جدید ، کلاسهایی که از خصوصیات Animal استفاده می کنند باید لمس شوند و دوباره کامپایل شوند.
می بینید که این سیستم بوی سفتی می دهد ، مثل یک اثر دومینو است ، یک کارت را لمس کنید روی همه کارتهای دیگر تأثیر می گذارد.
برای مطابقت با SRP ، کلاس دیگری ایجاد می کنیم که مسئولیت ذخیره سازی یک حیوان را در پایگاه داده انجام می دهد:


class Animal {
constructor(name: string){ }
getAnimalName() { }
}
class AnimalDB {
getAnimal(a: Animal) { }
saveAnimal(a: Animal) { }
}

هنگام طراحی کلاسهای خود ، ما باید هدف قرار دادن ویژگیهای مرتبط را در کنار هم داشته باشیم ، بنابراین هر زمان که تمایل به تغییر دارند به همان دلیل تغییر می کنند. و اگر سعی کنیم ویژگی ها به دلایل مختلف تغییر کنند ، باید از هم جدا شویم. – استیو فنتون
با استفاده مناسب از این موارد ، برنامه ما بسیار منسجم می شود.

Open-Closed Principle

نهادهای نرم افزاری (کلاس ها ، ماژول ها ، توابع) باید برای توسعه باز باشند ، نه اصلاح.
بیایید با کلاس Animal خود ادامه دهیم.


class Animal {
constructor(name: string){ }
getAnimalName() { }
}

ما می خواهیم از طریق لیستی از حیوانات تکرار کنیم و صداهای آنها را ایجاد کنیم.


//...
const animals: Array = [
new Animal('lion'),
new Animal('mouse')
];
function AnimalSound(a: Array) {
for(int i = 0; i <= a.length; i++) {
if(a[i].name == 'lion')
log('roar');
if(a[i].name == 'mouse')
log('squeak');
}
}
AnimalSound(animals);

عملکرد AnimalSound با اصل بسته باز مطابقت ندارد زیرا نمی توان آن را در برابر انواع جدید حیوانات بست.
اگر یک حیوان جدید اضافه کنیم ، مار:


//...
const animals: Array = [
new Animal('lion'),
new Animal('mouse'),
new Animal('snake')
]
//...

ما باید عملکرد AnimalSound را اصلاح کنیم:

//...
function AnimalSound(a: Array) {
for(int i = 0; i <= a.length; i++) {
if(a[i].name == 'lion')
log('roar');
if(a[i].name == 'mouse')
log('squeak');
if(a[i].name == 'snake')
log('hiss');
}
}
AnimalSound(animals);

می بینید که برای هر حیوان جدید ، منطق جدیدی به عملکرد AnimalSound اضافه می شود. این یک مثال کاملاً ساده است. وقتی برنامه شما رشد کرده و پیچیده شود ، می بینید که دستور if بارها و بارها در عملکرد AnimalSound هر بار که حیوان جدید اضافه می شود ، در کل برنامه تکرار می شود.
چگونه سازگار آن (AnimalSound) با OCP باشیم؟

class Animal {
makeSound();
//...
}
class Lion extends Animal {
makeSound() {
return 'roar';
}
}
class Squirrel extends Animal {
makeSound() {
return 'squeak';
}
}
class Snake extends Animal {
makeSound() {
return 'hiss';
}
}
//...
function AnimalSound(a: Array) {
for(int i = 0; i <= a.length; i++) {
log(a[i].makeSound());
}
}
AnimalSound(animals);

Animal اکنون یک روش مجازی makeSound دارد. ما از هر حیوان می خواهیم کلاس Animal را گسترش داده و روش مجازی makeSound را پیاده سازی کنیم.
هر حیوانی عملکرد خاص خود را در مورد چگونگی تولید صدا در makeSound اضافه می کند. AnimalSound از طریق مجموعه ای از حیوانات تکرار می شود و فقط روش makeSound خود را فراخوانی می کند.
اکنون ، اگر یک حیوان جدید اضافه کنیم ، AnimalSound نیازی به تغییر ندارد. تمام کاری که ما باید انجام دهیم افزودن حیوان جدید به آرایه حیوانات است.
AnimalSound اکنون با اصل OCP مطابقت دارد.
مثالی دیگر:
بیایید تصور کنیم شما یک فروشگاه دارید و با استفاده از این کلاس به مشتریان مورد علاقه خود 20٪ تخفیف می دهید:


class Discount {
giveDiscount() {
return this.price * 0.2
}
}

وقتی تصمیم دارید دو برابر تخفیف 20 درصدی به مشتریان VIP ارائه دهید. شما می توانید کلاس را به این شکل تغییر دهید:


class Discount {
giveDiscount() {
if(this.customer == 'fav') {
return this.price * 0.2;
}
if(this.customer == 'vip') {
return this.price * 0.4;
}
}
}

نه ، این اصل OCP را نقض می کند. OCP آن را منع می کند. اگر ما می خواهیم یک درصد تخفیف جدید بدهیم ، ممکن است تغییر کند. نوع مشتری ، خواهید دید که منطق جدیدی اضافه خواهد شد.
برای اینکه از اصل OCP پیروی کند ، کلاس جدیدی اضافه خواهیم کرد که تخفیف را گسترش می دهد. در این کلاس جدید ، رفتار جدید آن را پیاده سازی می کنیم:


class VIPDiscount: Discount {
getDiscount() {
return super.getDiscount() * 2;
}
}

اگر تصمیم دارید 80٪ تخفیف به مشتریان فوق العاده VIP اختصاص دهید ، باید اینگونه باشد:


class SuperVIPDiscount: VIPDiscount {
getDiscount() {
return super.getDiscount() * 2;
}
}

Liskov Substitution Principle

یک زیر کلاس باید جایگزین ابر کلاس خود شود
هدف از این اصل این است که اطمینان حاصل شود یک زیر کلاس می تواند جای سوپر کلاس خود را بدون خطا بدست آورد. اگر این کد در حال بررسی نوع کلاس باشد ، باید این اصل را نقض کرده باشد.
بیایید از مثال Animal خود استفاده کنیم.


//...
function AnimalLegCount(a: Array) {
for(int i = 0; i <= a.length; i++) {
if(typeof a[i] == Lion)
log(LionLegCount(a[i]));
if(typeof a[i] == Mouse)
log(MouseLegCount(a[i]));
if(typeof a[i] == Snake)
log(SnakeLegCount(a[i]));
}
}
AnimalLegCount(animals);

این اصل LSP را نقض می کند (و همچنین اصل OCP). این نرم افزار باید از هر نوع حیوانی اطلاع داشته باشد و عملکرد شمارش پایه را نیز فراخوانی کند.
با ایجاد هر حیوان جدید ، عملکرد باید تغییر کند تا حیوان جدید را بپذیرد.


//...
class Pigeon extends Animal {
}
const animals[]: Array = [
//...,
new Pigeon();
]
function AnimalLegCount(a: Array) {
for(int i = 0; i <= a.length; i++) {
if(typeof a[i] == Lion)
log(LionLegCount(a[i]));
if(typeof a[i] == Mouse)
log(MouseLegCount(a[i]));
if(typeof a[i] == Snake)
log(SnakeLegCount(a[i]));
if(typeof a[i] == Pigeon)
log(PigeonLegCount(a[i]));
}
}
AnimalLegCount(animals);

برای اینکه این عملکرد از اصل LSP پیروی کند ، ما این الزامات LSP را که توسط استیو فنتون فرض شده دنبال خواهیم کرد:
اگر super-class (Animal) روشی داشته باشد که پارامتر نوع super-class (Animal) را می پذیرد. زیر کلاس آن (کبوتر) باید یک نوع فوق کلاس (نوع حیوانات) یا زیر کلاس (نوع کبوتر) را به عنوان آرگومان بپذیرد.
اگر super-class یک نوع فوق کلاس را بازگرداند (Animal). زیر کلاس آن باید یک نوع فوق کلاس (نوع حیوانات) یا زیر کلاس (کبوتر) را برگرداند.
اکنون می توانیم تابع AnimalLegCount را دوباره پیاده سازی کنیم:


function AnimalLegCount(a: Array) {
for(let i = 0; i <= a.length; i++) {
a[i].LegCount();
}
}
AnimalLegCount(animals);

تابع AnimalLegCount کمتر از نوع Animal منتقل می شود ، فقط متد LegCount را فراخوانی می کند. تنها چیزی که می داند این است که پارامتر باید از نوع Animal باشد ، یا کلاس Animal یا زیر کلاس آن.
اکنون کلاس Animal باید یک روش LegCount را پیاده سازی و تعریف کند:


class Animal {
//...
LegCount();
}

و زیر مجموعه های آن باید روش LegCount را پیاده سازی کنند:


//...
class Lion extends Animal{
//...
LegCount() {
//...
}
}
//...

وقتی به تابع AnimalLegCount منتقل شد ، تعداد پاهایی را که یک شیر دارد بازمی گرداند.
می بینید که AnimalLegCount برای بازگشت تعداد پای خود نیازی به دانستن نوع Animal ندارد ، فقط روش LegCount از نوع Animal را فراخوانی می کند زیرا طبق قرارداد یک زیر کلاس از کلاس Animal باید عملکرد LegCount را پیاده سازی کند.

Interface Segregation Principle

رابط های ریزدانه ای را ایجاد کنید که مخصوص client باشد
client نباید مجبور به وابستگی به رابط هایی شوند که از آنها استفاده نمی کنند.
این اصل با معایب اجرای رابط های بزرگ سرو کار دارد.
بیایید به رابط زیر IShape نگاهی بیندازیم:


interface IShape {
drawCircle();
drawSquare();
drawRectangle();
}

این رابط مربع ها ، دایره ها ، مستطیل ها را ترسیم می کند. کلاس Circle ، Square یا Rectangle که رابط IShape را پیاده سازی می کند باید روش های drawCircle () ، drawSquare () ، drawRectangle () را تعریف کند.


class Circle implements IShape {
drawCircle(){
//...
}
drawSquare(){
//...
}
drawRectangle(){
//...
}
}
class Square implements IShape {
drawCircle(){
//...
}
drawSquare(){
//...
}
drawRectangle(){
//...
}
}
class Rectangle implements IShape {
drawCircle(){
//...
}
drawSquare(){
//...
}
drawRectangle(){
//...
}
}

نگاه کردن به کد بالا کاملاً خنده دار است. کلاس Rectangle روشهایی را پیاده سازی می کند (drawCircle و drawSquare) هیچ استفاده ای از آن ندارد ، به همین ترتیب مربع از drawCircle و drawRectangle و کلاس Circle (drawSquare ، drawSquare) پیاده سازی می کند.
اگر یک روش دیگر مانند drawTriangle () به رابط IShape اضافه کنیم ،


interface IShape {
drawCircle();
drawSquare();
drawRectangle();
drawTriangle();
}

کلاسها باید روش جدید را اجرا کنند یا خطایی ایجاد می شود.
می بینیم که پیاده سازی شکلی که بتواند دایره بکشد اما مستطیل یا مربع یا مثلث نباشد غیرممکن است. ما فقط می توانیم روش هایی را برای ایجاد خطایی که نشان می دهد عملیات انجام نمی شود ، پیاده کنیم.
ISP در برابر طراحی این رابط IShape اخم می کند. مشتریان (در اینجا مستطیل ، دایره و مربع) مجبور نیستند به روشهایی که نیازی به آنها ندارند و یا از آنها استفاده می کنند بستگی داشته باشند. همچنین ، ISP اظهار می دارد که رابط ها باید فقط یک کار را انجام دهند (دقیقاً مانند اصل SRP) هر گروه رفتار اضافی باید در رابط دیگری انتزاع شود.
در اینجا ، رابط IShape ما اقداماتی را انجام می دهد که باید به طور مستقل توسط رابط های دیگر اداره شوند.
برای انطباق رابط IShape با اصل ISP ، اقدامات را به رابط های مختلف تفکیک می کنیم:


interface IShape {
draw();
}
interface ICircle {
drawCircle();
}
interface ISquare {
drawSquare();
}
interface IRectangle {
drawRectangle();
}
interface ITriangle {
drawTriangle();
}
class Circle implements ICircle {
drawCircle() {
//...
}
}
class Square implements ISquare {
drawSquare() {
//...
}
}
class Rectangle implements IRectangle {
drawRectangle() {
//...
}
}
class Triangle implements ITriangle {
drawTriangle() {
//...
}
}
class CustomShape implements IShape {
draw(){
//...
}
}

رابط ICircle فقط رسم دایره ها را کنترل می کند ، IShape نقاشی های هر شکل را کنترل می کند ISquare رسم فقط مربع ها و IRectangle رسم مستطیل ها را کنترل می کند.
یا
کلاسها (دایره ، مستطیل ، مربع ، مثلث و غیره) فقط می توانند از رابط IShape ارث ببرند و رفتار رسم خود را پیاده سازی کنند.


class Circle implements IShape {
draw(){
//...
}
}
class Triangle implements IShape {
draw(){
//...
}
}
class Square implements IShape {
draw(){
//...
}
}
class Rectangle implements IShape {
draw(){
//...
}
}

سپس می توانیم از I-Interface استفاده کنیم تا مشخصات Shape مانند Semi Circle ، مثلث قائم الزاویه ، مثلث متساوی الاضلاع ، مستطیل Blunt-Edged و غیره را ایجاد کنیم.

Dependency Inversion Principle

وابستگی باید به انتزاع باشد نه به زیرکلاس ها. (Dependency should be on abstractions not concretions)

الف) ماژول های سطح بالا نباید به ماژول های سطح پایین وابسته باشند. هر دو باید به انتزاع بستگی داشته باشند.
ب- انتزاع نباید به جزئیات بستگی داشته باشد. جزئیات باید به انتزاع بستگی داشته باشد.
یک نکته در توسعه نرم افزار وجود دارد که برنامه ما عمدتا از ماژول ها تشکیل خواهد شد. وقتی این اتفاق می افتد ، ما باید با استفاده از تزریق وابستگی همه چیز را پاک کنیم. اجزای سطح بالا بستگی به عملکرد اجزای سطح پایین دارد.


class XMLHttpService extends XMLHttpRequestService {}
class Http {
constructor(private xmlhttpService: XMLHttpService) { }
get(url: string , options: any) {
this.xmlhttpService.request(url,'GET');
}
post() {
this.xmlhttpService.request(url,'POST');
}
//...
}

در اینجا ، Http جز component سطح بالا است در حالی که HttpService جز component سطح پایین است. این طراحی نقض DIP A است: ماژول های سطح بالا نباید به ماژول های سطح پایین وابسته باشند. این باید به انتزاع آن بستگی داشته باشد.
کلاس Ths Http مجبور است به کلاس XMLHttpService وابسته باشد. اگر بخواهیم سرویس اتصال Http را تغییر دهیم ، شاید بخواهیم از طریق Nodejs یا حتی سرویس http را به تمسخر بگیریم و به اینترنت متصل شویم. ما با زحمت باید تمام موارد Http را ویرایش کنیم تا کد را ویرایش کنیم و این اصل OCP را نقض می کند.
کلاس Http کمتر از نوع سرویس Http استفاده می کنید. ما یک رابط اتصال ایجاد می کنیم:


interface Connection {
request(url: string, opts:any);
}

رابط اتصال یک روش درخواست دارد. با استفاده از این ، ما یک آرگومان از نوع اتصال به کلاس Http خود را وارد می کنیم:


class Http {
constructor(private httpConnection: Connection) { }
get(url: string , options: any) {
this.httpConnection.request(url,'GET');
}
post() {
this.httpConnection.request(url,'POST');
}
//...
}

بنابراین اکنون ، بدون توجه به نوع سرویس اتصال Http که به Http منتقل می شود ، می تواند به راحتی به یک شبکه متصل شود بدون اینکه زحمت شناخت نوع اتصال شبکه را داشته باشد.
اکنون می توانیم کلاس XMLHttpService خود را مجدداً پیاده سازی کنیم تا رابط اتصال را پیاده سازی کنیم:


class XMLHttpService implements Connection {
const xhr = new XMLHttpRequest();
//...
request(url: string, opts:any) {
xhr.open();
xhr.send();
}
}

ما می توانیم بسیاری از انواع اتصال Http را ایجاد کنیم و بدون هیچ سر و صدایی در مورد خطاها ، آن را به کلاس Http منتقل کنیم.


class NodeHttpService implements Connection {
request(url: string, opts:any) {
//...
}
}
class MockHttpService implements Connection {
request(url: string, opts:any) {
//...
}
}

اکنون ، می بینیم که هر دو ماژول سطح بالا و ماژول سطح پایین به انتزاعات بستگی دارند. کلاس Http (ماژول سطح بالا) به نوبه خود به رابط اتصال (انتزاع) و انواع سرویس Http (ماژول سطح پایین) بستگی دارد ، به رابط اتصال (انتزاع) بستگی دارد.
همچنین ، این DIP ما را مجبور می کند که اصل تعویض Liskov را نقض نکنیم: انواع اتصال Node-XML-MockHttpService برای نوع اصلی اتصال قابل تعویض است.

نتیجه
ما پنج اصل را که هر سازنده نرم افزار باید به آن رعایت کند در اینجا ذکر کردیم. ممکن است در ابتدا رعایت کردن همه این اصول دلهره آور باشد ، اما با تمرین و پایبندی مداوم ، بخشی از ما خواهد شد و تأثیر بسزایی در حفظ و نگهداری برنامه های ما خواهد داشت.
اگر در مورد این یا هر چیزی که باید اضافه کنم ، تصحیح یا حذف کنم ، سوالی دارید ، در صورت تمایل در زیر نظر دهید. ما را خوشحال میکنید!

منبع : medium

بدون نظر

**پرسش و پاسخ** سوال خود مطرح کنید.
امتیاز شما*