کاربرد اصول SOLID در React!

تو همه مصاحبه های این چند سال یکی از سوالایی که همش میپرسن اینه: "با اصول SOLID کار کردی؟ آشنایی داری؟". بعد برای اینکه مطمئن بشن میگن مثلا مفهوم Open-closed رو توضیح بده ببینم بلدی عمو؟ 😕 حالا گیرم که اصلا شما هم جوابشو هم بدی، مگه خوندن یه مفهوم به معنی فهمیدن یا به کار بردنش توی دنیای واقعی میشه؟

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

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

چی هستن این اصول اصلا؟

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

این پنج تا اصل اینان:

  • Single responsibility principle (SRP)
  • Open-closed principle (OCP)
  • Liskov substitution principle (LSP)
  • Interface segregation principle (ISP)
  • Dependency inversion principle (DIP)

اسمارو دیدید حال کردیدا 😁 ولی خیلی خبری نیست. در واقع تمام این ژانگولر ها و ... برای اینه که کمک کنه مدیریت کدتون راحت تر بشه و همچنین اجزای سیستم هم Reusable تر بشه.

یه نکته اساسی در مورد این اصول اینه که توی چارچوب Object Oriented معنی پیدا میکنن و خیلی از چیز هایی که توی این practice ها بهش اشاره میشه از مفاهیم شئ گرایی درش استفاده شده. پس این نکته رو توی ذهنتون داشته باشید. البته این مطلب در مورد نحوه پیاده سازی این اصول توی React هست که تقریبا یه دنیای Functional هست و دقیقا مقابل OOP قرار میگیره ولی نگران نباشید، چیزی که از OOP بودن این اصول مهم تره، مغز کلامشونه که خیلی هم ساده و قشنگه.

نکته بعد اینکه من اینجا میخوام روش استفاده از این اصول توی React رو بگم ولی این مفاهیم خیلی کلی هستند و توی هر زبان برنامه نویسی و تکنولوژی و هر چیزی امکان پیاده سازی دارن.


تک وظیفه ای بودن (Single Responsibility Principle)

این اصل میگه که هر کامپوننت یا تابع باید فقط یک کار رو انجام بده. تقریبا شبیه مفهوم Pure Function تو دنیای Functional هست (نه دقیقا ولی حدودا). میدونیم که میشه توی ری-اکت برای هرچیزی یک کامپوننت ایجاد کرد و نکته ای که این اصل بهش اشاره میکنه اینه که سعی کن هر کامپوننت یا تابعی فقط و فقط یک کار رو انجام بده و نه بیشتر. اینطوری مدیریتشون راحت تره، تست کردنشون لذت بخش تره، خوندنشون هم مثل خوندن کتاب قصه شیرین میشه.

بین تمام اصول SOLID این اصل به نظرم از همه شون ساده تره و در حین حال اگر درست پیاده بشه چنان تاثیری روی کار میذاره که به وجد میاید. قبل تر ها (حدود ۳ سال پیش) یه مطلبی منتشر کردم در مورد بهبود کد React Native فکر کنم. یکی از چیز هایی که اونجا هم بهش اشاره کردم همین موضوع بود. البته اون زمان جوون بودم و به اندازه الانم نمیفهمیدم دلیل خیلی چیز ها رو ولی اشاره کرده بودم که کامپوننت های بزرگتر رو باید شکست به کامپوننت های کوچیک تر با مسئولیت کمتر.

توی ری-اکت یه فرمول ساده و باحال وجود داره برای اینکه بخوایم کار شکستن کامپوننت ها به اجزای کوچکتر رو انجام بدیم و اونم این سه مرحله هست:

  1. کامپوننت هایی که کار های زیادی انجام میدن رو پیدا کنید (معمولا بیشتر از ۲ تا کار)
  2. منطق پیاده شده داخلشون رو ببرید به فایل ها و فانکشن های utility جداگانه.
  3. اگر ارتباطی بین کامپوننت ها وجود داره به کمک custom hook ها یا HOC به همدیگه وصلشون کنید.

مثال:

این کامپوننت رو در نظر بگیرید:

این فیلتر های دیجی کالا هست تو دسته بنده لباس مردانه
این فیلتر های دیجی کالا هست تو دسته بنده لباس مردانه


این کامپوننت داره چندتا کار انجام میده:

  1. نمایش لیست برند ها
  2. جستجو و فیلتر برند ها بر اساس ورودی که بالاش هست
  3. دریافت لیست برند ها از سمت سرور (این واقعا نیست ها ولی برای این مثال ما اینطوری فکر میکنیم، حله؟)

