خیلی وقته که تو فکرم هست بنا به درخواست بعضی دوستانم شروع کنم به توضیحات الگوهای GOF از روی کتاب اصلی و صد البته با دیدگاه اصلی خودشون و گفتم الان دیگه وقتشه تا استارت این موضوع رو بزنم🤓. شاید با آهنگ آهسته تری مطالب رو انتشار بدم ولی نمیزارم از ریتم بیفته😉 پس اگر مسافرین محترم آماده هستید بنظرم قبلش یه مررو کلی با نگاه و تعاریف نویسنده های کتاب (و بعضی جاها خودم یکم چکش کاریش کردم 🫡😛) داشته باشیم تا کمی هم صفحه بشیم و بعدش بریم برای اصل کار! خوب قهوه یا چایی رو برای خودتون بیارید و کمربندها رو ببندید که راننده آماده رفتن به سمت دنیای الگوهای طراحی از جاده OOP هست🤓
در دنیای شی گرا، برنامه های ما مملو از object ها هستند. این object ها data و procedure هایی که وظیفه اجرای اکشن را روی داده های خود دارند، package میکنند. همچنین ما این procedure ها را معمولا operation یا method می نامیم. یک object زمانی عملیاتی را انجام یا اجرا میکند که یک درخواست (request) یا پیام (message) از client دریافت کند.
بایستی به یاد داشته باشیم request تنها راه برای اجرای یک operation در object است و همچنین کانالی برای تغییر داده در object می باشد. دلیل این موضوع هم محدودیت هایی است که برروی object وجود دارد و ما آن را با نام encapsulation می شناسیم همچنین یکی دیگر از دلایل، مخفی بودن داده ها نسبت به دنیای بیرون object است و به سمت information hiding ما رو سوق می دهد.
بخش سخت در مورد نگاه و طراحی شی گرا، تجزیه یک سیستم به اشیا است. این کار دشوار است زیرا عوامل زیادی در این کار نقش دارند ماننده: encapsulation، granularity، dependency، flexibility، performance، evaluation، reusability و… که همه این عوامل به صورت مستقیم و غیر مستقیم برروی هم تاثیر میگذارند. اما متدولوژی های طراحی شی گرا از بسیاری از رویکردهای مختلف استفاده می کنند. می توانید یک شرح ازproblem ها بنویسید، اسم ها و افعال را جدا کنید و کلاس ها و operation های مربوطه را ایجاد کنید. یا می توانید روی همکاری ها و مسئولیت ها در سیستم خود تمرکز کنید. یا میتوانید دنیای واقعی را مدل سازی کنید و اشیایی که در طول تجزیه و تحلیل یافت میشوند را به طراحی تبدیل کنید، همیشه در مورد اینکه کدام رویکرد بهترین است، اختلاف نظر وجود دارد.
بسیاری از اشیاء در یک طرح از مدل تجزیه و تحلیل می آیند. اما طراحی های شی گرا اغلب به کلاس هایی ختم می شوند که هیچ شباهتی به دنیای واقعی ندارند. برخی از این ها، کلاس های سطح پایینی مانند آرایه ها هستند و بقیه عموما سطح بسیار بالاتری تعریف می گردند. به عنوان مثال الگوی composition یک abstraction را برای رفتار یکنواخت اشیاء معرفی می کند که مشابه فیزیکی ندارند. این نکته بسیار مهم است که مدلسازی دقیق دنیای واقعی منجر به سیستمی میشود که واقعیتهای امروزی را منعکس میکند اما نه لزوماً فردا. انتزاعاتی که در طول طراحی ظاهر می شوند کلید ایجاد یک طرح قابل انعطاف هستند همچنین تعیین granularity اشیاء می توانند از نظر اندازه و تعداد بسیار متفاوت باشند. آنها می توانند همه چیز را تا سخت افزار یا تا کل برنامه ها نشان دهند. هنر اصلی این است که چگونه تصمیم بگیریم که چه چیزی باید یک object باشد.
همانطور که گفته شد هر object دارای procedure یا operation می باشد و این operation ها دارای عنوان، اشیایی که به عنوان پارامتر میگیرد و مقدار بازگشتی عملیات می باشد و به آن signature یک operation میگوییم و مجموعه ای از تمامی signature های تعریف شده توسط operation یک object را interface به object می نامیم. در اصل Interface یک object مجموعه کامل درخواست هایی را که میتوان به object ارسال شود را مشخص میکند.
در اصل Interface ها در سیستم های شی گرا از موارد کلیدی هستند چرا که اشیا فقط از طریق رابط هایشان شناخته می شوند. هیچ راهی برای دانستن چیزی در مورد یک شی یا درخواست انجام کاری بدون گذر از رابط آن وجود ندارد. Interface یک شی چیزی در مورد پیاده سازی آن نمی گوید و اشیاء مختلف آزاد هستند تا درخواست ها را متفاوت پیاده سازی کنند. این بدان معناست که دو object کاملاً متفاوت می توانند پیاده سازی هایی با Interface یکسانی داشته باشند.
هنگامی که یک درخواست به یک شی ارسال می شود، اجرای یک operation خاص هم به request و هم به object دریافت کننده بستگی دارد. اشیاء مختلف که درخواست های یکسان را پشتیبانی می کنند ممکن است پیاده سازی های متفاوتی از عملیات داشته باشند که این خواسته ها را برآورده می کنند. ارتباط Run-Time یک request به یک object و یکی از operation های آن به عنوان Dynamic binding شناخته می شود.
Dynamic binding به این معنی است که صدور یک درخواست از ساید شما را تا زمان اجرا، متعهد به اجرای خاصی نمی کند در نتیجه میتوانید برنامههایی بنویسید که انتظار یک object با یک interface خاص را دارند، با علم به اینکه هر شیئی که interface درستی داشته باشد، درخواست را میپذیرد(یعنی شما مقید به interface هستید و درخواست را بر اساس امضاء آن ارسال میکنید.). علاوه بر این Dynamic binding به شما امکان میدهد در زمان اجرا، اشیایی را که دارای رابطهای یکسان هستند جایگزین کنید. این جایگزینی Polymorphism شناخته شده است و یک مفهوم کلیدی در سیستم های شی گرا است. Polymorphism تعاریف کلاینت ها را ساده می کند، object را از یکدیگر جدا می کند و به آنها اجازه می دهد روابط خود را با یکدیگر در Run-Time تغییر دهند.
اما موضوع بعدی که باید به آن توجه داشت درک تفاوت بین Class و Object نوع آن است.
Class نحوه پیاده سازی Object را تعریف می کند. Class وضعیت داخلی (internal state) شی و اجرای operation های آن را تعریف می کند. در مقابل، نوع یک شی فقط به رابط آن , مجموعه درخواست هایی که می تواند به آنها پاسخ دهد اشاره دارد. یک شی می تواند انواع مختلفی داشته باشد و اشیاء با کلاس های مختلف می توانند یک نوع داشته باشند.
البته، رابطه نزدیکی بین کلاس و نوع وجود دارد. از آنجا که یک کلاس عملیاتی را که یک شی می تواند انجام دهد، تعریف می کند نوع شی را نیز مشخص می کند. وقتی می گوییم یک شی نمونه ای از یک کلاس است به این معنی است که شی از رابط تعریف شده توسط کلاس پشتیبانی می کند.
درک تفاوت بین وراثت کلاس و وراثت نیز مهم است. وراثت کلاس، پیاده سازی یک شی را بر حسب اجرای یک شی دیگر تعریف می کند. به طور خلاصه مکانیزمی برای به اشتراک گذاری کد و نمایش آن است. در مقابل، وراثت ، رابط زمانی را توصیف می کند که یک شی می تواند به جای شی دیگر استفاده شود.
وراثت کلاس اساساً فقط مکانیزمی برای گسترش عملکرد برنامه با استفاده مجدد از عملکرد در کلاسهای superclass است. این به شما امکان می دهد نوع جدیدی از شی را به سرعت در قالب یک شی قدیمی تعریف کنید و همچنین این به شما امکان می دهد پیاده سازی های جدید را تقریباً آزادانه دریافت کنید و بیشتر آنچه را که نیاز دارید از کلاس های موجود به ارث ببرید.
با این حال، استفاده مجدد از پیاده سازی تنها نیمی از داستان است. توانایی وراثت برای تعریف خانوادههای اشیاء با رابطهای یکسان (معمولاً با ارث بردن از یک کلاس انتزاعی) نیز مهم است. چرا؟ زیرا Polymorphism به آن بستگی دارد.
وقتی از وراثت با دقت استفاده می شود، تمام کلاس های مشتق شده (subclass) از یک کلاس انتزاعی، رابط آن را به اشتراک خواهند گذاشت. این بدان معناست که یک subclass صرفاً عملیات را اضافه یا override می کند و عملیات کلاس والد را پنهان نمی کند. سپس همه subclass ها میتوانند به درخواستهای واسط این کلاس انتزاعی پاسخ دهند و همه آنها را به زیرگروههای کلاس انتزاعی تبدیل کنند.
همچنین این امر وابستگی های پیاده سازی بین زیرسیستم ها را تا حد زیادی کاهش می دهد که منجر به قانون زیر می گردد:
Programming to an interface, not an implementation
تکنیک های رایج در طراحی های شی گرا:
ابتدا لازم است در رابطه با دو تکنیک رایج برای استفاده مجدد از functionality های ایجاد شده در سیستم های شی گرا صحبت کنیم: class inheritance و object composition.
همانطور که توضیح دادیم وراثت این امکان را به شما می دهد تا پیاده سازی یک کلاس را بر اساس کلاس دیگر تعریف کنید. استفاده مجدد از طریق ارث اغلب به عنوان استفاده مجدد از دیدگاه white-box شناخته می شود. اصطلاح “white-box” به Visibility اشاره دارد به این معنی که با وراثت اجزای داخلی کلاس های superclass اغلب برای subclass ها مشخص هستند.
اما object composition جایگزینی برای class inheritance است. در اینجا عملکرد جدید با assembling یا object composition برای به دست آوردن عملکرد پیچیده تر به دست می آید. object composition مستلزم آن است که اشیاء ساخته شده دارای رابط های کاملاً تعریف شده باشند. این سبک استفاده مجدد اغلب به عنوان black-box شناخته می شود زیرا جزئیات داخلی اشیا قابل مشاهده نیست و اشیاء فقط به عنوان “black-box” ظاهر می شوند.
inheritance و composition هر کدام مزایا و معایب خود را دارند. inheritance به صورت static در compile-time تعریف می شود و استفاده از آن ساده است، زیرا مستقیماً توسط زبان برنامه نویسی پشتیبانی می شود همچنین اصلاح پیاده سازی و استفاده مجدد را آسان تر می کند. هنگامی که یک subclass برخی از operation ها را override میکند ( نه همه operation ها) میتواند بر عملیاتهایی که به ارث میبرد تأثیر بگذارد، با این فرض که client فراخوانی را از طریق operation های override شده انجام میدهد.
اما ارث بری کلاس معایبی نیز دارد. اول شما نمی توانید پیاده سازی های به ارث رسیده از کلاس های والد را در زمان اجرا تغییر دهید، زیرا همانطور که گقته شد وراثت در زمان کامپایل تعریف می شود. دوم کلاس های والد اغلب حداقل بخشی از نمایش فیزیکی زیر کلاس های خود را تعریف می کنند به این معنی که وراثت، یک subclass را در معرض جزئیات پیادهسازی superclass آن قرار میدهد و اغلب گفته میشود که وراثت کپسولهسازی را خراب میکند (inheritance breaks encapsulation) و در نهایت اجرای یک subclass به قدری با اجرای کلاس superclass آن مرتبط می شود که هر تغییری در اجرای superclass سریعا subclass را مجبور به تغییر می کند.
از جهتی دیگر وقتی میخواهید از یک subclass استفاده مجدد کنید وابستگیهای پیادهسازی میتوانند باعث ایجاد مشکل شوند. اگر هر جنبه ای از پیاده سازی از دیدگاه ارث بری مناسب نباشد (کلاس های فرزند برای پیاده سازی های جدید و یا تغییرات جاری دارای محدودیت گردد)، superclass بایستی بازنویسی شود یا با چیزی مناسب تر جایگزین شود. این وابستگی انعطاف پذیری و در نهایت قابلیت استفاده مجدد را محدود می کند. یکی از راههای رفع این مشکل این است که فقط از کلاسهای abstract ارث برده شود، زیرا آنها معمولاً پیادهسازی کمی دارند.
اما object composition به صورت dynamic و در run-time از طریق اشیایی که به اشیاء دیگر ارجاع می دهند تعریف می شود.Composition به object هایی نیاز دارد که از رابطهای یکدیگر پیروی کنند و به نوبه خود به رابط هایی با دقت طراحی شده نیاز دارد که مانع از استفاده از یک شی با بسیاری از شیهای دیگر نشود. اما یک نکته وجود دارد، از آنجایی که اشیاء تنها از طریق interface آنها قابل دسترسی هستند ما encapsulation را نمی شکنیم. هر شی را می توان در زمان اجرا با دیگری جایگزین کرد تا زمانی که دارای همان نوع باشد. علاوه بر این، از آنجا که پیادهسازی یک شی بر حسب interface های شی نوشته میشود وابستگیهای پیادهسازی به طور قابل توجهی کمتر است.
همچنین object composition تأثیر دیگری بر طراحی سیستم دارد، ترجیح دادن object composition نسبت به وراثت کلاس به شما کمک می کند تا هر کلاس را به سمت encapsulation ببرید و روی یک کار متمرکز کنید. کلاس ها و سلسله مراتب طبقاتی (از دیدگاه ارث بری) شما کوچک باقی می مانند و احتمال اینکه تبدیل به هیولاهای غیرقابل مدیریت شوند کمتر خواهد بود. از سوی دیگر طراحی مبتنی بر object composition دارای اشیاء بیشتری خواهد بود (اگر کلاسهای کمتری داشته باشد) و رفتار سیستم به جای اینکه در یک کلاس تعریف شود به روابط متقابل آنها خلاصه می گردد لذا این موضوع ما را به اصل دوم طراحی شی گرا هدایت می کند:
Favor object composition over class inheritance
در حالت ایده آل برای دستیابی به استفاده مجدد مجبور نیستید اجزای جدیدی ایجاد کنید بلکه شما باید بتوانید تمام عملکردهای مورد نیاز خود را فقط با مونتاژ اجزای موجود از طریق object composition بدست آورید. اما این به ندرت اتفاق می افتد زیرا مجموعه اجزای موجود در عمل هرگز به اندازه کافی غنی نیستند. استفاده مجدد از طریق interface ساخت اجزای جدید را که می توان با اجزای قدیمی ترکیب کرد آسان تر می کند.
تجربه ثابت کرده است که طراحان بیش از حد از interface به عنوان استفاده مجدد استفاده می کنند.
تکنیک و طرح ها اغلب با وابستگی بیشتر به object composition قابل استفاده مجدد (و ساده تر) می شوند برای نمونه شما در الگوهای GOF این موضوع را بارها و بارها مشاهده می کنید.
یکی دیگر از تکنیکهای (نه کاملاً شی گرا) برای استفاده مجدد از عملکرد، از طریق parameterized type است که به نامهای generic یا temple نیز شناخته میشوند. این تکنیک به شما امکان می دهد یک type را بدون تعیین تمام type های دیگری که استفاده می کند تعریف کنید. به عنوان مثال یک List class را می توان بر اساس نوع عناصری که در آن قرار دارد parameterized کرد یا برای اعلام لیستی از اعداد صحیح، نوع “integer” را به عنوان پارامتر به نوع پارامتری List وارد می کنید و یا برای اعلام لیستی از اشیاء String نوع “String” را به عنوان پارامتر وارد می کنید.
در اصل parameterized type راه سومی را علاوه بر class inheritance و object composition برای ترکیب رفتار در سیستم های شی گرا به ما می دهد. بسیاری از طراحان ها با استفاده از هر یک از این سه تکنیک پیاده سازی های خود را انجام میدهند.
البته تفاوت های مهمی بین این تکنیک ها وجود دارد: object Composition به شما امکان می دهد رفتاری که در run-time ایجاد می شود را تغییر دهید اما استفاده از یک شی یا لایه میانی واسط (برای دستیابی به یک هدف خاص) می تواند کارایی کمتری داشته باشد (این مورد در کتاب GOF و توضیح الگوها با نام requires indirection اعلام میگردد.)
inheritance به شما امکان می دهد پیاده سازی های پیش فرض را برای operation ها ارائه دهید و به subclass ها اجازه می دهد آنها را override کنند.
و در آخر parameterized type به شما امکان می دهد type هایی را که یک کلاس می تواند استفاده کند تغییر دهید.
اما نه inheritance و نه parameterized type نمی توانند در run-time تغییر کنند. اینکه کدام رویکرد بهتر است بستگی به محدودیت های طراحی و پیاده سازی شما دارد.
خوب اگر موافق باشید سفر ما تا اینجا باشه و بزنیم کنار تا شما یه استراحت بکنید و در قسمت بعد وارد الگوهای طراحی بشیم و همچنین با نگاه طراحان الگوها ذره ذره بیشتر آشنا بشیم 😉
راستی نظرات یادتون نره🫡
اگر مطالب تا اینجا به دردتون خورده خوشحال میشم با کلیک روی تصویر زیر برای من یه قهوه خوشمزه بگیرید تا مغزم حسابی کیف کنه 😍!!!