الگوهای طراحی، انواع مختلفی دارند که شامل رایجترین الگوها مانند پروتوتایپ، Factory و … میشوند. یکی دیگر از الگوهای شناختهشده طراحی نرمافزار، الگوی طراحی Adapter است. در این مقاله قصد داریم با مثال و قطعه کد، این الگو را بررسی کنیم.
الگوی طراحی آداپتور یا تبدیلکننده (Adapter) چیست؟
همانطور که از اسم این الگو مشخص است، زمانی که دو کلاس واسطهای غیرمرتبط و ناهمگون داشته باشند، این الگو واسط یکی را به دیگری تبدیل میکند که بتوانند با یکدیگر ارتباط برقرار کنند.
از این الگو که یک الگوی ساختاری است، زمانی استفاده میشود که در یک برنامه، بخواهیم دو کلاس غیرمرتبط با یکدیگر کار کنند. از این الگو در برنامههایی با کلاسهای آماده و یا از پیش نوشته شده، که طراحان نرمافزار اجازه تغییر در این کلاسها را نداده باشند، استفاده میشود. بسیاری از مثالهای الگوی Adapter درگیر ورودی و خروجی هستند؛ زیرا این دامنهای است که همیشه تغییر میکند.
این الگو مفهوم آداپتور را در دنیای واقعی شبیهسازی کردهاست. در واقعیت زمانی که دو موجودیت ناسازگار داریم که نمیتوانند به صورت مستقیم با یکدیگر کار کنند، یک آداپتور یا تبدیلکننده بین آنها قرار میدهیم که این دو موجودیت باهم سازگار شوند.
در واقع پلی بین دو موجودیت ناسازگار ایجاد میکنیم که آن دو موجودیت بتوانند باهم کار کنند. به عنوان مثال ما نمیتوانیم لپ تاپ یا دوربین عکاسی را مستقیما به برق وصل کنیم و از یک تبدیل استفاده می کنیم. الگوی adapter دقیقا از دنیای واقعی مفهوم گرفتهاست.
هدف این الگوی طراحی، تبدیل واسط موجود (جدید) به واسط مورد انتظار سیستم است و زمانی کاربرد دارد که واسط موجود قابل تغییر نباشد و امکان تغییر کد نیز وجود نداشته باشد. با کمک این الگو، امکان همکاری بین اشیایی که قبلا بخاطر داشتن رابط ناسازگار نمیتوانستند با هم کار کنند، فراهم میشود.
مثالی از استفاده Adapter Design Pattern
برای مثال، برنامههایی که در دهه ۸۰ میلادی نوشته شدهاند دارای UI بسیار ضعیفتری نسبت به برنامههای نوشته شده در قرن ۲۰ و ۲۱ میلادی هستند. اگر بازنویسی برنامههای قبلی بهصرفه نباشد و بخواهیم این برنامهها با سختافزارهای جدید همخوانی و سازگاری داشته باشند، باید برنامهای جدید طراحی کنیم.
این برنامه باید نقش واسط برنامه قدیمی و سختافزارهای جدید باشد و بین این دو برنامه ارتباط برقرار کند. به برنامهای که بین این دو برنامه قرار میگیرد، Adapter میگوییم.
ضرورت استفاده از الگوی Adapter
شاید اینطور به نظر برسد که استفاده از Adapter ضروری نیست و میتوانیم با تغییر واسط یکی از کلاسها، برنامه را اجرا کنیم. اما دقت کنید؛ این کار زمانی ممکن است که بتوانیم به کد کلاسها دسترسی داشته باشیم و تغییر کلاسها باعث به وجود آمدن مشکل در برنامه نشود و پیچیدگی برنامه را افزایش ندهد. این کار در اکثر مواقع بهصرفه و عملی نیست.
الگوی Adapter الگویی است كه در دنیای واقعی نمونههای زیادی از آن وجود دارد و به همین خاطر درك این الگو زیاد مشكل نخواهد بود.
چه زمانی از Adapter استفاده میکنیم؟
دانستن اینکه Adapter چیست، کافی نیست؛ باید بدانیم چه زمانی و در چه شرایطی این الگو بهترین انتخاب است. این بخش به شما کمک میکند کاربردهای واقعی آن را درک کنید.
به طور کلی استفاده از Adapter زمانی مفید است که دو کلاس یا سیستم ناسازگار داریم و نمیخواهیم یا نمیتوانیم کدهای آنها را تغییر دهیم. این الگو مثل پلی بین سیستمهای قدیمی و مدرن عمل میکند و ارتباط بین آنها را برقرار میکند. بنابراین:
- زمانی که میخواهید از کلاسی استفاده کنید، اما رابط آن با آنچه نیاز دارید منطبق نیست.
 - وقتی که دسترسی به کد کلاس اصلی ندارید و نمیتوانید آن را تغییر دهید.
 - هنگام طراحی سیستمی قابلگسترش که باید با کدهای قدیمی یا کتابخانههای ناسازگار کار کند.
 - وقتی نیاز دارید بدون تغییر کلاسهای اصلی، یک لایه میانی برای سازگاری بین آنها ایجاد کنید.
 
