SAJAD noroozi
SAJAD noroozi
خواندن ۱۷ دقیقه·۴ سال پیش

شی گرایی بدون خونریزی یاS.O.L.I.D برای آنان که می اندیشند

مقداری پیش درآمد:

از کجا و چطوری میشه گفت یه کد تمیزه و درست نوشته شده اصن قصه چیه به یه توسعه دهنده یه کد نشون میدی و میگه وای وای وای چه کد آزار دهنده و کثیفی و همون کد به یکی دیگه نشون میدی میگه نه خوب نوشته شده ! اصن کد تمیز یعنی چی؟ زمانی که داشتم برای پیداکردن موضوع پست جدید میگشتم دوتا موضوع توجهم رو جلب کرد یکی Tensorflow در اندروید و یکی هم معماری MVI ولی همین سوال کد تمیز یعنی چی؟ یه دفعه منو کشوند به سمت نوشتن این پست

خب تا اونجایی که من بلدم کد تمیز پارامترهای خاص خودشو داره قابل تست بودن داشتن کامنت و نامگذاری های قابل درک و.... از نظر من کدی تمیزه که بشه طیف وسیعی از برنامه نویس های پلتفرم و زبان بدون تغییر ساختار و گیج شدن توسعه اش بدن این توسعه میتونه شامل اضافه کردن ویژگی های جدید برداشتن بخشی از کد به عنوان ماژول و.... باشه البته من نه صاحب نظر میدونم خودمو نه اونقدر سابقه و تجربه در خودم میبینم که بخوام بیام و یکی یکی توضیح و تفسیر ارائه بدم همین موضوع منو کشوند سمت کتابی که دوسال پیش خوندم The Clean Coder یا کد نویس تمیز که ترجمه هم شده و چقدر هم کتاب خوبی بود یکی از چیزیهایی که بی قید و شرط درکتاب ذکر شده که هربرنامه نویس عاقل و حرفه ای باید بلد باشه SOLID هست. ربطش به کد تمیز چیه؟ خب SOLID کمک میکنه یونیت تست رو راحتتر بنویسیم توسعه پذیری به شکل اصولی انجام بشه و تا حد زیادی کد خوانا تر بشه( این آخری رو براساس تجربه گفتم)

خب من رفتم سراغ SOLID و تمام هم و غمم رو گذاشتم سر این موضوع که SOLID رو تا جایی که میتونم توی کد نویسیم رعایت کنم و بکار بگیرم نتیجه این شد که علاوه بر شفاف شدن پوستم دیدم خوانایی کدم برای خودم وقتی بعد از مدتها برمیگردم سر یه ماژول بالاتر رفته و عملا با تفکیک و تکنیک هایی که سالید داره کد واقعا تمیز تر میشه بگذریم از بحث تست نوشتن و اینکه تا چه اندازه قابل تست بودن کد با SOLID بالا میره .

البته اینو بگم میشه به جرات گفت حتی توی بهترین نرم افزارهای دنیا هم سالید به شکل صددرصد و ایده آل خودش پیاده سازی نمیشه پس اگر فردا روزی رفتید پیش برنامه نویس ارشد تیم و ازش ایراد گرفتید چرا فلان فانکشن اصل دوم رو نقض میکنه ممکنه خیلی سخت باهاتون برخورد کنه در کل سالید وحی منزل نیست خیلی کمک میکنه که کد خوانا تر باشه قابل تست تر باشه و قابل توسعه تر شه ولی به جرات میتونم بگم توی این موضوع مته به خشخاش گذاشتن خیلی کار جالبی نیست ولی بطور کلی رعایت کردنش خیلی خیلی کمک میکنه و اگر قابل اندازه گیری باشه معمولا نه صددرصد استفاده میشه نه صفر بلکه کد هر نرم افزار حرفه ای بسته به نوع پروژه و توسعه دهنده اش میتونه خیلی کم یا خیلی زیاد رعایتش کنه

خلاصه دیدم بهتره یه نیم نگاهی بهش داشته باشم و اینجا بنویسم درباره اش هرچند منابع فارسی خوب هم براش زیاده ولی من گاهی دوست دارم تفسیر خودم رو از یه موضوع بیان کنم :

