Mohammad Ghodsian
Mohammad Ghodsian
خواندن ۸ دقیقه·۶ سال پیش

یک فنجان جاوا - دیزاین پترن ها - Bridge

میتونین برای آشنایی با الگوهای طراحی (یا همون دیزاین پترن های) زبان جاوا به مقاله ی یک فنجان جاوا - دیزاین پترن ها Design Patterns مراجعه کنین.

(همونطور که گفته شده این الگو زیرمجموعه ی الگوهای ساختاری (Structural) هست)

الگوی Bridge

این الگو انتزاع را از پیاده سازی جدا می کند، بنابراین می توانند مستقل از هم باشند.

جمله ی بالا گفته ی گروه GOF هست که این الگو رو معرفی کردن. به عبارت دیگه این نوع الگو، از اون جایی که کلاسِ پیاده سازی شده رو جداسازی و کلاسی را با ارئه ی ساختارِ Bridge بین اونا abstract میکنه، به این نام معروف شده. در حقیقت وقتی که نیاز داریم انتزاع (Abstraction) رو از پیاده سازی (Implementation) مجزا کنیم، از Bridge استفاده میکنیم. بصورت کلی زمانی که کلاس ها کاری که انجام میدن نسبت به همدیگه تفاوتای زیادی داشته باشن، کلاس ها مدام در حال تغییر باشن، یا مواردی مشابه، از الگوی پل استفاده میکنیم. در این الگو خودِ کلاس به عنوان انتزاع در نظر گرفته و کاری که انجام میده در مرحله پیاده سازی مشخص میشه. همچنین الگوی پل میتونه به عنوان دو لایه از انتزاع در نظر گرفته بشه. این الگو از چهار قسمت اصلی تشکیل میشه:

  • کلاسی Abstract که با client سرو کار داره و ارجاع یک شی از نوع Implementor را نگهداری می کند.
  • کلاس RefinedAbstraction که کلاس Abstraction را توسعه میده تا چندین نسخه از متدهای abstract داشته باشه.
  • اینترفیس Implementor یا Bridge که مثل یه پل یا رابط بین کلاس های abstract و کلاس های معمولی عمل میکنه.
  • کلاس ConcreteImplementor که اینترفیس Implementor رو پیاده میکنه.

تا اینجا مفهوم کلی این الگو رو شناختیم. با یه مثال بیشتر با مطالب گفته شده آشنا میشیم:

اول کلاس DrawApi رو تعریف میکنیم

public interface DrawAPI { public void drawCircle(int radius, int x, int y); }

توی مرحله ی بعد کلاس های Concrete Implementor یا همون رابط هستن رو ببینیم (دو تا کلاس دایره سبز و دایره قرمز)

public class RedCircle implements DrawAPI { @Override public void drawCircle(int radius, int x, int y) { System.out.println("Drawing Circle[ color: red, radius: " + radius + ", x,y: " + x + ", " + y + "]"); } } public class BlueCircle implements DrawAPI { @Override public void drawCircle(int radius, int x, int y) { System.out.println("Drawing Circle[ color: blue, radius: " + radius + ", x,y: " + x + ", " + y + "]"); } }

حالا میایم یه کلاس انتزاعی به اسم Shape تعریف میکنیم (که داخلش یه رابط DrawAPI داره که در عمل یکی از کلاس های RedCircle یا BlueCircle هستن، و همچنین یه تابع draw که هر کلاسی از این کلاس ارث ببره باید پیاده سازیش بکنه)

public abstract class Shape { protected DrawAPI drawAPI; protected Shape(DrawAPI drawAPI){ this.drawAPI = drawAPI; } public abstract void draw(); }

و در نهایت هم کلاس Circle که نوعی Shape هست رو مینویسیم (که یه DrawAPI میده به پدرش و خودش هم x و y و radius رو نگه میداره و داخل تابع draw تابع drawCircle از DrawAPI رو صدا میزنه)

public class Circle extends Shape { private int x, y, radius; public Circle(int x, int y, int radius, DrawAPI drawAPI) { super(drawAPI); this.x = x; this.y = y; this.radius = radius; } @Override public void draw() { drawAPI.drawCircle(radius,x,y); } }

شاید بهتر بود اول این عکسو میدیدیم ولی برای اینکه متوجه بشیم تا حالا چیکار کردیم خوبه که نگاهی به این عکس بندازیم.

خب برای تست قطعه کد های زیر رو اجرا میکنیم:

Shape redCircle = new Circle(100,100, 10, new RedCircle()); redCircle.draw();
Shape blueCircle = new Circle(100,100, 10, new BlueCircle()); blueCircle.draw();

و همونطور که احتمالاً انتظار داریم خروجی به شکل زیره:

Drawing Circle[ color: red, radius: 10, x,y: 100, 100]
Drawing Circle[ color: blue, radius: 10, x,y: 100, 100]

متوجه شدین چی شد؟! اومدیم یه اینترفیس ساختیم به اسم DrawAPI که هر کلاسی که قرار شکلی بکشه این کلاس رو Implement میکنه (برای مثال کلاس های RedCircle و BlueCircle که تعریف کردیم). بعدش کلاسی تعریف کردیم به اسم Shape که قرار هر شکلی که میخواد کشیده بشه از این کلاس ارث میبره. در نهایت هم نوبت رسید به ساخت شکل جدید (Circle دایره) که زمان تعریف مشخص میکنه که از چه نوعیه (RedCircle یا BludCircle)و تنها کاری که باقی میمونه تعریف دایره هست! همین... اگه متوجه شده باشین ما عملیات Draw رو پیاده سازی کردیم، و با کمی تغییر توی این کد، هر شکلی جدیدی که نیاز داریم، مثل مستطیل، مربع، و ... فقط میتونن از کلاس Shape ارث بری کنن. البته مثال بالا تأکیدش روی دایره هست و اگه بخوایم بقیه ی شکل ها رو هم ساپورت کنیم باید تغییرات لازمه رو بدیم، برای مثال تبدیل کلاس DrawAPI به شکل زیر:

public interface DrawAPI { public void draw(int radius, int x, int y); }

(که تابع drawCircle به draw تغییر کرده و نتیجتاً کلاس های RedCircle به Red و BlueCircle به Blue تبدیل میشه و سایر تغییرات لازمه رو باید اعمال کنیم) یکی از مزیت های این روش تغییر پذیریشه، و در کنار اینکه امکان تغییر کد رو میده، اون تغییرات لازم نیس توی همه جا تأثیر گذار باشن. برای مثال براحتی میشه کد بالا رو به ساختار زیر تغییر داد:

ولی برای اینکه مثال دیگه ای هم زده باشیم (و البته ساختار تمیز تری داشته باشیم که بتونیم هر شکلی با هر رنگی تعریف کنیم) این ساختار رو به یه صورت دیگه از اول پیاده سازی میکنیم:

اول اینترفیسی به اسم Color تعریف میکنیم که طبیعتاً نشون دهنده ی رنگ هست (مشخصه که هر کلاسی از این اینترفیس ارث ببره باید applyColor رو پیاده سازی بکنه)

public interface Color { public void applyColor(); }

حالا کلاس شکل (Shape) رو به صورت زیر تعریف میکنیم (مشخصه که زمان تعریف هر شئ از این کلاس باید یه Color بهش بدیم و هر کلاسی هم از این کلاس ارث میبره باید تابع انتزاعی applyColor رو پیاده سازی بکنه)

public abstract class Shape { //Composition - implementor protected Color color; //constructor with implementor as input argument public Shape(Color c){ this.color=c; } abstract public void applyColor(); }

حالا وقت پیاده سازی کلاس های مثلث و پنج ضلعی رسیده (که هردو شکل هستن و از کلاس Shape ارث میبرن)

public class Triangle extends Shape { public Triangle(Color c) { super(c); } @Override public void applyColor() { System.out.print("Triangle filled with color "); color.applyColor(); } } public class Pentagon extends Shape { public Pentagon(Color c) { super(c); } @Override public void applyColor() { System.out.print("Pentagon filled with color "); color.applyColor(); } }

خب تا اینجا کلاس های شکل (Shape) و دو تا فرزندش (Triangle و Pentagon) رو تعریف کردیم. حالا میمونه تولید چند تا رنگ (که فعلاً دو رنگ قرمز و سبز رو پیاده میکنیم)

public class RedColor implements Color { public void applyColor(){ System.out.println("red."); } } public class GreenColor implements Color { public void applyColor(){ System.out.println("green."); } }

و تمام! ما هم کلاس شئ با دو فرزند (مثلث و پنج ضلعی) رو ساختیم و هم کلاس رنگ رو با دو کلاسی که پیاده سازیش کردن (قرمز و سبز). و الآن میتونیم شکل های مقابل رو تعریف کنیم: مثلث قرمز، مثلث سبز، پنج ضلعی قرمز و پنج ضلعی سبز. کد زیر رو ببینیم (یه مثلث قرمز و یه پنج ضلعی سبز):

Shape tri = new Triangle(new RedColor()); tri.applyColor();
Shape pent = new Pentagon(new GreenColor()); pent.applyColor();

به همین راحتی! خروجی کد هم بصورت زیره:

Triangle filled with color red.
Pentagon filled with color green.

و با این ساختاری که پیاده کردیم، هر زمانی بخوایم رنگ یا شکل جدیدی ایجاد کنیم به راحتی امکان پذیره و فقط کافیه رنگ ها، اینترفیس Color رو پیاده سازی کنن و شکل ها از کلاس Shape ارث ببرن.

در نهایت برای درک بهتر، نگاهی بندازیم به یه UML مربوط به الگوی Bridge:

Bridge design pattern
Bridge design pattern


ساختار الگوی Bridge پیچیده نیست، فقط باید یذره تمرین کنیم تا به مرور زمان متوجه شیم که کی و کجا باید ازش استفاده بشه. این پترن تا حدودی شبیه پترن Adapter هست و ممکنه با دیدن این دو پترن کمی گیج بشین. باید دقت کنیم که از الگوی Bridge زمانی استفاده میشه که نیاز داریم بین متدهای انتزاعی و پیاده سازی های مختلف یکی رو انتخاب کنیم، و هم متدهای انتزاعی و هم پیاده سازیِ اونا به صورت مستقل از هم بروزرسانی بشن. ولی از الگوی Adapter برای کار کردن با کلاس های ناهمسازگار استفاده میشه که قابلیت های یکسانی دارن، ولی اسم متدها یا نوع بازگشتیشون فرق داره.


منتشر شده در ویرگول توسط محمد قدسیان https://virgool.io/@mohammad.ghodsian

https://virgool.io/@mohammad.ghodsian/java-bridge-design-pattern-behjtyt9cibz

برنامه نویسینرم افزارdesign patternbridgejava
مهندس نرم افزار و کارشناس ارشد مدیریت IT (کسب و کار الکترونیک)
شاید از این پست‌ها خوشتان بیاید