از Adapter استفاده میکنیم.
ویژگیها و اجزای الگوی Adapter
برای درک عمیقتر الگوهای طراحی، آشنایی با اجزای تشکیلدهندهی آنها و نحوه تعامل آنها بسیار ضروری است. در این بخش ساختار کلاسها و نقش هرکدام از آنها را بررسی میکنیم.
Class Diagram
در شکل زیر، کلاس دیاگرام مربوط به این الگو را میبینید و در ادامه هرکدام از اجزای الگو معرفی شدهاند.
بنابر گفته GoF هدف از الگوی Adapter عبارت است از:
تبدیل واسط یک کلاس به واسط دیگری که کاربر توقع دارد. Adapter امکان همکاری بین چند کلاس را که به علت ناهمگونی واسطههایشان به روش دیگری نمیتوانند باهم کار کنند، فراهم میکند.

شکل ۱: کلاس دیاگرام الگوی Adapter
نقش کلاسها
- Target:
 
ارائه واسط که کلاینت از آن استفاده میکند.
- Adapter:
 
وفق دادن Interface Adaptee به Target Interface
- Adaptee:
 
کلاسی با واسط ناهماهنگ که میخواهیم در برنامه از آن استفاده کنیم.
- Client:
 
ارتباط با شی Target Interface
سایر ویژگیهای کلیدی الگوی طراحی Adapter
در جدول زیر به صورت خلاصه هدف الگو، نام یا نامهای دیگری که الگو با آن شناخته میشود، مواقعی که میتوان از این الگو استفاده کرد، نتایج استفاده از این الگو و الگوهای مرتبط به این الگو توضیح داده شده است.
جدول ۱: ویژگیهای Adapter
| 
 عنوان  | 
 شرح  | 
| 
 نام  | 
 Adapter Pattern  | 
| 
 هدف  | 
 واسط یک کلاس را به واسط دیگری که مورد انتظار یک مشتری است تبدیل میکند. این الگو امکان همکاری بین اشیایی را که قبلا بهخاطر داشتن واسطهای ناسازگار نمیتوانستند با هم کار کنند، فراهم میکند.  | 
| 
 نامهای دیگر  | 
 این الگو با نام Wrapper نیز شناخته می شوند.  | 
| 
 کاربرد  | 
 زمانی از الگوی Adapter استفاده کنید که : 
  | 
| 
 نتایج  | 
  | 
| 
 الگوهای مرتبط  | 
 Factory Method: در تطبیقدهندههای شی بهتر است نمونهسازی از Adaptee را به طور مستقیم انجام ندهید و به جای آن تکنیکی مانند Factory Method را به کار ببرید.  | 