S . O . L . I . D:

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

  • Single responsibility principle
  • Open–closed principle
  • Liskov substitution principle
  • Interface segregation principle
  • Dependency inversion principle

اصل تک مسئولیتی:

اصل تک مسئولیتی میگه که اگر شما قراره مقادیری رو جمع کنید یک تابع داشته باشید که فقط کارش جمع کردنه ! اگر قراره یک Json پارس شه یک فانکشن فقط مسئولیت پارس کردن داشته باشه فقط یک کار! همین. بدون اینکه هیچ اطلاعی داشته باشه از اینکه این داده ها از کجا اومده و خروجی قراره کجا بره ( یه جورایی در راستای انکپسولیشن کلاس هم هست ) شما نباید کد هوشمند بزنید قرار نیست تابع شما علاوه بر جمع کردن دوتا مقدار کنترل کنه که مقدار از یه حدی بیشتر یا کمتره قرار نیست بعد از جمع کردن نتیجه با چیزی مقایسه شه قرار نیست درون تابع چیزی رو کنترل کنید فرض کنید چنین کاری کردید چندمدل دیتا باید سمتش پاس داده شه تا شرایط مختلف توی یونیت تست مورد بررسی قرار بگیره؟ خیلی زیاد بازهم ممکنه پوشش صددرصدی برای عملکرد صحیح محسوب نشه. هرکدوم از این کارها باید تفکیک شه یک تابع برای مقایسه یک تابع برای اعتبار سنجی مقادیر ورودی و یک تابع برای جمع کردن اینجوری برای هر تابع 3-4 مدل ورودی توی یونیت تست احتمالا بتونه به شما اطمینان خاطر بیشتری بده که کدتون تا حد زیادی عملکرد مناسبی داره به عبارتی هرچه تابعی که نوشتید باهوش تر باشه کنترل و تست کردنش سخت تره پس عملا با رعایت این قانون یک قدم به بهتر تست نوشتن نزدیکتر میشیم.

یه نگاهی به شبه کد پایین بندازید

