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

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

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


Flyweight - Structural Design Patterns
Flyweight - Structural Design Patterns


الگوی Flyweight

دیده شده که الگوی flyweight رو flyweight-factory هم صدا میزنن، و خوبه که با توجه به مشابهتِ کلیِ این الگو (از نظر مفهومی و صرفاً بصورت کلی) به الگوی Factory نگاهی هم به اون الگو بندازین، البته دقت کنیم که این الگو ساختاری هست و اون الگو ساختنی و برای ایجاد اشیا بکار میره.

هدف اصلی توی پیاده سازی این الگو اینه که بتونیم تعداد شئ کمتری بسازیم و میزان استفاده از حافظه رو کاهش بدیم. اگه به اسم این الگو هم دقت کنیم Flyweight ترکیبیه از مگس و وزن یا یجورایی سبک وزنِ خودمون! عملاً میخوایم بتونیم بجای نگهداری یعالمه اطلاعات و داده، چیزای مختصرتری ازشون مثل اشتراکات و وضعیت ارتباطشون رو نگهداریم تا بتونیم تعداد شئ بیشتری بدون افزایش میزان حافظه داشته باشیم.
برای اینکه یذره بهتر متوجه بشیم، بازی های آنلاین رو در نظر بگیریم، مثلا دو تا بازی به اسامی TankiOnline
و Agar.io بود نمیدونم هنوزم هستن یا نه ولی چند سال پیش که گاهی واسه رفع خستگی و تنوع میرفتیم سراغشون، این مشکل پیش میومد که گاهی وقتی سرعت اینترنت کم میشد، ما حرکت میکردیم به دشمن هم تیر میزدیم ولی یهو بعد چند ثانیه میگریدیم سر جای قبلیمون و متوجه میشدیم که باختیم! این مثال دقیقا با Flyweight شاید پیاده سازی نشده باشه ولی مثال خوبیه، دقت کنیم برای اینکه حافظه کمتری داشته باشیم احتمالا هر تانک فقط چیزای اصلیِ مرتبط با خودش (به اصطلاح intrinsic که به ذاتِ خودش ربط داره) رو نگه میداره برای خودش، و موارد دیگه مثل شلیک به بقیه و حتی شاید لوکیشنش (به اصطلاح extrinsic که به بیرون از خودش بیشتر مرتبطه تا ذاتِ خودش) رو میفرسته سمت سرور. پس عملاً اتفاقی که میفته، نه سمت کلاینت کل نقشه نگه داشته نمیشه.
مثلاً برای بازیِ کانتر نمیایم کل افراد و کل سلاح هاشون و کل جزئیات نقشه و غیره رو توی مدلِ هر کاربر نگه داریم، در عوض اطلاعات مختصِ خودش مثل کاراکتر و بقیه چیزارو میتونیم جزء ویژگی های ذاتیش در نظر بگیریم، و هروقت خواست حرکت یا تیر اندازی کنه، جزءِ ویژگی های خارجیش در نظر بگیریم اکشنِ لازمه رو بفرستیم به توابعِ دیگه که هندل کنن ماجرارو (طبیعتاً منظور اکشن خارجی هست، وگرنه اینکه موقع تیر اندازی خودِ کاراکتر چه حرکاتی رو نشون بده رو میشه جزءِ ویژگی های ذاتیش در نظر گرفت، و اینها مثال هستن و شاید بشه با بررسی بیشتر، حالات بهینه تری هم براشون پیدا کرد).

خب حالا که با کلیتِ این الگو آشنا شدیم، بریم سراغِ یه مثال تا بهتر درک کنیم موضوع رو.

اینترفیسِ زیر رو ببینید:

public interface Shape {
   void draw();
}

