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

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

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

الگوی Composite

الگوی Composite کمک میکنه تا بتونیم اشیاء رو بصورت سلسله مراتبی یا به عبارتی ساختار درختی داشته باشیم. این الگو اجازه میده بتونیم ساختار درختی داشته باشیم و بتونیم به هر عضو از درخت تسکی بدیم. در حقیقت وقتی میخوایم با گروهی از اشیا کار داشته باشیم یا اونها رو یه شئ واحد ببینیم این الگو کمکمون میکنه. این الگو اجازه میده که سلسله مراتب داشته باشیم و بتونیم با اشیاء تکی و یا بصورت گروهی از افراد بطور یکنواخت رفتار کنیم.
برای پیاده سازی این الگو معمولا اول یه Component و بصورت interface در نظر میگیریم، سپس نود یا پره هایی در نظر میگیریم که هر گره میتواند یکی از دو نوع کامپوزیت (Composite ) یا برگ (leaf) باشه، کامپوزیت به این معنیه که میتونه زیرمجموعه داشته باشه و برگ یعنی اینکه زیرمجموعه ی دیگه ای نداره.

از مزایای این الگو میشه جدای از توضیحات ابتدایی، به موارد زیر هم اشاره کرد:

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

خب سریع بریم سراغ مثال، هر شرکتی مدیرانی داره و افرادی که زیر نظر اون مدیر فعالیت میکنن که خودشون هم میتونن مدیر باشن. حال میخواهیم اطلاعات هر کارمند را در دسترس داشته باشیم، نگاهی به شکل زیر بکنین:

به اینترفیس کارمند ئر نظر میگیریم:

public interface Employee{ 
    public void showEmployeeDetails(); 
}

حالا میریم سراغ برنامه نویس های شرکت. کلاس زیر رو ببینیم:

public class Developer implements Employee { 
    private String name; 
    private long empId; 
    private String position; 
    
    public Developer(long empId, String name, String position) { 
        this.empId = empId; 
        this.name = name; 
        this.position = position; 
    } 
    
    @Override
    public void showEmployeeDetails()  { 
        System.out.println(empId+" " +name); 
    }
} 

همونطور که میبینیم کلاس Developer شامل اسم و شماره کارمندی و پوزیشن شخص میشه. حالا میریم سراغ مدیرا که توی این مثال تفاوتی از نظر جزییات با کلاس Developer ندارن:

public class Manager implements Employee { 
    private String name; 
    private long empId; 
    private String position; 
    
    public Manager(long empId, String name, String position) { 
        this.empId = empId; 
        this.name = name; 
        this.position = position; 
    }
    
    @Override
    public void showEmployeeDetails()  { 
        System.out.println(empId+" " +name); 
    }
} 

و حالا کلاس سازمانمون رو تشکیل میدیم که یسری کارمند داره (حالا یا برنامه نویس یا مدیر):

public class Company implements Employee { 
    private List<Employee> employeeList = new ArrayList<Employee>(); 
    
    @Override
    public void showEmployeeDetails()  { 
        for(Employee emp:employeeList) { 
            emp.showEmployeeDetails(); 
        } 
    } 
    
    public void addEmployee(Employee emp) { 
        employeeList.add(emp); 
    }
    
    public void removeEmployee(Employee emp) { 
        employeeList.remove(emp); 
    }
} 

همونطور که میبینیم، این کلاس هم اینترفیس Employee رو implement کرده، به این صورت که تابع showEmployeeDetails به ازای تک تک افراد موجود توی لیست employeeList تابع showEmployeeDetails مربوط به هر کدوم رو اجرا میکنه. دو تابع دیگه هم داره برای اضافه کردن کارمند جدید و حذف کارمند از لیست.
برای تست کدمون اول یه بخش تیم فنی تعریف میکنیم:

Company engDirectory = new Company();

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

Developer dev1 = new Developer(100, "Mohammad Ghodsian", "Pro Developer");
Developer dev2 = new Developer(101, "Name Family", "Developer");