پس کامپوننت بزرگی محسوب میشه، چرا بزرگه؟ چون بیشتر از یک کار داره انجام میده. من کد این کامپوننت رو که ندارم ولی فرض میکنیم احتمالا یه چیزی شبیه این باشه:

https://gist.github.com/hesan-aminiloo/9d014cf42cd99a30dd055b2f65c4f50e

خب چه خبره؟

  • یه کامپوننت به اسم BrandsSidebar داریم. توی useEffect به یه endpoint که مهم هم نیست چیه یه درخواست ارسال میکنه و لیست brand ها رو میگیره (این کار اول)
  • برند های دریافتی رو به کمک متد filter بر اساس مقدار state filter فیلتر میکنه (این کار دوم)
  • لیست نهایی رو به کمک map نمایش میده (این کار سوم)

خب پس با یه کامپوننت بزرگ طرفیم، گرچه تو این مثال خیییلی هم بزرگ نیست ولی به اندازه ای هست که بشه شکستش به تیکه های کوچک تر. طبق اصل Single Responsibility هر چیزی باید فقط یک کار انجام بده پس بریم این کامپوننت رو بشکنیم به قطعات کوچک تر.

توی قدم اول میتونیم برای دریافت لیست برند ها یه custom hook ایجاد کنیم:

https://gist.github.com/hesan-aminiloo/55007d03cff880dc421f8a5129e77711

حالا کامپوننت BrandsSidebar یکم کوچیک تر شد ولی همچنان داره دو تا کار انجام میده (فیلدر کردن، نمایش دادن آیتم ها)

یکی دیگه از اتفاقاتی که با بوجود اومدن useBrands میوفته اینه که این functionality رو reusable میکنه و میتونیم جاهای دیگه هم ازش استفاده کنیم.

کار بعدی اینه که برای هر کدوم از سطر هایی که قراره رندر بشه یه کامپوننت مجزا بوجود بیاریم:

https://gist.github.com/hesan-aminiloo/46ac77b14f518baba5860608e3c884b3

خب همونطور که میبینید، کامپوننت هر برند رو هم جدا کردیم. میدونم که اینجا یه checkbox داریم که براش حتما باید یه eventListener داشته باشیم ولی برای مثال های اینجا خیلی مهم نیست که در نظر نگرفتمش.

یکی دیگه از کار هایی که میکرد هم ازش کم شد و فقط موند فیلتر کردن دیتا ها. میتونیم برای فیلتر هاش هم یه utility بنویسیم که برامون اینکارو انجام بده:

https://gist.github.com/hesan-aminiloo/42210232436e3d2f4ec7715ae9e85b7b

چقد داره خلوت میشه مگه نه؟ این تا اینجا خوبه. یه بهبود ریز دیگه هم میشه داد که مثلا لاجیک filter کردن رو هم برد توی یک هوک مجزا و ازش استفاده کرد. این قسمت برای طولانی نشه من اونو نمینویسم دیگه. ولی اگه دوست داشتید خودتون میتونید انجامش بدید. کار سختی نیست.

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


باز برای گسترش و بسته برای تغییرات (Open-closed Principle)

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

این مثال رو از medium پیدا کردم خیلی عالی بود:

https://gist.github.com/hesan-aminiloo/f21139728d9179be1ab934acb7a961d1

بر اساس اینکه کاربر توی کدوم صفحه باشه لینکی که توی هدر هست تغییر میکنه. این حالت برای خیلی جاها بوجود میاد. مثلا زمانی که کاربر لاگین هست و زمانی که نیست. در هر صورت ما از Header توی دو صفحه Home و Dashboard استفاده کردیم. حالا وای به روزی که بخوایم یه شرط دیگه به اینا اضافه کنیم. میبینید؟ هی مجبوریم بریم توی Header رو تغییر بدیم. پس در واقع داریم هی دست به آچار میشیم و موتور پراید رو تقویت می‌کنیم.