مشخصه که میخوایم شکل داشته باشیم، حالا چه اَشکالی؟ بیاین با دایره شروع کنیم:

 public class Circle implements Shape {
   private String color;
   private int x;
   private int y;
   private int radius;

   public Circle(String color){
      this.color = color;		
   }

   public void setX(int x) {
      this.x = x;
   }

   public void setY(int y) {
      this.y = y;
   }

   public void setRadius(int radius) {
      this.radius = radius;
   }

   @Override
   public void draw() {
      System.out.println(&quotCircle: Draw() [Color : &quot + color + &quot, x : &quot + x + &quot, y :&quot + y + &quot, radius :&quot + radius);
   }
}

کلاس بالا یسری ویژگی ها برای خودش داره، مثل x و y و radius و رنگ و توابع مرتبط با اونا، و البته تابع draw که قراره مثلاً شکلِ دایره رو سر جای خودش بکشه، ولی فعلاً علی الحساب اینجا ما میخوایم فقط اطلاعات دایره رو نشون بدیم.

و حالا الگوی سبک وزن یا مگس وزن رو ببینیم:

public class FlyweightPatternDemo {
   private static final String colors[] = { &quotRed&quot, &quotGreen&quot, &quotBlue&quot};
   public static void main(String[] args) {

      for(int i=0; i < 10; ++i) {
         Circle circle = new Circle(getRandomColor());
         circle.setX(getRandomX());
         circle.setY(getRandomY());
         circle.setRadius(100);
         circle.draw();
      }
   }
   private static String getRandomColor() {
      return colors[(int)(Math.random()*colors.length)];
   }
   private static int getRandomX() {
      return (int)(Math.random()*100 );
   }
   private static int getRandomY() {
      return (int)(Math.random()*100);
   }
}

و تمام! چیشد؟ اومدیم ده تا دایره درست کردیم (که طبیعتاً چون سه تا رنگ داریم فقط، تعداد دایره های با رنگ مشابه زیاده). توابع getRandomColor و getRandomX و getRandomY هم که مشخص هستن (رنگ و پوزیشنای x و y رندم). ولی یه سوال مهم، این الان کجاش Flyweight بود؟ جواب اینه که هنوز هیچ کجاش! پس ادامه میدیم، یه کلاس دیگه میسازیم که بتونه کاری که میخوایم (بهینه سازی حافظه) رو برامون انجام بده:

import java.util.HashMap;

public class ShapeFactory {

   // Uncomment the compiler directive line and
   // javac *.java will compile properly.
   // @SuppressWarnings(&quotunchecked&quot)
   private static final HashMap<String, Circle> circleColoredMap = new HashMap();

   public static Shape getColoredCircle(String color) {
      Circle circle = circleColoredMap.get(color);

      if(circle == null) {
         circle = new Circle(color);
         circleColoredMap.put(color, circle);
         System.out.println(&quotCreating circle of color : &quot + color);
      }
      return circle;
   }
}

و توی کلاس FlyweightPatternDemo داخل تابع main و داخل خط اول لوپ، خط زیر

Circle circle = new Circle(getRandomColor());

رو زیر جایگزین میکنیم:

Circle circle = ShapeFactory.getColoredCircle(getRandomColor());

حالا اتفاقی که میفته، هر وقت که میخوایم یه دایره با یه رنگ خاص رو داشته باشیم، کلاس ShapeFactory چک میکنه که قبلاً دایره ای با این رنگ داشتیم یا نه، اگه داشتیم همون رو برمیگردونه در غیر این صورت یه دایره با اون رنگ میسازه. اینجا عملاً اتفاقی که میفته اینه که ما در مجموع به تعداد رنگ ها دایره توی حافظه نگه میداریم (یعنی حدأکثر سه تا چون توی مثالمون سه تا رنگ داریم فقط) به جای اینکه ده تا دایره داشته باشیم.
دقت کنیم که این کد فقط میخواد دایره رسم کن و بعدش دیگه کاری باهاش نداره، واسه همین هربار که دایره با رنگ قبلی رو میگیره، مختصات جدید بهش میده و جای جدید رسمش میکنه. چون به کمک HashMapی که توی کلاس ShapeFactory تعریف کردیم، هر شئِ دایره، با رنگ خودش داخل مپ قرار میگیره و اگه از همون رنگ بخوایم مجدد از همون شئ استفاده میکنیم. پس خیلی دقت کنیم که اینجا ویژگی ذاتیِ دایره، رنگش در نظر گرفته شده و باقی اطلاعات (پوزیشن و شعاعش) عمومی هست. برای بهتر متوجه شدن میتونین به خروجی کد دقت کنین:

Creating circle of color : Red
Circle: Draw() [Color : Red, x : 36, y :71, radius :100
Creating circle of color : Green
Circle: Draw() [Color : Green, x : 27, y :27, radius :100
Circle: Draw() [Color : Red, x : 64, y :10, radius :100
Circle: Draw() [Color : Red, x : 15, y :44, radius :100
Circle: Draw() [Color : Green, x : 19, y :10, radius :100
Circle: Draw() [Color : Green, x : 94, y :32, radius :100
Creating circle of color : Blue
Circle: Draw() [Color : Blue, x : 13, y :4, radius :100
Circle: Draw() [Color : Green, x : 21, y :21, radius :100
Circle: Draw() [Color : Blue, x : 55, y :86, radius :100
Circle: Draw() [Color : Green, x : 78, y :3, radius :100

مشخصه که اگه کلاس آخر رو استفاده نمیکردیم، به ازای ساخته شدنِ هر دایره (ده بار) خط زیر رو میدیدیم:

Creating circle of color : ...

برای انکه یذره بهترهم متوجه بشیم، با یذره وَر رفتن با کد و تغییراتی که نیازه، میشه شکل زیر رو ایجاد کرد (میدونم شبیه این میمونه که توی کلاس، عملیات جمع و تفریق یاد بدی، بعد انتظارِ حلِ معادله چندمجهولی داشته باشی! ولی مفهومو میرسونه)

یذره اگه دقت کنیم متوجه میشیم که برای ایجاد این عکس، اگه از این الگو استفاده نکنیم (حالتی که هنوز کلاس ShapeFactory رو ایجاد نکردیم) به ازای هر چیزی که میبینیم (دایره رنگی، خط دایره، خط صاف، بیضی و ...) باید یه شیء توی حافظه داشته باشیم، در صورتی که میشه بعد از کشیدن هر کدوم، اگه بازم بهش نیاز شد نگهش داریم و فقط یسری پارامتراش مثل طول و عرض و مکان و غیره رو تغییر بدیم و رسمش کنیم. اگه خوب و اونطور که میخوایم پیاده سازی کنیم همچین خروجی ای میتونیم داشته باشیم:

Creating Line object
Creating Oval object with fill=true
Creating Oval object with fill=false

فقط سه بار ایجاد شئ برای کشیدن این همه شکل، خیلی بهینه تره، نه؟!


میخواستم یه مثال دیگه هم بزنم که این مقاله رو توی ویرگول دیدم، میتونید از اون هم به عنوان نمونه ی دیگه استفاده کنید: https://vrgl.ir/u76O4


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

https://vrgl.ir/AcH62