و بصورت زیر اونها رو به لیست افراد شرکت اضافه میکنیم:

engDirectory.addEmployee(dev1);
engDirectory.addEmployee(dev2);

برای ایجاد مدیرها هم کاری مشابه رو انجام میدیم:

Manager man1 = new Manager(200, "Mohammad Ghodsian", "Sr Manager");
Manager man2 = new Manager(201, "Name Family", "Manager");

با این تفاوت که این افراد رو دیگه توی بخش فنی یا همون engDirectory قرار نمیدیم:

Company accDirectory = new Company();
accDirectory.addEmployee(man1);
accDirectory.addEmployee(man2);

و در نهایت فقط کافیه بصورت زیر دو تا بخش engDirectory و accDirectory رو به یه Company اضافه کنیم:

Company directory = new Company(); 
directory.addEmployee(engDirectory); 
directory.addEmployee(accDirectory); 
directory.showEmployeeDetails(); 

متوجه شدیم دیگه! الآن ما یه ساختار درختی داریم که هر کدوم میتونن تسک خودشون رو داشته باشن. در حقیقت directory شامل engDirectory و accDirectory میشه که هر کدوم از اونها خودشون اعضایی دارن که همه این کلاس ها Employee رو implement کردن یا به عبارت دیگه نگاه ما بهشون کلن یه موجودیته که فقط جزییاتشون تفاوت داره، و به همین ترتیب میتونیم انواع دیگه کارمند و بخش های متفاوت رو ایجاد کنیم.


خب بریم سراغ یه مثال دیگه که یذره جزییات بیشتر داشته باشه و توی درک ماهیت این الگو کمکمون بکنه. مثل مثال قبلی با یه Component که یه اینترفیس هست شروع میکنیم:

public interface Employee {
    public String getName();     
    public double getSalary();
    public void add(Employee employee);
    public void remove(Employee employee);
    public Employee getChild(int i);
    public void print();
}

پس میگیم که هر کارمند نام و حقوق داره، تابع add برای اضافه کردن کارمندی تحت نظر این کارمند و تابع remove برای حفظ شخصی از افراد تحت نظر این کارمنده. تابع getChild هم که یکی از افراد زیر نظر رو برمیگردونه و تابع print قراره جزییات این کارمند رو اعلام کنه.
الآن نوبت میرسه به Composite که در مثال ما مدیرها هستن، پس کلاس Manager رو پیاده میکنیم (دقت کنیم که توی این کلاس از Iterator زبان جاوا استفاده شده):

public class Manager implements Employee{
    private String name;
    private double salary;

    public Manager(String name,double salary){
        this.name = name;
        this.salary = salary;
    }

    List<Employee> employees = new ArrayList<Employee>();
    
    public void add(Employee employee) {
        employees.add(employee);
    }
    
    public Employee getChild(int i) {
        return employees.get(i);
    }
    
    public String getName() {
        return name;
    }
    
    public double getSalary() {
        return salary;
    }

    public void print() {
        System.out.println("-------------");
        System.out.println("Name ="+getName());
        System.out.println("Salary ="+getSalary());
        System.out.println("-------------");
        
        Iterator<Employee> employeeIterator = employees.iterator();
        
        while(employeeIterator.hasNext()){
            Employee employee = employeeIterator.next();
            employee.print();
        }
    }
    
    public void remove(Employee employee) {
        employees.remove(employee);
    }
}

و نوبت میرسه به کلاس Developer. این کلاس شباهت زیادی به کلاس قبلی داره و پارامترهای name و salary و تابع سازنده و توابع getNamr و getSalary و print دقیقا مشابه مثال قبل هستن. اما تفاوت اونجاس که توابع add و remove و getChild معنی ندارن، چون قرار نیست هیچ Developerی زیرمجموعه ای داشته باشه، یا به مفهوم دیگه Developer الزاماً leaf هست.

