میتونین برای آشنایی با الگوهای طراحی (یا همون دیزاین پترن های) زبان جاوا به مقاله ی یک فنجان جاوا - دیزاین پترن ها Design Patterns مراجعه کنین.
(همونطور که گفته شده این الگو زیرمجموعه ی الگوهای ساختاری (Structural) هست)
بذارین با اسم این الگو شروع کنیم. آداپتور! همونجوری که توی واقعیت عمل میکنه، اینجا هم یه آداپتور به دو رابط ناسازگار اجازه میده تا بتونن با هم کار کنن. این یه تعریف کلی از مفهوم آداپتوره. ممکنه رابط ها ناسازگار باشن ولی قابلیت درونی آنها باید سازگار با نیاز باشن. به عبارت دیگه، الگوی طراحی آداپتور، به کلاسای ناسازگار اجازه میده تا بتونن از قابلیتهای همدیگه استفاده کنن. چیزی که در ساخت آداپتور مهمه اینه که لازم نیست رفتار شئ رو تغییر بدیم و تنها چیزی که لازمه اینه که آداپتوری ایجاد کنیم که بتونه با کلاسایی که باید، ارتباط برقرار کنه. بصورت خلاصه میشه گفت:
الگوی آداپتور، مبدلیه بین دو کلاس یا شی!
طبیعتاً این الگو نه تنها امکان استفادهٔ مجدد از اعمال رو به هر صورت که بخوایم بهمون میده، بلکه از بازنویسی کدای مربوط به اعمال خاص، که قصد استفاده از اونا رو در مکان دیگری داریم هم جلوگیری میکنه.
خب حالا که با مفهوم این الگو آشنا شدیم، فرض کنیم میخوایم به نرم افزار ساده برای بانک بنویسیم.
با کلاسی به اسم BankDetails شروع میکنیم که اطلاعات حساب یه شخص به همراه شماره حساب و نامش رو ذخیره میکنه:
public class BankDetails { private String bankName; private String accHolderName; private long accNumber; public String getBankName() { return bankName; } public void setBankName(String bankName) { this.bankName = bankName; } public String getAccHolderName() { return accHolderName; } public void setAccHolderName(String accHolderName) { this.accHolderName = accHolderName; } public long getAccNumber() { return accNumber; } public void setAccNumber(long accNumber) { this.accNumber = accNumber; } }
حالا میایم به اینتر فیس به اسم CreditCard بصورت زیر مینویسیم:
public interface CreditCard { public void generateBankDetails(); public String getCreditCard(); }
همونطور که میبینیم این اینترفیس شامل تابع generateBankDetails که باید جزییات بانک رو بسازه، و getCreditCard که جزییات کارت اعتباری رو برگردونه میشه.
کلاس بعدی ای که مینویسیم BankCustomer هست که اطلاعات بانکی و کارت اعتباری (Credit Card) رو نگه میداره:
public class BankCustomer extends BankDetails implements CreditCard { @Override public void generateBankDetails() { setAccHolderName("CodeGate"); setAccNumber(1234567); setBankName("CG "); } @Override public String getCreditCard() { long accno = getAccNumber(); String accholdername = getAccHolderName(); String bname = getBankName(); return ("The Account number " + accno + " of " + accholdername + " in " + bname + "bank is valid and authenticated for issuing the credit card. "); } }
همونطور که میبینیم این کلاس از BankDetails ارث برده و اینترفیس CreditCard رو پیاده سازی کرده.
و تمام! ما یه مثال ساده از الگوی Adapter رو پیاده سازی کردیم. برای تست خروجی، کدهای زیر رو مینویسیم:
CreditCard targetInterface = new BankCustomer(); targetInterface.generateBankDetails(); System.out.print(targetInterface.getCreditCard());
اتفاقی که اینجا افتاده به این صورته که کلاس BankCustomer از کلاس BankDetails ارث میبره و اینترفیس CreditCard رو پیاده میکنه. در حقیقت توی این مثال ما یه کلاس مشتری میخواستیم که اسمشو BankCustomer گذاشتیم، ولی این کلاس در عین اینکه کاملاً به کلاس BankDetails وابستس، باید ازش مجزا باشه، به همین خاطر اینترفیس CreditCard رو نوشتیم.
بذارین برای اینکه این مفهوم بهتر جا بیفته، یه مثالی بزنیم که هر دو نوع وابستگی این الگو (که بر اساس شئ یا بر اساس کلاس هست) رو شامل بشه.
میخوایم دیاگرام زیر رو پیاده سازی بکنیم:
میخوایم برنامه ای برای آداپتورای توی دنیای واقعی بنویسیم. با کلاس ولتاژ شروع میکنیم:
public class Volt { private int volts; public Volt(int v){ this.volts=v; } public int getVolts() { return volts; } public void setVolts(int volts) { this.volts = volts; } }
و کلاسی به اسم Socket که طبیعتاً هر سوکتی لتاژ خودش رو داره (و بصورت پیشفرض ولتاژ 120 رو در نظر میگیریم):
public class Socket { public Volt getVolt(){ return new Volt(120); } }
و یه اینترفیس یا همون آداپتور به اسم SocketAdapter تعریف میکنیم:
public interface SocketAdapter { public Volt get120Volt(); public Volt get12Volt(); public Volt get3Volt(); }
تا اینجا زیرساخت پیاده سازی الگوی Adapter رو نوشتیم. اینجا دو راه جلومونه، یکی پیاده سازی بر اساس شئ و یکی پیاده سازی بر اساس کلاس!
اول ببینیم اگه بخوایم بر اساس کلاس پیاده سازی کنیم چه ساختاری باید داشته باشیم:
public class SocketClassAdapterImpl extends Socket implements SocketAdapter{ @Override public Volt get120Volt() { return getVolt(); } @Override public Volt get12Volt() { Volt v= getVolt(); return convertVolt(v,10); } @Override public Volt get3Volt() { Volt v= getVolt(); return convertVolt(v,40); } private Volt convertVolt(Volt v, int i) { return new Volt(v.getVolts()/i); } }
همونطور که میبینیم توی این حالت کلاسمون از Socket ارث برده و SocketAdapter رو پیاده سازی کرده.
و اگه بخوایم این ساختار بر اساس شئ پیاده سازی بشه به شکل زیر عمل میکنیم:
public class SocketObjectAdapterImpl implements SocketAdapter{ //Using Composition for adapter pattern private Socket sock = new Socket(); @Override public Volt get120Volt() { return sock.getVolt(); } @Override public Volt get12Volt() { Volt v= sock.getVolt(); return convertVolt(v,10); } @Override public Volt get3Volt() { Volt v= sock.getVolt(); return convertVolt(v,40); } private Volt convertVolt(Volt v, int i) { return new Volt(v.getVolts()/i); } }
همونطور که میبینیم این کلاس فقط اینترفیس SocketAdapter رو پیاده کرده.
در حقیقت بر خلاف روش قبلی، بجای ارث بردن از کلاس Socket، یه آبجکت از این کلاس رو نگداری میکنه (که عملاً اینجا تنها تفاوت دو روش کلاس محور و شئ محور همینه).
حالا کافیه برای تست تابعی به شکل زیر بنویسیم:
private static Volt getVolt(SocketAdapter sockAdapter, int i) { switch (i){ case 3: return sockAdapter.get3Volt(); case 12: return sockAdapter.get12Volt(); case 120: return sockAdapter.get120Volt(); default: return sockAdapter.get120Volt(); } }
برای تست حالت اول این قطعه کد رو بنویسیم:
SocketAdapter sockAdapter = new SocketClassAdapterImpl(); Volt v3 = getVolt(sockAdapter,3); Volt v12 = getVolt(sockAdapter,12); Volt v120 = getVolt(sockAdapter,120); System.out.println("v3 volts using Class Adapter="+v3.getVolts()); System.out.println("v12 volts using Class Adapter="+v12.getVolts()); System.out.println("v120 volts using Class Adapter="+v120.getVolts());
و برای تست حالت دوم این قطعه کد رو مینویسیم:
SocketAdapter sockAdapter = new SocketObjectAdapterImpl(); Volt v3 = getVolt(sockAdapter,3); Volt v12 = getVolt(sockAdapter,12); Volt v120 = getVolt(sockAdapter,120); System.out.println("v3 volts using Object Adapter="+v3.getVolts()); System.out.println("v12 volts using Object Adapter="+v12.getVolts()); System.out.println("v120 volts using Object Adapter="+v120.getVolts());
همون طور که میبینیم، چیزی که ما موقع استفاده میبینیم (نحوه ی تعریف شئ و صدا کردن توابع هر دو کلاس SocketObjectAdapterImpl و SocketClassAdapterImpl) دقیقاً یکیه. نتایج خروجی تست ها هم بصورت زیره:
v3 volts using Class Adapter=3 v12 volts using Class Adapter=12 v120 volts using Class Adapter=120 v3 volts using Object Adapter=3 v12 volts using Object Adapter=12 v120 volts using Object Adapter=120
به عنوان توضیح تکمیلی و برای مشخص تر شدن اینکه چه اتفاقی افتاد، میتونی بگیم که ما دو تا کلاس داشتیم، یکی ولتاژ و اون یکی سوکت. هر سوکت با اینکه کلاسش باید مجزا از کلاس ولتاژ میبود، خودش یه ولتاژ نیاز داشت. پس اومدیم از اینترفیس SocketAdapter استفاده کردیم تا به عنوان یجور واسطه بین این کلاس ها عمل کنه، و با توجه نوع پیاده سازی ساختارمون کلاس های SocketObjectAdapterImpl و SocketClassAdapterImpl رو پیاده سازی کردیم.
ساختار الگوی Adapter پیچیده نیست، فقط باید یذره تمرین کنیم تا به مرور زمان متوجه شیم که کی و کجا باید ازش استفاده بشه. این پترن تا حدودی شبیه پترن Bridge هست و ممکنه با دیدن این دو پترن کمی گیج بشین. باید دقت کنیم که از الگوی Adapter برای کار کردن با کلاس های ناهمسازگار استفاده میشه که قابلیت های یکسانی دارن، ولی اسم متدها یا نوع بازگشتیشون فرق داره. از طرفی، از الگوی Bridge زمانی استفاده میشه که نیاز داریم بین متدهای انتزاعی و پیاده سازی های مختلف یکی رو انتخاب کنیم، و هم متدهای انتزاعی و هم پیاده سازیِ اونا به صورت مستقل از هم بروزرسانی بشن.
منتشر شده در ویرگول توسط محمد قدسیان https://virgool.io/@mohammad.ghodsian
https://virgool.io/@mohammad.ghodsian/java-adapter-design-pattern-rtzy6rmmhwks