class Customer { public void Add() { try { // Database code goes here } catch (Exception ex) { Log.d(&quotadd function exeption&quot,ex.geterror.tostring()); } } }

اگر دقت کنید میبینید فانکشن اضافه کردن مشتری علاوه بر انجام کار اصافه کردن مشتری کار لاگ انداختن رو هم انجام میده و این غلطه !

زمان نوشتن بلاک های کدتون حتما اصل KISS رو بیادداشته باشید keep it simple, stupid

به هیچ وجه سعی نکنید همه کارها رو توی یک بلاک کد انجام بدید این کار باعث میشه زمان Debug کردن به مشکل بخورید و از اون مهمتر تست نوشتن رو بشدت دشوار میکنه پس باز هم برای تاکید میگم هر کلاس و تایع دقیقا یک کار معین و مشخص تمام! علاوه بر اون با اینکار میتونید به reusability کدتون کمک کنید مثلا نیاز نیست هربار یک فرآیند رو جاهای مختلف تکرار کنید و اصطلاحا کپی کنید از طرفی اگر تو دیباگ مشکلی باشه میتونید فقط با یک تغییر در یک کلاس کل مشکل ( اینجا لاگ زدن) رو برطرف کنید حالا فرض کنید جاهای مختلف لاگ زدن رو کپی کرده باشید احتمال خیلی زیادی هست بالاخره یه جا از قلم بیافته و کدتون با باگ منتشر شه

class FileLogger { public void Handle(string error) { Log.d(&quotadd function exeption&quot,ex.geterror.tostring()); } }


class Customer { private FileLogger obj = new FileLogger(); publicvirtual void Add() { try { // Database code goes here } catch (Exception ex) { obj.Handle(ex.ToString()); } } }

اصل باز بسته :


اسم عجیبی هست برای یک اصل طراحی شی گرا!

خب فرض کنید قراره شما تابعی بنویسید که یک کد رو برای کاربر ایمیل کنه مدتی بعد تصمیم گرفته میشه که کاربر میتونه انتخاب کنه کد تایید اعتبارش به جای ایمیل براش SMS شه چه کار باید کرد؟

تجربه من میگه اکثرمواقع اصل اول و دوم باهم نقض میشه یعنی برنامه نویس میاد توی همون تابعی که قراره کد تایید اعتبار رو ایمیل کنه یه شرط درنظر میگیره که تحت شرایط یک تابع ارسال ایمیل صدا زده شه و تحت شرایط دو تایع ارسال SMS فراخوانی شه یعنی توی یک بلاک کد دو تا کار امکان انجام دارند! دوتا بلاک کدی که احتمالا بخش زیادی شون مشابه هست از طرفی شما تابعی رو که قبلا نوشتید دارید تغییر میدید به شکلی که یک تایپ به ورودی های اضافه میکنید و یک شرط مطمنید این تغییر فقط رو بخشی که باید اثر میذاره ؟ اگر بدون اطلاع دارید رو کد برنامه نویس دیگه ای کار میکنید این احتمال بشدت زیاده که یکی دو خط تغییر دو سه ساعت کار رو دستتون بذاره تا همه چیز رو مطابق شرایط فعلی تغییر بدید و هرچه شما تغییر ایجاد کنید باز هم جاهایی جدیدی هست که باید با شرایط جدید منطیق بشن درون یک چرخه بی نهایت یا سیاه چاله ای افتادید که اگر کد یک برنامه نویس دیگه باشه و پروژه نسبتا بزرگ یه ریفکتور خیلی زیبا براتون به ارمغان میاره چیکار باید کرد ؟

اسم Open - closed با یک جمله قابل تشریح هست : "بلاک های کد (یا نهاد های نرم افزاری یا ماژولها یا کلاس ها و...) دربرابر اکستنشن و توسعه باز هستند و در برابر اصلاحات بسته". شما اجازه اصلاح تابع برای ارسال SMS رو ندارید اما اجازه اضافه کردن تابعی برای ارسال SMS رو دارید ! و البته باید کدی که نوشته شده به شکلی باشه که "اجازه توسعه بدهد و نیاز به تغییر دادن نداشته باشد" یعنی هم باید زمان نوشتن کد اونها رو به شکل بسته بنویسیم هم زمان اضافه کردن قابلیت و تغییر اون ها رو بسته نگه داریم اگر کدی به ما اجازه بده که با ارث بری و... قابلیت به برنامه اضافه کنیم بسته است و اگر کدی این اجازه رو بده ما نده باز هست

حس میکنم یکم پیچیده شد! بیاید یه جور دیگه به موضوع نگاه کنیم فرض کنید ما یه اینترفیس authentication code داریم این اینترفیس کلا کارش جابجایی یک رشته هست که باید برای کاربر ارسال شه (چیزی که فانکشن درون اینترفیس انجام میده گرفتن کد اعتبار سنجی از یک کلاس و دادنش به کلاس دیگه هست همین !)خب حالا ما یک کلاس ارسال کد داریم که فقط ایمیل میفرسته میایم و با تغییر دادنش و اضافه کردن if یا switch بررسی میکنیم که کد برای کاربر ایمیل شه یا SMS و بعدش هم رخ عقاب میگیریم که به به تسک داون شد :|

برای اینکار باید اینترفیس تغییر کنه و کلاسهای پیاده سازی کننده اش هم تغییر کنند اینجوری ما یک قطعه کد که اجازه توسعه میده ولی بسته است رو داریم باز میکنیم و این غلطه خب این کار به وضوح اصل دوم رو نقض میکنه و اصل اول رو زیر پا له میکنه ! چون هم تو کلاس تغییر ایجاد کردیم هم اینکه یک بلاک کد هوشمند داریم که تصمیم میگیره از چه طریق کد ارسال کنه! چیکار کنیم؟

ما یک کلاس جدید بوجود میاریم برای ارسال کد با SMS و باز هم اینترفیس توش Implement میشه و کد ارسال SMS درونش نوشته میشه اینجوری دوتا کلاس داریم توی کد قبل تغییری ایجاد نکردیم (که یک کد بسته بوده) بلکه قابلیت های تعیین شده رو گسترش دادیم (و بازش نکردیم) این به معنای واقعی کلمه رعایت اصل اول و دومه ! و اگر احیانا به بخش هایی از کلاس ارسال ایمیل نیاز داریم میتونیم از همون کلاس ارسال ایمیل اکستندش کنیم تا توابعی که از کلاس ایمیل نیاز داریم رو اینجا بتونیم داشته باشیم و توش تغییر ایجاد کنیم و باهاش SMS ارسال کنیم بدون اینکه ساختار ورژن قبلی کد رو با خطر ناپایداری و اون چرخه بی پایان که بالاتر گفتم مواجه کنیم چرا اینترفیس؟ بعدا میگم ولی تا همینجا توضیحات کافی هست
مثال سوم ساده ترین مثال هست رو با کد توضیح میدم فرض کنید نیاز هست بسته به نوع زبان انتخابی کاربر در یک مرحله ما بهش پیغام خطا نشون بدیم


class ٍError { public say(lang) { if (lang == 'pr') { return 'خطا'; } else if (lang == 'en') { return 'Error'; } }} Error obj = new Error; console.log(obj.say('pr'));

تو قطعه کد بالا یک کد کاملا باز رو می بینید این ساختار برای اضافه کردن زبان سوم به هیچ عنوان راهی جز باز کردن کلاس نمیده پس عملا قابلیت توسعه رو از ما صلب کرده حالا چطوری باید نوشته شه ؟ به شکل زیر؟

class ٍError { public say(lang) } public class persian { .... return 'خطا'; } public class English{ ..... return 'Error'; } Error obj = new Error; console.log(obj.say(persian))

نکته : پیشرفت خوبیه برای اینکه اصل دوم نقض نشده ولی من باز هم میگم نه چرا؟ چون من میخوام توی تمام زبان ها حتما یک تعریف مشخص برای خطا ارسال بشه از طرفی اونقدر حرفه ای نیست که ما کلاسهایی شبیه هم داشته باشیم که خروجی های یکسانی ازشون میگیریم ولی ساختار و قالبی نداشته باشن که تضمین کنه خروجی به شکلی که من میخوام ارسال بشه از طرفی هر تغییر تو کلاسهایpersian یا english یا تغییر پیاده سازی زبانهای جدید میتونه روی Error اثر گذار باشه در حالی که Error یه کلاس سطح بالا و بالادستی هست این رو نگه دارد برای اصل پنجم به این موضوع برمیگردیم و حسابی بهش رسیدگی میکنیم

اصل لیسکوف:


این اصل خیلی ساده تر و سر راست تره اگر شما کلاسی داشته باشید که از یک کلاس دیگه ارث بری کرده باشه (extend) شده باشه باید بتونید هرجا از کلاس والد استفاده کردید کلاس فرزند رو جایگزین کنید به عبارتی وقتی کلاسی رو می نویسید باید حداقل ویژگی ها رو شامل بشه و ویژگی هایی که ممکنه گاهی وجود نداشته باشن رو ازش حذف کنید و درکلاس های extend شده اش قرار بدید

فرض کنید ما کلاس A رو داریم که شامل یک فانکشن edit هست در جاهای مختلف ازش استفاده میکنیم و میخوایم حالا کلاسی بسازیم که editable نباشه و اسمش رو بذاریم B تفاوت اینه که کلاس اول قابل تغییر و ادیت و کلاس دوم غیرقابل تغییر و ادیت هست! و کلاس دوم یعنی B از کلاس اول یعنی A ارث بری میکنه (Extend) شده وقتی کلاس B رو در برنامه جایگزین A کنید وقتی نیاز به فراخوانی فانکشن Edit باشه یه مشکل بزرگ داریم اونم اینه که تو کلاس B هیچ فانکشن editی وجود نداره و کاربر با EXception روبرو میشه !

راه حل چیه؟ اینکه از اول توی کلاس A ادیت نداشته باشیم بلکه فقط ویژگی هایی رو در کلاس A داشته باشیم که بی کم و کاست در تمام کلاسهای فرزندش تکرار بشن و وقتی به چیزی شبیه edit نیاز بود یک کلاس جدید با این ویژگی ایجاد کنیم که از A ارث بری میکنه مثال زیر رو ببینید:

public class A { // this is Parent class void read() void getinstance()}
public class c extends A{ // editable One void edit() }
public class B{ // not editable one .... }
A a=new A() a.read()
را میتوان به اشکال زیر نوشت بدون اینکه مشکلی بوجود بیاید
b.read()
c.read()

اصل تفکیک اینترفیس ها :

این اصل از ساده ترین اصول این لیست هست که میگه اگر شما یک interface رو در یک کلاس implements کردید لزوما باید تمامی فانکشن های تعریف شده در interface در این کلاس پیاده سازی داشته باشند این اصل ساده میگه که اگر شما اینترفیسی دارید که توی چند کلاس اون رو پیاده سازی میکنید و توی هر کلاس فقط بعضی از فانکشن ها دارای کد و پیاده سازی اند شما مشکلی به نام fat interface دارید و به عبارتی ساختاری در کلاس شما وجود داره که به اون نیاز ندارید

به عنوان مثال اگر یک اینترفیس داشته باشیم که شامل 4 فانکشن show-edit-save-read باشه تمام کلاس ها پیاده کننده باید از هر 4 فانکشن استفاده کنند اگر فقط یک کلاس نیاز به edit نداشته باشه باید edit در اینترفیس دیگری قرار بگیره و کلاس هایی که بهش نیاز دارند هر دو اینترفیس رو پیاده سازی کنند

در ساده ترین تعریف اگر فانکشن F رو در اینترفیس A دارید که بعضی کلاسهای پیاده کننده اینترفیس بهش نیاز ندارند و بدنه اون خالی میمونه باید F رو به اینترفیس B منتقل کنید هرجا نیاز به پیاده سازی F بود هر دو اینترفیس A و ‌B رو پیاده سازی کنید و هرجا نیاز به F نبود فقط A رو پیاده سازی کنید همین!

وارونگی وابستگی :


این شاید پیچیده ترین اصل این 5 تا باشه پس باید یکم با دقت بیشتری بررسی شه

در اصل دوم من یک راه حل ارائه دادم و گفتم این حداقل نیاز ها رو برطرف میکنه ولی پیاده سازی کاملا صحیحی نیست فقط حداقل اصل دوم رعایت میشه و راه حل دومی وجود داره که بهتره و در پایان مقاله درباره اش بحث میکنیم ! دلیل حداقلی بودنش وابستگی کلاس سطح بالاتر به کلاس های سطح پایینه ! این یعنی چی؟ و راه حل بهتر چیه؟

انتزاع: ما بخش های در برنامه داریم شامل abstract calsses و intrface که اینها به خودی خود اجرا نمیشن بلکه قالب یک کلاس قابل اجرا میشن یعنی میشه اونها رو به عنوان ساختار به یک کلاس معرفی و پیاده سازی کرد کاری به تفاوت این دو ندارم موضوع بیشتر انتزاع هست! در انتزاع ما تصور میکنیم این قاعده و ساختار باید باشد فارغ از نوع پیاده سازی جزییات مثلا در اینترفیس مثال اصل دوم باید showError در تمامی کلاس های پیاده کننده وجود داشته باشد فارغ از نوع پیاده سازی جزییات هر کلاس ( در این مثال فارغ از نوع زبان)

کلاس سطح بالا و پایین : در ساده ترین تعریف ممکن کلاس های سطح بالا تعیین کننده روال ها در برنامه اند اینکه داده از کجا بیاید به کجا برود نتیجه این کار به کجا فرستاده شود مثلا اگر نیاز به حذف یک رکورد از دیتا بیس باشد کلاسهای سطح بالا تصمیم میگیرند رکورد وارد شده به کجا برود نتیجه موفقیت آمیز یا عدم موفقیت آن پس از بازگشت به کجا ارسال شود ! در اندروید ViewModel ها مثال خیلی خوبی برای کلاس های سطح بالا هستند آنها مستقیما به دیتا بیس کوئری ارسال نمیکنند یا مستقیما یک Json دریافتی را parse نمیکنند بلکه تصمیم میگیرند چه بخش از json پارس شده که الان به شکل یک آبجکت است به کجا برود در مقابل کلاسهایی که کوئری میزنند دریافت و ارسال اطلاعات را انجام میدهند و.... همگی کلاسهای سطح پاییند که به نوعی پیاده کننده نیازهای کلاسهای سطح بالاترند

حال اگر در یک کلاس سطح بالا نیاز باشد که از کلاس دیتا بیس فانکشن ذخیره یک داده صدا زده شود باید در کلاس سطح بالا یک آبجکت از کلاس سطح پایین ساخته شود این یعنی وابستگی کلاس سطح بالا به کلاس سطح پایین! و هر تغییر در کلاس سطح پایین نیاز به اعمال تغییر در کلاس سطح بالا دارد! اصل دقیقا بیانگر این است که این رابطه باید تغییر کند!

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

حالا بیاید برگردیم به مثال آخر اصل دوم که گفتم به دلایلی این راه حل خوب نیست و تغییرات کلاس های پایین دستی مثل Persian میتونه باعث شه که شما برید و تو کلاس بالا دستی ٍError تغییرات اعمال کنید تو مثال زیر راه حل کامل و درستش رو میبینید که علاوه بر اصل دوم اصل پنجم هم توش رعایت شده کلاسهای زبانها با کمک یک لایه انتزاع یعنی LanguageInterface پیاده سازی شدن و کلاس Error به جای وابستگی به کلاسهای مختلف مثل persian و english به یک اینترفیس ( انتزاع ) وابسته است

interface LanguageInterface { public String showError()} class ٍError { public say(LanguageInterface lang) } public class persian implements LanguageInterface { public String showError(){ return 'خطا'; } } public class English implements LanguageInterface{ public String showError(){ return 'Error'; } } Error obj = new Error; console.log(obj.say(persian))

خب همونطوری که مشخصه وقتی میخوای تو کلاس Error یک ارور نمایش بدم نیاز به تعریف ابجکت از کلاس پایین دستی نیست چرا؟ چون English و persian به شکل زیر تعریف میشن! اینجوری کلاس Error دیگه به Persian یا English وابسته نیست بلکه به LanguageInterface وابسته است

LanguageInterface persian=new Persian() LanguageInterface english=new English()

نکته مهم : راه حل های متفاوتی برای پیاده سازی وارونگی وابستگی هست که یک مدلش رو دربالا دیدید ولی متدی هست که فکر کنم همه باهاش آشناییم به نام Dependency Injection اینجوری که شما تو یه لایه جداگانه آبجکت های کلاسهای سطح پایین رو ایجاد میکنید و آبجکت ها به شکل ورودی های از قبل ایجاد شده به کلاسهای سطح بالا تزریق میشن اینجوری نیازی هم به Interface به این شکل نداریم یکی از مهمترین کتابخونه های برای پیاده سازی این متد Dagger هست که توی جاوا و اندروید حسابی به کار میاد!

سخن پایانی:

بالا گفتم که منابع فارسی زیادی برای توضیح این اصول وجود داره اصولی که برای برنامه نویسی شی گرا ضروری هستند و هر برنامه نویسی که با زبانهای شی گرا کار میکنه باید بتونه اینها رو تشخیص بده به کار ببنده و پیاده سازی کنه دلیل نوشتن این متن شرح و بسط موضوع از نگاه خودم بود قطعا ولی دلیل مهمترش اینه که خیلی وقتها برنامه نویسهایی میبینم که شاید بشدت آپدیتن میتونن به شما بگن ورژن آخر کتابخانه فلان دقیقا چنده و مثلا تو کنفرانس آخر Google IO چه چیزی معرفی شده و چیکار میکنه ولی وقتی سراغ اساسی ترین اصول توی کدشون بگردید ناامید میشید پس اصول خیلی مهمن:) امیدوارم براتون راهگشا باشه!

از وقتی که برای مطالعه گذاشتید متشکرم

اندرویدjavasolidobjecorientedandroid
کمی اندروید دولوپر کمی کنجکاو
شاید از این پست‌ها خوشتان بیاید