این مفهوم میگه که باید کامپوننت ها به نحوی پیاده سازی بشن که این اتفاق نیوفته. خب چطوری میشه حلش کرد؟ یه pattern خود React پیشنهاد داده به اسم Render Props. میتونید چیز هایی که میخواید بر اساس prop تعیین و رندر بشه رو از اول از طریق prop به کامپوننت پاس بدید و کامپوننت هر چیزی که میاد دستش رو render کنه. حالا حتی میتونید یه قدم جلو تر هم برید و به کمک HOC ها اون ها رو enhance کنید. از این روش اگه لاجیک خاصی هم قراره توشون اتفاق بیوفته هندل میشه. توی این مثال نهایتا Header میتونه این شکلی بشه:

https://gist.github.com/hesan-aminiloo/115ba17b5b812c9f278feb52876ee0d7

زیبا نیست؟😍 حالا همونطور که گفتم Header هر چیزی براش بیاد رندر میکنه و اگه لاجیک خاص تری هم داشت میشه با یه HOC هندل کرد. همچنین دیگه تو هر شرایطی نیاز به تغییر Header نداریم. صفحات هرجا که بودن همونطور فقط از Header استفاده می‌کنن و چیزایی که میخوان توش رندر کنن رو بهش پاس میدن. این مثال البته سوراخ زیاد داره ها! ولی میخوام فقط ایده رو بگیرید. مثال دیگه اش میشه تگ Head توی NextJS. اونم دقت کنید همین ساختار رو داره یا همه Container Component ها، شبیه Formik.


اصل جایگزینی یا تعویض پذیری لیسکوف (Liskov Substitution Principe)

اسم به این سختی آخه خدا... خانم باربارا لیسکوف سال ۱۹۸۸ (حدودا ۳۴ سال پیش 😐) این ایده رو مطرح کردن که شرح کاملش رو میتونید توی ویکیپدیا بخونید. موضوع اصلیش اینه که امکان جایگزینی Object ها با instance هایی از کلاس های subtype شون رو داشته باشن بدون اینکه رفتار اصلی شون تغییری پیدا کنه. من خیلی سعی کردم این مثال رو تطبیق بدم با یه چیزی که توی React وجود داشته باشه یا یه مثال عملی براش پیدا کنم ولی دست آخر یه مثال از Academind پیدا کردم. که ربطی به React نداره ولی میشه ازش یه چیزایی فهمید.

https://gist.github.com/Hurly77/f8886dd07f0c7265a87c56f6ba9d7170

دقت کنید که این مفهوم، خالص OOP هست. فکر کنم به خاطر همینم هست که توی دنیای JS چیز زیادی در موردش وجود نداره، مثالی هم که دیدید به کمک Typescriptهست. توی این مثال کلاس پرنده یا Bird قابلیت پرواز داره و کلاس عقاب یا Eagle که همون کلاس Bird رو extend کرده قابلیت شیرجه زدن داره.

ولی اگه یه کلاس دیگه به ام پنگوئن داشته باشیم که همون کلاس Bird رو extend بخواد بکنه پنگوئن هم میتونه پرواز کنه که درست نیست!

https://gist.github.com/Hurly77/aeda344009fdb949f9ab7add1828d19e

یه مدل عجیبی از روش OOP زبان Java رو یادم میندازه. در هر صورت اشاره بهش خالی از لطف نبود ولی من کاربری براش توی React ندیدم که احتمالا به خاطر بی سوادی منه. اگه شما چیزی به ذهنتون رسید به منم یاد بدید.


تفکیک روابط (Interface Segregation Principle)

بر اساس این اصل، اجزا سیستم نباید به ورودی هایی که ازشون استفاده نمیکنن وابسته باشن. یعنی مثلا اگه یه تابع نوشتید که دو تا ورودی داره ولی به لحاظ منطقی میتونه با یه دونه هم کارتون رو انجام بده نباید به شما گیر بده که حتما ۲ تا ورودی رو به من بده تا کارتو انجام بدم. یا اگه یه دونه کامپوننت دارید که قراره یه دیتایی رو رندر کنه، فقط باید دیتای لازم رو بگیره و به چیز دیگه وابستگی نداشته باشه.

مثال:

ما توی فیدیبو برای هر آیتم کتاب که میخوایم نشون بدیم یه تصویر داریم. بعضی کتاب ها ممکنه کتاب صوتی باشن، بعضی ممکنه باندل باشن، بعضی ممکنه سریال باشن یا اصلا پادکست باشه. در هر صورت یه تصویر باید نمایش داده بشه.

اگر این رو کامپوننت رو به عنوان لیست کتاب ها در نظر بگیریم:

https://gist.github.com/hesan-aminiloo/2dfd5c88ff72e6bc7faea26e3d5b9a24

