نویسندگان این نوشته: علی احمدی، امیرمحمد محمدی، سجاد فقفور مغربی، متین مرادی و محمدصادق سلیمی
در مهندسی نرمافزار یکی از مهمترین بخشها، الگوهای طراحی هستند. اما در ابتدا باید بگوییم الگو چیست.
الگو را تشکیل شده از سه بخش:
میدانند و آن را این گونه تعریف میکنند:
الگو جوابی کلی به یک سوال تکرار شونده در یک زمینه مخصوص است.
پس با این تعریف دلیل اهمیت الگوها تکرار شوندگی مسئله است به صورتی که هر مهندس نرمافزار باید به این الگوها مسلط باشد و از آنها در کارهای روزمره استفاده بنمایید.
یکی از مهمترین سری الگوهای مهندسی نرمافزار، الگوهای طراحی هستند که به دلیل این که به صورت کامل و جمعبندی شده در کتابی به نام Design Pattern معرفی شده اند به الگو های Gang of Four یا Go4 معروف هستند.
این الگوها به سه زیر دسته
تقسیم شدهاند و هر کدام از این دستهها شامل چندین الگو هستند. ما در این مقاله تلاش میکنیم هر کدام را با توضیحی مختصر و تا حد امکان کافی به همراه مثالهایی آنها را نمایش دهیم. در انتها هم یکی از الگوها به شکل مفصلتری توضیح داده شده است، به همراه نمونه های کاملتر، پیادهسازی مثالی واقعی و رسم دیاگرام مربوط به آن با توجه به الگوی معرفیشده و ...
الگوی یگانه یک الگوی آفرینشی است که بیان میکند که تنها یک نمونه از یک کلاس میتواند ساخته شود.
معمولا در هنگام استفاده از این الگو یک اشارهگر عمومی به این نمونه ساخته میشود تا تمامی کلاسهای دیگر بتوانند از این نمونه استفاده کنند، همچنین تابع سازنده کلاس یگانه (تابعی که مسئول ساخت نمونههای جدید است) به شکل خصوصی تعریف میشود تا دیگر کلاسها دسترسی به این تابع و در نتیجه توانایی ساختن نمونهای جدید از کلاس را نداشته باشند.
این الگو در مواقع زیادی میتواند مورد استفاده قرار بگیرد؛ برای مثال در کلاسهایی که وظیفه مدیریت موارد یا نمونههای دیگر را دارند این الگو استفاده میشود. برای مثال فرض کنید یک سیستم مدیریت رستوران داریم؛ در این سیستم تعداد زیادی نمونه سفارش وجود دارد اما تنها یک نمونه از کلاس مدیریت سفارشها وجود دارد. مورد دیگر در واقع ابزارهایی هستند که تنها به یک نمونه از آنها نیاز است، مثلا در همان مثال مدیریت رستوران تنها به یک نمونه از کلاس رابط با پایگاه داده نیاز است.
در مورد الگوی یگانه باید توجه داشت که بعضی افراد این الگو را با اصل مسئولیت یکتا (Single Responsibility Principle) متناقض میدانند، همچنین برخی معتقدند این الگو به دلیل ایجاد کردن یک حالت عمومی آزمودن کد را سخت میکند و الگوی مناسبی نیست.
در این الگو در ابتدای شروع برنامه یک نمونه از کلاس ساخته می شود و در ادامهی اجرا هنگامی که یک موجودیت دیگر درخواست ساخت و دسترسی به یک نمونه جدید از این کلاس را داشت به جای ساخت یک نمونه جدید به حالت معمول، یک کپی از نمونه اولیه ساختهشده ایجاد و به عنوان نمونه جدید به درخواستدهنده برگردانده میشود. در این الگو هم شبیه به الگوی یگانه تابع سازنده نمونه جدید خصوصی میشود تا کلاسهای دیگر نتوانند از طریق این تابع نمونهی جدیدی ایجاد کنند.
در بسیاری از موارد ایجاد یک نمونه جدید از یک کلاس میتواند از لحاظ محاسباتی امر پر هزینهای باشد؛ مثلا ممکن است به مقادیر زیادی خواندن از حافظه، به محاسبات اولیه زیاد و… نیاز داشته باشد. در این مواقع ایجاد دوباره یک نمونه از این کلاس هر زمانی که یک موجودیت جدید درخواست آن را داشت میتواند به سادگی باعث کندی و کاهش سرعت برنامه شود؛ بنابراین در این زمانها از الگوی نمونه اولیه استفاده میشود و در هنگام راه اندازی برنامه که نیاز زیادی به سرعت بالا در اجرا نیست، مثالهایی از کلاسهای این چنینی ساخته میشود تا در طول اجرای برنامه مورد استفاده قرار گیرد.
در این الگو کلاس یا کلاسهایی ساخته میشوند که به کمک آنها بتوان نمونههایی از یک کلاس دیگر که معمولا یک کلاس بزرگ و پیچیده است ساخت. معمولا در این الگو یک کلاس پدر ساخته میشود که تابعهای مختلفی دارد که هر کدام از آنها در ساخت نمونه از کلاس هدف نقش دارند، سپس برای ایجاد هر نوع نمونه جدید، یک کلاس فرزند این کلاس ساخته میشود و مقادیر این تابعها را متناسب با نیاز خود تغییر میدهد. سپس از این کلاس برای ساخت نمونه هدف استفاده میکند.
برای مثال فرض کنید که یک کلاس مجموعه داده (dataset) داریم که هر نمونه آن یک مجموعه داده خاص است. برخی از مجموعه دادهها از حافظه خوانده میشوند، بعضی از اینترنت گرفته میشوند، بعضی به شکل جویبار داده از اینترنت استفاده میشوند، برخی با استفاده از دیگر ابزارها در لحظه تولید می شوند و ... . حال اگر بنا بر این باشد که همهی این نمونههای متفاوت به شکل مشابهی ساخته شوند هم میزان کد مشابه زیادی تولید خواهد شد و هم کار برای برنامهنویسهایی که میخواهند از این کلاس نمونه ایجاد کنند سخت خواهد بود. (چون احتمالا روش صحیح کار با این کلاس و ساخت نمونه را نمیدانند.) بنابراین در ایجاد، الگوی سازنده وارد عمل میشود و یک کلاس مجموعه دادهساز ایجاد میکند که میتواند یک نمونه ساده مجموعه داده ایجاد کند و سپس در موارد دیگر میتوان با ایجاد یک کلاس فرزند و تغییر بخشهایی از این کلاس، کلاسهای سازنده مشابهی را ساخت که مجموعه دادههای مورد نیاز را ایجاد کنند.
این الگو یک رابط ساده برای کاربر ارائه میدهد تا با استفاده از این رابط با یک ساختار پیچیده که در پشت آن مخفی شده است ارتباط برقرار شود.
چرا؟ اگر این الگو رعایت نشود شما برای استفاده از کد دیگران باید کارهای جانبی بسیاری انجام بدهید و کاری مثل استفاده از یک تابع ساده هم نیاز به تنظیمات جانبی دیگر و بسیار پیچیده میشد.
یک مثال در دنیای واقعی:
وقتی شما از یک فروشگاه آنلاین خرید میکنید و خرید خود را تحویل میگیرید در حقیقت سایت آن فروشگاه یک نما است؛ زیرا شما با انجام عملیاتهای ساده توانستید یک کالا تحویل بگیرید و به مسائل پشت تحویل کالا مثل انبارداری، حمل و نقل، بستهبندی مناسب توجهی نمیکنید.
استفاده از encapsulation به طور صحیح در مفاهیم OOP نمونه دیگری از کاربرد این الگو است.
این الگو برای تبدیل رابط (interface) یک شیء به یک شیء دیگر استفاده میشود. به طور مثال، اگر در حال طراحی یک نرمافزار داشبورد بررسی و تحلیل داده باشیم، در یک بخش برنامه ممکن است از دادهای با فرمت جیسون (JSON) و در جای دیگر از داده با فرمت XML استفاده کرده باشیم. در این شرایط میتوانیم برای تبدیل این دادهساختارها به یکدیگر از الگوی سازگارگر استفاده کنیم.
الگوی سازگارگر میتواند دو طرفه باشد. به طور نمونه در مثال بالا هم میتواند جیسون را به XML و هم XML را به جیسون تبدیل کند. مهمترین مزیت این الگو حفظ اصل مسئولیت یکتا (Single Responsibility Principle) و ارائه راهحلی برای ساخت و تغییر شیء تبدیل با حفظ اصل باز/بسته (Open-Closed Principle) است.
مثال در دنیای واقعی:
یک نمونهی جالب و آشنا برای ما ایرانیها، آداپتورهای لپتاپ هستند. از آنجایی که پریزهای برق در ایران فقط دوشاخ میپذیرد، برای تبدیل سهشاخ که یکی به زمین میرود نیاز به یک آداپتور داریم. همان طور که مشخص است این آداپتورها عموما یکطرفه هستند.
الگوی طراحی نهانگاه، از یک شیء استفاده میکند که با به اشتراک گذاشتن بخشی از دادههایش با شیءهای مشابه دیگر، حافظه مصرفی را کاهش میدهد. این الگو وقتی مفید است که با تعداد زیادی از اشیاء با عناصر تکراری ساده سر و کار داشته باشیم که اگر به صورت جداگانه ذخیره شوند، مصرف حافظه زیادی خواهند داشت. در این الگو، دادههای مشترک در ساختماندادهای خارج از شیء اصلی نگهداری میشوند و به اشیاء flyweight منتقل میشوند. به این ترتیب، تعداد اشیاء فیزیکی ایجاد شده کاهش مییابد و امکان استفاده مجدد از اشیاء flyweight با اشاره (pointer) به وضعیت آنها فراهم میشود.
چند نکته:
مثال در دنیای واقعی:
در مواقعی که تعداد زیادی برگهی امتحانی باید تصحیح شود، به ازای هر برگه یک پاسخنامه جداگانه در نظر گرفته نمیشود بلکه به یک پاسخنامه که معیار نمرهدهی است ارجاع داده خواهد شد. با این کار میزان کاغذ استفاده شده تا حد بسیار زیادی کاهش میدهیم.
در این الگو ما هر درخواست کاربر (کوئری و …) را به صورت یک شی از جنس دستور میبینیم و آن را در یک شیء دیگر میگنجانیم و سپس آن را به دریافتکننده دستور ارسال میکنیم.
چرا و چگونه؟ این الگو به ما کمک میکند که برای درخواستهای خود یک صف یا تاخیر اجرا کنیم و دستورات با ترتیب خاص اجرا شوند و همچنین ساختار هر درخواست را بدانیم تا اگر المان جدیدی به برنامه خود اضافه کردیم و میخواستیم از آن درخواست دوباره استفاده کنیم این تکرار آسانتر باشد.
مثال در دنیای واقعی:
فرض کنید به یک رستوران بسیار شلوغ مراجعه کردید. بعد از نشستن در محل پیشخدمت به شما مراجعه میکند و سفارش شما را در برگهای ثبت میکند. سپس این پیشخدمت به آشپزخانه میرود و سفارش شما که در قالب یک برگه است را به آن جا تحویل میدهد. بعد از مدتی (یا بر اساس اولویت و سختی غذاها) آشپزخانه سفارش شما را میبیند و از طریق نوشتهها سفارش شما را آماده میکند.
در این مثال درخواست ما درست کردن غذا است ولی ما آن را در قالب یک دستور یعنی برگه پیشخدمت به آشپزخانه ارائه میدهیم. اگر ما این درخواست را خودمان مینوشتیم مطمئنا ساختار یکسانی نداشت و آشپزخانه گیج میشد. اگر ما خودمان درخواست را شخصا به آشپزخانه میبردیم آشپزخانه شلوغ میشد و اولویتها را نمیتوانست مدیریت کند.
در این الگو، یک تعداد شیء با اطلاعات بااهمیت داریم که به آنها اشتراکدهنده یا publisher میگوییم. از طرف دیگر تعدادی شیء وجود دارند که به این دادهها در این اشیاء اهمیت میدهند. آنها میخواهند با ایجاد برخی تغییرات از وضعیت جدید باخبر شوند. به این اشیاء subscriber یا اشتراکگیرنده میگوییم.
حال به ازای هر تغییر بااهمیت در وضعیت شیء اشتراکدهنده شیء اشتراکگیرنده از آنها باخبر میشود. باید دقت شود که اشیاء مشترک میتوانند از کلاسهای مختلفی باشند بنابراین باید حتما از یک رابط (interface) مشترک در پیادهسازی مشاهدهگر استفاده شود.
مثال در دنیای واقعی:
وقتی در یک خبرنامه اشتراک میگیریم، به ازای هر بار انتشار آن به صورت آنلاین یا فیزیکی خبرنامه جدید را دریافت میکنیم. در این جا ما به عنوان اشتراکگیرنده و شرکت مدیریتکنندهی مجله به عنوان اشتراکدهنده عمل میکند.
با استفاده از این الگو ما میتوانیم تصویری از یک شیء داشته باشیم و در صورت نیاز به آن تصویر از شیء بازگشت انجام دهیم. البته داشتن این تصویر به معنای دسترسی به اجزای داخلی شیء نیست. برای داشتن این تصویر از شیء ما ساخت این تصویر را به خود شیء میسپاریم و تنها از او میخواهیم که یک یادگاری به ما بدهد و اگر ما یک یادگاری از خودش به او دادیم بتواند به حالت آن زمان یادگاری برگردد.
مثال دنیای واقعی:
یک نمایشگر متن را در نظر بگیرید که در هر لحظه به شما اجازه میدهد کار قبلی خود را حذف کنید و به حالت قبل از آن برگردید. این نمایشگر احتمالا از این الگو استفاده میکند و در هر لحظه بعد از هر تغییر حالت خود را نگهداری میکند و وقتی شما به آن دستور میدهید که بازگردد، حالت قبلی خود را که قبلا ذخیره کرده است بازیابی میکند.
تصور کنید که روی یک سیستم سفارش آنلاین کار میکنید. شما میخواهید دسترسی به سیستم را محدود کنید تا فقط کاربران تأیید شده بتوانند سفارش ایجاد کنند. همچنین کاربرانی که دارای مجوزهای مدیریتی هستند باید به تمامی سفارشات دسترسی کامل داشته باشند.
بعد از کمی برنامهریزی متوجه میشوید که این بررسیها باید به صورت متوالی انجام شود. هر زمان که درخواستی حاوی اطلاعات کاربری کاربر دریافت شد، برنامه میتواند برای احراز هویت کاربر در سیستم تلاش کند. با این حال، در هر مرحلهی بررسی، اگر احراز هویت نامعتبر بود و یا به هر دلیلی به مشکل خوردیم، دلیلی برای انجام بررسیهای بعدی وجود ندارد.
درنهایت سیستم را به این شکل طراحی میکنید:
اما پس از چند ماه، به دلیل ذات تغییرپذیری نیازمندیها، لازم میشود چند مورد دیگر از این بررسیهای متوالی را پیاده سازی کنید. نتیجه این که برنامهی شما به این شکل درمیآید:
با توجه به دانش مهندسی نرمافزاری که در این درس و درسهای مشابه پیدا کردیم، میدانیم این برنامه، برنامهی مطلوبی نیست چرا که سختفهم است، قابلیت استفاده مجدد ندارد، نگهداری پرهزینهای دارد و …
اما راه حل چیست؟ استفاده از الگوی طراحیِ «زنجیرهی مسئولیت»؛ الگوی طراحی زنجیرهی مسئولیت در برنامهنویسی شیءگرا شامل یک زنجیره از اشیاء به عنوان پردازشگر (handler) است. هر پردازشگر در زنجیرهی مسئولیت، نوعی از اشیاء را میپذیرد، بررسی میکند و به بعدی انتقال میدهد و سایر آنها را به پردازشگر بعدی در زنجیره انتقال نمیدهد. این الگو این امکان را فراهم میکند که به سادگی پردازشگرهای جدید را به انتهای زنجیره اضافه کرد. به عبارت دیگر، هر پردازشگر در زنجیرهی مسئولیت، نوعی از اشیاء را میپذیرد و به بررسی آنها میپردازد. در صورتی که بررسیها با موفقیت به پایان رسید و شیء شرایط مناسب را برای انتقال به پردازشگر بعدی داشت، به پردازشگر بعدی در زنجیره منتقل میشود. الگوی زنجیره مسئولیت در حل مسائل رایج و تکراری در طراحی نرمافزار شیءگرا استفاده میشود. این الگو به طراحی نرمافزاری انعطافپذیر و قابل استفاده مجدد و همچنین کاهش وابستگی بین فرستنده و گیرنده درخواست کمک میکند و امکان پشتیبانی از چندین گیرنده برای یک درخواست را به صورت سادهتری ممکن میسازد.
با توجه به این الگو، میتوان مثال گفته شده در ابتدا را به این شکل پیادهسازی کرد. زنجیرهای از handlerها که هر کدام وظیفهی خاصِ خود را دارند و در صورتی که بررسی هویت در آن fail شود، مستقیما نتیجه را گزارش میدهند:
به عنوان یک مثال دیگر ولی این دفعه مختصرتر و البته عملیتر، سیستمِ نشان دادن help در یک نرمافزار را درنظر بگیرید. اصولا هر اِلِمان در UI، زیرمجموعهی یک اِلِمان دیگر قرار میگیرد. وقتی کاربر روی componentی قرار میگیرد و دکمه F1 را میفشارد، باید help مربوطه نشان داده شود. فرض کنید درخت کامپوننتها به شکل زیر باشد:
در چنین حالتی با توجه به آن چه تاکنون آموختیم، میتوانیم برنامه را به شکل دیاگرام زیر پیادهسازی کنیم. وقتی موس کاربر روی یک المان میرود و F1 میزند، برنامه ابتدا componentی را که زیر موس قرار دارد را شناسایی کرده و به آن یک request برای help میفرستد. این درخواست در میان containerهای آن المان تا جایی بالا میرود که یک المان بتواند اطلاعات help را نشان دهد:
مثال در دنیای واقعی:
وقتی برای پشتیبانی از یک محصول با call center یا مرکز تماس کارخانه تماس میگیریم، در واقع داریم این الگو را مشاهده میکنیم. ابتدا درخواست ما توسط ماشین مورد ارزیابی قرار میگیرد. سپس اگر مشکل حل نشده بود، به یک پاسخگوی غیر متخصص متصل میشویم. اگر باز هم مشکل برطرف نشد به یک تکنسین و متخصص متصل میشویم تا به احتمال بیشتری مشکل حل شود.
از زمانی که ۲۳ تا الگوی طراحی توسط ۴ نفر در ۱۹۹۴ پیشنهاد و در کتابی به همین نام چاپ شد، حدود ۳ دهه میگذرد. در این سالها، بیشتر و بیشتر از این الگوها در برنامههای واقعی استفاده میشود و از طرف دیگر تحقیقات و مقالات مختلفی درباره آنها منتشر میشود. امروز حوزههای تحقیقات در این موضوع شامل موارد متفاوتی میشود از جمله مثل بررسی بهینگی و نقاط مثبت و منفی الگوها، تعمیم و پیشنهاد الگوهایی برای حوزههای نوین مانند سیستمهای هوش مصنوعی و یادگیری ماشین، آموزش مفاهیم الگوهای طراحی به شکل موثر و … [1]
برای نمونه میتوان به مقالهای اشاره کرد تحت عنوان
Design Patterns for AI-based Systems: A Multivocal Literature Review and Pattern Repository
که به تعمیم و پیشنهاد الگوهای طراحی برای سیستمهای مبتنی بر هوش مصنوعی میپردازد. در قسمتی از آن میخوانیم:
Many implementation patterns are also adaptations of the original “Gang of Four” design patterns to an AI context, e.g., Strategy, Adapter, or Decorator.
یا مقالهی
Evaluating an Interactive Tool for Teaching Design Patterns
از دانشگاه Auckland در نیوزیلند که راهکاری جدید برای آموزش الگوهای طراحی پیشنهاد میدهد و میخواهد این مفاهیم نسبتا انتزاعی را با ترکیبی از ابزارهای بصری و تمرینات برنامهنویسی به یادگیرندگان، آموزش دهد.
[1] The state of the art on design patterns