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

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

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

الگوی Prototype

هنگامی که سیستم باید مستقل از چگونگیِ ایجاد و ارائه محصولاتش باشه، وقتی که کلاس ها برای نمونه سازی در زمان اجرا مشخص می شن، زمانی که میخوایم شئ های مختلفی از یک کلاس را نسازیم و یک شئ را که داریم ساده تر کنیم و هزینه‌ای بابت ساخت شئ جدید نپردازیم، و سایر اوقات مشابه، از دیزاین پترن Prototype استفاده میکنیم.

با یه مثال ساده از این پترن شروع میکنیم:

برای پیاده سازی این پترن، اول یه اینترفیس بصورت زیر میسازیم:

interface Prototype {
    public Prototype getClone();
}

این اینترفیس یه تابع داره به اسم getClone پس هر کلاسی بخواد از این کلاس ارث ببره باید این تابع رو پیاده بکنه. خب برای اینکه ببینیم چجوری میخوایم از این اینترفیس استفاده کنیم بریم سراغ کلاس کارمند:

public class EmployeeRecord implements Prototype {
    private int id;
    private String name, designation;
    private double salary;
    private String address;
 
    public EmployeeRecord() {
        System.out.println("   Employee Records of CodeGate Corporation ");
        System.out.println("---------------------------------------------");
        System.out.println("Eid" + "\t" + "Ename" + "\t" + "Edesignation" + "\t" + "Esalary" + "\t\t" + "Eaddress");
    }
 
    public EmployeeRecord(int id, String name, String designation,
        double salary, String address) {
 
          this();
          this.id = id;
          this.name = name;
          this.designation = designation;
          this.salary = salary;
          this.address = address;
    }
 
    public void showRecord() {
        System.out.println(id + "\t" + name + "\t" + designation + " "
  + salary + "\t\t" + address);
    }
 
    @Override
    public Prototype getClone() {
        return new EmployeeRecord(id, name, designation, salary, address);
     }
}

همونطور که توی کلاس بالا میبینیم، تابع getClone اصطلاحاً override شده که وظیفش برگردوندن یه شئ جدید از شئ فعلی هست، و خروجی این تابع Prototype هست (که این کلاس ما هم خودش نوعی Prototype هست). ما در حقیقت از شئ فعلی یه کپی تهیه میکینم و برمیگردونیم. برای پیاده سازی این روند هم به شکل زیر عمل میکنیم:

int eid = 12345;
String ename = "Mohammad Ghodsian";
String edesignation = "software engineer";
String eaddress = "Tehran Iran ...";
double esalary = 1;
EmployeeRecord e1 = new EmployeeRecord(eid, ename, edesignation,esalary, eaddress);
e1.showRecord();
EmployeeRecord e2 = (EmployeeRecord) e1.getClone();
e2.showRecord();

همین! ما یه Prototype pattern ساده رو پیاده کردیم. در حقیقت اومدیم از پترن Prototype برای ساخت شئ جدید بدون وارد کردن تک تک اطلاعات استفاده کردیم و فقط یه کپی از شئ قبلی ساختیم و برگردوندیم.


برای اینکه بیشتر با این مفهوم آشنا شیم بریم سراغ یه مثال دیگه.

این دفعه هم یه اینترس فیس البته به اسم Cloneable ، و یه کلاس که لیستی از اسم کارمندها رو نگه میداره تعریف میکنیم:

interface Cloneable {
    public Object clone();
} 
public class Employees implements Cloneable{
    private List<String> empList;
    public Employees(){
        empList = new ArrayList<String>();
    }
    public Employees(List<String> list){
        this.empList=list;
    }
    public void loadData(){
        empList.add("Misagh");
        empList.add("Mahdi");
        empList.add("Javad");
        empList.add("Mostafa");
        empList.add("RemoveTest");
    }
    public List<String> getEmpList() {
        return empList;
    }
    @Override
    public Object clone() throws CloneNotSupportedException{
        List<String> temp = new ArrayList<String>();
        for(String s : this.getEmpList()){
            temp.add(s);
        }
        return new Employees(temp);
    }
}

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

Employees emps = new Employees();
emps.loadData();

Employees empsAddTest = (Employees) emps.clone();
Employees empsRemoveTest = (Employees) emps.clone();

List<String> list = empsAddTest.getEmpList();
list.add("AddTest");

List<String> list1 = empsRemoveTest.getEmpList();
list1.remove("RemoveTest");

System.out.println("emps List: "+emps.getEmpList()); 		System.out.println("empsAddTest List: "+list);
System.out.println("empsRemoveTest List: "+list1);

خروجی کد بالا بصورت زیره:

emps List: [Misagh, Mahdi, Javad, Mostafa, RemoveTest]
empsAddTest List: [Misagh, Mahdi, Javad, Mostafa, RemoveTest, AddTest]
empsRemoveTest List: [Misagh, Mahdi, Javad, Mostafa] 

همونطور که میبینیم، براحتی شئ های جدیدی ساختیم و هرکدوم مستقل از هم عمل میکنن. به عبارت دیگه لازم نیست هربار قطعه کد زیر صدا زده بشه:

Employees emps = new Employees();
emps.loadData(); 

و برای ساخت لیست جدید فقط از تابع clone استفاده میکنیم.


خب بریم سراغ یه مثالی که یکمی با دو تا مثال قبلش متفاوته.

(خودِ جاوا کلاسی داره به اسم Clonable که میتونین در موردش مطالعه کنین)

کلاس رنگ رو بصورت زیر در نظر میگیریم:

abstract class Color implements Cloneable { 
    protected String colorName;
    protected int count = 0;
    abstract void addColor(); 
    public Object clone()
    { 
        Object clone = null; 
        try { 
            clone = super.clone(); 
        }  catch (CloneNotSupportedException e)  { 
            e.printStackTrace(); 
        } 
        return clone; 
    }
}

و کلاس های رنگ آبی و سیاه رو از این کلاس ارثبری میکنیم (با پارامتر count بعداً کار داریم):

class BlueColor extends Color {
    public blueColor()  { 
        this.colorName = "blue"; 
    } 
      
    @Override
    void addColor()  { 
        count+=1;
        System.out.println(count + " Blue color added");
    }
} 
  
class BlackColor extends Color{ 
   public blackColor() { 
       this.colorName = "black"; 
   } 
   @Override
   void addColor() { 
       count+=1;
        System.out.println(count + " Black color added");
    } 
}

خب تا اینجا که فقط یه کلاس رنگ و دو تا کلاس آبی و سیاه که از کلاس رنگ ارث میبرن تعریف کردیم. حالا کلاس ColorStore رو پیاده میکنیم:

class ColorStore {
    private static Map<String, Color> colorMap = new HashMap<String, Color>();
    static { 
        colorMap.put("blue", new BlueColor());
        colorMap.put("black", new BlackColor());
    }
    public static Color getColor(String colorName){
        return (Color) colorMap.get(colorName).clone();
    } 
} 

این کلاس یه Map استاتیک داره که لیستی از رنگ ها رو نگهداری میکنه (که داخل قسمت static{...} دو تا رنگ سیاه و آبی بصورت پیشفرض بهش اضافه شدن). و یه تابع که با گرفتن اسم رنگ، یه شئ از اون رنگ برمیگرودنه. کد زیر رو ببینیم تا بیشتر متوجه بشیم میخوایم چیکار کنیم:

Color blueColor = ColorStore.getColor("blue");
Color blackColor = ColorStore.getColor("black");

blueColor.addColor(); 
blackColor.addColor();  

خب خروجی طبیعتاً به شکل زیر هست:

1 Blue color added
1 Black color added 

حالا اگه به جای کد بالا این کدهارو بنویسیم:

Color blueColor = ColorStore.getColor("blue");
Color blackColor = ColorStore.getColor("black"); 

blueColor.addColor();
blackColor.addColor();

Color newBlueColor = ColorStore.getColor("blue");
Color newBlackColor = ColorStore.getColor("black");  
 
newBlueColor.addColor();
newBlackColor.addColor(); 

نتایج زیر دیده میشه:

 1 Blue color added 
 1 Black color added  
 
2 Blue color added
2 Black color added   

دلیلش هم اینه که newBlueColor اول از blueColor کپی میشه و مقدار countش برابر 1 هست و با فراخوانی دوباره ی تابع addColorش یدونه به 1 اضافه میکنه و نتیجه میشه 2. ولی نکته اینجاست که شئ newBlueColor با شئ blueColor تفاوت داره، به این معنی که اگه در ادامه ی کد بالا این خطوط رو اضافه کنیم:

newBlueColor.addColor();
newBlackColor.addColor(); 

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

3 Blue color added 
3 Black color added  

و اگه همینجا توی ادامه خطوط زیر رو اضافه کنیم:

blueColor.addColor();
blackColor.addColor(); 

اعداد count اشیا blueColor و blackColor هنوز 1 هستن که با فراخوانی تابع addColor به 2 تغییر میکنن:

2 Blue color added
2 Black color added  

این مثال بیشتر به درد چه زمانهایی میخوره.؟ مثلاً فرض کنیم میخوایم از هر رنگ چندتا تعریف کنیم، مثلاً سه رنگ آبی، آبی روشن و آبی تیره. کافیه یه پارامتر (مثلاً concentration) که نوع روشن و تیره بودن رو مشخص میکنه به کلاس blueColor اضافه کنیم، و برای تعریفشون خطوط زیر رو صدا بزنیم:

Color blueColor = ColorStore.getColor("blue");
Color lightColor = ColorStore.getColor("blue");
lightColor.setConcentration("light");
Color darkColor = ColorStore.getColor("blue");
darkColor.setConcentration("dark");

طبیعتاً این فقط یه مثاله و میشه بجای string از کد رنگی یا هر چیز دیگه ای استفاده کنیم، یا مثلاً به رنگ ها ویژگی اضافه یا تغییراتی توشون اعمال کرد. مهم اینه که مفهوم clone شدن رو متوجه بشیم و بدونیم خط زیر هربار یه کپی از رنگ آبی اولیه ای که به colorMap اضافه کرده بودیم برمیگردونه.

ColorStore.getColor("blue");


خب اگه خوب متوجه شده باشیم این الگو میتونه خیلی کارآمد باشه و احتمالاً بخاطر این دلایل بتونه به دردمون بخوره: سهولت توی تعریف اشیا، حذف بعضی از وابستگی های شئ گرایی، کاهش ارثبری های بیهوده، کپی کردن سریع اشیا و ...


و البته که مفهوم کلیِ Prototyping و فلسفش بطور خلاصه ایجاد ساختار اولیه یا به اصطلاح مشخص کردن یسری خط مشی واسه پیاده سازی کدهاست. و ما فقط بخشی از مثال های این مفهوم رو که تحت عنوان الگوی Prototype design pattern توی زبان جاوا قابل پیاده سازی هست دیدیم.



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