یک مثال برای الگوی طراحی Adapter
در زیر مثالی از این الگو پیادهسازی شدهاست.
مساله: در یک آزمایشگاه شیمی قصد داریم که ویژگیهایی مانند ساختار، وزن، نقطه جوش و انجماد مربوط به آب، بنزن و الکل را به دانشجویان این رشته نشان دهیم. (قابل ذکر است، از قبل سرویسی برای بیان ویژگیهای مواد تعریف شده که با واسط برنامه ما همخوانی ندارد و ما باید ارتباط بین این دو را برقرار کنیم).
راهحل: به دلیل تفاوت در واسطی که از قبل برای ویژگیهای مواد پیادهسازی شده است، با واسط مربوط به نمایش برای کاربر، از الگوی Adapter استفاده میکنیم.
Compound: طبق کلاس دیاگرام الگو نقش کلاس Target را دارد، که در آن متغیرهایی برای نام و ویژگیهای مواد تعریف شده و دارای یک متد Virtual به نام Display است که در آن نام ماده مربوطه را نمایش میدهد.
ChemicalDatabank: نقش کلاس Adaptee را دارد. کلاسی با واسط ناهماهنگ، که میخواهیم در برنامه خود از آن استفاده کنیم. این کلاس دارای سه متد است که نقطه جوش، انجماد، ساختار و وزن مواد را برمیگردانند.
RichCompound: نقش کلاس Adapter را دارد که کلاس Compound را به ارث میبرد و در متد Display، یک نمونه از کلاس ChemicalDatabank ایجاد کرده و متدهای آن را فراخوانی میکند.
پیادهسازی:
| 
					 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82  | 
						namespace Chemical_Adapter {    class Program {       static void Main(string[] args)  {          Compound water = new RichCompound(“Water”);           water.Display();           Compound benzene = new RichCompound(“Benzene”);            benzene.Display();           Compound ethanol = new RichCompound(“Ethanol”);            ethanol.Display();            Console.ReadKey(); } }  } class Compound     {         protected string _chemical;         protected float _boilingPoint;         protected float _meltingPoint;         protected double _molecularWeight;         protected string _molecularFormula;         public Compound(string chemical) {           this._chemical = chemical; }         public virtual void Display() {            Console.WriteLine(“\nCompound: {0} —— “, _chemical);   }  } class RichCompound : Compound  {         private ChemicalDatabank _bank;         public RichCompound(string name)             : base(name) {  }         public override void Display()              // The Adaptee             _bank = new ChemicalDatabank();             _boilingPoint = _bank.GetCriticalPoint(_chemical, “B”);             _meltingPoint = _bank.GetCriticalPoint(_chemical, “M”);             _molecularWeight = _bank.GetMolecularWeight(_chemical);             _molecularFormula = _bank.GetMolecularStructure(_chemical);             base.Display();             Console.WriteLine(” Formula: {0}”, _molecularFormula);             Console.WriteLine(” Weight : {0}”, _molecularWeight);             Console.WriteLine(” Melting Pt: {0}”, _meltingPoint);             Console.WriteLine(” Boiling Pt: {0}”, _boilingPoint); }  } class ChemicalDatabank {     public float GetCriticalPoint(string compound, string point) {          // Melting Point          if (point == “M”)  {               switch (compound.ToLower()) {                     case “water”: return 0.0f;                     case “benzene”: return 5.5f;                     case “ethanol”: return –114.1f;                     default: return 0f; }   }             // Boiling Point            Else {                 switch (compound.ToLower()) {                     case “water”: return 100.0f;                     case “benzene”: return 80.1f;                     case “ethanol”: return 78.3f;                     default: return 0f; }  } }         public string GetMolecularStructure(string compound) {             switch (compound.ToLower())  {                 case “water”: return “H20”;                 case “benzene”: return “C6H6”;                 case “ethanol”: return “C2H5OH”;                 default: return “”;  } }         public double GetMolecularWeight(string compound) {             switch (compound.ToLower()) {                 case “water”: return 18.015;                 case “benzene”: return 78.1134;                 case “ethanol”: return 46.0688;                 default: return 0d;  } } }  | 
					
خروجی :

تفاوت Adapter Pattern، Facade و Bridge
در نگاه اول، این سه الگو ممکن است شبیه به هم بهنظر برسند، اما کاربردها و اهدافشان متفاوت است:
| ویژگی | Adapter | Facade | Bridge | 
|---|---|---|---|
| هدف | تطبیق رابط ناسازگار | سادهسازی یک سیستم پیچیده | جداسازی abstraction از implementation | 
| سطح استفاده | معماری و استفاده مجدد | سادهسازی API | طراحی معماری منعطف | 
| تغییر در ساختار کلاسها | ندارد | ندارد | دارد | 
| مثال واقعی | مبدل دوشاخه برق | پنل سادهای برای یک سیستم بزرگ | پلتفرم جدا از UI | 
جمعبندی
همانطور که در این مقاله گفتیم، الگوی آداپتور تا حد زیادی به کاربرد آداپتور در دنیای واقعی شباهت دارد. در واقع در الگوی طراحی Adapter، ما به دنبال نوشتن یک ساختار و برنامه واسط، برای ایجاد ارتباط بین دو ساختار و رابط ناهمگون هستیم.
به طور معمول از این الگوی طراحی زمانی استفاده میکنیم که امکان ایجاد تغییر در برنامهها و واسطهای فعلی را نداریم.
منابع:
oodesign.com
codeproject.com
dofactory.com
سوالات متداول
Object Adapter از ترکیب (composition) استفاده میکند، در حالیکه Class Adapter بر پایه وراثت (inheritance) ساخته میشود. در Object Adapter میتوان بهراحتی از چند کلاس Adaptee مختلف استفاده کرد، اما Class Adapter معمولا فقط یک کلاس را به ارث میبرد و در زبانهایی که چندارثی ندارند (مثل Java یا C#)، محدودتر است.
Adapter برای سازگار کردن دو رابط ناسازگار طراحی شده اما Facade برای سادهسازی رابط کاربری یک سیستم پیچیده استفاده میشود. Adapter به کارکرد جزئی یک کلاس تمرکز دارد، ولی Facade کل سیستم را پنهان میکند تا استفاده از آن آسانتر شود.
بله. یکی از رایجترین کاربردهای Adapter، سازگار کردن کدهای قدیمی با ساختارهای جدید است. بهجای تغییر در کد اصلی که ممکن است پرریسک باشد، با ساخت یک Adapter میتوانید رابط آن را به فرم دلخواه تبدیل کنید.
در تمام زبانهای شیگرا مثل Java، C#، Python و ++C میتوان Adapter را پیادهسازی کرد. در زبانهایی که از وراثت چندگانه پشتیبانی میکنند (مثل C++)، پیادهسازی کلاس Adapter آسانتر است. اما در زبانهایی که فقط ترکیب دارند (مثل Java)، معمولاً از Object Adapter استفاده میشود.
بله، استفادهی نابجا از Adapter ممکن است باعث افزایش سطح انتزاع و کاهش خوانایی شود. توصیه میشود فقط زمانی از Adapter استفاده کنید که واقعا نیاز به سازگار کردن واسطها دارید و تغییر مستقیم کد امکانپذیر یا بهصرفه نیست.
بله، هیچ محدودیتی وجود ندارد. اگر با چند کلاس ناسازگار مختلف روبهرو هستید، میتوانید برای هر مورد یک Adapter متفاوت پیادهسازی کنید، یا یک Adapter عمومی بسازید که چندین مورد را پوشش دهد، بسته به نیاز سیستم.


دیدگاهتان را بنویسید