پنج اصل اولیه طراحی شی گرا SOLID

S.O.L.I.D     OBJECT ORIENTED DESIGN
S.O.L.I.D OBJECT ORIENTED DESIGN

سولید ( SOLID ) یک کلمه مخفف برای پنچ اصل اولیه طراحی شئ گرا است که رابرت سیسیل مارتین معروف به عمو باب ( uncle bob ) اون رو مطرح کرد.

این اصول زمانی که دست به دست هم میدن، کار گسترش یا اضافه کردن قابلیت های جدید به برنامه و نگهداری یا همون دیباگ یک برنامه رو برای برنامه نویس ها آسان می کنند.

نکته: این فقط یک مطلب کوچیک و تعریفی از مفهوم S.O.L.I.D برای ورود به این مبحث هستش

بیاین یک نگاهی به این کلمه مخفف بندازیم و برای هر کدام یک معادل فارسی شاید یک کم عجیب براشون بنویسیم


  • Single-responsiblity principle - S اصل مسئولیت واحد یا اصل تک مسئولتی بودن کلاس ها
  • Open-closed principle - O اصل باز/بسته بودن کلاس ها
  • Liskov substitution principle - L اصل جانشینی لیسکوف
  • Interface segregation principle - I اصل تفکیک اینترفیس
  • Dependency Inversion Principle - D اصل انطباق پذیری


اصل مسئولیت واحد یا اصل تک مسئولتی بودن کلاس ها S - Single-responsiblity principle

خب برای درک بهتر این اصول بیاید برای هر اصل توضیح و با یک مثال اون رو برسی کنیم.


# اصل مسئولیت واحد یا Single-responsibility Principle

حرف s به معنی Single-responsibility Principle یا به صورت خلاصه SRP :

که به معنی این است که یک کلاس باید فقط و فقط به یک دلیل تغییر کند یعنی این کلاس باید تنها یک کار انجام دهد.

بیاید با یک مثال موضوع رو درک کنیم :

برای مثال ما تعدادی اشکال هندسی داریم و میخواهیم که جمع مساحت این شکل ها رو حساب کنیم

https://gist.github.com/vahid-almasi/487a16ae68d8847922b2f756e3db469d

خب در قدم اول ما کلاس شکل های مختلف رو میسازیم و پارامتر های الزامی هر کدام از شکل ها رو با تابع constructor سِت میکنیم، در قدم بعدی میریم که کلاس محاسبه کننده مساحت ( AreaCalcuator ) و بعد از اون میریم که منطق برای جمع زدن این مساحت ها رو درست کنیم

https://gist.github.com/vahid-almasi/e83c677425543f856ed2533260dd75f6

برای استفاده از کلاس AreaCalculator به سادگی از این کلاس instance میگیریم و یک آرایه از شکل ها رو به اون پاس میدیم و در آخر خروجی رو نمایش میدیم.

https://gist.github.com/vahid-almasi/9096b43f46a24841e1eecf71a820de4c

مشکلی که وجود داره تعیین شکل یا نوع خروجی (output) بر عهده کلاس AreaCalculator است یا به عبارت دیگه منطق خروجی داخل کلاسِ AreaCalulator است. خب چی می شد اگر کاربر داده خروجی رو به شکل json یا هر شکل دیگری نیاز داشت؟

در حال حاضر تمام منطق توسط کلاس AreaCalculator داره شکل میگیره و این چیزی نیست که ما به اون SRP میگیم، کلاس AreaCalculator باید تنها مساحت شکل ها رو جمع بزنه و کاری با این که کاربر خروجی Json نیاز داره یا HTML نداشته باشه.

خب پس برای درست کردن این موضوع یک کلاس SumCalculatorOutputter درست میکنیم و منطق این که خروجی چجوری باشه رو داخل این کلاس میگذاریم

بنابر این کلاس SumCalculatorOutputter باید به این شکل کار کنه:

https://gist.github.com/vahid-almasi/4bf0fdcbe3eefd6395dff702764a33c6

هممم، حالا منطق و شکل داده خروجی در کلاس SumCalculatorOutputter قرار دارد.

# اصل باز/بسته Open-closed Principle

به زبان ساده کلاس ها باید به سادگی و بدون نیاز به تغییر قابل گسترش ( extendable ) باشند. بیاید یک نگاه به کلاس AreaCalculator به خصوص تابع sum.

https://gist.github.com/vahid-almasi/657ddcd7d17e02bc8bf1eea0bb2b1346