میبینید که کامپوننت Image عملا کل دیتای مربوط به هر کتاب رو داره به عنوان ورودی می‌گیره. این بده 🤨 فرض کنیم که ساختار هر item این شکلی باشه:

https://gist.github.com/hesan-aminiloo/9c6320e5b253848762495535b0ed1bc0

خب، حالا کامپوننت Image هم این شکلیه:

https://gist.github.com/hesan-aminiloo/a87f33001d30523c94966da0dfc8025e

این کامپوننت به مقدار item و bookCover وابسته هست و اگر ما ازش بخوایم که عکس کاور پادکست یا باندل و ... رو برامون رندر کنه دقیقا باید عین همین دیتا با همین key value رو داشته باشیم که تو حالت فعلی چنین چیزی امکان نداره. پس باید مشکل رو حل کنیم.

ساختار Image به این شکل تغییر میکنه:

https://gist.github.com/hesan-aminiloo/4c523592d170e79693f8f4cd9efcf333
الانImageفقطیکمقدارCoverمیگیرهازنوعرشتهوفارغازاینکهاینکاوربرایپادکستهیابرایکتابیاهرچیهمونونمایشمیده.اینیهطرفماجرابود.بریمسراغصفحهلیست:
https://gist.github.com/hesan-aminiloo/239049e36e696219540cba02fdc2cce0

سعی کردم مثالی بزنم که مفهومش رو خوب برسونه،‌ امیدوارم این کارساز بوده باشه. ولی حتما بهم بگید نظرتونو.



وارونگی وابستگی ها (Dependency Inversion Principle)

این اصل میگه یه کامپوننت نباید مستقیما به یک کامپوننت دیگه وابستگی داشته باشه. حالا من میگم کامپوننت ولی برای فانکشن ها و بقیه چیزا هم صادقه. کلا چیزی به صورت مستقیم نباید وابستگی برای چیز دیگه ای ایجاد کنه. معمولا وقتی اصل Single Responsibility رعایت نشده باشه احتمال اینکه این اصل هم رعایت نشده باشه زیاد هست. پس یکی از نشونه هاش همینه.

مثال:

https://gist.github.com/hesan-aminiloo/a9cb2fc695ed86e06f7b8fe25898751d

همینطور که میبینید اینجا اصل اول هم رعایت نشده، ولی کاری به اون نداریم. اگر هر کامپوننتی بخواد خودش دیتا خودش رو بگیره و اینجا ما داریم از fetch استفاده می‌کنیم که اصلا بد هم نیست. ولی به هر دلیلی اگه بعدا نظرتون عوض بشه و بخواید به جاش از axios استفاده کنید چی؟ اگه بعد تر بخواید سیستم cache پیاده سازی کنید چی؟ اگه دیتا تون رو بخواید mock کنید چطور؟

باید تک تک جاهایی که از این API دارن استفاده میکنن رو تغییر بدید. بنابراین بهتره یه مدل Abstract از تابعی که به کمکش دیتا میگیرید داشته باشید که بعدا اگر نیاز به تغییرش پیدا کردید یا خواستید هر کاری باهاش انجام بدید بتونید اینکارو خیلی راحت تر انجام بدید.

توی این مثال هم میشه اینکارو به کمک یک Utility Function انجام داد هم یک Custom Hook.

این میشه مدل Custom Hook:

https://gist.github.com/hesan-aminiloo/cf4988045c4d53c4ab96049e332cba79

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

https://gist.github.com/hesan-aminiloo/1f0a1fbe4fa67b5c3749d53223a68c23



حرف آخر

تمام این موارد بالا که بهش اشاره کردم، زمانی میتونه به شما کمک کنه که دلیل کاری که انجام می‌دید رو بدونید. این خیلی نکته مهمیه. هدف نوشتن این مطالب هم همینه. یادتون باشه اصول SOLID خوبن، جذابن، شما رو خفن نشون میدن ولی استفاده ازشون یه لِم و قلقی داره که باید با تجربه کردن متوجه شون بشید. سعی کنید Over engineered نکنید پروژه تون رو. ایجاد مفاهیم Abstract تا یه جایی خوبه، از حد که بگذره نتیجه معکوس داره و نتیجه معکوس اینجا یعنی پیچیده شدن، سخت شدن برای فهمیدن و خوندن کد. یادتون نره که اگه اول راهید همیشه میتونید از تجربه دیگران استفاده کنید.😉