یکی از مباحث بسیار مهم در دنیای برنامه نویسی، الگوهای طراحی (Design Pattern) می باشند. در مطلب میخواهیم با زبان سی شارپ به بررسی الگوهای طراحی کاربردی و مهم بپردازیم و ببینیم دلیل استفاده از الگوهای طراحی (Design Patterns) چیست؟ با توجه به این که امروزه نرم افزارها بخش مهمی از زندگی بشر رو تشکیل داده اند و بسیاری از کارهای روز مره و محاسبات توسط نرم افزارها انجام می شود، طراحی و توسعه نرم افزار یکی از کار های مهم در جوامع کنونی است. به دلیل پویایی بیش از حد نرم افزار و تغییرات بسیار سریع تکنولوژی ها و نیازمندی های کاربران نرم افزار باید به گونه ای باشد که بتوان به آسانی آن را فهمید و تغییر داد. یکی از راه حل ها برای حل مشکلات گفته شده، الگوهای طراحی شی گرا (Object Oriented Design Pattern) می باشد.
این الگو ها به قدری مهم هستند که اگر کسی بخواهد در شرکتی به عنوان برنامه نویس استخدام شوند یکی از سوالاتی که در مصاحبه کاری از وی پرسیده می شود تسلط بر این الگوها می باشد. به دلیل این که منبع فارسی جامعی برای این مباحث وجود ندارد تصمیم گرفتم سری مقالات و آموزش الگوهای طراحی را به یاری خداوند شروع کنم.
سری مطالبی که به این منظور در سایت قرار خواهم داد این گونه خواهد بود که اول در قالب یک مقاله یک یا چند الگو را توضیح داده و مشکلات و چالش های آن را بررسی خواهم کرد سپس در قالب یک آموزش به توضیح و نحوه استفاده از الگوی مورد نظر را بیان می کنم.الگو هایی که در این سری مطالب بررسی می کنیم شامل الگوهای زیر می باشند:
در آموزش های گفته شده بیشتر از کد های C# استفاده خواهد شد ولی سعی می کنم کد های Java رو هم در کد ها بیاورم. برای درک کامل مطالبی که در این سری ارائه می شود لازم است که خوانندگان به مفاهیم شی گرایی و همچنین زبان مدل سازی uml آشنا باشند.از آنجایی که هیچ مطلبی خالی از اشکال نیست از دوستان خواهش می کنم که با نظرات خودشون بنده رو در این کار یاری کنند.حال به امید خدا شروع می کنیم.
برای شروع مقدمه ای از شی گرایی و نمودار های uml می گم تا در مطالب بعدی به الگوها می پردازم.ما در یک دنیای شی گرا زندگی می کنیم به گونه ای که خود شما یک شی هستید. شما با اشیا دیگر که ممکن است انسان، حیوان، و یا هرچیز دیگری ارتباط برقرار می کنید. همه اشیا دارای خصوصیاتی هستند. خود شما نیز دارای خصوصیاتی هستید مانند قد، وزن، رنگ مو و..... همچنین رفتار هایی از همه ی اشیا سر می زند یا بر روی اشیا رفتارهایی صورت می گیرد.
خود شما نیز دارای رفتارهایی مثل غذا خوردن و راه رفتن اجرا می کنید.اشیا در برنامه نویسی هم ساختارهایی هستند که دارای خصوصیات و رفتارهایی می باشند. که خصوصیات برای نگهداری داده های مربوط به شی مورد نظر مورد استفاده قرار می گیرندو و رفتار ها بر روی خصوصیات خود شی و یا خصوصیات اشیا دیگر عملی انجام می دهند. خصوصیات در برنامه نویسی شی گرا به نام های property,field شناخته می شوند و رفتارها با نام های method,function شناخته می شوند.
همه ی اشیا از مفهومی به نام کلاس ساخته می شوند که هر کلاس مشخص می کند که اشیائی که از آن کلاس ساخته می شوند باید چه خصوصیات و رفتارهایی داشته باشند. به طور مثال شما یک شی از کلاس انسان ساخته شده اید و خصوصیات و رفتارهایی را که انسان باید داشته باشد را دارید. اما خود کلاس یک مفهوم انتزاعی(abstract) است یعنی همانطور که انسان یک مفهوم است و وجود خارجی ندارد خود کلاس نیز وجود ندارد و برای درک آن باید شیئی که از آن ساخته شده را دید.
یکی از مفاهیمی که در برنامه نویسی شی گرا به آن برمی خوریم مفهوم کپسوله سازی(Encapsulation) است. همانطور که گفته شد همه ی اشیا دارای خصوصیات و رفتارهایی هستند که برخی بر همه آشکار است و برخی دیگر از دید دیگران مخفی هستند. به این مفهوم کپسوله سازی می گویند که یکی از مفاهیم بسیار مهم شی گرایی می باشد و باعث می شود اطلاعاتی برای دیگر اشیا آشکار شود که مورد نیاز آنها است و باقی اطلاعات از دید کاربران مخفی است و همینطور باعث بالا رفتن امنیت داده ها می شود.
همینطور با استفاده از کپسوله سازی می توان بر چگونگی استفاده از خصوصیات اشیا نیز نظارت داشت.از مفاهیم دیگر در برنامه نویسی شی گرا وراثت(inheritance) می باشد که به توضیح آن می پردازیم. اگر شما خود را به عنوان یک شی در نظر بگیرید که از کلاس انسان ساخته شده باشید. این نکته بدیهی است که انسان یک موجود زنده است، زیرا خواص و رفتارهای موجودات زنده را دارد مانند تنفس و تغذیه و رشد و ترمیم.
در مفاهیم شی گرایی می گوییم کلاس انسان کلاس موجودات زنده را به ارث می برد یا کلاس انسان از کلاس موجودات زنده مشتق می شود. در برنامه نویسی شی گرا نیز وراثت به این شکل است و کلاس ها مانند دنیای واقعی به شکل سلسله مراتبی ساخته می شوند که یک کلاس می تواند از کلاس دیگر مشتق شود و خصوصیات و متدهای کلاس پدر را داشته باشد.
یکی از مفاهیم دیگر در شی گرایی چندریختی(polymorphism) می باشد که به این معنی است که همه ی کلاس های مشتق شده از یک کلاس خاص می توانند یک رفتار خاص را به گونه ای که مورد نظر خودشان است پیاده سازی کنند. برای مثال تصور کنید در کلاس حیوانات پستانداری که در خشکی زندگی می کنند عمل راه رفتن وجود دارد ولی حیوانی مانند ببر بر روی پنجه راه می رود و حیوانی مانند اسب بر روی سم راه می رود.
هر دو کلاس اسب و ببر عمل راه رفتن را انجام می دهند ولی این متد را هر کدام به شکل گوناگون پیاده سازی کرده اند.مفهومی که در شی گرایی به وفور از آن استفاده می شود مفهوم انتزاع(abstraction) است. تلویزیون خانه خود را در نظر بگیرید . این دستگاه دارای یک کنترل از راه دور است که با استفاده از آن می توان اعمالی را برروی تلویزیون مثل روشن و خاموش کردن انجام داد
ولی لازم نیست که کاربران درباره ی چگونگی ارتباط کنترل از راه دور با دستگاه و نحوه پردازش امواج توسط کنترل اطلاعی داشته باشد و فقط باید روش کارکردن با آن را بلد باشد. دربرنامه نویسی نیز کلاس ها و اشیا بسیاری هستند که ما باید فقط روش کارکردن با آنها را بلد باشیم و نیازی نیست که بدانیم چگونه طراحی شده و از چه الگوریتم هایی بهره گرفته اند و فقط می توانیم از خصوصیات و ویژگی های مرتبط آنها اطلاع داشته باشیم.
اینترفیس(Interface): در زبان هایی مثل C#, Java اینترفیس ها بیان کننده ی قوانینی هستنند که اگر کلاسی بخواهد از اینترفیس مورد نظر استفاده کند مجبور است که همه ی قوانین را اجرا می کند. این قوانین شامل خصوصیات کلاس و نوع آن و همچنین شامل متدها و نوع برگشتی و تعداد آرگومان های آن است. نکته ای که وجود دارد این است که اینترفیس هیچ کدام از این متدها رو پیاده سازی نمی کند و خصوصیات را مقدار دهی نمی کند و فقط مشخص می کند که کلاس های مشتق شده باید این ویژگیها را حتما باید داشته باشند.
اینترفیس ها در داخل الگوهای طراحی بسیار کاربرد دارد که در مقالات بعدی بیشتر آشنا خواهیم شد. قسمت های گفته شده تنها یک معرفی ساده و خیلی کوتاه از مفاهیم شی گرایی می باشد که لازم بود برای وارد شدن به مبحث الگوهای طراحی گفته شوند. در ادامه به معرفی چند نمودار UML می پردازیم که در شرح الگوهای طراحی از آنها بسیار استفاده خواهیم نمود. نمودار کلاس (class diagram) این نمودار همه ی کلاس ها و خصوصیات و متدهای آنها و همچنین ارتباط بین کلاس ها و نوع این ارتباط ها را نشان می دهد.
مهمترین عنصر در این نمودار عنصر کلاس است که به شکل مستطیل است که دارای سه قسمت می باشد: در بالای این شکل اسم کلاس آورده شده است و در قسمت Attributes خصوصیات کلاس و در قسمت Operations متدهای کلاس آورده می شود.ارتباط بین کلاس ها با خطوطی مشخص می شوند که هر کدام دارای مفهوم خاصی می باشد. یکی از ارتباط های مهم، ارتباط وراثت می باشد که به این شکل نشان داده می شود.
در شکل بالا کلاس های Teacherو Student از کلاس Person ارث بری می کنند. توجه داشته باشید که کلاس ها می توانند از یک اینترفیس نیز ارث بری داشته باشند. اینترفیس ها به شکل زیر در نمودار کلاس نشان داده می شوند.
قسمت های گفته شده پیش نیاز هایی برای ورود به مباحث الگوهای طراحی شی گرا بود. در بیان این مطالب سعی شده بود که به شکل ساده و ملموس گفته شود. دوستان توجه داشته باشند که این مطالب فقط یک مقدمه بودند و به صورت اجمالی و مختصر گفته شده اند. به امید خدا مطالبی را در تکمیل مباحث گفته شده خواهم آورد.
در بخش قبل مطالبی در باره ی شی گرایی و مفاهیم کلی آورده شد و در این بخش به شکل خاص به الگوهای طراحی خواهیم پرداخت و در بخش های بعد به شرح یک یا دو الگو می پردازیم.
طراحی برنامه های شی گرا به خودی خود سخت است و طراحی برنامه های شی گرایی که قابل استفاده مجدد هم باشند از آن نیز سخت تر است. قابلیت استفاده مجدد برای برنامه های نوشته شده بسیار مهم است. نکته ای که طراحان خبره برای حل یک مسئله میدانند اینست که نباید هر مسئله را از اول حل کرد. یعنی نباید برای حل آن از ابتدای کار شروع کنیم. بلکه باید با توجه به ابزار موجود و استفاده مجدد از آنها این کار انجام شود.
آنها وقتی که یک راهحل مناسب را یافتند، آنرا دفعات زیادی استفاده خواهند کرد. این تجربه، یکی از مسائلی است که آنها را خبره میکند. در نتیجه شما الگوهایی تکراری از کلاسها و اشیاء مرتب را در بسیاری از سیستمهای شئگرا خواهید دید. هر الگو یک مشکل خاص را در طراحی نرمافزار حل میکند و باعث میشود که طراحی شئگرای ما انعطافپدیرتر، زیباتر و در نهایت قابل استفاده مجدد باشد.این الگو ها به طراحان کمک میکنند که با مبنا قرار دادن تجربیات گذشته در طرح های جدید طرحهای موفق را مجددا استفاده نمایند. طراحی که با این الگو ها آشنایی بیشتری دارد، میتواند بلافاصله برای طراحی راه حل یک مسئله جدید، آنها را بکار گیرد.
بدون اینکه مجبور به کشف مجدد آنها باشد. شباهت ها میتوانند به تجسم نقاط مشترک بین الگوهای ،کمک کنند.الگوهای طراحی، استفاده مجدد از طرح ها و معماری های موفق را آسانتر می کنند. بیان نمودن تکنیک های اثبات شده و کارا بصورت الگوهای طراحی، آنها را برای برنامه نویسان سیستم های جدید بسیار قابل دسترس تر می کند. الگوهای طراحی به شما کمک می کنند تا از بین انتخاب های موجود، طرح هایی را بکار گیرید
که قابلیت استفاده مجدد از سیستم شما را بالا می برند و از انتخاب طرح هایی که قابلیت استفاده مجدد را کاهش می دهند، جلوگیری می کنند. بصورت ساده تر؛ الگوهای طراحی به طراح کمک می کنند تا طرح صحیح را سریعتر بیابد. هر الگو مسأله اي را شرح مي دهد که در محيط ما به طور مکرر رخ داده ، و سپس راه حل اصلي مسأله را شرح مي دهد ، به گونه اي که مي توانيد بيش از يک ميليون بار از اين راه حل استفاده کنيد ، بدون اين که دوبار آن را به يک طريق اجرا کنيد.
به دليل اين که الگوهاي طراحي فراواني وجود دارد نياز به شيوه اي جهت سازماندهي آن ها داريم . اين بخش الگوهاي طراحي را به گونه اي طبقه بندي مي کند که بتوانيم به خانواده هاي الگوهاي مربوطه ارجاع نمائيم . اين طبقه بندي به شما کمک مي کند الگوهاي موجود را سريعتر بياموزيد.
ما با دو ضابطه الگوهاي طراحي را طبقه بندي مي کنيم . ضابطه ي اول که هدف ناميده مي شود منعکس کننده کار الگو است . الگوها مي توانند داراي اهداف آفرينش(creational) ، ساختاري(structural) يا رفتاري(behavioral) مي باشند . الگوهاي آفرينش به فرآيند ايجاد اشیا مربوط مي شوند . الگوهاي ساختاري به ترکيب کلاس ها يا اشیا مي پردازند . الگوهاي رفتاري شيوه هايي را که در آن ها کلاس ها يا اشیا تعامل مي کنند را مشخص کرده و وظایف را توزيع مي کنند .
ضابطه دوم که حوزه(Scope) ناميده مي شود و مشخص مي کند که الگو در مرحله نخست در کلاس ها يا اشیا به کار مي رود . الگوهاي کلاس به روابط ميان کلاس ها و کلاس هاي فرعي آن ها مي پردازد . اين روابط به گونه اي از طريق وراثت مستقر مي گردند که در زمان کامپايل ايستا باشند . الگوهاي شی به روابط اشیا مي پردازند که مي تواند در حين اجرا تغيير کنند و پوياتر هستند . تقريبا کليه الگوها تا حدي از وراثت استفاده مي کنند . توجه داشته باشيد اغلب الگوها در حوزه شی قرار مي گيرند .
الگوهاي کلاس آفرينش بخشي از آفرینش اشیا را به کلاس هاي فرعي واگذار مي کنند ، در حاليکه الگوها ي شی آفرينشي آن را با شی ديگري تعويض مي کنند. الگوهاي کلاس ساختاري از وراثت جهت ساختن کلاس ها استفاده مي کنند، در حاليکه الگوهاي شی ساختاري به شرح راه هايي جهت نصب اشیا مي پردازد. الگوهاي کلاس رفتاري از وراثت جهت توصيف الگوريتم ها و جريان کنترل استفاده مي کنند ، در حاليکه الگوهاي شی رفتاري شرح مي دهند که چگونه يک گروه از اشیا جهت انجام وظيفه اي که هيچ شی منفردي قابل به انجام آن نيست مشارکت مي کنند.
شيوه هاي ديگري جهت سازماندهي الگوها وجود دارد. اغلب بعضي از الگوها با يکديگر استفاده مي شوند . به عنوان مثال ، اغلب Composite با Iterator يا Visitor استفاده مي شود . بعضي الگوها جايگزين هستند : اغلب الگو ي Prototype جايگزين Abstract Factory است . بعضي الگوها با وجود آن که داراي هدف هاي متفاوتي هستند به طراحي مشابه منجر مي شوند .
به عنوان مثال ، نمودارهاي ساختار Composite و Decorator مشابه هستند .روشن است که راههاي بسياري براي سازماندهي الگوهاي طراحي وجود دارد. داشتن چندين راه براي فکر کردن درباره الگوها ، بينش شما را در مورد اين که آنها چه کاري انجام مي دهند، چگونه مقايسه مي شوند و چه وقت به کار گرفته مي شوند، عميق تر مي کند.
مطالب گفته شده مقدمه ای برای آشنایی با الگوهای طراحی بود و در بخش های بعدی به مطالعه دقیق هر کدام از الگوهای گفته شده و موارد استفاده آنها خواهیم پرداخت.
در بخش های قبل در مباحثی کلی درباره ی شی گرایی و الگوهای طراحی مطرح شد و از این بخش به بعد به تک تک الگو ها می پردازیم و هرکدام را توضیح خواهیم داد. در این بخش به الگوهای Singleton و Adapter می پردازیم که نسبتا الگوهای ساده تری هستند می پردازیم.
این الگو یکی از الگوهای سازنده (creational) است و در ساختن اشیا از کلاس ها کاربرد دارد. این الگو یکی از معروف ترین الگوهای مطرح شده توسط Gang of Four است.
حالتی را در نظر بگیرید که کلاسی دارید که می خواهید در کل برنامه خود فقط یک شی از این کلاس ساخته شود و اجازه ساخته شدن نمونه جدید به استفاده کنندگان از این کلاس داده نشود. این عمل در مواقعی بسیار لازم و ضروری است. به طور مثال حالتی را درنظر بگیرید که در برنامه کلاسی وجود دارد که واسط تنظیمات برنامه است یعنی برنامه هنگام لود شدن تنظیمات خود را در آن بارگزاری می کند.
حال اگر این شی یک شی در کل برنامه یکتا نباشد و هر بخش از برنامه که بخواهد تنظیمات را بخواند و یا تغییری در آن ایجاد کند مجبور باشد که یک شی جدید از کلاس تنظیمات بسازد مدیریت این اشیا مشکل می شود و همچنین باعث نابه سامانی در تنظیمات خواهد شد. راه حل این مسئله این است که از همان اول کلاس به گونه ای تعریف شود که اجازه ساختن نمونه جدید به کاربران ندهد و تنها کاربران بتوانند یک نمونه از کلاس به وجود آورند.
اگر به برخی از قسمت های سیستم عامل ویندوز هم نگاهی بکنیم متوجه می شویم که چنین مکانیزمی در آن تعبیه شده است مثلاً یک سیستم فایل برای مدیریت فایل ها وجود دارد و یا با وجود این که ممکن است چندین پرینتر به کامپیوتر متصل باشد ولی فقط یک printer spooler در سیستم وجود دارد.
برای به وجود آوردن چنین مکانیزمی شاید اولین راه حل این باشد که یک شی سراسری در کل برنامه تعریف شده و هروقت که برنامه ای بخواهد از این کلاس استفاده کند از شی سراسری تعریف شده استفاده کند. اما مشکلی که این راه حل دارد این است که این راه حل مانع ساخت شی جدید از کلاس نمی شود. یک راه حل بهتر این است که خود کلاس را مسئول این کار قرار دهیم که مواظب باشد که یک نمونه بیشتر نداشته باشد. این کار را الگوی Singleton انجام می دهد.
روش کار این الگو به این صورت است که یک نمونه از خود کلاس در داخل کلاس ساخته می شود و هر وقت که کلاس های دیگر خواستند از این کلاس استفاده کنند تنها می توانند به این نمونه ساخته شده دسترسی داشته باشند و نمی توانند از کلاس نمونه سازی کنند.ساختار این الگو به این شکل است:
همانطور که در شکل می بینید یک فیلد به نام instance داریم که از خود کلاس است و این فیلد تنها الگویی که قرار است ساخته شود را در خود نگهداری می کند. سازنده(constructor) این کلاس از نوع private بوده بنابراین نمی توان از این کلاس یک نمونه جدید توسط عملگر new ایجاد کرد. تنها راه ساختن یک نمونه از این کلاس فراخوانی متد GetInstance است.
این الگو باعث می شود که دسترسی به نمونه های کلاس کنترل شود و تعداد خاص از نمونه ها ایجاد شود.با استفاده از این الگو نیاز به ساختن متغیر های سراسری در فضای namespace کمتر شده و از اشغال فضای namespace توسط متغیرهای سراسری جلوگیری می کند. کد پیاده سازی این الگو در زبان C# به شکل زیر است
public sealed class Singleton { private Singleton() { } private readonly static Singleton instance=new Singleton(); public static Singleton UniqueInstance { get { return instance; } } }
همانطور که درکد مشاهده می شود یک بار یک نمونه از کلاس ساخته می شود و تنها راه دسترسی به کلاس استفاده از ویژگی UniqueInstance می باشد که نمونه ساخته شده را برمی گرداند. در این نوع پیاده سازی از همان ابتدا یک نمونه از کلاس ساخت شده است و استفاده می شود ولی می توان این نمونه را وقتی ساخت که درخواستی برای اولین باربه کلاس ارسال شود که به این شیوه Lazy instantiate گفته می شود. کد این روش به شکل زیر است.
public class LazySingleton { private LazySingleton(){} private static LazySingleton instance; public static LazySingleton UniqueInstance { get { return instance!=null?instance:new LazySingleton();} } }
کدی که آورده شده است تا حدی می تواند هدف این الگو را که ساخته شدن یک نمونه از کلاس باشد را برآورده کند ولی اگر برنامه دارای چند thread باشد ممکن است در مواردی بیشتر از یک نمونه از کلاس ساخته شود که در اصطلاح گفته می شود برنامه Thread Safe نیست که به جای کد بالا از کد پایین استفاده می کنیم.
public class Singleton { private Singleton(){} class SingletonCreator { static SingletonCreator() { } internal static readonly Singleton UniqueInstance = new Singleton(); } public static Singleton UniqueInstance { get { return SingletonCreator.UniqueInstance; } } }
در کد بالا از یک کلاس داخلی برای ساخت نمونه از کلاس استفاده می شود که علاوه بر Thread safe بودن خاصیت Lazy instatiate هم دارد.حال اگر نیاز باشد که از یک خانواده کلاس ها تنها یک نمونه ایجاد شود پیاده سازی این الگوی کمی متفاوت تر خواهد شد. برای مثال تصور کنید که یک کلاس میوه دارید که از آن کلاس های دیگری مانند سیب و پرتقال مشتق شده اند و لازم است که در برنامه فقط یک عدد میوه وجود داشته باشد یعنی اگر یک سیب وجود دارد دیگر نتوان میوه ی دیگری(چه سیب و چه پرتقال) ایجاد کرد. به منظور پیاده سازی چنین روشی به شکل زیر عمل می کنیم.
public class SingleFruit { public string name; protected SingleFruit() { } protected static SingleFruit Instance ; public static SingleFruit UniqueInstance { get { return Instance; } } } public class SingleApple : SingleFruit { private SingleApple() { name = "Apple"; } public static SingleFruit UniqueInstance { get { return Instance != null ? Instance :Instance= new SingleApple(); } } } public class SingleOrange : SingleFruit { private SingleOrange() { name = "Orange"; } public static SingleFruit UniqueInstance { get { return Instance != null ? Instance :Instance= new SingleOrange(); } } }
با توجه به کد بالا اگر به کلاس های فرزندان مقداردهی نشده باشد استفاده از کلاس پدر باعث خطای NullReference خواهد شد. و همچنین این کد Thread safe نمی باشد.الگوهای مرتبط با الگوی Singleton الگوهای Façade و Abstract Factory و بسیاری از الگوهای دیگر هستند که لازم است در برنامه از آنها فقط یک نمونه ساخته شده باشند که در زمان معرفی این الگوها توضیحات لازم ارایه خواهد شد.
همان گونه که از اسم این الگو مشخص است، هنگامی که دو کلاس واسط(Interface) های غیرمرتبط با یکدیگر داشته باشند این الگو واسط یکی را به دیگری تبدیل می کند که بتوانند با یکدیگر ارتباط برقرار کند.از این الگو که یک الگوی ساختاری است زمانی استفاده می شود که در یک برنامه بخواهیم که دو کلاس غیرمرتبط با یکدیگر کار کنند.
این الگو در برنامه هایی که از کلاس های آماده استفاده می شود و یا از کلاس هایی استفاده می شود که قبلا نوشته شده اند و به گونه ای باشد که طراحان نرم افزار اجازه تغییر در این کلاس ها را نداشته باشند. بسیاری از مثال های الگوی Adapter درگیر ورودی و خروجی هستند زیرا این دامنه ای است که همیشه تغییر می کند.
برای مثال برنامه هایی که در دهه 80 میلادی نوشته شده اند دارای UI بسیار ضعیف تری نسبت به برنامه های نوشته شده در هزاره سوم میلادی هستند. حال اگر دوباره نویسی همان برنامه های قبلی به صرفه نباشد و بخواهیم این برنامه ها با سخت افزارهای جدید همخوانی و سازگاری داشته باشند باید برنامه ای طراحی کنیم که بین این دو برنامه یعنی برنامه قدیمی و راه اندازهای سخت افزارهای جدید قرار بگیرد و بین این دو برنامه ارتباط برقرار کند.
به برنامه ای که بین این دو برنامه قرار می گیرد Adapter گویند. حال شاید به نظر برسد که استفاده از Adapter اصلاً نیاز نباشد و می توان واسط یکی از کلاس ها را تغییر داد تا بتواند کار کند، این کار زمانی ممکن است که بتوان به کد کلاس ها دسترسی داشت و تغییر کلاس ها باعث به وجود آمدن مشکل در برنامه نباشد و پیچیدگی برنامه را افزایش ندهد که در اکثر مواقع این عمل به صرفه نیست.ساختار این الگو به این شکل است:
همانطور که در شکل دیده می شود کلاس Adapter از اینترفیس ITarget مشتق شده است و در یکی از متدهایی که در این کلاس وجود دارد (Request) متد مورد نظر را از کلاس Adaptee فراخوانی می کند. کلاس Adapter متدهای Adaptee را با متدها و خصوصیای Target وفق می دهد. به طور کلی اینترفیس ITarget همان اینترفیسی است که کاربر آن را پیاده سازی می کند و کلاس Adaptee همان کلاسی است که باید با برنامه وفق داده شود. کاربران برای استفاده فقط از کلاس Adapter نمونه سازی می کنند و نیازی به درگیری با ارتباط Target و Adaptee نخواهند بود.کد این الگو به شرح زیر است.
using System; public class Adaptee { public double SpecificRequest(double a, double b) { return a/b; } } public interface ITarget { string Request(int a); } public class Adapter : Adaptee, ITarget { public string Request(int a) { return "the result is" + (int) Math.Round(SpecificRequest(a, 5)); } }
ساده ترین شکل الگوی Adapter شکل یک طرفه آن است که یک کلاس یا اینترفیس Target بوده و کلاس دیگر نقش Adaptee را داشت که تا این قسمت شرح داده شد. حال فرض کنید که می خواهیم هر دو طرف هم نقش Adaptee و هم نقش Target را داشته باشند. به گونه ای که از کلاس Adapter بتوان همه به شکلی استفاده کرد که متدهای هر دو طرف را داشته باشند.
در این صورت کلاس Adapter دارای متدها و خصوصیات هر دو کلاس می باشد. یک Adapter دو طرفه می تواند به جای هر دو کلاس Target و Adaptee مورد استفاده قرار بگیرد. برای این کار در زبانهایی مانند ++C که وراثت از چندین کلاس را پشتیبانی می کنند کار آسانی است. به این شکل که کلاس Adapter از دو کلاس دیگر ارث بری داشته باشد همانند ساختار زیر
اما در بسیاری از زبان های برنامه نویسی رایج مانند #C و Java ارث بری از چندکلاس پشتیبانی نمی شود. بنابراین این راه حل در این زبان ها پاسخگو نیست. برای این که در این زبان ها از Adapter دو طرفه استفاده کنیم به شکل زیر عمل می کنیم.
public class Adaptee { public double SpecificRequest(double a, double b) { return a/b; } } public interface ITarget { string Request(int a); } public class Adapter : Adaptee, ITarget { public string Request(int a) { return "the result is" + (int) Math.Round(SpecificRequest(a, 5)); } } //////////////////////////////////////////////////////// internal class Program { private static void Main(string[] args) { ITarget aa = new Adapter(); Console.WriteLine(aa.Request(5)); Console.WriteLine((aa as Adaptee).SpecificRequest(8, 3)); } }
در کد بالا یکی از اینترفیس ها (IAircraft ) که همان Target است را در کلاس Adapter پیاده سازی می کنیم و از کلاس Adaptee ارث بری می نماییم. ساختار این عمل شبیه به همان استفاده از الگوی Adapter به شکل یک طرفه آن است ولی در استفاده به این شکل عمل می کنیم. همانطور که مشاهده می شود می توان از کلاس Adapter هم به عنوان یک نمونه از Target و هم به عنوان یک نمونه از Adaptee استفاده نمود. مشکلی که در زبان هایی مانند C# یا Java وجود دارد این است که باید Target را به عنوان اینترفیس معرفی کرده و در کلاس Adapter آن را پیاده سازی نمود ولی در زبانی مانند C++ چنین محدودیتی وجود ندارد.
در این مقاله الگوی Observer معرفی خواهد شد و ساختار آن تشریح خواهد شد.
حالتی را تصور کنید که چند شی در برنامه وجود دارد که تغییر حالات یک شی دیگر را دنبال می کنند و با توجه به هر تغییر عملی را انجام می دهند. شاید اولین راه حلی که در این حالت، یافت شود این است که اشیا در یک حلقه شی مورد نظر را بررسی کنند تا اگر تغییری به وجود آمده بود با توجه به آن کاری انجام دهند. اما این بدترین راه حل است و باعث افت کارایی و همچنین طراحی بد برنامه خواهد شد. برای حل این مسئله الگوی Observer معرفی می شود.الگوی Observer یک ارتباط بین اشیا ایجاد می کند به طوری که وقتی یکی از اشیا تغییر حالت می دهد همه اشیا مرتبط با آن از این تغییر حالت اطلاع پیدا می کنند. معمولاَ در یک مجموعه از اشیا مرتبط این چنینی، یک منتشر کننده(publisher) و جود دارد و چندین شی وجود دارد که از تغییر حالت شی منتشر کننده آگاه می شوند.
در دنیای کامپیوتر امروزی از این الگو بسیار استفاده شده است. به عنوان مثال حالتی را تصور کنید که در یک سایت آمار فروش به صورت یک نمودار نمایش داده می شود. وقتی که یک عمل فروش انجام می شود این نمودار باید با توجه مقدار حجم فروش ها خود را بروزرسانی کند. حال شاید بتوان گفت که این کار را می توان بدون استفاده از الگویی پیاده سازی کرد ولی وقتی نیاز به الگوی Observer احساس می شود که در زمان اجرا بخواهیم مجموعه اشیائی را که تغییر شی منتشر کننده را دنبال می کنند را تغییر بدهیم
یعنی چند شی به این مجموعه اضافه کنیم و یا از آن کم کنیم. الگوی Observer یک الگوی رفتاری است.از استفاده های دیگری که از این الگو استفاده شده است مدیریت رویداد های یک برنامه است. به طور مثال وقتی در زبان #C در رویداد formLoad متدی را فرخوانی می کنیم وقتی که این رویداد در فرم رخ بدهد فرم همه ی توابع و اعمالی را که برای آن تعریف شده است را آگاه می کند که اجرا شوند.به طور کلی هرگاه بخواهیم با تغییر حالت یک شی، اشیا دیگر عمل خاصی را انجام دهند از این الگو استفاده می شود.
از این الگو در موارد زیر استفاده می شود:
وقتی که یک abstraction دارای دو جنبه باشد که یک به دیگری وابسته باشد و هر کدام از این جنبه ها در اشیا جداگانه باشند و بتوانند به طور مستقل عمل کنند و تغییرات مورد نیاز خود را روی اشیا خود اعمال کنند.
وقتی که با به وجود آمدن یک تغییر در یک شی لازم باشد که اشیا دیگر با توجه به تغییر به وجود آمده عمل خاصی انجام دهند و همچنین مشخص نیست که چندتا از اشیا باید عمل مورد نظر را انجام دهند.
وقتی که لازم باشد که یک شی بتواند به اشیا دیگر بدون توجه به ماهیت آن و یا شناختن آن اطلاع رسانی نماید.
برای تشریح ساختار این الگو از عبارات زیر استفاده شده است:
ساختار این الگو به این صورت است که هر شی Observer که بخواهد از تغییر حالت شی Subject آگاه باشد باید خود را در لیست دنبال کنندگان Subject ثبت کند. برای این کار شی Observer متد Attach شی Subject را فراخوانی می کند و خود شی را به عنوان ورودی متد می فرستد و با این کار در ساختمان داده شی Subject ثبت می شود و هر گاه که رویدادی در Subject رخ دهد، این شی متد Notify خود را فراخوانی می کند که با این کار به همه Observerها اطلاع داده می شود.
اگر یک Observer نخواهد که دیگر رویدادها را دنبال کند متدل Detach مربوط به Subject را فراخوانی می کند و خود را به عنوان ورودی به آن می فرستد و با این کار از ساختمان داده ی Subjectحذف می شود و دیگر رویداد ها به آن اطلاع داده نخواهد شد. همه ی اشیای Observer اینترفیس IObserver را پیاده سازی می کنند. نمودار Class diagram این الگو به این شکل است.
برای ارتباط بین Subject و observer دو مکانیزم وجود دارد. مکانیزم اول که Push نام دارد به این شکل است که Subject وقتی می خواهد به observer اطلاع دهد تغییرات را با تمام جزئیات می فرستد و وقتی که observer این تغییرات را دریافت می کند همه ی اطلاعات را برای این که خود را update کند دارد و دیگر نیاز به اطلاعات بیشر از طرف subject نیست.
مکانیزم دوم که pull نام دارد این گونه عمل می کند که subject پیامی شبیه به آگهی به همه ی observer ها می فرستد و هر کدام از observer ها که بخواهد خود را آپدیت در خواست خود را به subject می فرستد تا اطلاعات مورد نیاز را بدست آورد.الگوی observer از هر دو مدل گفته شده پشتیبانی می کند.
از مزایای این الگو می توان موارد زیر را نام برد:
این الگو باعث کم کردن وابستگی observer و subject به یکدیگر می شود و در اصطلاح این دو موجودیت در برنامه tightly couple نخواهند بود و بنابراین می توان این دو موجودیت را در لایه های مختلف برنامه تعریف کرد و از این الگو استفاده نمود. این الگو از ارتباط broadcast پشتیبانی می کند.
در ادامه به برخی از مسائلی که ممکن است در هنگام استفاده و پیاده سازی از این الگو پیش بیاید پرداخته می شود.
الگوی Mediator در مقاله های بعدی شرح داده خواهد شد.
پایه و اساس پیاده سازی این الگو به همان شکلی است که class diagram آن نشان داده شده است. بنابراین اگر بخواهیم این الگو را با زبانی مانند ++C پیاده سازی کنیم کد آن به شکل زیر خواهد شد.
class Subject; class Observer { public: virtual ~Observer(); virtual void Update(Subject *changedSubject) = 0; private: protected: Observer(); }; //////////////////////////////////////////////////////////////////////// class Subject { public: ~Subject(); void Attach(Observer*); void Detach(Observer*); int Number; private: void Notify(); List<Observer*> *_observers; }; void Subject::Attach(Observer *o) { _observers->Append(o); } void Subject::Detach(Observer *o) { _observers->Remove(o); } void Subject::Notify() { ListIterator<Observer*> i(_observers); for (i.First(); !i.IsDone(); i.Next()) { i.CurrentItem()->Update(this); } } Subject::~Subject() { ListIterator<Observer*> i(_observers); for (i.First(); !i.IsDone(); i.Next()) { Detach(i.CurrentItem()); } } ////////////////////////////////////////////////////////////////// class ConcreteObserver:public Observer { public: ConcreteObserver(Subject* subject) { attachedSubject = subject; attachedSubject->Attach(this); } void Observer::Update(Subject *changedSubject) { cout << "changedSubject is:" << changedSubject->Number<<endl; } ~ConcreteObserver() { attachedSubject->Detach(this); } private: Subject* attachedSubject; }; //////////////////////////////////////////////////////////////////////// void main() { Subject* subject = new Subject; ConcreteObserver observer(subject); }
در کد بالا کلاس Observer نقش همان اینترفیس را در class diagram آورده شده دارد. با توجه به کد بالا هر کلاسی که بخواهد خود را به عنوان observer مربوط به کلاس Subject قرار دهد باید از کلاس Observer ارث بری کند و متد Attach را از کلاس Subject فراخوانی کند(همانند Constructor مربوط به کلاس ConcreteObserver). با این کار خود را در لیست observers اضافه می کند و هر زمان که در کلاس Subject متد Notify فراخوانی شود متد Update این کلاس نیز فراخوانی خواهد شد.
هنگامی که خود کلاس observer از بین برود خود را از لیست observers حذف می کند و همچنین اگر کلاس Subject از بین برود همه کلاس های Observer حذف خواهند شد این عمل برای جلوگیری از به وجود آمدن dangling reference می باشد.ولی در زبان #C پیاده سازی این الگو به روش دیگری صورت می گیرد که بسیار راحت تر از کد آورده شده است. در زبان#C با استفاده از امکاناتی مانند event ها می توان این الگو را به راحتی پیاده سازی نمود. پیاده سازی این الگو در زبان #C به شرح زیر است:
using System; using System.Collections; using System.Threading; public class ObserverPattern { private class Subject { public delegate void Callback(string s); public event Callback Notify; private readonly Simulator simulator = new Simulator(); private const int speed = 200; public string SubjectState { get; set; } public void Go() { new Thread(Run).Start(); } private void Run() { foreach (string s in simulator) { Console.WriteLine("Subject: " + s); SubjectState = s; Notify(s); Thread.Sleep(speed); // milliseconds } } private interface IObserver { void Update(string state); } private class Observer : IObserver { private readonly string name; private Subject subject; private string state; private readonly string gap; public Observer(Subject subject, string name, string gap) { this.subject = subject; this.name = name; this.gap = gap; subject.Notify += Update; } public void Update(string subjectState) { state = subjectState; Console.WriteLine(gap + name + ": " + state); } } private static void Main() { var subject = new Subject(); var Observer = new Observer(subject, "Center", "\t\t"); var observer2 = new Observer(subject, "Right", "\t\t\t\t"); subject.Go(); } private class Simulator : IEnumerable { private readonly string[] moves = {"5", "3", "1", "6", "7"}; public IEnumerator GetEnumerator() { foreach (var element in moves) yield return element; } } } }
همانطور که در این کد مشاهده می شود در کلاس Subject یک event به نام Notify تعریف شده است که یک رشته به عنوان ورودی می گیرد. هر کلاس Observer که بخواهد تغییرات Subject را دنبال کند باید متد Update خود را به شکل زیر به Notify معرفی کند:
subject.Notify += Update;
و اگر کلاسی بخواهد که دیگر تغییرات را دنبال نکند در خط بالا به جای علامت += از علامت -= استفاده می کند. در قطعه کد بالا که از برنامه نویسی چند نخی (Thread) استفاده شده است یک thread مسئول اجرای عمل Notify می باشد که با اجرا شدن این عمل متد Update کلاس های Observer فراخوانی می شوند. همانطور که مشاهده می شود برخلاف زبان C++ نیازی به تعریف لیست و اضافه و حذف کردن و در کل درگیری مستقیم با لیست Observer ها نخواهیم داشت.لازم به ذکر است که در تعریف کلاس Simulator از الگوی iterator استفاده شده است که این الگو در مقاله های بعدی شرح داده خواهد شد.از الگوهای مرتبط با این الگو می توان Singleton, Mediator, State, Memento را نام برد.
در ادامه سری مقالات الگوی های طراحی نرم افزار های شی گرا به الگوی iterator خواهیم پرداخت.
این الگو راهی برای دست یافتن به عناصر موجود در یک مجموعه را فراهم می کند. این دست یابی بدون توجه به ساختار مجموعه به صورت ترتیبی است. همچنین این الگو می تواند عناصر را به شکل های گوناگونی فیلتر نماید.
برای ذخیره داده های مرتبط در زبان های برنامه نویسی از مجموعه عناصر استفاده می شود. این مجموعه عناصر می توانند مجموعه ها ، لیست ها و آرایه ها و... باشند. هر کدام از این مجموعه ها با توجه به شرایط استفاده می تواند بسیار مفید باشد. هر کدام از این مجموعه ها عناصر خود را به گونه ای متفاوت ذخیره می کنند. حال تصور کنید که در برنامه خود از آرایه استفاده می کرده اید
و بنابر تغییرات برنامه و نیاز به وجود آمده ناگزیر به استفاده از لیست باشید باید همه ی قسمت های برنامه که از آرایه مذکور استفاده می کرده اند را تغییر داده و بازنویسی کنید که این بار از لیست به جای آرایه استفاده کنند. خود این عمل بسیار مشکل است و می تواند باعث به وجود آمدن ناسازگاری های بسیاری شود. راه حل این است که روش استفاده از مجموعه عناصر به شکل یکتا پیاده سازی شود.
الگوی iterator یکی از ساده ترین الگوها و در عین حال یکی از الگوهای پرکاربرد در بین الگوهای طراحی می باشد. این الگو یک واسط استاندارد برای استفاده کنندگان فراهم می کند که بدون توجه به چگونگی پیاده سازی یک مجموعه از عناصر، استفاده کنندگان بتوانند به اعضای مجموعه دسترسی داشته باشند. بسیاری از زبان های برنامه نویسی شی گرا مانند #C و Java در مجموعه های خود از این الگو استفاده کرده اند.
در استفاده از این الگو باید دو مفهوم iterator و enumerator را تعریف کنیم. مفاهیم iterator و enumerator دارای قدمت طولانی می باشد. Enumerator مسئول یافتن شی بعدی است که شرط خاصی را دارد. به طور مثال اگر بخواهیم عدد اول کوچکتر از هزار بعدی را پیدا کنیم از enumerator استفاده می کنیم و این مجموعه اعداد 1 تا 1000 را مجموعه enumerable می نامیم. مفهوم iterator به این معنی است که توسط حلقه ای یک مجموعه از اشیا را از اول تا آخر پیمایش نماییم. الگوی iterator باید هر دوی این مفاهیم را پیاده سازی کند.
از این الگو در موارد زیر استفاده می شود:
ساختار این الگو در زبان C# اینگونه پیاده سازی شده است که برای پیاده سازی iterator از حلقه foreach استفاده می شود و برای هر مجموعه ای که بخواهد از این الگو استفاده کند باید اینترفیس Ienumerable را پیاده سازی کند. ساختار این الگو به این شکل است:
همانطور که در شکل دیده می شود مجموعه ها اینترفیس IEnumerable را پیاده سازی می کنند. این اینترفیس دارای یک تابع به نام GetEnumerator() می باشد که همان کار enumerator را پیاده سازی می کند. اگر این اینترفیس و این تابع در مجموعه ای پیاده سازی نشده باشد قسمت iterator یعنی حلقه foreach نمی تواند کار خود را انجام دهد. در زبان هایی که این مکانیزم در آن پیاده سازی نشده است مانند++C باید از ساختار زیر استفاده کرد:
در این نمودار برنامه برای پیمایش یک لیست در این ساختار از ListIterator استفاده می شود که در طول یک حلقه به کاوش لیست می پردازد.
اول پیاده سازی این الگو برای زبان ++C را توضیح می دهم که هیچگونه اینترفیس آماده ای ندارد و کاربر باید از اول خود به پیاده سازی این الگو بپردازد و سپس به پیاده سازی این الگو در زبان #C خواهیم پرداخت.برای پیمایش یک مجموعه از عناصر باید توابع داخل کلاس Iterator را بازنویسی نماییم. یعنی کلاس ConcreteIteratorاین توابع را با توجه به یک مجموعه خاص پیاده سازی می کند. قطعه کد زیر این مثال را برای یک لیست نشان می دهد.
template <class Item> class Iterator { public: virtual void First() = 0; virtual void Next() = 0; virtual bool IsDone() const = 0; virtual Item CurrentItem() const = 0; protected: Iterator(); }; template <class Item> class ListIterator : public Iterator<Item> { public: ListIterator(const List<Item>* aList); virtual void First(){ _current = 0; } virtual void Next(){ _current++; } virtual bool IsDone() const{ return _current >= _list->Count(); } virtual Item CurrentItem() const{ if (IsDone()) { throw IteratorOutOfBounds; } return _list->Get(_current); } private: const List<Item>* _list; long _current; }; void PrintNumbers(ListIterator<int> i) { for (i.First(); !i.IsDone(); i.Next()) { cout<<i.CurrentItem()<<endl; } }
همانگونه که در کد نشان داده شده است کلاسی به نام Iterator وجود دارد که فقط چند تابع virtual دارد و این توابع توسط کلاس ListIterator که از کلاس قبلی ارث بری کرده است پیاده سازی شده اند. در تابع PrintNumbers نحوه استفاده از این الگو را می توان دید که از اولین عنصر آن پیمایش شروع شده و تا آخرین عنصر پیش می رود و هرکدام از عناصر را در یک خط چاپ می نماید.در زبان های سطح بالا تر مانند Java و #C اینترفیس هایی وجود دارند که کار را آسان تر کرده و نیازی نیست که از ابتدا شروع به نوشتن این الگو نماییم.قطعه کد زیر پیاده سازی این الگو را برای ماه های سال در زبان#C نشان می دهد.
using System; using System.Collections; public class Iterator { public class MonthCollection:IEnumerable { string[] months = { "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" }; public IEnumerator GetEnumerator() { foreach (var month in months) { yield return month; } } } static void main() { var collection = new MonthCollection(); foreach (var element in collection) { Console.WriteLine(element); } } }
همان طور که در کد بالا دیده می شود مجموعه ای که قصد پیاده سازی این الگو در آن را داریم یک آرایه رشته ای است که ماه ها را در خود ذخیره کرده است. توسط کلمه کلیدی yield می توان به کامپایلر فهماند که مقداری که return می کنیم مقدار enumerator است یعنی عنصر بعدی است. البته می توان در داخل foreach موجود در تابع getenumerator شرط گذاشت که همه ماه ها را به خروجی نفرستد. با این کار هرکدام از عناصر که شرط مورد نظر را داشته باشند به foreach موجود در تابع main ارسال می شود.
این الگو به این شکل است که بین کاربری که یک درخواست را ارسال کرده و شیئی که می تواند آن دستور را اجرا کند یک فاصله می اندازد. این الگو یک الگوی چندکاره است که امکانات زیر را پشتیبانی می کند.
لگوی Command در سیستم منو های بسیاری از برنامه های معروف استفاده می شود. در بسیاری از نرم افزار هایی که امروزه با آنها سرو کار داریم منویی به نام Edit دارند که اعمال کپی و کات کردن را داخل منوی Edit دارند و از طرفی هم همان اعمال کپی و کات کردن در داخل نوار ابزار بالای صفحه نیز موجود می باشد. این به این معنی است که هر دوی آنها یک عمل را انجام می دهند. و این اعمال دارای قابلیت Undo نیز می باشند. همه ی این اعمال کاربرد های الگوی Command می باشد.
طراحی الگوی command به شکل زیر است. کلاس Client روش معینی دارد که درخواست خود را اعلام کند. Receiver ها که ممکن است بیش از یکی باشند می دانند که چگونه باید درخواست کاربر را اجرا نمایند با توجه به مثال منو ها که قبلا گفتیم یک فرمان Cut برای یک متن در قسمتی از برنامه اجرا می شود و فرمان Cut مربوط به یک عکس در قسمت دیگری از برنامه هندل می شود.کلاس Command به عنوان یک واسط (interface)بین Client و Receiver عمل می نماید. در یک شی Command درخواست های Client مشخص شده است و همچنین اعمال مربوط به این درخواست ها و گیرنده آن نیز در کلاس Command معین گردیده است.
ممکن است که Client بخواهد وضعیت خود را نیز برای دریافت کننده ارسال کند که کلاس Command این اجازه را می دهد. کلاس Invoker برای جدا کردن کلاس client از کلاس Receiver می باشد. به بیان ساده تصور کنید که Client تابع Execute مربوط به Invoker را فراخوانی می کند. با این کار درخواست کاربر به Command ارسال می شود و تابع Action مربوط به Receiver مورد نظر اجرا می شود. یک برنامه می تواند دارای تعداد زیادی درخواست با انواع متفاوت باشد که هر کدام به گیرنده های مربوط به خود ارسال می شوند. اینترفیس ICommand اطمینان حاصل می کند که هرکدام از درخواست ها به فرم استاندارد برای گیرنده مورد نظر باشد.
کلاس هایی که در این الگو نقش دارند عبارتند از:
از ویژگی های الگوی Command می توان به موارد زیر اشاره کرد:
یک مثال ساده از پیاده سازی این الگو در ادامه آمده است.
using System; internal class Program { private delegate void Invoker(); private static Invoker Execute, Undo, Redo; private class Command { public Command(Receiver receiver) { Execute = receiver.Action; Redo = receiver.Action; Undo = receiver.Reverse; } } public class Receiver { private string build, oldBuild; private readonly string s = "Itpro string "; public void Action() { oldBuild = build; build += s; Console.WriteLine("Receiver is adding " + build); } public void Reverse() { build = oldBuild; Console.WriteLine("Receiver is reversing to " + build); } } private static void Main(string[] args) { new Command(new Receiver()); Execute(); Redo(); Undo(); Execute(); Console.ReadKey(); } }
کلاس Invoker وICommand به عنوان یک نوع delegate معرفی شده اند و در خط بعد سه شی از آن ساخته شده است. نام هرکدام از اشیا با توجه به کاری که کاربر از آنها می خواهد نام گذاری شده اند. دستور مربوط به کلاس Command در داخل Receiver با استفاده از دو تابع Action و Reverse پیاده سازی شده است. دستورات Execute و Redo هر دو تابع Action را اجرا می نمایند و دستور Undo تابع Reverse را اجرا می کند. در این کد Receiver مسئول نگهداری حالت شی است و همانگونه که در کد می بینید هر بار که اجرا می شود مقدار s تغییر می کند. همچنین در این کد عمل undo کردن فقط برای یک بار ممکن است ولی اگر بخواهیم این کار را در واقعیت انجام دهیم باید برای نگهداری حالت های قبلی باید بیشتر از یک عمل را در نظر بگیریم که مکانیزم های متفاوتی برای پیاده سازی آن وجود دارد.
حال اگر در یک برنامه چندین نوع delegate داشته باشیم و بیشتر از یک گیرنده داشته باشیم چه اتفاقی خواهد افتاد؟ اول از همه باید نام delegate ها (Invoker) باید با هم متفاوت باشد. اشیاء Command باید به گونه ای باشند که بین اشیا delegate و گیرنده ها ارتباط ایجاد کند. در برنامه ای که در ادامه آورده شده است، دو کلاس Command و دو گیرنده وجود دارند. چون هر دوی command ها از delegateها (invoker) یکسان استفاده می کنند، Client دستورات را اول در یکی از گیرنده ها و سپس در دیگری اجرا می کند ولی ممکن است این کار لازم نباشد. برای این که این مشکل را حل کنیم می توانیم اشیا delegaet بیشتری معرفی کنیم یا آنها را در داخل Command ها گروه بندی نماییم که در مثال زیر نشان داده شده است.
public class MultiReceiver { private delegate void Invoker(); private delegate void InvokerSet(string s); private static Invoker Execute, Redo, Undo; private static InvokerSet Set; class Command { public Command(Receiver receiver) { Set = delegate { Console.WriteLine("Not implemented- default of Itpro used"); receiver.S = "Itpro"; }; Execute = receiver.Action; Redo = receiver.Action; Undo = receiver.Reverse; } } class Command2 { public Command2(Receiver2 receiver2) { Set = receiver2.SetData; Execute = receiver2.DoIt; Redo = receiver2.DoIt; Undo = () => Console.WriteLine("Not Implemented"); } } class Receiver { private string build, oldBuild; private string s = "Itpro String "; public string S { get { return s; } set { s = value; } } public void Action() { oldBuild = build; build += s; Console.WriteLine("Receiver is adding "+build); } public void Reverse() { build = oldBuild; Console.WriteLine("Receiver is reverting to "+build); } } class Receiver2 { private string build, oldBuild; private string s = "Itpro String2 "; public void SetData(string s) { this.s = s; } public void DoIt() { oldBuild = build; build += s; Console.WriteLine("Receiver is building "+build); } } class Client { public void ClientMain() { new Command(new Receiver()); Execute(); Redo(); Undo(); Set("III"); Execute(); Console.WriteLine(); new Command2(new Receiver2()); Set("Hoses "); Execute(); Set("Castles "); Undo(); Execute(); Redo(); } } private static void Main(string[] args) { new Client().ClientMain(); } }
از کاربرد های دیگر این الگو ثبت لاگ برنامه است که یک تاریخچه از کارهایی که انجام شده است نگهداری می کند مثلا ساده ترین نوع لاگ گیری این است که تعداد انجام یک کار را بشماریم. برای این کار عملیات مورد نظر را در قسمت Invoker پیاده سازی می کنیم.
در ادامه با بحث الگوهای طراحی به شرح الگوی Meditor خواهیم پرداخت
همان طور که در بخش اول این سری مقالات اشاره کردیم یکی از اصولی که در طراحی نرم افزار باید رعایت شود این است که در طراحی نرم افزار ها کلاس ها و اشیائی که طراحی می شوند باید حداقل وابستگی را به یکدیگر داشته باشند. به این معنی که هرکدام به شکل جداگانه کار خود را انجام بدهند و در صورت نیاز فارغ از این که هرکدام چگونه وظیفه خود را انجام می دهند با یکدیگر تعامل داشته باشند.
این تعامل باید به گونه ای باشد که وقتی تغییری در یکی از کلاس ها اعمال شد کلاسی که با کلاس تغییر داده شده همکاری می کند نیازمند تغییر نباشد. به این اصل در طراحی نرم افزار loosly coupling گفته می شود. یکی از الگو های طراحی که در رعایت اصل گفته شده بسیار تاثیر گذار است الگوی mediator می باشد.
این الگو شیئی را تعریف می کند که این شیء ارتباط بین مجموعه ای از اشیا دیگر را برقرار می کند و نوع این ارتباط را کپسوله سازی می کند. به این معنی که این الگو زمینه ای برای ارتباط بین اشیا به وجود می آورد به گونه ای که اشیا از ماهیت یکدیگر اطلاعی ندارند و همان طور که از نام این الگو می توان فهمید این الگو یک میانجی بین سایر اشیا دیگر است. این الگو همچنین می تواند پروتکل هایی را قرار دهد که سایر اشیا از آن ها پیروی کنند.
در طراحی نرم افزار های شی گرا ترجیح بر این است که اعمال و رفتار ها بین اشیا توزیع و تقسیم شوند. خرد کردن سیستم به بخش های متفاوت باعث می شود که قابلیت استفاده مجدد (reusablity) سیسم افزایش پیدا کند. مشکلی که در انجام این کار به وجود می آید این است که این بخش های مختلف سیستم نیازمند ارتباط با یکدیگر می باشند که اگر این ارتباطات کنترل نشوند خود همین مکانیزم ارتباطی پیچیدگی بسیاری برای سیستم به وجود می آورد که قابلیت reusablity و نگهداری و تغییر سیستم را پایین می آورد.
وظیفه این الگو ارتباط است. این ارتباط ها می توانند متفاوت باشند. مثلا می توان بین دو شی ارتباط برقرار کرد و یا بین گروهی از اشیا ارتباط برقرار کرد. اغلب پروتکلی وجود دارد که همه ی اعضای گروه از آن پیروی می کنند. الگوی mediator با استفاده از این پروتکل ارتباط بین اشیا را برقرار می کند.
برای این که اهمیت این الگو مشخص شود، یک شبکه کامپیوتری را در نظر بگیرید که در آن کامپیوتر های با یکدیگر ارتباط دارند و هرکدام از کامپیوتر ها می توانند به راحتی می توانند با دیگری ارتباط داشته باشند. اگر پیاده سازی شما به این گونه باشد که این کامپیوتر ها را دو به دو به یکدیگر متصل کنید باید بگویم که یک پیاده سازی فاجعه انجام داده اید.
زیرا این روش هم هزینه پیاده سازی زیادی دارد و هم پیچیده است و هم خطایابی آن سخت است. به جای این کار در شبکه های امروزی از یک سوییچ استفاده می شود که همه کامپیوتر ها به آن متصل شده و از طریق آن با یکدیگر تعامل دارند. در برنامه های چت نیز چنین مکانیزمی پیاده سازی شده است.
اگر بخواهیم الگوی Mediator را با این مکانیزم مقایسه کنیم این الگو همان سوییچ را پیاده سازی می نماید. حال یک مثال نرم افزاری شرح می دهیم. یک لیست ایمیل را در نظر بگیرید که در آن ایمیل های اعضای سایت Itpro ثبت شده است. وقتی که مطلب جدیدی در سایت گذاشته می شود به همه ی اعضای این لیست برای آگاهی یک ایمیل ارسال خواهد شد.
در این صورت سرویسی که این لیست ایمیل را در اختیار دارد و ایمیل ها را ارسال می کند از الگوی Mediator استفاده می کند. حال اگر کاربری این قصد را داشته باشد که فقط مطالب قسمت انجمن نرم افزار سایت را دنبال کند و با بقیه مطالب کاری نداشته باشد به این سرویس می گوید که این کار را برای آن انجام دهد. حالت دیگر این است که یک کاربر نمی خواهد هیچ ایمیلی درباره ی مطالب سایت به او ارسال شود و این کار را با همان سرویس میانجی انجام می دهد.
حال به شرح UML الگوی Mediator می پردازیم. همانگونه که در شکل دیده می شود این الگو فقط از دو کلاس تشکیل شده است که با ارسال پیام با یکدیگر ارتباط برقرار می کنند. این کلاس ها عبارتند از:
نکته هایی که در پیاده سازی این الگو وجود دارد این است که هر شی از کلاس Colleage هنگام ساخته شدن یک شی mediator می گیرند و آن را به صورت private نگهداری می کنند. و نکته دیگر این که هر شی از کلاس Mediator یک لیست از اشیا Colleage نگهداری می کند و آن را به صورت private نگهداری می نماید. برای ارسال به همه اشیا Colleage ثبت شده در یک mediator ما باید نام آنها به همراه تابع Respond آنها را در اختیار داشته باشیم که این کار را می توانیم توسط یک دیکشنری انجام دهیم. ولی راه ساده تر از این کار در زبان C# استفاده از دلیگیت ها است. کد زیر کار کردن این متد را نمایش می دهد.
using System; using System.Collections.Generic; namespace MediatorPattern { public class Mediator { public delegate void Callback(string message, string from); private Callback respond; public void SignOn(Callback method) { respond += method; } public void Block(Callback method) { respond -= method; } public void Unblock(Callback method) { respond += method; } public void Send(string message, string from) { respond(message, from); Console.WriteLine(); } } public class Colleague { private Mediator mediator; protected string name; public Colleague(Mediator mediator, string name) { this.mediator = mediator; mediator.SignOn(Receive); this.name = name; } public virtual void Receive(string message, string from) { Console.WriteLine(name + " received from " + from + ": " + message); } public void Send(string message) { Console.WriteLine("Send (From " + name + "): " + message); mediator.Send(message, name); } } public class ColleagueB : Colleague { public ColleagueB(Mediator mediator, string name) : base(mediator, name) { } public override void Receive(string message, string from) { if (!String.Equals(from, name)) Console.WriteLine(name + " received from " + from + ": " + message); } } static void Main() { Mediator m = new Mediator( ); Colleague head1 = new Colleague(m,"John"); ColleagueB branch1 = new ColleagueB(m,"David"); Colleague head2 = new Colleague(m,"Lucy"); head1.Send("Meeting on Tuesday, please all ack"); branch1.Send("Ack"); m.Block(branch1.Receive); head1.Send("Still awaiting some Acks"); head2.Send("Ack"); m.Unblock(branch1.Receive); head1.Send("Thanks all"); } }
خروجی کد بالا به شکل زیر خواهد بود.
Send (From John): Meeting on Tuesday, please all ack John received from John: Meeting on Tuesday, please all ack David received from John: Meeting on Tuesday, please all ack Lucy received from John: Meeting on Tuesday, please all ack Send (From David): Ack John received from David: Ack Lucy received from David: Ack Send (From John): Still awaiting some Acks John received from John: Still awaiting some Acks Lucy received from John: Still awaiting some Acks Send (From Lucy): Ack John received from Lucy: Ack Lucy received from Lucy: Ack Send (From John): Thanks all John received from John: Thanks all Lucy received from John: Thanks all David received from John: Thanks all
در کد بالا وقتی که می خواهیم یک شی از کلاس colleage بسازیم یک شی mediator به همراه نام خود شی به آن اختصاص می دهیم. وقتی هم که می خواهیم یک شی را برای mediator معرفی کنیم تابع send آن شی را به تابع SignOn مربوط به mediator میفرستیم. از دیگر اعمالی که کد بالا انجام می دهد این است که این شی امکان بلاک کردن برخی اشیا را دارد که البته مکانیزم آن بسیار ساده است.یک برنامه چت داینامیک تر و پویاتر از برنامه ارسال ایمیل است. زیرا پیامی که ارسال می شود به سرعت توسط دیگر کاربران دریافت می شود و مانند مثال قبل نیست که فقط یک ایمیل ارسال شود.
الگوی Mediator در بسیاری از سیستم های مدرن که از پروتکل Send/receive استفاده می کنند مانند list serverها و چت روم ها استفاده شده است. الگوی mediator علاوه بر ارتباطات انسانی نقش بسیار مهمی در متمرکز کردن ارتباطات بین اشیا دارد. اگر اشیا به طور مستقیم با یکدیگر ارتباط داشته باشند درک ارتباطات آنها بسیار مشکل خواهد بود. پیاده سازی این الگو می تواند نقش بسیار مهمی در کارایی سیستم داشته باشد. زیرا که هرچقدر ارتباطات بین اشیا سریع تر انجام بشود کارایی بالا می رود و گاهی اوقات ممکن است که یک گلوگاه شود. این الگو همچنین در امنیت سیستم ها نیز تاثیر بسیاری دارد. از مزایا و معایب این الگو می توان به موارد زیر اشاره کرد. مزایا:
معایب
در ادامه سری مقالات الگوهای طراحی قصد دارم که الگوی طراحی Memento رو شرح بدهم. این الگو را با نام Token نیز می شناسند. نام این الگو از روی یک فیلم برداشته شده است که عکس آن فیلم را ملاحظه می کنید. داستان این فیلم به این گونه بود که نقش اول این فیلم هیچ حافظه ای نداشت و مطالبی را که قصد به خاطر سپردن آن ها را داشت بر روی برگه های کوچکی یادداشت می کرد و از روی نوشته هایش تصمیم گیری می کرد.
این الگو نیز به همین شکل است. همان گونه که در الگوی Command توضیح دادیم می توان از الگوی Command برای پیاده سازی عمل undo استفاده کرد اما مکانیزم آن را به طور کامل شرح ندادیم. الگوی Command زمینه را برای پیاده سازی این عمل فراهم می کند ولی اطلاعاتی که در هر تغییر باید بازگردانی شوند باید با استفاده از الگوی Memento ذخیره گردند. الگوی memento همان مکانیزم یادداشت ها را در فیلم گفته شده فراهم می کند.
به طور کلی این الگو برای نگهداری وضعیت داخلی یک شی و ذخیره آن به صورت خارج کلاسی می باشد که بعدا بتوان این حالت شی را به وضعیت قبلی بازگرداند.
همه ی نرم افزارهای مدرن امروزه از قابلیتی به نام Undo استفاده می نمایند. این قابلیت به این صورت است که اگر ما کاری را در یک نرم افزار انجام داده باشیم و بخواهیم که برنامه این عمل ما را نادیده فرض کنید از این قابلیت استفاده می نماییم. و برنامه را به حالت قبل از عمل برمی گرداند. به طور مثال اگر در یک برنامه واژه پرداز (word processor) متنی را نوشته باشیم
یا متنی را به عمد و یا غیر عمد حذف کرده باشیم ولی از انجام آن پشیمان شویم از قابلیت undo استفاده می نماییم. همچنین در بازی های رایانه ای موجود وقتی به مرحله ی خاصی از بازی رسیدیم وضعیت بازی ذخیره می شود. که به هر قسمت ذخیره شده یک check point گفته می شود. ذخیره سازی وضعیت نرم افزار در برنامه گفته شده و check point بازی با استفاده از الگوی memento انجام می شود.
ذخیره وضعیت برنامه می تواند مستقل از خود شی باشد و این نکته کلیدی الگوی memento است. شکل نمودار uml این الگو در زیر آورده شده است:
کلاسی است که اشیا آن می توانند وضعیت خود را ذخیره کنند. این کلاس می تواند تصمیم بگیرد که هر بار چند تا از وضعیت ها ذخیره شوند. متغیر state شامل اطلاعات وضعیت شی originator است. این متغیری است که ما آن را ذخیره و بازیابی می کنیم. تابع CreateMemento برای ذخیره وضعیت originator استفاده می شود. این تابع یک شی Memento می سازد و شی state را با استفاده از آن ذخیره می نماید.
تابع SetMemento شی وضعیت originator را با گرفتن شی Memento بر می گرداند. این تابع برای بازیابی وضعیت قبل می باشد که قبلا در شی memento ذخیره شده بود. کلاس Memento عمل ذخیره سازی وضعیت originator را به شکل تاریخچه ای انجام می دهد و Caretaker وضعیت های ذخیره شده را در خود نگهداری می کند. به این شکل که لیستی از memento ها را در خود دارد. الگوی memento دارای دو واسط به شکل زیر است:
کلاس Memento وضعیت Originator را در خود نگهداری می کند ولی به کلاس های دیگر اجازه دسترسی به آن وضعیت را نمی دهد. با این کار محدودیت های کپسوله سازی (encapsulation) رعایت خواهد شد. دقت داشته باشید که شی کلاس memento هیچگاه توسط کاربر در دسترس نخواهد بود و همه ی اعمال توسط کلاس Caretaker انجام می شود. زیرا نحوه ذخیره و بازیابی به کلاس client مربوط نمی شود.
پیاده سازی این الگو در شکل زیر آمده است.
class Program { static void Main(string[] args) { Originator<string> orig = new Originator<string>(); orig.SetState("state0"); Caretaker<string>.SaveState(orig); //save state of the originator orig.ShowState(); orig.SetState("state1"); Caretaker<string>.SaveState(orig); //save state of the originator orig.ShowState(); orig.SetState("state2"); Caretaker<string>.SaveState(orig); //save state of the originator orig.ShowState(); //restore state of the originator Caretaker<string>.RestoreState(orig, 0); orig.ShowState(); //shows state0 } } //object that stores the historical state public class Memento<t> { private T state; public T GetState() { return state; } public void SetState(T state) { this.state = state; } } //the object that we want to save and restore, such as a check point in an application public class Originator<t> { private T state; //for saving the state public Memento<t> CreateMemento() { Memento<t> m = new Memento<t>(); m.SetState(state); return m; } //for restoring the state public void SetMemento(Memento<t> m) { state = m.GetState(); } //change the state of the Originator public void SetState(T state) { this.state = state; } //show the state of the Originator public void ShowState() { Console.WriteLine(state.ToString()); } } //object for the client to access public static class Caretaker<t> { //list of states saved private static List<memento><t>> mementoList = new List<memento><t>>(); //save state of the originator public static void SaveState(Originator<t> orig) { mementoList.Add(orig.CreateMemento()); } //restore state of the originator public static void RestoreState(Originator<t> orig, int stateNumber) { orig.SetMemento(mementoList[stateNumber]); } }
نتیجه اجرای کد بالا به شکل زیر است:
عکس
امروزه از الگوی memento در بسیاری از نرم افزارها استفاده می شود و حتی برخی از کامپوننت های برنامه نویسی خود از این الگو استفاده می نمایند(مانند کنترل RichTextBox در سی شارپ که قابلیت undo دارد) ولی وقتی نیاز به این الگو حس می شود که در برخی پردازش های مهندسی که ممکن است چند روز طول بکشد
وسط اجرای پردازش مشکلی پیش بیاید و مجبور شویم پردازش را از سر بگیریم که باعث اتلاف وقت و هزینه بسیاری است. برای جلوگیری از این اتلاف هزینه و زمان از مکانیز گرفتن check point استفاده می شود که این مکانیزم هم برای پیاده سازی از الگوی memento استفاده می کند و در هر بار خرابی سیستم از آخرین check point کار را ادامه می دهد.
در این مطلب قصد داریم که یکی دیگر از الگوهای طراحی شی گرا را شرح دهیم. الگویی که قصد توضیح آن را داریم الگوی Strategy می باشد. این الگو یک الگوی رفتاری یا Behavioral می باشد. الگوهای رفتاری با الگوریتم ها و ارتباط بین آن ها درگیر می باشند. اگر برنامه ما یک الگوریتم یکتا را پیاده سازی می کند گرچه ممکن است از چندین کلاس استفاده کند ولی به خاطر بالا رفتن پیچیدگی ممکن است که برای مدیریت برنامه و نگهداری کد مشکلاتی به جود بیاید. ما با استفاده از الگوهای طراحی رفتاری سعی می کنیم که از این پیچیدگی ها کم کنیم. حال به بررسی الگوی طراحی Strategy می پردازیم.
این الگو به این منظور استفاده می شود که یک الگوریتم را از کلاس میزبان جدا کرده و در کلاس دیگر نگهداری نماید. ممکن است که ما در برنامه خود چندین الگوریتم(استراتژی) داشته باشیم. حال اگر بخواهیم همه این الگوریتم ها را در برنامه خود داشته باشیم در صورت عادی باید از عبارات شرطی بسیاری استفاده کنید و همین عمل باعث می شود که پیچیدگی برنامه به شدت بالا برود. الگوی strategy کاربر را قادر می سازد که از داخل یک گروه از الگوریتم ها یک الگوریتم را برای استفاده انتخاب نماید.
اگر بخواهیم یک مثال ساده از الگوی Strategy بزنیم می توانیم برنامه ای را مثال بزنیم که با استفاده از روش های متفاوت مرتب سازی یک سری اعداد را مرتب سازی می کند. الگوریتم های متفاوتی برای مرتب سازی وجود دارد که می توان به QuickSort و یا MergeSort اشاره کرد.
هرکدام از این روش ها می توانند در شرایط خاصی بهترین انتخاب موجود باشند. می توان تصمیم گیری این شرایط را با استفاده از الگوی Strategy پیاده سازی نمود. به این شکل که یک مجموعه از ورودی ها را داریم. ولی هرکدام از روش های مرتب سازی را به صورت جداگانه پیاده سازی می کنیم. ولی ورودی ها و خروجی ها ی یکسانی داشته باشند. در این صورت با توجه به شرایط ورودی ها را به یک الگوریتم داده و خروجی آن را دریافت خواهیم کرد.
طراحی الگوی Strategy در قالب نمودار UML در شکل زیر نشان داده شده است. همانگونه که در شکل دیده می شود می توان از داخل استراتژی های موجود در یک گروه استراتژی مناسب انتخاب کرد.
کلاس هایی که در این الگو هستند به شرح زیر است:
کلاس Context یک شی از نوع استراتژی انتخاب شده می سازد. کلاس Strategy ممکن است که به وضعیت Context دسترسی داشته باشد.
پیاده سازی برای الگوی Strategy نیازمند ویژگی های خاص و جدید C# نیست و می توان آن را به سادگی پیاده سازی کرد. در پیاده سازی کلاس Context یک نمونه از اینترفیس IStrategy ساخته می شود. کلاس استفاده کننده متد الگوریتم را از داخل Context فراخوانی می کند و این کلاس در خواست را به استراتژی مورد نظر هدایت می کند. در مثال زیر هر کدام از استراتژی ها یک متد move دارند و یک تولید کننده عدد تصادفی در کلاس استفاده کننده مشخص می کند که چه زمانی زمان تعویض الگوریتم می باشد. این مثال در کد زیر آورده شده است.
using System; // The Context class Context { // Context state public const int start = 5; public int Counter = 5; // Strategy aggregation IStrategy strategy = new Strategy1( ); // Algorithm invokes a strategy method public int Algorithm( ) { return strategy.Move(this); } // Changing strategies public void SwitchStrategy( ) { if (strategy is Strategy1) strategy = new Strategy2( ); else strategy = new Strategy1( ); } } // Strategy interface interface IStrategy { int Move (Context c); } / Strategy 1 class Strategy1 : IStrategy { public int Move (Context c) { return ++c.Counter; } } // Strategy 2 class Strategy2 : IStrategy { public int Move (Context c) { return --c.Counter ; } } // Client static class Program { static void Main ( ) { Context context = new Context( ); context.SwitchStrategy( ); Random r = new Random(37); for (int i=Context.start; i<=Context.start+15; i++) { if (r.Next(3) == 2) { Console.Write("|| "); context.SwitchStrategy( ); } Console.Write(context.Algorithm( ) +" "); } Console.WriteLine( ); } }
خروجی کد بالا به شکل زیر خواهد بود:
/* Output 65 4 || 5 6 7 || 6 || 7 8 9 10 || 9 8 7 6 || 7 || 6 5 66 */
برخی از نکات کلیدی درباره ی پیاده سازی الگوی Strategy عبارتند از:
یکی از موارد استفاده از الگوی Strategy استفاده از این الگو در پیاده سازی تکنولوژی Linq در زبان C# می باشد که می توان به راحتی برای انواع پایگاه های داده و انواع مجموعه داده ها از آن استفاده کرد.
به طور کلی در زمان های زیر از الگوی Strategy استفاده می کنیم.
زمانی پیش می آید که Context نمی خواهد از هیچکدام از استراتژی های موجود استفاده کند در این صورت می گوییم که Context از استراتژی do nothing استفاده می کند. یعنی هیچ کاری انجام نمی دهد.
در ادامه مباحث الگوهای طراحی شی گرا قصد داریم الگوی Factory Method را شرح خواهیم داد.
الگوی Factory Method راهی برای ساخت اشیا می باشد و برای تولید اشیا از کلاس ها مورد استفاده قرار می گیرد. اما تفاوتی که وجود دارد این است که این الگو به زیرکلاس ها (subclass) اجازه می دهد که از کلاسی که مورد نظرشان است شی را بسازد. زیرکلاس های متفاوت ممکن است که پیاده سازی های متفاوتی داشته باشند. الگوی Factory Method با توجه به اطلاعات کاربر از آن زیرکلاسی که مدنظر است شیئی را می سازد.
اگر یک میوه فروشی موجود باشد که در کل روزهای سال میوه آوکادو می فروشد. این میوه فروشی باید مطمئن باشد که می تواند در طول سال می تواند این میوه را از کشاورزان تهیه نماید. این میوه فروشی باید از نواحی و کشورهای مختلف این میوه را در طول سال خریده و در داخل فروشگاه عرضه نماید. این میوه در کشورهای اسپانیا، آفریقای جنوبی و کنیان در فصل های متفاوتی رشد می کند. الگوی Factory Method شبیه به این میوه فروشی عمل می کند.
طراحی این الگو به این شکل است که کاربر یک متغیر Product معرفی می کند و از Factory Method می خواهد که آن را مقدار دهی نماید. شیئی که ساخته می شود به این بستگی دارد که چه تصمیمی برای ساخت شی product گرفته شده است. نمودار uml این الگو به شکل زیر است. دقت کنید که در این نمودار دو نوع product به نام های productA و productB وجود دارد.
عناصری که در این نمودار وجود دارند به شرح زیر هستند:
طراحی این الگو باعث می شود که بتوانیم در مورد این که کدام شی ساخته شود در یک زمان تصمیم گیری کنیم. نیازی نیست که کاربر بداند که زیرکلاس های مختلف را چگونه استفاده کند و فقط از اشیا ساخته شده استفاده می کند.
اگر بخواهیم برای این الگو یک مثال بیاوریم همان فراهم کردن میوه آوکادو را پیاده سازی می کنیم. اگر هرکدام از کشورهای تولید کننده آوکادو را به عنوان یک کلاس در نظر بگیریم و هرکدام اینترفیس IProduct را پیاده سازی می کند. بنابراین این کشورها می توانند آوکادو برای میوه فروشی فراهم کنند. کشورهای تولید کننده آوکادو با توجه به این که در کدام ماه از سال قرار داشته باشیم انتخاب می شوند. کد پیاده سازی و نتیجه آن به این شکل است.
using System; using System.Collections; class FactoryPattern { interface IProduct { string ShipFrom( ); } class ProductA : IProduct { public String ShipFrom ( ) { return " from South Africa"; } } class ProductB : IProduct { public String ShipFrom ( ) { return "from Spain"; } } class DefaultProduct : IProduct { public String ShipFrom ( ) { return "not available"; } } class Creator { public IProduct FactoryMethod(int month) { if (month >= 4 && month <=11) return new ProductA( ); else if (month == 1 || month == 2 || month == 12) return new ProductB( ); else return new DefaultProduct( ); } } static void Main( ) { Creator c = new Creator( ); IProduct product; for (int i=1; i<=12; i++) { product = c.FactoryMethod(i); Console.WriteLine("Avocados "+product.ShipFrom( )); } } }
و نتیجه اجرای برنامه به شکل زیر خواهد شد
Avocados from Spain Avocados from Spain Avocados not available Avocados from South Africa Avocados from South Africa Avocados from South Africa Avocados from South Africa Avocados from South Africa Avocados from South Africa Avocados from South Africa Avocados from South Africa Avocados from Spain
همانگونه که در کد دیده می شود این ما در متد main فقط از creator استفاده کرده ایم که این creator به اقتضای زمان تصمیم می گیرد که از کدام کلاس استفاده کند و هر کدام از کلاس ها یک متد ShipFrom دارند که آن را با توجه به نیاز خود پیاده سازی کرده اند.
از این الگو در موارد زیر استفاده می شود.
در ادامه مباحث الگوهای طراحی در این مطلب قصد دارم که به الگوی state بپردازم، امیدوارم که مفید باشد. الگوی state یک الگوی رفتاری می باشد. این الگو سازوکاری میچیند که ارتباطات بین کلاس ها و موجودیت ها کنترل شود. این الگو را می توان نسخه پویا و دینامیک الگوی strategy دانست. رفتار این الگو به این شکل است که وقتی که حالت داخلی یک شی تغییر می کند با توجه به آن تغییر این الگو رفتار خود را تغییر می دهد. این تغییر با تغییر اعمال درون الگو می باشد. این عمل در صورتی قابل انجام است که برای یک شی تعریف شده زیر کلاس (subclass) آن را تغییر بدهیم.
یک برنامه پرواز را در نظر بگیرید که توسط یک شرکت هواپیمایی استفاده می شود. کار این برنام به این شکل است که هر مسافر که برای اولین بار برای پرواز به این شرکت می آید در وضعیت آبی قرار دارد. سیستم برای مسافران وضعیت آبی امکانات و تسهیلات خاصی در نظر می گیرد. و قیمت پرواز را برای آنان به گونه ای حساب می کند که تخفیف ندارد و همچنین مسافت پروازی آنان نیز محدود است.
بعد از 5 پرواز که همان مسافر با این شرکت هواپیمایی انجام داد و پروازش در این سیستم ثبت شد مسافر از وضعیت آبی به وضعیت نقره ای ارتقاع پیدا می کند. سیستم برای مسافران وضعیت نقره ای برای هر پرواز 5 درصد تخفیف در نظر می گیرد و همچنین مسافت پرواز را به اندازه ای افزایش می دهد. حال اگر شخصی بیش از 15 پرواز انجام داده باشد به وضعیت طلایی خواهد رسید و برای هر پرواز 15 درصد تخیفیف خواهد گرفت.
دقت کنید که مسافر باید این پرواز ها را در طول یک سال انجام دهد. حال وضعیت مسافر را توسط الگوی State پیاده سازی می کنیم. در الگوی State رفتار کلاس با توجه به حالت (State) کلاس تغییر می کند. این الگو یک الگوی رفتاری است. در این الگو ما اشیائی را می سازیم که حالت های مختلف یک کلاس را دارا می باشد.
الگوی State بین کلاس Context و State یک تعامل جالب ایجاد می کند. کلاس Context شامل اطلاعاتی است که اکثرا ثابت است در حالی که State می تواند در طی اجرای برنامه تغییر کند. کلاس UML این ای الگو به شکل زیر است.
همانطور که در شکل مشاهده می شود اجزای این الگو عبارتند از:
کلاس Context یک متغیر از نوع IState در خود دارد که به یک شی خاص (مثل stateA) اشاره می کند. همه ی عملیات از طریق متد Handle انجام می شوند. همانطور که در شکل دیده می شود کلاس State به داده های Context دسترسی کامل دارد. بنابراین در هر زمانی هم کلاس Context و هم شی State می توانند تصمیم بگیرند که تغییر حالت بدهند. این کار با انتساب ویژگی های state جدید به شی Context صورت می گیرد. وقتی که این کار انجام شود همه ی درخواست ها به state جدید ارسال می شوند. که ممکن است اعمالی که در این State انجام می شود با اعمال State قبلی کاملا متفاوت باشد.
همانند الگوی استراتژی الگوی State هم به مکانیزم اینترفیس و تجمع بستگی دارد. در مثال زیر دو کلاس state به نام های NormalState و FastState وجود دارد که برای شمارش شمارنده داخل کلاس Context با سرعت های متفاوتی عمل می کنند. هر دو کلاس اینترفیس IState را پیاده سازی می کنند و هردو کلاس ها دارای دو تابع MoveUpو MoveDown هستند. خود state ها تصمیم می گیرند که چه زمانی به state دیگر سوییچ کنند. در این مثال بر پایه مقدار عددی شماره تصمیم گیری می شود.
using System; using System.Collections.Generic; interface IState { int MoveUp(Context context); int MoveDown(Context context); } // State 1 class NormalState : IState { public int MoveUp(Context context) { context.Counter+=2; return context.Counter; } public int MoveDown(Context context) { if (context.Counter < Context.limit) { context.State = new FastState( ); Console.Write("|| "); } context.Counter-=2; return context.Counter; } } // State 2 class FastState : IState { public int MoveUp(Context context) { context.Counter+=5; return context.Counter; } public int MoveDown(Context context) { if (context.Counter < Context.limit) { context.State = new NormalState( ); Console.Write("||"); } context.Counter-=5; return context.Counter; } } // Context class Context { public const int limit = 10; public IState State {get; set; } public int Counter = limit; public int Request(int n) { if (n==2) return State.MoveUp(this); else return State.MoveDown(this); } } static class Program { // The user interface static void Main ( ) { Context context = new Context( ); context.State = new NormalState( ); Random r = new Random(37); for (int i = 5; i<=25; i++) { int command = r.Next(3); Console.Write(context.Request(command)+" "); } Console.WriteLine( ); } }
خروجی این کد به شکل زیر خواهد بود
8 10 8 || 6 11 16 11 6 ||1 3 || 1 ||-4 || -6 -1 4 ||-1 || -3 2 7 ||2 4
دقت کنید که علامت || در خروجی نشان دهنده محل تغییر state می باشد. مسئله ای که قابل تامل است این است که آیا ما اشیا State را د آغاز برنامه بسازیم یا ساخت آنها را به زمان نیاز داشتن به آن اشیا موکول کنیم. انجام این کار بستگی به تعداد دفعات تغییر حالت در برنامه و اندازه داده شی State بستگی دارد. برای حالت هایی که تغییرات زیادی داریم بهتر است که شی را از همان ابتدا بسازیم.
این الگو برای پیاده سازی ابزارهای گرافیکی مورد استفاده قرار بگیرد و در کل برای اشیائی که باید در زمان اجرا تغییر کند. همچنین برای اشیایی که در حال پیچیده شدن هستند و شرط های زیادی دارند قابل استفاده است.
بنیانگذار توسینسو و برنامه نویس
مهدی عادلی، بنیان گذار TOSINSO. کارشناس ارشد نرم افزار کامپیوتر از دانشگاه صنعتی امیرکبیر و #C و جاوا و اندروید کار می کنم. در زمینه های موبایل و وب و ویندوز فعالیت دارم و به طراحی نرم افزار و اصول مهندسی نرم افزار علاقه مندم.
زمان پاسخ گویی روز های شنبه الی چهارشنبه ساعت 9 الی 18
فقط به موضوعات مربوط به محصولات آموزشی و فروش پاسخ داده می شود