اگر ما بخواهیم که این تابع قابلیت جمع زدن مساحت شکل های بیشتری رو برای ما داشته باشه باید شروط (if/else) بیشتری تعریف کنیم، که این برخلاف Open-closed principle است.

پس بهتره منطق حساب کننده مساحت هر شکل رو از تابع sum بیرون بیاریم و به کلاس شکل ها اضافه اش کنیم

https://gist.github.com/vahid-almasi/5951433bbd49a33e2b69a140f28ac4e6


همین کار هم باید برای کلاس Circle انجام بشه و تابع area به این کلاس اضافه بشه. خب برای محاسبه جمع مساحت هر شکل ارائه شده باید به همین اندازه ساده باشه:


https://gist.github.com/vahid-almasi/3a5b037f30b2a73bdf6acac2f9db2e49

حالا می تونیم هر شکل دیگه ای که نیاز داریم اضافه کنیم بدون این که اصل دوم SOLID رو زیر پا بزاریم. خب حالا یک مشکل دیگه وجود داره، چطور میشه فهمید که شئی (‌ object) که به AreaCalculator پاس داده میشه آیا یک شکل هندسیه؟ اگر هم شکل باشه آیا تابعی به نام area داره؟

نوشتن یک interface بخش جدایی ناپذیر از S.O.L.I.D است، پس باید یک interface بنویسیم و هر شکل جدیدی که قرار بود اضافه بشه رو باید از این اینترفیس implements کنیم، در کد پایین این کار برای کلاس Circle انجام شده:

https://gist.github.com/vahid-almasi/eaf5b2c8cac485dfac6abd3856bd9f61

و در اخر در تابع sum در کلاس AreaCalculator ما چک می کنیم که همه شکل ها و شی هایی که به ما داده شده از ShapeInterface اینستنس (instance) گرفته شده باشن، در غیر این صورت برای خروجی یک Exception ارسال میکنیم

https://gist.github.com/vahid-almasi/fd076f3be0ebccb2cda8ab30e4853fe1


# اصل جانشینی لیسکوف Liskov substitution principle

