Ali Fazeli
Ali Fazeli
خواندن ۷ دقیقه·۳ سال پیش

کد تمیز و شیئ گرایی

این مطلب در ادامه سری برداشت‌ها و خلاصه ی کتاب Clean Code، با نگاه به فصل ششم کتاب نوشته شده.

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

این بخش به طور مشخص برنامه نویسی شیئ گرا رو مورد هدف قرار داده و نکته هاش برای کسایی که با زبان های برنامه نویسی شیئ گرا (مثل #C و جاوا و ...) کار می‌کنند بیشتر کاربردیه.

قانون اول تعریف ‌‌‌‌Object ها:
هیچ متغیری را به صورت ‌public تعریف نکن مگر این که دلیل محکمی برای اون وجود داشته باشه.

مفهوم Abstraction

یکی از اولین اصطلاحاتی که موقع شروع برنامه نویسی با زبان های شیئ گرا با اون آشنا می‌شید ‌Abstraction نام داره. به طور خلاصه به این مفهومه که ‌Object ای که تعریف می‌شه باید اطلاعات و عملکرد ها رو تا حد ممکن به شکل ساده سازی شده و خلاصه ارائه (expose) کنه. برای درک این مفهوم مثال های بعدی رو ببینید:

هدف کلاس Point به طور مشخص در بر گرفتن اطلاعات یک نقطه در دستگاه مختصات است. در مثال اول در قالب یک کلاس تعریف شده که به طور مشخص به ما اجازه تغییر جداگانه مقادیر x و y را میده و تمام ساختار درونی خودش رو ‌‌expose می‌کنه.‍

مثال دوم اما با تغیر یک ‌interface نحوه کار و تغییر اطلاعات رو به شکل مشخصی ساختارمند می‌کنه. به طور مشخص ما رو مجبور می‌کنه که مقادیر ‌x و y رو یکجا و با هم تغییر بدیم و همینطور در صورت مقدار دهی قطبی هم شعاع و زاویه باید همزمان ست بشن.

هردوی این مثال ها نمونه هایی از تعریف یک ‌Data Structure در قالب شیئ گرایی هستند اما مثال دوم با پنهان کردن ساختار داخلی خودش، روش مشخصی برای کار کردن با Object رو تعریف کرده.

مفهوم Abstraction به پنهان کردن ساختار داخلی خلاصه نمیشه و ما رو تشویق می‌کنه که موقع تعریف کردن متد های عمومی یک کلاس هم اون ها رو به شکل ساده شده تعریف کنیم. مثال های بعدی رو ببینید:

در دو نمونه interface تعریف شده برای وسیله نقلیه (Vehicle)، مورد اول ما می‌تونیم به طور مشخص میزان سوخت موجود در باک و ظرفیت کلی باک رو جداگانه دریافت کنیم و با اون ها هر کاری لازم داریم انجام بدیم اما توی مثال دوم با در نظر گرفتن Abstraction این دو متد حذف شدند و یک متد جایگزین شده که درصد سوخت باقیمانده نسبت به ظرفیت سوخت وسیله نقلیه رو برمیگردونه و ما دسترسی به اطلاعات درونی ‌‌Object نداریم.

پس یادمون باشه که موقع تعریف کلاس ها، فقط اطلاعاتی که لازم هست رو به طور ‌Abstract ارائه کنیم و جزییاتی که لازم نیست رو داخل کلاس نگه داریم.

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

تفاوت بین ‌Object و Data Structure

هر دو مفهموم Object و Data Structure توی زبان های شیئ گرا توسط کلاس ها تعریف می‌شن و ظاهرشون کاملا شبیه همه. توی این بخش راجع به تفاوت شون وکاربرد هاشون صحبت می‌کنیم و هدفمون اینه که وقتی کد یک کلاس رو می‌خونیم بدونیم با کدوم شون مواجه ایم و همینطور موقع نوشتن کد بهتر انتخاب کنیم که از Object استفاده کنیم یا Data Structure.

به طور خلاصه Object ها کلاس هایی هستند که یک شیئ با عملکرد های مختلف مربوط به اون رو در بر می‌گیرند و Data Structure ها کلاس هایی اند که فقط محل قرارگیری یک سری اطلاعات و مقادیر هستند و برای نگه داشتن اون ها استفاده می‌شن و دارای منطق یا عملکرد خاصی نیستند.

به عنوان مثال ببینیم که با استفاده از Object ها یا Data Structure ها چطوری میشه یک سری شکل و محاسبه مساحت اون ها رو مدل کرد:

توی کد بالا همون طور که می‌بینین شکل های مختلف (دایره، مربع و مستطیل) در قالب ‌Data Structure ها تعریف شدند که فقط در بر گیرنده اطلاعات لازم برای تعریف اون هاست و برای محاسبه مساحت اون ها کلاسی به اسم Geometry تعریف شده که دارای یک متد برای محاسبه مساحت همه شکل هاست و همونطور که می‌بینین متد area از ویژگی های شیئ گرایی استفاده ای نکرده و کاملا به شکل procedural نوشته شده.

اما روش دیگه پیاده کردن همین کد در قالب ‌‌Object های OOP (برنامه نویسی شیئ گرا) به چه شکلی می‌شه؟

توی این روش یک interface به نام shape تعریف شده که تمام شکل ها اون رو پیاده سازی می‌کنن و طبق اون هر کلاس یک متد به اسم area داره که مقدار مساحت اون شکل رو برمیگردونه. تفاوت های کلاس Rectangle توی دو تا مثال رو نگاه کنید تا تفاوت Object و Data Structure رو به طور واضح متوجه شید:

  • در اولی (Data Structure) متغیر ها عمومی هستند و در دومی (Object) متغیر ها خصوصی
  • در Data Structure متدی وجود نداره ( مگر setter و getter) ولی Object ها دارای متد های مربوط به اون Object هستند

هیچ کدوم از این دو روش درست یا غلط نیست و هدف از دیدن شون درک تفاوت هاست تا موقع استفاده بتونید تصمیم بگیرید که کدوم مناسب تر هست.

برای درک منظور تصور کنید که میخوایم تابعی به اسم perimeter اضافه کنیم تا محاسبه محیط رو انجام بده. اگر به روش اول پیاده سازی انجام شده باشه کافیه که این متد داخل کلاس Geometry پیاده بشه و نیازی به تغییر در هیچ کدوم کلاس های Circle و Rectangle و ... نیست. در حالی که اگر به روش دوم پیاده کرده باشیم، باید متد perimeter داخل هرکدوم از کلاس ها جداگانه تعریف بشه و به این ترتیب پیاده سازی طولانی تر و حجم تغییرات بیشتر می‌شه.

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

پس به طور خلاصه اگر در آینده امکان اضافه شدن عملکرد های جدید بیشتر باشه استفاده از Data Structure l انتخاب بهتریه و اگر احتمال اضافه شدن نوع های جدید به سیستم وجود داشته باشه (مانند مثال مثلث) بهتره که از روش Object استفاده کنیم.

به طور خلاصه، قرار نیست همه چیز، همیشه در قالب Object تعریف بشه و گاهی وقت ها بهتره که از Data Structure ها استفاده کنیم و procedure هایی داشته باشیم که روی اون ها کار می‌کنند.

قانون Demeter

قاعده دیمیتر (Law of Demeter) برای جلوگیری از ایجاد ارتباط های پیچیده و عمیق بین ‌Object هاست.

به طور دقیق این قانون می‌گوید که متد f از کلاس C فقط می‌تواند متد هایی از این ‌ Object ها را استفاده کند:

  • متد های کلاس C
  • متد های Object ای که داخل خودش ساخته شده
  • متد های Object ای که جزو ورودی هایش هست
  • متد های Object ای که به عنوان instance variable داخل کلاس C وجود دارد

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

منظور از همه چیزهایی که گفتیم چیه؟

کلاس User رو ببینید، به طور مشخص این کلاس می‌تونه به متد های کلاس Account دسترسی داشته باشه، همینطور به متد های داخلی خودش و البته داخل متد discountedPrice، می‌تونه به متد های کلاس
Coupon هم دسترسی داشته باشه. اما قانون Demeter توی این مثال کجا نقض شده؟

توی خط شماره ۷ جایی که از متد getPrice که مربوط به کلاس Plan هست استفاده شده. این کلاس در دسترس کلاس User نیست و توسط کلاس Account برگردونده شده و نباید دسترسی به داخلش پیدا کنیم.

به طور ساده قانون Demeter می‌گه هر کلاسی فقط باید با دوستان خودش صحبت کنه و نباید با غریبه ها حرف بزنه. و همینطور می‌گه که دوستان دوستان شما غریبه هستند.

حالا وقتی به چنین عملکردی نیاز داریم چیکار کنیم؟

راه حل گذشتن از این طور مشکل ها یادآوری یک نکته در مورد پیاده سازی ‌Class هاست:

یک Object باید عملکرد های مورد نیاز رو ارائه کنه نه مقادیر رو! یعنی چی؟ یعنی اینجا کلاس Account باید به جای برگردوندن ساده مقدار plan و واگذار کردن بقیه چیزها به کلاس User، مسئولیت محاسبه تخفیف رو به عهده بگیره. در این صورت کد به شکل زیر در میاد و مشکل قانون Demeter به طور کلی حل می‌شه.

این ها بخشی از نکاتی بود که رابرت سی مارتین برای پیاده سازی Object ها در کتاب Clean Code بهش اشاره کرده. مثل تمام پست های این سری، پیشنهاد می‌کنم که اگر این مفاهیم براتون جذاب هست کتاب رو به طور کامل مطالعه کنید.

برنامه نویسیمهندسی نرم افزارکد تمیزclean code
مهندس نرم افزار
شاید از این پست‌ها خوشتان بیاید