وراثت یا کامپوزیشن؟ مسئله این است

نحوه یادگیری یک زبان برنامه نویسیقدم هایی که در راستای "برنامه نویس بهتری شدن" برداشته میشه; برای من، شما و یا هر شخص دیگه کم و بیش یکسان هستند.ما شروع به یادگیری اصول اولیه می کنیم، بعدش به آرامی به سمت برنامه نویسی رویه ای حرکت می کنیم. و کم‌کم با مفهوم شئ گرایی آشنا میشیم و شروع میکنیم به کار کردن با کلاس ها و ساخت آبجکت ها.وقتی احساس کنیم با اصول اولیه شئ گرایی آشنا هستیم، به سراغ کلاس های انتزاعی (Abstract) میریم و با اینترفیس ها کار می‌کنیم; و میتونیم روابط رو در کدهامون با استفاده از وراثت پیاده‌سازی کنیم. بعد برای نوشتن کدهای قابل توسعه و نگهداری، مجبوریم بریم سراغ قواعد سالید و با الگوهای طراحی هم آشنا بشیم.در طول این مسیر مثال ها و سورس کدهای زیادی رو بررسی خواهیم کرد.. و نکته ای که ممکنه توجه ما رو جلب کنه اینه که: اون طور که باید به وراثت بها داده نشده; و اکثر برنامه نویس های با تجربه به جای وراثت از مفهومی به نام کامپوزیشن (Composition) استفاده می‌کنند. البته وراثت هم قطعا جایگاه خودش رو داره.فرهنگ لغت به ما میگه که کلمه Composite یک فعله; که به عمل ساختن یا فرم دادن یک کل، از طریق ترکیب بخش‌ها یا المان‌های کوچکتر گفته میشه.در علوم کامپیوتر کامپوزیشن، به پروسه ای گفته میشه که ما طی اون المان های مختلف برنامه رو برای ساخت یک آبجکت، ترکیب می‌کنیم. پس می‌تونیم بگیم: يک شئ هنگامی كه ويژگی های خاصی را مشخص می كنه، میتونه کامپوزيت بشه.وقتی شما اشیاء خود را بسته به آنچه كه هستند طراحی كنید ، وراثت اتفاق می افته.اما اگر اشیاء رو مطابق آنچه انجام می دهند طراحی كنید، در عوض كامپوزیت اتفاق می افته.تفاوت یک رابطه "is-a" با یک رابطه "has-a"به عنوان مثال ، اتومبیل‌ها و موتور سیکلت‌ها جز وسایل نقلیه هستند پس می‌تونند از کلاس وسیله نقلیه (Vehicle) ارث بری کنند. پس:a Car is a Vehicleهمچنین صحیحه اگر بگیم: اتومبیل‌ها و موتور سیکلت‌ها، یک موتور دارند.. که با استفاده از اون المان کامپوزیت (تشکیل) میشند. پس:a Car has an Engineپس نرم افزاری که شامل این المان‌ها باشه میتونه از هر ۲ مفهوم وراثت و کامپوزیشن استفاده کنه و هیچ یک از اونها اشتباه نیست. پس سوال اینه که: چرا این موضوع میتونه مهم باشه؟ مشکل رو در قالب یک مثال با هم بررسی می‌کنیم:فرض کنید مشتری ما، صاحب یک نمایندگی اتومبیل هست و طبق پلن تجاریش به ۲ شکل کار میکنه:ماشین ها رو میفروشهماشین ها رو کرایه میدههمون طور که در دیاگرام UML می‌بینید، رابطه کلاس ها در اینجا کاملاً واضحه ، ما باید یک کلاس Car که انتزاعی (Abstract) هست داشته باشیم و دو ساب کلاس که از اون ارث‌بری می‌کنند.OnSaleCar is a Car --- and --- ForLeaseCar is a Carتا اینجا همه چیز خوبه.. فرض کنید بعد از چند وقت مشتری ما تصمیم میگیره که میخواهد موتور سیکلت رو هم به محصولاتش اضافه کنه. اگر بخواهیم با وراثت کار رو ادامه بدیم، نتیجه شبیه به زیر میشه:در اینجا یه سری مشکل وجود داره.. ما توابع رو همه جا تکثیر کردیم.کلاس های Car و Motorbike باید اتربیوت های Vehicle رو به ساب کلاس هاشون پاس بدند.اگر مشتری خواست سال بعد در کنار این محصولات دوچرخه هم بفروشه چی؟به این ترتیب مجبوریم برای هر وسیله نقلیه جدید، ۲ کلاس جدید هم بسازیم. پس میبینید که وراثت راه حل چندان خوبی در این مورد نیست. هر چند که چنین نرم‌افزاری کار خواهد کرد، اما این کار کردن با هزینه زیادی در نگهداری همراه خواهد بود.یک راه برای برطرف کردن این مشکل، اینه که فقط قسمت محاسبه قیمت (متد cost) رو داخل کلاس انتزاعی Vehicle داشته باشیم و با استفاده از یک switch و بر اساس نوع درخواست (خرید یا اجاره) مبلغ رو حساب کنیم:که نهایتا چنین دیاگرامی خواهیم داشت:در مقایسه با دیاگرام قبلی، این یه پیشرفت حساب میشه و حالا مدیریت چنین سیستمی کمی آسون تره اما هنوز یک مشکل داریم.. در حال حاظر داریم از یک کاندیشن (شرط) استفاده میکنیم در حالی که گزینه مناسب برای این کار استفاده از پلی‌مورفیسم (Polymorphism) هست. مشکل اینه که ما باید این کاندیشن رو به تمام متدهای کلاس انتزاعی‌مون اضافه کنیم; و اگر در آینده یک کلاس جدید Bicycle اضافه کنیم => مجبوریم کاندیشن داخل تمام متد ها رو هم آپدیت کنیم.اگر دقت کنید، من دارم احتمالاتی که ممکنه در آینده اتفاق بیفتند و یا اتفاق نیفتند، رو در نظر می‌گیرم. و نهایتا هدف اینه که: یک محصول مقیاس پذیر رو با کمترین هزینه نگهداری ایجاد کنیم. پس از کامپوزیشن استفاده می‌کنیم.کامپوزیشنمی‌بینید که وراثت برای ساخت چنین سیستمی یه سری محدودیت‌ ها داره; و در یک پروژه بلند مدت هزینه نگهداری زیادی رو برای ادامه توسعه لازم داره.حالا می‌خواهیم ببینیم که چطور می‌تونیم مفهوم کامپوزیشن رو پیاده سازی کنیم و کد هامون رو با روشی مطمئن و مقیاس پذیر بهبود ببخشیم.چیزی که نیاز داریم تعریف انواع مختلف معامله ها هست. پس استراتژی فروش و کرایه رو به کلاس های خودشون منتقل میکنیم و از الگوریتم ها به شکل قابل تعویض، استفاده می‌کنیم. Strategy Design Patternکلاس DealStrategy رو می‌سازیم که داخلش هزینه و نوع استراتژی (فروش یا کرایه) مشخص میشه. متد ()cost ما به Instance ای از کلاس Vehicle نیاز داره. در حال حاظر کلاس Vehicle از طریق کلاس DealStrategy قابل دسترسی هست.پس Vehicle نمیدونه که در چه نوع معامله ای قرار گرفته (که خوبه)یکی دیگه از جنبه های مثبت این ریفکتور اینه که: ما می‌تونیم انواع معاملات دیگه رو هم اضافه کنیم بدون اینکه اصلاً به کدهای کلاس Vehicle دست بزنیم.و حالا می‌تونیم به این شکل عمل کنیم:به این ترتیب، ما نوع معامله رو در زمان اجرا مشخص می‌کنیم. پس هیچ یک از کلاس های وسایل نقلیه به آنچه اتفاق افتاده اهمیتی نمی‌دهند و ما می‌تونیم وسایل نقلیه جدید و نوع معاملات جدیدی رو، بدون نگرانی در مورد عدم مقیاس پذیری صحیح و نگهداری مکرر، پیاده‌سازی کنیم.مثال بالا، نمونه ساده ای از الگوی طراحی استراتژی بود، که در یکی دیگر از مقالات کاملا توضیح داده شده: https://virgool.io/@ali.bayat/%D8%A7%D9%84%DA%AF%D9%88%DB%8C-%D8%B7%D8%B1%D8%A7%D8%AD%DB%8C-%D8%A7%D8%B3%D8%AA%D8%B1%D8%A7%D8%AA%DA%98%DB%8C-strategy-%D8%AF%D8%B1-%D8%B2%D8%A8%D8%A7%D9%86-php-tyo2whv4cd3t مفهوم Decouplingمی خواهم مثال بالا رو با جزئیات بیشتری تجزیه و تحلیل کنیم.. چه مشکلی باعث شد ما الگوریتم معامله را به چند قسمت تقسیم کنیم و کلاسهای مجزا برای هر معامله داشته باشیم؟مشکل اساسی این بود که بعد از اعمال برخی تغییرات، ما در شرایطی قرار می‌گرفتیم که اگر می‌خواستیم کدمون رو آپدیت کنیم، نیاز به ویرایش چندین متد داشتیم.مثلا اگر می‌خواستیم switch ی رو که داخل متد cost کلاس Vehicle بود، آپدیت کنیم; مجبور بودیم تمام متد های دیگه که این کاندیشن داخلشون بود، رو هم آپدیت کنیم.و اگر حتی یکی از این متدها رو فراموش کنیم، برنامه دیگه کار نمیکنه و برای دیباگ کردن هم پروسه نسبتا دشواری داریم. بخش های چنین اپلیکیشنی اتصال و روابط زیادی رو با هم دارند. به این مفهوم Coupling می‌گیم.و عمل از دست دادن کاپلینگ (اتصالات) یک کلاس رو دی‌کاپلینگ (Decoupling) می‌گیم.یک سیستم خوب ساختار یافته، دارای سطح اتصالات کمی هست... در نهایت هدف اینه که: بخش های مختلف اپلیکیشن ما، هیچ آگاهی نسبت به هم نداشته باشند و برای عمل کردن به بخش دیگه‌ای متکی نباشند. و نتیجه‌اش اینه که: بدون آپدیت‌کردن حتی یک خط کد، در هر یک از کلاسهای مربوط به وسایل نقلیه ، حالا میتونیم انوع معاملات رو اضافه کنیم و یا حتی تغییر بدیم.استفاده از Interfaceمفهوم دیگه‌ای که می‌تونیم بهش اشاره کنیم اینه که: ما وسایل نقلیه رو به یک نوع خاص از معامله محدود نکردیم; که این کار رو از طریق تزریق DealStrategy به متد سازنده کلاس Vehicle انجام دادیم. و به این ترتیب هر وسیله نقلیه میتونه از هر نوع معامله (خرید، اجاره، ..) استفاده کنه.اما اگر متد سازنده رو محدود کنیم و بجای DealStrategy از OnSaleStrategy استفاده کنیم، چه اتفاقی میفته؟از اونجا که ما به جای یک اینترفیس (البته ما در کدهامون از یک کلاس انتزاعی استفاده کردیم...) یک کلاس concrete رو قرار دادیم، حالا استراتژی های ما برای این وسیله نقلیه، تنها شامل فروش اون میشه.البته هر کدوم از این ۲ روش مزایای خودش رو داره. با استفاده از اینترفیس ها میتونیم سیستم های منعطف تری رو طراحی کنیم; و در کنارش به پلی‌مورفیسم دسترسی داریم، که احتمالا دیر یا زود بهش نیاز خواهیم داشت.در عوض استفاده از کلاس های concrete باعث امنیت بیشتر کدهای میشه. و همیشه می‌دونیم که باید از چه نوع آبجکتی استفاده کنیم. این سطح بالای ایمنی، پلی‌مورفیسم رو حذف میکنه و بعد از چند وقت ممکنه احساس کنید که دارید با دست‌بند کد ‌می‌نویسید.نتیجه گیریدر این نوشته نگاهی داشتیم به حالاتی که ممکنه وراثت برای ما مشکل ساز بشه.. و حالت بهینه رو هم با استفاده از کامپوزیشن بررسی کردیم. همچنین مثالی از الگوی طراحی استراتژی رو داشتیم.به عنوان یه قاعده کلی مراقب استفاده از وراثت باشید. وراثت حتما جایگاه خودشو داره اما مراقب باشید که تمام عملکردهایی که ممکنه حتی بهشون نیاز هم نداشته باشید رو به کلاستون منتقل نکنید. در حالی که می‌تونید از الگو های کامپوزیت یا دکوراتور استفاده کنید.