فرض کنیم (q(x یک ویژگی قابل اثبات در مورد اشیاء x از نوع T است. آنگاه(q(y باید قابل اثبات برای اشیاء y از نوع S باشد که در آن S یک زیر گروه از T است.

اینم نسخه اصلی نوشته است:

Let Φ(x) be a property provable about objects x of type T. Then Φ(y)should be true for objects y of type S where S is a subtype of T.

خوب، بیایید صادق باشیم یک همچین تعریف علمی ممکنه لازم باشه، اما در کار روزمره ما به عنوان توسعه دهنده کمک زیادی نمی کنه. خب، این چه معنی دارد؟

همه این کلمات به این معنی هستند که هر زیرکلاس(subclass) یا کلاس مشتق گرفته شده ( derived class ) باید قابلیت تعویض با کلاس پایه یا والد( parent ) رو داشته باشن.

یا به زبان ساده تر هر کلاسی که از کلاس دیگر ارث بری میکنه هرگز نباید رفتار کلاس والد رو تغییر بده.

تعاریف زیاد شد پس بریم سراغ مثال:

اجازه بدید که باز هم از کلاس AreaCalculator استفاده کنیم،‌ پس یک کلاس VolumeCalculator میسازیم که از این کلاس extends گرفته شد


https://gist.github.com/vahid-almasi/17cf0959530b4d52b421da2c6cd591e0

و داخل کلاس SumCalculatorOutputter

https://gist.github.com/vahid-almasi/8cdee8bc20e05b28b32d78bba7a889e5

اگر ما تلاش کنیم که یک همچین چیزی اجرا کنیم


https://gist.github.com/vahid-almasi/f69a08e7be8c3ef7689990184cdf4c9b


برنامه بدون مشکل اجرا میشه، اما زمانی که تابع HTML رو از output2 بخواهیم ما یک E_NOTICE میگیریم که میگه یک آرایه ای باید تبدیل به رشته (string) بشه.

برای رفع این مشکل باید به جای خروجی ارایه در تابع sum در کلاس VolumeCalculator ، آن را ساده برگیدانیم


https://gist.github.com/vahid-almasi/9dcd66436a9ff63bf949b4df489b250a

حالا دیگه خروجی میتونه یک عدد کسری یا صحیح یا ... باشه


# اصل تفکیک اینترفیس Interface segregation principle

یک توسعه دهنده نباید مجبور implement کردن یک interface بشه که از اون استفاده نمیکنه یا توسعه دهنده نباید به اجبار وابسته به تابعی باشه که براش بی استفاده اس.

یا به عبارت دیگر به جای یک interface بزرگ باید چندین interface تفکیک شده و کوچک داشته باشیم

خب ما هنوز میخوایم از همون شکل ها مثال بیاریم، بیایم محاسبه حجم رو هم اضافه کنیم و اول از همه از ShapeInterface شروع کنیم


https://gist.github.com/vahid-almasi/7a4bc628a8c41c6bb2a61438cfabd741


حالا دیگه هر شکل جدیدی که بخوایم اضافه کنیم باید تابع volume رو داشته باشه، یک لحظه صبر کنید، همه میدونیم مربع شکل دو بعدی (flat) و عملا حجمی نداره و این interface کلاس Square رو مجبور میکنه که تابعی داشته باشه که هیچ وقت قرار نیست مورد استفاده قرار بگیره و این خلاف اصل ISP است.

برای رعایت اصل تفکیک اینترفیس SolidShapeInterface رو می نویسیم که کلاس های شکل هایی که دارای حجم هستند مثل مکعب از این اینترفیس implement بگیرند


https://gist.github.com/vahid-almasi/fbf42881c5ee99abb8e993693bd7237f

حالا رویکرد کار خیلی بهتر شد، اما مراقب تله type-hinting برای interface ها باشید.


# اصل انطباق پذیری Dependency Inversion Principle

این اصل به ما میگه که کدهای موجود باید به مفاهیم (abstractions) متکی باشند نه به خواص. کلاس های سطح بالا نباید وابستگی مستقیم به کلاس های سطح پایین باشن که در صورت بروز تغییر در کلاس های سطح پایین مجبور باشیم که کلاس های سطح بالا رو هم تغییر بدیم.

توی تعریف ممکنه سخت به نظر بیاد اما بیایم با یک مثال اون رو کاملا قابل درک کنیم:

https://gist.github.com/vahid-almasi/2a33b241f5ca64be318ed8de21d182ce

خب توی کد بالا همون طور که میبینید کلاس PasswordReminder سطح بالا (high level) و کلاس MySQLConnection سطح پایین (low level) است، اما کد بالا خلاف اصل D از S.O.L.I.D است چون PasswordReminder که سطح بالا است به MySQLConnection وابسته است.

یعنی اگر ما بخواهیم دیتابیس انجین خودمون رو عوض کنیم مثلا از mongoDB استفاده کنیم باید کلاس PasswordReminder رو تغییر بدیم که به این ترتیب اصل باز/بسته یا Open-close principle رو هم رعایت نکردیم.

کلاس PasswordReminder باید وابستگیی به این که چه دیتابیس انجینی استفاده میشه نداشته باشه، و باز هم برای درست کردن این بخش از کد ما میتونیم یک اینترفیس بنویسیم چون کلاس های سطح بالا و پایین باید به مفاهیم abstractions وابسته باشند به معنی که وابسته بودن به یک اینترفیس مشکلی ندارد:

https://gist.github.com/vahid-almasi/d0f12c3c359a08257c1467a21c26c08e

اینترفیس ما یک تابع connect دارد و کلاس MySQLConnection هم از این اینترفیس implements میگیرد، و همچنین مستقیما کلاس MySQLConnection در تابعِ constructor کلاسِ PasswordReminder تایپ هینتینگ (type-hinting) میشه، حالا دیگه مهم نیست که برنامه شما از چه نوع دیتابیسی استفاده میکنه و کلاس PasswordReminder به سادگی و بدون مشکل و بدون نقض OCP به دیتابیس وصل میشه.

https://gist.github.com/vahid-almasi/ace359ae4ce7298ee722b9f1b978c9f5

با توجه به کد بالا ، شما می توانید ببینید که هر دو ماژول سطح بالا و سطح پایین با abstraction یا مفهوم به مربوط هستند.

این هم از لینک گیت هاب پروژه برای درک بهتر مطلب، داخل این پروژه سعی کردم که کامیت ها به ترتیب توضیحات باشه


https://github.com/vahid-almasi/solid

منابعی که ازشون توی نوشتن این مطلب استفاده کردم :

https://scotch.io/bar-talk/s-o-l-i-d-the-first-five-principles-of-object-oriented-design

و طبیعتا ویکی پدیا