دیزاین پترن (Design Pattern) در جاوا

دیزاین پترن از شهرت بالایی میان برنامه‌نویسان برخوردارند. دیزاین پترن، یک راه‌حل خوب‌ طراحی‌شده برای یک مسئله‌ی رایج نرم‌افزاری است.دیزاین پترن های جاوادر ادامه، برخی از مزایای استفاده از دیزاین پترن‌ها را عنوان می‌کنیم:1. دیزاین پترن‌ها از قبل تعریف‌شده‌اند و از یک رویکرد استاندارد برنامه‌نوسی برای حل یک مسئله‌ی رایج استفاده می‌کنند. درنتیجه با استفاده‌ی مناسب از دیزاین‌ پترن‌ها می‌توانیم در زمان صرفه‌جویی کنیم. جاوا دیزاین پترن‌های زیادی دارد که می‌توانیم از آنها در پروژه‌های جاوا استفاده کنیم.2. با استفاده از دیزاین پترن‌ها قابلیت استفاده‌ی مجدد (Reusability) کد بالاتر‌می‌رود. قابلیت استفاده‌ی مجدد بالاتر نیز منجر به کدی قوی‌تر با هزینه‌های نگه‌داری کمتر می‌شود. در نهایت، محصول نرم‌افزاری حاصل، هزینه‌ی کل مالکیت (TCO) کمتری دارد.3. دیزاین پترن‌ها از قبل تعریف‌شده‌اند و به همین دلیل فهم و دیباگ کد را ساده‌تر می‌کنند. سرعت توسعه‌ی کد بالاتر می‌رود و اعضای جدید تیم راحت‌تر می‌توانند کدهای نوشته‌شده را بفهمند.دیزاین پترن های جاوا به سه دسته تقسیم می‌شوند. دیزاین پترن‌های سازنده (Creational)، ساختاری (Structural) و رفتاری (Behavioral). در این مقاله به معرفی تمام دیزاین پترن‌های موجود در هر دسته می‌پردازیم. دیزاین پترن های سازندهدیزاین پترن‌های سازنده، بهترین راهکارِ موجودِ نمونه‌سازی از یک شئ (Instantiate) را در شرایطی خاص ارائه می‌دهند.Singleton (سینگلتون) این پترن نمونه‌سازی از یک کلاس را محدود می‌کند. با Singleton، تنها یک نمونه از یک کلاس می‌تواند در ماشین مجازی جاوا وجود داشته باشد. دیزاین پترن ساده‌ای به‌نظر می‌رسد، اما نگرانی‌های پیاده‌سازی بسیاری را هنگام پیاده‌سازی به همراه دارد. برنامه‌نویسان همیشه درباره‌ی استفاده از پترن Singleton با هم اختلاف نظر داشته‌اند.Singleton یکی از شناخته‌شده‌ترین دیزاین پترن‌های جاواست.Factory (فکتوری)از این Design Pattern در شرایطی استفاده می‌شود که یک کلاس پدر داریم که چند کلاس فرزند دارد و می‌خواهیم بر اساس ورودی یکی از کلاس‌های فرزند را به عنوان خروجی بفرستیم.با این پترن، مسئولیت ساخت یک نمونه از کلاس برعهده‌ی برنامه‌ی کلاینت نیست و کلاس Factory این مسئولیت را به‌عهده می‌گیرد.می‌توانیم پترن Singleton را روی کلاس Factory اعمال کنیم یا متد Factory را استاتیک تعریف کنیم.Abstract Factory (فکتوری انتزاعی)این Design Pattern شبیه Factory است. اگر با Design Pattern Factory جاوا آشنا باشید، می‌دانید که ما یک کلاس Factory داریم که بر اساس ورودی، کلاس‌های فرزند مختلفی را برمی‌گرداند. کلاس Factory برای انجام این کار از دستور if-else یا Switch استفاده می‌کند.در Abstract Factory، بلوک if-else را حذف و برای هر کلاس فرزند از یک کلاس Factory استفاده می‌کنیم. یک کلاس Abstract Factory بر اساس کلاس Factory ورودی، یک کلاس فرزند را برمی‌گرداند.Builder (بیلدر)وقتی Object در پترن‌های Factory و Abstract Factory دارای خصیصه‌های (Attribute) زیادی باشد، مشکلاتی رخ می‌دهد. Builder برای حل این مشکلات معرفی شد.Builder شئ را مرحله به مرحله می‌سازد و متدی برای برگرداندن شئ نهایی ارائه می‌کند. به این ترتیب مشکلات مرتبط با داشتنِ پارامترهای اختیاری زیاد و Inconsistent State را برطرف می‌کند.Prototype (پروتوتایپ)از این پترن زمانی استفاده می‌کنیم که ساخت یک شئ جدید هزینه، زمان و منابع زیادی می‌گیرد و یک شئ آماده‌ی مشابه آن نیز داریم.Prototype مکانیزمی دارد که شئ اصلی را در یک شئ جدید کپی می‌کنیم و بعد آن را طبق نیازمان تغییر می‌دهیم. این Design Pattern از Cloning جاوا برای کپی شی استفاده می‌کند.در پترن Prototype ضروری است شئ‌ای که قصد کپی کردن آن را داریم، قابلیت کپی را در اختیارمان قرار دهد. هیچ کلاس دیگری نباید این کار را انجام دهد.کپی سطحی (Shallow Copy) یا عمیق (Deep Copy (Propertyهای شئ هم بسته به شرایط است و یک تصمیم طراحی محسوب می‌شود. دیزاین پترن های ساختاریپترن‌های ساختاری روش‌هایی مختلف را برای ساختِ ساختار یک کلاس ارائه می‌کنند. مثلاً استفاده از شئ‌گرایی (Inheritance) و ترکیب (Composition) برای ساخت اشیاء بزرگ‌تر با استفاده از اشیاء کوچک‌تر.Adapter (آداپتور)این پترن نوعی Design Pattern ساختاری است و زمانی از آن استفاده می‌کنیم که دو واسط غیرمرتبط بخواهند با هم همکاری کنند.شئ‌ای که این دو واسط غیرمرتبط را به هم وصل می‌کند، Adapter نامیده می‌شود. یک مثال از دنیای واقعی شاید به درک بهتر این پترن کمک کند.یک شارژر موبایل یک Adapter است، چون باتری به 3 ولت نیاز دارد، اما برق شهری 120 (ایالات متحده) یا 240 ولت (هند) است. بنابراین شارژر موبایل یک Adapter بین برق شهری و باتری است.Compositeاین پترن یکی از Design Patternهای ساختاری است و زمانی از آن استفاده می‌کنیم که قصد نمایش یک سلسله‌مراتب جزء-کل را داشته باشیم. وقتی می‌خواهیم ساختاری بسازیم که با همه‌ی اشیاء موجود در آن به گونه‌ای یکسان رفتار شود، از Composite استفاده می‌کنیم.یک مثال از دنیای واقعی شاید به درک بهتر این پترن کمک کند. یک دیاگرام شامل اشیائی مثل دایره، خط، مثلث و … است و وقتی طرح را رنگ می‌کنیم (مثلا قرمز)، اشیاء درون آن نیز قرمز می‌شوند.در اینجا طرح از قسمت‌های مختلفی تشکیل شده است و همه‌ی آنها وظایفی یکسان دارند.Proxy (پروکسی)هدف Proxy این است که “جایگزین یا نماینده‌ای برای شئ دیگری ارائه کند تا دسترسی به آن کنترل شود”. تعریف بالا همه‌چیز را به خوبی عنوان می‌کند و از این Design Pattern برای کنترل دسترسی به یک عملکرد استفاده می‌کنیم.کلاسی را درنظر بگیرید که دستوراتی را روی سیستم اجرا می‌کند. حالا اگر خودمان بخواهیم از این کلاس استفاده کنیم، مسئله‌ای پیش نمی‌آید، اما اگر بخواهیم آن را به یک اپلیکیشن کلاینت بدهیم، ممکن است مشکلاتی بسیار بد رخ دهد.مثلاً برنامه‌ی کلاینت می‌تواند دستوراتی اجرا کند که برخی فایل‌های سیستمی پاک شوند یا تنظیمات سیستم را به‌گونه‌ای تغییر دهند که ما نمی‌خواهیم.Flyweightوقتی قصد ساخت تعداد زیادی شئ از یک کلاس را داریم، از این Design Pattern استفاده می‌کنیم.هر شئ فضای حافظه می‌گیرد و این مسئله می‌تواند برای دستگاه‌هایی با حافظه‌ی محدود، مثل گوشی‌های موبایل یا سیستم‌های تعبیه‌شده، مشکل‌ساز باشد.با استفاده از Flyweight می‌توانیم با به اشتراک گذاشتن اشیاء، بار حافظه را کم کنیم. پیاده‌سازی String Pool در جاوا یکی از بهترین مثال‌های دیزاین پترن Flyweight است.Façadeاین Design Pattern برای تعامل ساده‌تر اپلیکیشن‌های کلاینت با سیستم طراحی شده است. فرض کنید یک اپلیکیشن با مجموعه‌ای از واسط‌ها را داریم.این اپلیکیشن قصد استفاده از پایگاه داده‌ی MySql/Oracle را دارد و می‌خواهد انواع مختلف گزارش مثل HTML، PDF و … را تولید کند.پس مجموعه‌ای دیگر از واسط‌ها را برای کار با پایگاه داده‌های مختلف خواهیم داشت. حالا یک اپلیکیشن کلاینت می‌تواند از این واسط‌ها برای دریافت اتصال (Connection) لازم به پایگاه داده استفاده و یک گزارش تولید کند.اما وقتی پیچیدگی برنامه بیشتر یا اسامی رفتارهای واسط گیج‌کننده می‌شوند، اپلیکیشن کلاینت برای مدیریت آنها به مشکل برمی‌خورد.برای حل این مشکل می‌توانیم از دیزاین پترن Facade استفاده کنیم. در این صورت یک واسط Wrapper روی تمام واسط‌های موجود می‌نویسیم تا به اپلیکیشن کلاینت کمک کنیم.Bridgeوقتی هم در واسط‌ها و هم در پیاده‌سازی دارای سلسله‌مراتب هستیم، از این Design Pattern برای جداسازی واسط از پیاده‌سازی استفاده می‌کنیم.در این حالت جزئیات پیاده‌سازی را از برنامه‌های کلاینت مخفی نگه‌ می‌داریم. این پترن نیز، مثل 5 پترن قبلی، یک دیزاین پترن ساختاری است.پیاده‌سازی دیزاین پترن Bridge نشان‌دهنده‌ی این موضوع است که برنامه‌نویس ترکیب را به ارث‌بری ترجیح می‌دهد.Decoratorوقتی قصد تغییر عملکرد یک شئ را در زمان اجرا داریم، از این Design Pattern استفاده می‌کنیم. نمونه‌های دیگر کلاس با این روش تغییر نمی‌کنند و تنها رفتار همان شئ تغییر می‌یابد.Decorator نیز یکی از دیزاین پترن‌های ساختاری است و از کلاس یا واسط Abstract و ترکیب برای پیاده‌سازی استفاده می‌کند.ما برای گسترش رفتار یک شئ از ارث‌بری یا ترکیب استفاده می‌کنیم، اما این کار در زمان کامپایل صورت می‌گیرد و روی همه‌ی اشیاء کلاس اعمال می‌شود.نمی‌توانیم در زمان اجرا، عملکردی جدید را برای حذف رفتارهای موجود یک شئ، اضافه کنیم و این همان شرایطی است که Decorator به کارمان می‌آید. دیزاین پترن های رفتاریپترن‌های رفتاری راهکارهایی را برای ارتباط بهتر اشیاء با هم ارائه می‌کنند. این پترن‌ها برای Extend بهتر، روش‌هایی را برای Lose Coupling و انعطاف عرضه می‌کنند.Template MethodTemplate یک Design Pattern رفتاری است و از آن برای ساخت یک استاب متد (Stub) و انجام برخی از مراحل پیاده‌سازی در کلاس فرزند استفاده می‌شود.متد Template مراحل اجرای یک الگوریتم را تعیین می‌کند و می‌تواند یک روش پیاده‌سازی پیش‌فرضِ رایج را برای همه یا تعدادی از کلاس‌های فرزند ارائه کند.فرض کنید می‌خواهیم یک الگوریتم برای ساخت یک ساختمان داشته باشیم. مراحل ساخت یک ساختمان به صورت زیرند:ساخت پِی، ساخت ستون‌ها، ساخت دیوارها و پنجره‌ها.نکته‌ی مهم این است که نمی‌توانیم ترتیب اجرای مراحل را تغییر دهیم. مثلاً نمی‌توانیم قبل از ساخت پی، پنجره بسازیم. در این شرایط، از یک متد Template استفاده می‌کنیم که از متدهای مختلفی برای ساخت ساختمان استفاده می‌کند.Mediatorاز این Design Pattern برای ارائه‌ی یک واسط ارتباطی متمرکز بین اشیاء سیستم استفاده می‌شود. در شرایطی که در یک اپلیکیشن سازمانی، اشیاء مختلفی داریم که با هم تعامل دارند، این دیزاین پترن بسیار کاربرد دارد.اگر اشیاء مستقیماً با هم تعامل داشته باشند، اجزای سیستم Tightly Coupled می‌شوند و این موضوع هزینه‌ی نگه‌داری را افزایش می‌دهد و توسعه‌ی نرم‌افزار را سخت می‌کند.دیزاین پترن Mediator تلاش می‌کند یک واسط (Mediator) بین اشیاء قرار دهد تا از طریق آن با هم ارتباط داشته باشند و به پیاده‌سازی loose Coupling بین اشیاء کمک می‌کند.برجِ کنترلِ ترافیکِ هوایی، یکی از بهترین مثال‌ها برای دیزاین پترن Mediator است، که در آن برج کنترل به عنوان واسطی بین پروازهای مختلف عمل می‌کند.Mediator مسیری بین اشیاء مختلف است و می‌تواند منطق خود را در روش‌های ارتباط داشته باشد.Chain of Responsibility (زنجیره‌ی مسئولیت)وقتی درخواست کلاینت برای پردازش به زنجیره‌ای از اشیاء داده می‌شود، برای داشتن Loose Coupling از این Design Pattern استفاده می‌شود.سپس خود اشیاء موجود در زنجیره تصمیم می‌گیرند که چه شئ‌ای درخواست را پردازش کند و آیا لازم است که درخواست به شئ بعدی در زنجیره فرستاده شود یا خیر.می‌دانیم که در یک دستور try-catch می‌توانیم چند بلوک Catch داشته باشیم. در اینجا هر بلوک Catch نوعی پردازنده برای پردازش Exception موردنظر است.پس وقتی یک Exception در بلوک Try رخ داد، آن Exception برای پردازش به اولین بلوک Catch فرستاده می‌شود.اگر بلوک Catch نتواند آن را پردازش کند، آن را به شئ بعدی در زنجیره می‌فرستد، که در اینجا یک بلوک Catch است.در نهایت اگر آخرین بلوک Catch هم نتوانست آن را پردازش کند، Exception بیرون انداخته و به برنامه‌ی اجرا کننده‌ی کد فرستاده می‌شود.Observerوقتی State یک شئ برایمان اهمیت دارد و می‌خواهیم از هر تغییری در آن مطلع شویم، از این Design Pattern استفاده می‌کنیم.در دیزاین پترن Observer، آن شئ‌ای که مراقب State یک شئ دیگر است، Observer نامیده می‌شود و به شئ‌ای که مراقب State آن هستیم، Subject می‌گوییم.در جاوا می‌توانیم با استفاده از یک پلتفرم داخلی و از طریق کلاس java.util.Observable و واسط java.util.Observer پترن Observer را پیاده‌سازی کنیم.اگرچه از این گزینه‌ها استفاده‌ی چندانی نمی‌شود، چون بسیار ساده هستند و اغلب اوقات نمی‌خواهیم فقط به هدف پیاده‌سازی پترن Observer از یک کلاس ارث‌بری داشته باشیم، چون جاوا از ارث‌بری چندگانه پشتیبانی نمی‌کند.(Java Message Service (JMS از پترن‌های Mediator و Observer استفاده می‌کند تا به اپلیکیشن‌ها این امکان را بدهد که برای هم داده بفرستند و یکدیگر را Subscribe کنند.Strategy (استراتژی)وقتی چند الگوریتم برای انجام یک کار داریم و کلاینت در زمان اجرا تصمیم می‌گیرد از چه الگوریتمی استفاده کند، از این Design Pattern استفاده می‌کنیم.نام دیگر دیزاین پترن Strategy، Policy است. چند الگوریتم تعریف می‌کنیم و کلاینت می‌تواند الگوریتم موردنظرش را به صورت یک پارامتر انتقال دهد. یکی از بهترین مثال‌های این دیزاین پترن، متد Collection.sort() است که پارامتر Comparator را می‌گیرد. اشیاء با توجه به پیاده‌سازی‌های مختلفِ واسط‌های Comprator، به انواع مختلفی Sort می‌شوند.Commandاز این Design Pattern برای داشتن Loose Coupling در یک مدل Request-Response استفاده می‌کنیم.در دیزاین پترن Command، درخواست به Invoker فرستاده می‌شود و این Invoker است که آن را به شئ Command کپسوله‌شده (Encapsulated) منتقل می‌کند.شئ Command درخواست را به متد مناسب Receiver می‌فرستد تا این متد عملی خاص را اجرا کند.فرض کنید می‌خواهیم یک سیستم فایل (File System) داشته باشیم که دارای متدهای Open، Write و Close است و باید از چند سیستم عامل مختلف مثل ویندوز و یونیکس پشتیبانی کند.برای پیاده‌سازی این سیستم فایل، اول باید کلاس‌های Receiver را بسازیم که در واقع انجام تمام کارها برعهده‌ی آنهاست.چون کدهایمان را بر اساس واسط‌های جاوا می‌نویسیم، می‌توانیم واسط FileSystemReceiver را طراحی کنیم.سپس می‌توانیم کلاس‌هایی را، که آن را Implement می‌کنند، برای سیستم‌های عامل مختلف مثل ویندوز، یونیکس، سولاریس و … بنویسیم.Stateزمانی از این Design Pattern استفاده می‌کنیم که یک شئ رفتارش را بر اساس State داخلی خود تغییر دهد.اگر بخواهیم رفتار یک شئ را، براساس State آن عوض کنیم، می‌توانیم یک متغیر State داشته باشیم. برای انجام عمل‌های متفاوت نیز براساس این متغیر، می‌توانیم از بلوک شرطی if-else استفاده کنیم.دیزاین پترن State از طریق پیاده‌سازی Context و State، روشی سیستماتیک و Loosely Coupled را عرضه می‌کند.Visitorوقتی می‌خواهیم عملی را روی گروهی از اشیاء مشابه انجام دهیم، از این Design Pattern استفاده می‌کنیم. با کمک دیزاین پترن Visitor، می‌توانیم منطق عملیاتی اشیاء را به کلاسی دیگر ببریم.برای مثال یک سبدخرید را درنظر بگیرید که می‌توانیم آیتم‌های (Element) مختلفی را به آن اضافه کنیم. وقتی روی گزینه‌ی پرداخت کلیک می‌کنیم، هزینه‌ی نهایی محاسبه می‌شود.حالا می‌توانیم این منطق محاسبه‌ی هزینه را در کلاس‌های مربوط به هر آیتم قرار دهیم یا آن را با استفاده از دیزاین پترن Visitor به کلاس دیگری ببریم.Interpreterاز این Design Pattern برای نمایش گرامری یک زبان استفاده می‌شود. این پترن یک مفسر برای کار با این گرامر ارائه می‌کند.بهترین مثال برای دیزاین پترن Interpreter کامپایلر جاواست که سورس کد جاوا را به بایت کد قابل فهم برای JVM تبدیل می‌کند.گوگل ترنسلیتور نیز مثالی دیگر از یک الگوی Interpreter است که در آن ورودی می‌تواند به هر زبانی باشد و ما خروجی ترجمه‌شده به زبانی دیگر را تحویل می‌گیریم.Iteratorاین پترن نیز یک Design Pattern رفتاری است و زمانی از آن استفاده می‌شود که به دنبال روشی استاندارد برای گشتن در میان گروهی از اشیاء هستیم.از دیزاین پترن Iterator در Java Collection Framework بسیار استفاده شده است. واسط Iterator در این فریمورک، متدهایی را برای گشتن در میان یک Collection ارائه می‌کند.از دیزاین پترن Iterator فقط به منظور گشتن در مجموعه‌ها استفاده نمی‌شود. می‌توانیم بر اساس نیازمان انواع مختلفی از Iteratorها را داشته باشیم.این دیزاین پترن پیاده‌سازی اصلی را پنهان می‌کند و برنامه‌های کلاینت تنها از متدهای Iterator استفاده می‌کنند.Mementoوقتی به‌منظور استفاده‌های بعدی، قصد ذخیره‌ی State یک شئ را داریم، از این Design Pattern استفاده می‌کنیم.دیزاین پترن Memento به گونه‌ای این کار را انجام می‌دهد که داده‌ی State شئ از بیرون قابل دسترسی نباشد. این کار صحت داده‌ی استیتِ ذخیره‌شده را حفظ می‌کند.دیزاین پترن Memento با استفاده از دو شئ پیاده‌سازی می‌شود، Originator و Caretaker. می‌خواهیم استیت شئ Originator را ذخیره و بعداً از آن استفاده کنیم.این شئ از یک کلاس داخلی (Inner) برای ذخیره‌ی استیت شئ استفاده می‌کند. این کلاس داخلی Private است و اشیاء دیگر نمی‌توانند به آن دسترسی داشته باشند. دیزاین پتزن های متفرقهDesign Patternهای بسیاری هستند که جزو هیچ‌کدام از دیزاین‌ پترن‌های گروه GoF نیستند. در ادامه، برخی از این دیزاین پترن‌های معروف را با هم بررسی می‌کنیم.DAOاز این Design Pattern برای بردن منطق ماندگاری داده (Data Persistence Logic) به یک لایه‌ی دیگر استفاده می‌شود.وقتی می‌خواهیم سیستمی طراحی کنیم که با پایگاه داده کار می‌کند، دیزاین پترن DAO بسیار مفید است. ایده‌ی اصلی این است که لایه‌ی Service را از لایه‌ی Data Access جدا کنیم.در این صورت منطق‌های برنامه را از هم جدا کرده‌ایم.Dependency Injectionبا این Design Pattern می‌توانیم Dependencyهای استاتیک (Hard-Coded) را حذف کنیم و اپلیکیشنی Loosely Coupled، قابل توسعه و با هزینه‌ی نگداری پایین داشته باشیم.می‌توانیم با پیاده‌سازی Dependency Injection در جاوا Dependencyها را از زمان کامپایل، به زمان اجرا ببریم. فریمورک Spring بر مبنای اصل Dependency Injection بنا شده است.MVCاین پترن یکی از قدیمی‌ترین معماری‌های موجود برای ساخت اپلیکیشن‌های تحت وب است. کلمه‌ی MVC مخفف سه کلمه‌ی Model، View و Controller است.معرفی Design Patternهای جاوا در اینجا به پایان می‌رسد. در این مقاله تنها هدفمان معرفی ساده‌ و ارائه‌ی لیستی کامل از آنها بود.