public class Developer implements Employee{
    private String name;
    private double salary;
    
    public Developer(String name,double salary){
        this.name = name;
        this.salary = salary;
    }
    
    public void add(Employee employee) {
        //this is leaf node so this method is not applicable to this class.
    }

    public Employee getChild(int i) {
        //this is leaf node so this method is not applicable to this class.
        return null;
    }
    
    public String getName() {
        return name;
    }
    
    public double getSalary() {
        return salary;
    }
    
    public void print() {
        System.out.println("-------------");
        System.out.println("Name ="+getName());
        System.out.println("Salary ="+getSalary());
        System.out.println("-------------");
    }

    public void remove(Employee employee) {
        //this is leaf node so this method is not applicable to this class.
    }
}

و تمام. همونطرو که دیدیم ما یه اینترفیس به اسم Employee تعریف کردیم، و کلاسهای Manager و Developer که Employee رو implement کردن، پس باید توابعش رو پیاده سازی بکنن. کلاس Manager تمام توابع رو پیاده سازی کرده ولی کلاس Developer چون قرار نیست زیرمجموعه ای داشته باشه، employees رو نداره (لیستی که کلاس Manager داره و قرار کارمندهای زیرمجموعه ی مدیر رو نگهداری کنه) و همچنین توابعی که مربوط به زیرمجموعه ها میشن رو هم هیچ پیاده سازی خاصی نکرده.
و ما برای اینکه کدمون رو تست کنیم بصورت زیر عمل میکنیم:

Employee emp1 = new Developer("emp1", 10000);
Employee emp2 = new Developer("emp2", 15000);
Employee manager1 = new Manager("manager1",25000);

دو تا کارمند تولید کردیم و یه مدیر، و بصورت زیر کارمندها رو به زیرمجموعه های مدیرمون اضافه میکنیم:

manager1.add(emp1);
manager1.add(emp2);

و حالا میخوایم یه کار دیگه بکنیم، میخوایم یه مدیر بالا سر manager1 باشه که به دو نفر نظارت داره، یک همین manager1 و یه Developer که مستقیما تحت نظر ایشون فعالیت میکنه. بصورت زیر عمل میکنیم:

Employee emp3 = new Developer("emp3", 20000);
Manager generalManager = new Manager("generalManager", 50000);
generalManager.add(emp3);
generalManager.add(manager1);

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

generalManager.print();

خروجی:

-------------
Name =generalManager
Salary =50000.0
-------------
-------------
Name =emp3
Salary =20000.0
-------------
-------------
Name =manager1
Salary =25000.0
-------------
-------------
Name =emp1
Salary =10000.0
-------------
-------------
Name =emp2
Salary =15000.0
-------------

خب حال که دو تا مثال مجدد اشاره کنیم که برای پیاده سازی این الگو میشه اول یه Component تعریف کرد، سپس بریم سراغ Leaf و Composite که اولی اون نودهایی هستن که فرزند یا زیرمجموعه ای ندارن و دومی نودهایی هستن که میتونن زیرمجموعه داشته باشن (مثل مدیرا که توی بخششون کارمند دارن).

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

در هر صورت دقت کنیم که این الگو بیشتر توی موارد زیر کاربرد داره:

  • جایی که می خوایم سلسله مراتب کامل یا جزئی از اشیاء رو داشته باشیم
  • فقط زمانهایی استفاده کنیم که قراره گروهی از اشیا بصورت شئ واحد رفتار کنن
  • جایی که ممکنه مسئولیت شئ گاهی تغییر کنه یا توی درختی که طراحی کردیم جابجا بشه
  • مواردی که باید اشیا داینامیک به عنوان زیرمجموعه اضافه بشن بدون اینکه روی اجسام دیگه تأثیر بذارن
  • وقتایی که واقعاً میخوایم یه درخت رو طراحی کنیم

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

https://virgool.io/@mohammad.ghodsian/java-composite-design-pattern-itrkhk38m6qv