توضیح دوات: برای راهنمایی و اطلاعات بیشتر در مورد نمودار کلاس به این مقاله مراجعه کنید.
الگوی طراحی Adapter یا مبدل تلاش میکند تفاوتهای بین دو رابط یا کلاس ناسازگار را برطرف کند. در این فرآیند، یکی از کلاسها را در لایهای قرار میدهد که امکان ارتباط با دیگری را فراهم میکند.
برای مشاهده کد نمونه این پست، میتوانید به GitHub مراجعه کنید.
درک مسئله
فرض کنید قصد داریم یک اپلیکیشن نظارت بر بازار سهام ایجاد کنیم. این اپلیکیشن دادههای مالی را از چندین منبع در قالب XML دریافت میکند، آنها را پردازش کرده و سپس در قالب نمودارها و دیاگرامهای زیبا به کاربر نمایش میدهد.
در نقطهای از پروژه تصمیم میگیریم اپلیکیشن را با اضافه کردن یک کتابخانه تصویری شخص ثالث ارتقا دهیم. اما مشکلی وجود دارد: این کتابخانه فقط با دادههایی در فرمت JSON کار میکند.
میتوانیم کتابخانه را تغییر دهیم تا با XML کار کند. اما این کار ممکن است باعث خرابی کد موجود شود که به این کتابخانه متکی است. علاوه بر این، با وجود متنباز بودن کتابخانه، دریافت نسخهای از آن و تغییر کد برای پشتیبانی از XML کاری بیهوده است (هرچند این کار را قبلاً هم دیدهام که انجام دادهاند…).
کاری که میتوانیم انجام دهیم، ایجاد یک Adapter است. Adapter یک شیء ویژه است که رابط یک شیء را طوری تبدیل میکند که شیء دیگری بتواند با آن ارتباط برقرار کند.
Adapter یکی از اشیا را در بر میگیرد تا پیچیدگی تبدیل را مخفی کند. شیء دربرگرفته حتی از وجود Adapter آگاه نیست. بهعنوان مثال، میتوانیم یک شیء که با واحد گرم و درجه سانتیگراد کار میکند را در Adapterی قرار دهیم که تمام دادهها را به واحدهایی مانند پوند و درجه فارنهایت تبدیل میکند.
Adapterها نه تنها میتوانند دادهها را به فرمتهای مختلف تبدیل کنند، بلکه میتوانند به اشیایی با رابطهای مختلف کمک کنند تا با یکدیگر همکاری کنند.
برگردیم به اپلیکیشن بازار سهام. برای حل مشکل فرمتهای ناسازگار، میتوانیم برای هر کلاس از کتابخانه تحلیلی که کد ما مستقیماً با آن کار میکند، یک Adapter XML-to-JSON ایجاد کنیم. سپس باید کد خود را طوری تنظیم کنیم که فقط از طریق این Adapterها با کتابخانه ارتباط برقرار کند. وقتی یک Adapter یک درخواست دریافت میکند، دادههای XML ورودی را به یک شیء JSON تبدیل کرده و درخواست را به متدهای مناسب شیء تحلیلی ارسال میکند.
ساختار الگوی Adapter
الگوی Adapter به دو نوع تقسیم میشود: Adapter شیء و Adapter کلاس.
Adapter شیء
در این پیادهسازی، از اصل ترکیب اشیاء استفاده میشود: Adapter رابط یک شیء را پیادهسازی کرده و دیگری را در بر میگیرد.
این پیادهسازی شامل چهار شرکتکننده است:
- Client: کلاس Client حاوی منطق تجاری موجود برنامه است. کد Client تا زمانی که با Adapter از طریق رابط Client ارتباط برقرار میکند، به کلاس Adapter وابسته نمیشود. به لطف این ویژگی، میتوانیم انواع جدیدی از Adapterها را به برنامه اضافه کنیم بدون اینکه کد Client موجود خراب شود. این ویژگی زمانی مفید است که رابط کلاس سرویس تغییر کند یا جایگزین شود. میتوانیم بدون تغییر کد Client، یک کلاس Adapter جدید ایجاد کنیم.
- Client Interface: رابط Client پروتکلی را توصیف میکند که سایر کلاسها برای همکاری با کد Client باید از آن پیروی کنند.
- Service: سرویس یک کلاس است (معمولاً از نوع شخص ثالث یا قدیمی) که Client نمیتواند مستقیماً از آن استفاده کند، زیرا رابط ناسازگار دارد.
- Adapter: Adapter کلاسی است که با Client و سرویس کار میکند: رابط Client را پیادهسازی میکند و در عین حال شیء سرویس را در بر میگیرد. Adapter تماسهای Client را از طریق رابط Adapter دریافت کرده و آنها را به قالب قابل درک برای شیء سرویس ارسال میکند.
Adapter کلاس
در این پیادهسازی از وراثت استفاده میشود: Adapter رابطهای هر دو شیء را به طور همزمان به ارث میبرد. توجه داشته باشید که این روش تنها در زبانهایی قابل پیادهسازی است که از چند وراثتی پشتیبانی میکنند.
در این پیادهسازی، Adapter کلاس هیچ شیئی را در بر نمیگیرد. رفتارها را از Client و سرویس به ارث میبرد و تطبیق در متدهای بازنویسی شده انجام میشود. Adapter حاصل میتواند به جای کلاس Client موجود استفاده شود.
برای نمایش عملکرد الگوی Adapter، پایگاه دادهای برای دمای مناسب پخت گوشت ایجاد خواهیم کرد.
در این مثال، یک سیستم قدیمی داریم که دادههای دما را ذخیره میکند. این سیستم قدیمی توسط MeatsDatabase نشان داده میشود و نقش سرویس ما را ایفا خواهد کرد. چنین سیستمی ممکن است به شکل زیر باشد:
namespace Adapter.Legacy
{
public enum TemperatureType
{
Fahrenheit,
Celsius
}
///
/// The legacy API must be converted to the new structure
///
public class MeatsDatabase
{
public float GetSafeCookingTemperature(string meat)
{
return meat.ToLower() switch
{
"beef" or "pork" => 145f,
"chicken" or "turkey" => 165f,
_ => 165f,
};
}
public int GetCaloriesPerOunce(string meat)
{
return meat.ToLower() switch
{
"beef" => 71,
"pork" => 69,
"chicken" => 66,
"turkey" => 38,
_ => 0,
};
}
public double GetProteinPerOunce(string meat)
{
return meat.ToLower() switch
{
"beef" => 7.33f,
"pork" => 7.67f,
"chicken" => 8.57f,
"turkey" => 8.5f,
_ => 0f,
};
}
}
}
حالا کلاس Meats
را بنویسیم
namespace Adapter
{
///
/// The new Meats class, which represents details
/// about a particular kind of meat.
///
public class Meats
{
protected string MeatName;
protected double SafeCookingTemperatureFahrenheit;
protected double SafeCookingTemperatureCelsius;
protected double CaloriesPerOunce;
protected double CaloriesPerGram;
protected double ProteinPerOunce;
protected double ProteinPerGram;
public Meats(string meatName)
{
this.MeatName = meatName;
}
public virtual void LoadData()
{
Console.WriteLine($"\nMeat: {MeatName} ------");
}
}
}
مشکل اینجاست که نمیتوانیم API قدیمی را که همان کلاس MeatDatabase است، تغییر دهیم. در اینجا، نقش Adapter مشخص میشود: به کلاسی نیاز داریم که از Meat
ارثبری کند اما یک مرجع به API نگه دارد تا دادههای API بتوانند در یک نمونه از کلاس Meat
بارگذاری شوند.
using Adapter.Legacy;
namespace Adapter
{
///
/// The Adapter class, which wraps the Meats class and
/// initializes that class's values.
///
public class MeatDetails : Meats
{
private MeatsDatabase meatsDatabase;
public MeatDetails(string name) : base(name)
{
}
public override void LoadData()
{
meatsDatabase = new MeatsDatabase();
SafeCookingTemperatureFahrenheit = meatsDatabase.GetSafeCookingTemperature(MeatName);
SafeCookingTemperatureCelsius = FahrenheitToCelsius(SafeCookingTemperatureFahrenheit);
CaloriesPerOunce = meatsDatabase.GetCaloriesPerOunce(MeatName);
CaloriesPerGram = PoundsToGrams(CaloriesPerOunce);
ProteinPerOunce = meatsDatabase.GetProteinPerOunce(MeatName);
ProteinPerGram = PoundsToGrams(ProteinPerOunce);
base.LoadData();
Console.WriteLine($" Safe Cooking Temperature (Fahrenheit): {SafeCookingTemperatureFahrenheit}");
Console.WriteLine($" Safe Cooking Temperature (Celcius): {SafeCookingTemperatureCelsius}");
Console.WriteLine($" Calories per Ounce: {CaloriesPerOunce}");
Console.WriteLine($" Calories per Gram: {CaloriesPerGram}");
Console.WriteLine($" Protein per Ounce: {ProteinPerOunce}");
Console.WriteLine($" Protein per Gram: {ProteinPerGram}");
}
private double FahrenheitToCelsius(double fahrenheit)
{
return (fahrenheit - 32) * 0.55555;
}
private double PoundsToGrams(double pounds)
{
return pounds * 0.0283 / 1000;
}
}
}
در نهایت، در متد Main()
خود میتوانیم تفاوت بین استفاده از کلاس قدیمی بهتنهایی و استفاده از کلاس Adapter را نشان دهیم:
using Adapter;
// Non-adapted
Meats unknown = new Meats("Beef");
unknown.LoadData();
// Adapted
MeatDetails beef = new MeatDetails("Beef");
beef.LoadData();
MeatDetails chicken = new MeatDetails("Chicken");
chicken.LoadData();
MeatDetails turkey = new MeatDetails("Turkey");
turkey.LoadData();
مزایا و معایب الگوی طراحی Adapter
مزایا
- میتوانیم کد تبدیل داده یا رابط را از منطق اصلی برنامه جدا کنیم و به این ترتیب اصل مسئولیت واحد را رعایت کنیم.
- میتوانیم انواع جدیدی از Adapterها را به برنامه اضافه کنیم بدون اینکه کد Client موجود خراب شود، به شرطی که آنها از طریق رابط Client با Adapterها کار کنند و به این ترتیب اصل باز/بسته را رعایت کنیم.
معایب
- پیچیدگی کلی کد افزایش مییابد، زیرا نیاز به معرفی مجموعهای از رابطها و کلاسهای جدید داریم. گاهی اوقات تغییر کلاس سرویس برای تطبیق با کد موجود سادهتر است.
روابط با سایر الگوها
- Bridge معمولاً از ابتدا طراحی میشود و به ما امکان میدهد بخشهای مختلف یک برنامه را به صورت مستقل توسعه دهیم. از سوی دیگر، Adapter معمولاً در برنامههای موجود استفاده میشود تا برخی کلاسهای ناسازگار را با یکدیگر هماهنگ کند.
- Adapter رابط یک شیء موجود را تغییر میدهد، در حالی که Decorator یک شیء را بدون تغییر رابط آن ارتقا میدهد. علاوه بر این، Decorator از ترکیب بازگشتی پشتیبانی میکند، که در استفاده از Adapter امکانپذیر نیست.
- Adapter یک رابط متفاوت برای شیء دربرگرفته ارائه میدهد، Proxy همان رابط را ارائه میکند، و Decorator یک رابط پیشرفته ارائه میدهد.
- Bridge، State، Strategy و تا حدی Adapter ساختارهای بسیار مشابهی دارند. در واقع، همه این الگوها بر اساس ترکیب بنا شدهاند که کار را به اشیاء دیگر واگذار میکند. با این حال، هر یک از این الگوها مشکلات متفاوتی را حل میکنند. یک الگو فقط دستورالعملی برای ساختاردهی کد به شیوهای خاص نیست.
نتیجهگیری
در این مقاله، درباره الگوی Adapter صحبت کردیم، زمان مناسب برای استفاده از آن و مزایا و معایب این الگو را بررسی کردیم. سپس به بررسی Adapter کلاس و Adapter شیء پرداختیم و ارتباط الگوی Adapter را با سایر الگوهای کلاسیک طراحی بررسی کردیم.
شایان ذکر است که الگوی Adapter، همراه با سایر الگوهای طراحی معرفیشده توسط Gang of Four، راهحل قطعی یا نهایی برای طراحی یک برنامه نیست. در نهایت این وظیفه مهندسان است که تعیین کنند چه زمانی از یک الگوی خاص استفاده کنند. در واقع، این الگوها زمانی مفید هستند که بهعنوان ابزاری دقیق استفاده شوند، نه بهعنوان یک ابزار سنگین و همهکاره.