سولید ( SOLID ) یک کلمه مخفف برای پنچ اصل اولیه طراحی شئ گرا است که رابرت سیسیل مارتین معروف به عمو باب ( uncle bob ) اون رو مطرح کرد.
این اصول زمانی که دست به دست هم میدن، کار گسترش یا اضافه کردن قابلیت های جدید به برنامه و نگهداری یا همون دیباگ یک برنامه رو برای برنامه نویس ها آسان می کنند.
نکته: این فقط یک مطلب کوچیک و تعریفی از مفهوم S.O.L.I.D برای ورود به این مبحث هستش
بیاین یک نگاهی به این کلمه مخفف بندازیم و برای هر کدام یک معادل فارسی شاید یک کم عجیب براشون بنویسیم
اصل مسئولیت واحد یا اصل تک مسئولتی بودن کلاس ها S - Single-responsiblity principle
خب برای درک بهتر این اصول بیاید برای هر اصل توضیح و با یک مثال اون رو برسی کنیم.
حرف s به معنی Single-responsibility Principle یا به صورت خلاصه SRP :
که به معنی این است که یک کلاس باید فقط و فقط به یک دلیل تغییر کند یعنی این کلاس باید تنها یک کار انجام دهد.
بیاید با یک مثال موضوع رو درک کنیم :
برای مثال ما تعدادی اشکال هندسی داریم و میخواهیم که جمع مساحت این شکل ها رو حساب کنیم
خب در قدم اول ما کلاس شکل های مختلف رو میسازیم و پارامتر های الزامی هر کدام از شکل ها رو با تابع constructor سِت میکنیم، در قدم بعدی میریم که کلاس محاسبه کننده مساحت ( AreaCalcuator ) و بعد از اون میریم که منطق برای جمع زدن این مساحت ها رو درست کنیم
برای استفاده از کلاس AreaCalculator به سادگی از این کلاس instance میگیریم و یک آرایه از شکل ها رو به اون پاس میدیم و در آخر خروجی رو نمایش میدیم.
مشکلی که وجود داره تعیین شکل یا نوع خروجی (output) بر عهده کلاس AreaCalculator است یا به عبارت دیگه منطق خروجی داخل کلاسِ AreaCalulator است. خب چی می شد اگر کاربر داده خروجی رو به شکل json یا هر شکل دیگری نیاز داشت؟
در حال حاضر تمام منطق توسط کلاس AreaCalculator داره شکل میگیره و این چیزی نیست که ما به اون SRP میگیم، کلاس AreaCalculator باید تنها مساحت شکل ها رو جمع بزنه و کاری با این که کاربر خروجی Json نیاز داره یا HTML نداشته باشه.
خب پس برای درست کردن این موضوع یک کلاس SumCalculatorOutputter درست میکنیم و منطق این که خروجی چجوری باشه رو داخل این کلاس میگذاریم
بنابر این کلاس SumCalculatorOutputter باید به این شکل کار کنه:
هممم، حالا منطق و شکل داده خروجی در کلاس SumCalculatorOutputter قرار دارد.
به زبان ساده کلاس ها باید به سادگی و بدون نیاز به تغییر قابل گسترش ( extendable ) باشند. بیاید یک نگاه به کلاس AreaCalculator به خصوص تابع sum.
اگر ما بخواهیم که این تابع قابلیت جمع زدن مساحت شکل های بیشتری رو برای ما داشته باشه باید شروط (if/else) بیشتری تعریف کنیم، که این برخلاف Open-closed principle است.
پس بهتره منطق حساب کننده مساحت هر شکل رو از تابع sum بیرون بیاریم و به کلاس شکل ها اضافه اش کنیم
همین کار هم باید برای کلاس Circle انجام بشه و تابع area به این کلاس اضافه بشه. خب برای محاسبه جمع مساحت هر شکل ارائه شده باید به همین اندازه ساده باشه:
حالا می تونیم هر شکل دیگه ای که نیاز داریم اضافه کنیم بدون این که اصل دوم SOLID رو زیر پا بزاریم. خب حالا یک مشکل دیگه وجود داره، چطور میشه فهمید که شئی ( object) که به AreaCalculator پاس داده میشه آیا یک شکل هندسیه؟ اگر هم شکل باشه آیا تابعی به نام area داره؟
نوشتن یک interface بخش جدایی ناپذیر از S.O.L.I.D است، پس باید یک interface بنویسیم و هر شکل جدیدی که قرار بود اضافه بشه رو باید از این اینترفیس implements کنیم، در کد پایین این کار برای کلاس Circle انجام شده:
و در اخر در تابع sum در کلاس AreaCalculator ما چک می کنیم که همه شکل ها و شی هایی که به ما داده شده از ShapeInterface اینستنس (instance) گرفته شده باشن، در غیر این صورت برای خروجی یک Exception ارسال میکنیم
فرض کنیم (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 گرفته شد
و داخل کلاس SumCalculatorOutputter
اگر ما تلاش کنیم که یک همچین چیزی اجرا کنیم
برنامه بدون مشکل اجرا میشه، اما زمانی که تابع HTML رو از output2 بخواهیم ما یک E_NOTICE میگیریم که میگه یک آرایه ای باید تبدیل به رشته (string) بشه.
برای رفع این مشکل باید به جای خروجی ارایه در تابع sum در کلاس VolumeCalculator ، آن را ساده برگیدانیم
حالا دیگه خروجی میتونه یک عدد کسری یا صحیح یا ... باشه
یک توسعه دهنده نباید مجبور implement کردن یک interface بشه که از اون استفاده نمیکنه یا توسعه دهنده نباید به اجبار وابسته به تابعی باشه که براش بی استفاده اس.
یا به عبارت دیگر به جای یک interface بزرگ باید چندین interface تفکیک شده و کوچک داشته باشیم
خب ما هنوز میخوایم از همون شکل ها مثال بیاریم، بیایم محاسبه حجم رو هم اضافه کنیم و اول از همه از ShapeInterface شروع کنیم
حالا دیگه هر شکل جدیدی که بخوایم اضافه کنیم باید تابع volume رو داشته باشه، یک لحظه صبر کنید، همه میدونیم مربع شکل دو بعدی (flat) و عملا حجمی نداره و این interface کلاس Square رو مجبور میکنه که تابعی داشته باشه که هیچ وقت قرار نیست مورد استفاده قرار بگیره و این خلاف اصل ISP است.
برای رعایت اصل تفکیک اینترفیس SolidShapeInterface رو می نویسیم که کلاس های شکل هایی که دارای حجم هستند مثل مکعب از این اینترفیس implement بگیرند
حالا رویکرد کار خیلی بهتر شد، اما مراقب تله type-hinting برای interface ها باشید.
این اصل به ما میگه که کدهای موجود باید به مفاهیم (abstractions) متکی باشند نه به خواص. کلاس های سطح بالا نباید وابستگی مستقیم به کلاس های سطح پایین باشن که در صورت بروز تغییر در کلاس های سطح پایین مجبور باشیم که کلاس های سطح بالا رو هم تغییر بدیم.
توی تعریف ممکنه سخت به نظر بیاد اما بیایم با یک مثال اون رو کاملا قابل درک کنیم:
خب توی کد بالا همون طور که میبینید کلاس PasswordReminder سطح بالا (high level) و کلاس MySQLConnection سطح پایین (low level) است، اما کد بالا خلاف اصل D از S.O.L.I.D است چون PasswordReminder که سطح بالا است به MySQLConnection وابسته است.
یعنی اگر ما بخواهیم دیتابیس انجین خودمون رو عوض کنیم مثلا از mongoDB استفاده کنیم باید کلاس PasswordReminder رو تغییر بدیم که به این ترتیب اصل باز/بسته یا Open-close principle رو هم رعایت نکردیم.
کلاس PasswordReminder باید وابستگیی به این که چه دیتابیس انجینی استفاده میشه نداشته باشه، و باز هم برای درست کردن این بخش از کد ما میتونیم یک اینترفیس بنویسیم چون کلاس های سطح بالا و پایین باید به مفاهیم abstractions وابسته باشند به معنی که وابسته بودن به یک اینترفیس مشکلی ندارد:
اینترفیس ما یک تابع connect دارد و کلاس MySQLConnection هم از این اینترفیس implements میگیرد، و همچنین مستقیما کلاس MySQLConnection در تابعِ constructor کلاسِ PasswordReminder تایپ هینتینگ (type-hinting) میشه، حالا دیگه مهم نیست که برنامه شما از چه نوع دیتابیسی استفاده میکنه و کلاس PasswordReminder به سادگی و بدون مشکل و بدون نقض OCP به دیتابیس وصل میشه.
با توجه به کد بالا ، شما می توانید ببینید که هر دو ماژول سطح بالا و سطح پایین با abstraction یا مفهوم به مربوط هستند.
این هم از لینک گیت هاب پروژه برای درک بهتر مطلب، داخل این پروژه سعی کردم که کامیت ها به ترتیب توضیحات باشه
منابعی که ازشون توی نوشتن این مطلب استفاده کردم :
و طبیعتا ویکی پدیا