تو همه مصاحبه های این چند سال یکی از سوالایی که همش میپرسن اینه: "با اصول SOLID کار کردی؟ آشنایی داری؟". بعد برای اینکه مطمئن بشن میگن مثلا مفهوم Open-closed رو توضیح بده ببینم بلدی عمو؟ ? حالا گیرم که اصلا شما هم جوابشو هم بدی، مگه خوندن یه مفهوم به معنی فهمیدن یا به کار بردنش توی دنیای واقعی میشه؟
از طرف دیگه چند نفر از بچه هایی که دارم منتورینگ شون میکنم این براشون خیلی سوال شده بود و مخصوصا تو این ۲-۳ هفته زیاد از من میپرسیدن، چون دقیقا اونا هم براشون پیش اومده که توی مصاحبه بحثش وسط کشیده شده و مشکل خوردن. جالب تر اینکه بعد از اون توی محل کار هم هیچوقت دیگه هیچکس نمیاد بگه بر اساس اصول سالید فلان و بهمان.
این شد که گفتم باید برم بررسی کنم و ببینم این اصول سالید توی کار واقعی چطور به کار میاد، چطور میشه مفاهیمش رو پیاده سازی کرد، چجوری باشه بهتره؟ این مطلب حاصل این مطالعات منه.
پنج اصل SOLID که هر کدوم از این حروف حرف اول یه عبارت خفنه که اگه بخونید پشماتون میریزه. بعضیاش هم واقعا خفن هست ولی کلا درگیر اصطلاحات نشید هیچوقت. همیشه یه عالم چیز وجود داره که اسم خفنی داره ولی خیلی سادست. از بحث دور نشیم، کلا مقصود اینه که اسمای اینا نترسونه شما رو. همه شون یه سری چیزای ساده هستن. حالا میبینیم.
این پنج تا اصل اینان:
اسمارو دیدید حال کردیدا ? ولی خیلی خبری نیست. در واقع تمام این ژانگولر ها و ... برای اینه که کمک کنه مدیریت کدتون راحت تر بشه و همچنین اجزای سیستم هم Reusable تر بشه.
یه نکته اساسی در مورد این اصول اینه که توی چارچوب Object Oriented معنی پیدا میکنن و خیلی از چیز هایی که توی این practice ها بهش اشاره میشه از مفاهیم شئ گرایی درش استفاده شده. پس این نکته رو توی ذهنتون داشته باشید. البته این مطلب در مورد نحوه پیاده سازی این اصول توی React هست که تقریبا یه دنیای Functional هست و دقیقا مقابل OOP قرار میگیره ولی نگران نباشید، چیزی که از OOP بودن این اصول مهم تره، مغز کلامشونه که خیلی هم ساده و قشنگه.
نکته بعد اینکه من اینجا میخوام روش استفاده از این اصول توی React رو بگم ولی این مفاهیم خیلی کلی هستند و توی هر زبان برنامه نویسی و تکنولوژی و هر چیزی امکان پیاده سازی دارن.
این اصل میگه که هر کامپوننت یا تابع باید فقط یک کار رو انجام بده. تقریبا شبیه مفهوم Pure Function تو دنیای Functional هست (نه دقیقا ولی حدودا). میدونیم که میشه توی ری-اکت برای هرچیزی یک کامپوننت ایجاد کرد و نکته ای که این اصل بهش اشاره میکنه اینه که سعی کن هر کامپوننت یا تابعی فقط و فقط یک کار رو انجام بده و نه بیشتر. اینطوری مدیریتشون راحت تره، تست کردنشون لذت بخش تره، خوندنشون هم مثل خوندن کتاب قصه شیرین میشه.
بین تمام اصول SOLID این اصل به نظرم از همه شون ساده تره و در حین حال اگر درست پیاده بشه چنان تاثیری روی کار میذاره که به وجد میاید. قبل تر ها (حدود ۳ سال پیش) یه مطلبی منتشر کردم در مورد بهبود کد React Native فکر کنم. یکی از چیز هایی که اونجا هم بهش اشاره کردم همین موضوع بود. البته اون زمان جوون بودم و به اندازه الانم نمیفهمیدم دلیل خیلی چیز ها رو ولی اشاره کرده بودم که کامپوننت های بزرگتر رو باید شکست به کامپوننت های کوچیک تر با مسئولیت کمتر.
توی ری-اکت یه فرمول ساده و باحال وجود داره برای اینکه بخوایم کار شکستن کامپوننت ها به اجزای کوچکتر رو انجام بدیم و اونم این سه مرحله هست:
مثال:
این کامپوننت رو در نظر بگیرید:
این کامپوننت داره چندتا کار انجام میده:
پس کامپوننت بزرگی محسوب میشه، چرا بزرگه؟ چون بیشتر از یک کار داره انجام میده. من کد این کامپوننت رو که ندارم ولی فرض میکنیم احتمالا یه چیزی شبیه این باشه:
خب چه خبره؟
خب پس با یه کامپوننت بزرگ طرفیم، گرچه تو این مثال خیییلی هم بزرگ نیست ولی به اندازه ای هست که بشه شکستش به تیکه های کوچک تر. طبق اصل Single Responsibility هر چیزی باید فقط یک کار انجام بده پس بریم این کامپوننت رو بشکنیم به قطعات کوچک تر.
توی قدم اول میتونیم برای دریافت لیست برند ها یه custom hook ایجاد کنیم:
حالا کامپوننت BrandsSidebar یکم کوچیک تر شد ولی همچنان داره دو تا کار انجام میده (فیلدر کردن، نمایش دادن آیتم ها)
یکی دیگه از اتفاقاتی که با بوجود اومدن useBrands میوفته اینه که این functionality رو reusable میکنه و میتونیم جاهای دیگه هم ازش استفاده کنیم.
کار بعدی اینه که برای هر کدوم از سطر هایی که قراره رندر بشه یه کامپوننت مجزا بوجود بیاریم:
خب همونطور که میبینید، کامپوننت هر برند رو هم جدا کردیم. میدونم که اینجا یه checkbox داریم که براش حتما باید یه eventListener داشته باشیم ولی برای مثال های اینجا خیلی مهم نیست که در نظر نگرفتمش.
یکی دیگه از کار هایی که میکرد هم ازش کم شد و فقط موند فیلتر کردن دیتا ها. میتونیم برای فیلتر هاش هم یه utility بنویسیم که برامون اینکارو انجام بده:
چقد داره خلوت میشه مگه نه؟ این تا اینجا خوبه. یه بهبود ریز دیگه هم میشه داد که مثلا لاجیک filter کردن رو هم برد توی یک هوک مجزا و ازش استفاده کرد. این قسمت برای طولانی نشه من اونو نمینویسم دیگه. ولی اگه دوست داشتید خودتون میتونید انجامش بدید. کار سختی نیست.
ما یه کامپوننت بزرگ رو اومدیم ریز کردیم به کامپوننت ها و اجزای کوچک تر، حواستون باشه به این موضوع که میشه اینکارو تا بی نهایت انجام داد ولی کار درستی نیست. تا جایی باید کامپوننت ها ریز بشن که هنوز معنی دار باشن. بعد از این کار مدیریت و تست کردن و خوندن این کد بسیار بسیار ساده تر از حالت اولیه اش هست. این چیزیه که توی تجربه و پروژه های بزرگ میشه کاملا حسش کرد.
ترجمه فارسیش دقیق این نمیشه? ولی بهترین چیزی که به ذهنم رسید همین بود. یعنی چی حالا؟ یعنی کامپوننت ها باید برای گسترش و هندل کردن حالت های دیگه آماده باشن در عین اینکه نیازی به تغییر دادن ساختار درونی خودشون نداشته باشن. یعنی اگر نیاز بود یه کامپوننت چندتا حالت خاص رو هندل کنه نباید به ساختار اصلی کامپوننت دست بخوره ولی تغییرات هم بتونه هندل کنه. یعنی شما فرض کن از پراید بخوای در حد بوگاتی کار بکشی بدون اینکه بخوای تغییری توی موتورش بدی ? با مثال براتون خوشگل جا میوفته
این مثال رو از medium پیدا کردم خیلی عالی بود:
بر اساس اینکه کاربر توی کدوم صفحه باشه لینکی که توی هدر هست تغییر میکنه. این حالت برای خیلی جاها بوجود میاد. مثلا زمانی که کاربر لاگین هست و زمانی که نیست. در هر صورت ما از Header توی دو صفحه Home و Dashboard استفاده کردیم. حالا وای به روزی که بخوایم یه شرط دیگه به اینا اضافه کنیم. میبینید؟ هی مجبوریم بریم توی Header رو تغییر بدیم. پس در واقع داریم هی دست به آچار میشیم و موتور پراید رو تقویت میکنیم.
این مفهوم میگه که باید کامپوننت ها به نحوی پیاده سازی بشن که این اتفاق نیوفته. خب چطوری میشه حلش کرد؟ یه pattern خود React پیشنهاد داده به اسم Render Props. میتونید چیز هایی که میخواید بر اساس prop تعیین و رندر بشه رو از اول از طریق prop به کامپوننت پاس بدید و کامپوننت هر چیزی که میاد دستش رو render کنه. حالا حتی میتونید یه قدم جلو تر هم برید و به کمک HOC ها اون ها رو enhance کنید. از این روش اگه لاجیک خاصی هم قراره توشون اتفاق بیوفته هندل میشه. توی این مثال نهایتا Header میتونه این شکلی بشه:
زیبا نیست؟? حالا همونطور که گفتم Header هر چیزی براش بیاد رندر میکنه و اگه لاجیک خاص تری هم داشت میشه با یه HOC هندل کرد. همچنین دیگه تو هر شرایطی نیاز به تغییر Header نداریم. صفحات هرجا که بودن همونطور فقط از Header استفاده میکنن و چیزایی که میخوان توش رندر کنن رو بهش پاس میدن. این مثال البته سوراخ زیاد داره ها! ولی میخوام فقط ایده رو بگیرید. مثال دیگه اش میشه تگ Head توی NextJS. اونم دقت کنید همین ساختار رو داره یا همه Container Component ها، شبیه Formik.
اسم به این سختی آخه خدا... خانم باربارا لیسکوف سال ۱۹۸۸ (حدودا ۳۴ سال پیش ?) این ایده رو مطرح کردن که شرح کاملش رو میتونید توی ویکیپدیا بخونید. موضوع اصلیش اینه که امکان جایگزینی Object ها با instance هایی از کلاس های subtype شون رو داشته باشن بدون اینکه رفتار اصلی شون تغییری پیدا کنه. من خیلی سعی کردم این مثال رو تطبیق بدم با یه چیزی که توی React وجود داشته باشه یا یه مثال عملی براش پیدا کنم ولی دست آخر یه مثال از Academind پیدا کردم. که ربطی به React نداره ولی میشه ازش یه چیزایی فهمید.
دقت کنید که این مفهوم، خالص OOP هست. فکر کنم به خاطر همینم هست که توی دنیای JS چیز زیادی در موردش وجود نداره، مثالی هم که دیدید به کمک Typescriptهست. توی این مثال کلاس پرنده یا Bird قابلیت پرواز داره و کلاس عقاب یا Eagle که همون کلاس Bird رو extend کرده قابلیت شیرجه زدن داره.
ولی اگه یه کلاس دیگه به ام پنگوئن داشته باشیم که همون کلاس Bird رو extend بخواد بکنه پنگوئن هم میتونه پرواز کنه که درست نیست!
یه مدل عجیبی از روش OOP زبان Java رو یادم میندازه. در هر صورت اشاره بهش خالی از لطف نبود ولی من کاربری براش توی React ندیدم که احتمالا به خاطر بی سوادی منه. اگه شما چیزی به ذهنتون رسید به منم یاد بدید.
بر اساس این اصل، اجزا سیستم نباید به ورودی هایی که ازشون استفاده نمیکنن وابسته باشن. یعنی مثلا اگه یه تابع نوشتید که دو تا ورودی داره ولی به لحاظ منطقی میتونه با یه دونه هم کارتون رو انجام بده نباید به شما گیر بده که حتما ۲ تا ورودی رو به من بده تا کارتو انجام بدم. یا اگه یه دونه کامپوننت دارید که قراره یه دیتایی رو رندر کنه، فقط باید دیتای لازم رو بگیره و به چیز دیگه وابستگی نداشته باشه.
مثال:
ما توی فیدیبو برای هر آیتم کتاب که میخوایم نشون بدیم یه تصویر داریم. بعضی کتاب ها ممکنه کتاب صوتی باشن، بعضی ممکنه باندل باشن، بعضی ممکنه سریال باشن یا اصلا پادکست باشه. در هر صورت یه تصویر باید نمایش داده بشه.
اگر این رو کامپوننت رو به عنوان لیست کتاب ها در نظر بگیریم:
میبینید که کامپوننت Image عملا کل دیتای مربوط به هر کتاب رو داره به عنوان ورودی میگیره. این بده ? فرض کنیم که ساختار هر item این شکلی باشه:
خب، حالا کامپوننت Image هم این شکلیه:
این کامپوننت به مقدار item و bookCover وابسته هست و اگر ما ازش بخوایم که عکس کاور پادکست یا باندل و ... رو برامون رندر کنه دقیقا باید عین همین دیتا با همین key value رو داشته باشیم که تو حالت فعلی چنین چیزی امکان نداره. پس باید مشکل رو حل کنیم.
ساختار Image به این شکل تغییر میکنه:
سعی کردم مثالی بزنم که مفهومش رو خوب برسونه، امیدوارم این کارساز بوده باشه. ولی حتما بهم بگید نظرتونو.
این اصل میگه یه کامپوننت نباید مستقیما به یک کامپوننت دیگه وابستگی داشته باشه. حالا من میگم کامپوننت ولی برای فانکشن ها و بقیه چیزا هم صادقه. کلا چیزی به صورت مستقیم نباید وابستگی برای چیز دیگه ای ایجاد کنه. معمولا وقتی اصل Single Responsibility رعایت نشده باشه احتمال اینکه این اصل هم رعایت نشده باشه زیاد هست. پس یکی از نشونه هاش همینه.
مثال:
همینطور که میبینید اینجا اصل اول هم رعایت نشده، ولی کاری به اون نداریم. اگر هر کامپوننتی بخواد خودش دیتا خودش رو بگیره و اینجا ما داریم از fetch استفاده میکنیم که اصلا بد هم نیست. ولی به هر دلیلی اگه بعدا نظرتون عوض بشه و بخواید به جاش از axios استفاده کنید چی؟ اگه بعد تر بخواید سیستم cache پیاده سازی کنید چی؟ اگه دیتا تون رو بخواید mock کنید چطور؟
باید تک تک جاهایی که از این API دارن استفاده میکنن رو تغییر بدید. بنابراین بهتره یه مدل Abstract از تابعی که به کمکش دیتا میگیرید داشته باشید که بعدا اگر نیاز به تغییرش پیدا کردید یا خواستید هر کاری باهاش انجام بدید بتونید اینکارو خیلی راحت تر انجام بدید.
توی این مثال هم میشه اینکارو به کمک یک Utility Function انجام داد هم یک Custom Hook.
این میشه مدل Custom Hook:
و ازش توی هرجایی که نیاز داشتید استفاده کنید. خوبیش اینه بعدا هر تغییری نیاز بود بدی راحت فقط با یه جا کار دارید و کافیه که تغییراتو اونجا بدید تا همه جا apply بشه. با این تفاسیر کامپوننت اصلی ما این ریختی میشه:
تمام این موارد بالا که بهش اشاره کردم، زمانی میتونه به شما کمک کنه که دلیل کاری که انجام میدید رو بدونید. این خیلی نکته مهمیه. هدف نوشتن این مطالب هم همینه. یادتون باشه اصول SOLID خوبن، جذابن، شما رو خفن نشون میدن ولی استفاده ازشون یه لِم و قلقی داره که باید با تجربه کردن متوجه شون بشید. سعی کنید Over engineered نکنید پروژه تون رو. ایجاد مفاهیم Abstract تا یه جایی خوبه، از حد که بگذره نتیجه معکوس داره و نتیجه معکوس اینجا یعنی پیچیده شدن، سخت شدن برای فهمیدن و خوندن کد. یادتون نره که اگه اول راهید همیشه میتونید از تجربه دیگران استفاده کنید.?