Composite Pattern

1. Giới Thiệu

1.1 Định Nghĩa

Composite Pattern là một structural pattern cho phép bạn tổ chức các đối tượng thành cấu trúc cây và làm việc với chúng như thể chúng là các đối tượng riêng lẻ.

1.2 Mục Đích

  • Tạo cấu trúc cây của các đối tượng
  • Cho phép client xử lý các đối tượng đơn và tổ hợp một cách thống nhất
  • Tạo ra hệ thống phân cấp part-whole
  • Đơn giản hóa code client khi làm việc với cấu trúc cây

2. Cấu Trúc Cơ Bản

2.1 Component Interface

public interface FileSystemComponent {
    void showDetails();
    long getSize();
    void add(FileSystemComponent component);
    void remove(FileSystemComponent component);
    FileSystemComponent getChild(int index);
}

// Leaf class
public class File implements FileSystemComponent {
    private String name;
    private long size;

    public File(String name, long size) {
        this.name = name;
        this.size = size;
    }

    @Override
    public void showDetails() {
        System.out.println("File: " + name + 
            " (Size: " + size + " bytes)");
    }

    @Override
    public long getSize() {
        return size;
    }

    @Override
    public void add(FileSystemComponent component) {
        throw new UnsupportedOperationException(
            "Cannot add to a file");
    }

    @Override
    public void remove(FileSystemComponent component) {
        throw new UnsupportedOperationException(
            "Cannot remove from a file");
    }

    @Override
    public FileSystemComponent getChild(int index) {
        throw new UnsupportedOperationException(
            "Cannot get child from a file");
    }
}

// Composite class
public class Directory implements FileSystemComponent {
    private String name;
    private List<FileSystemComponent> children = 
        new ArrayList<>();

    public Directory(String name) {
        this.name = name;
    }

    @Override
    public void showDetails() {
        System.out.println("Directory: " + name);
        for (FileSystemComponent component : children) {
            component.showDetails();
        }
    }

    @Override
    public long getSize() {
        return children.stream()
            .mapToLong(FileSystemComponent::getSize)
            .sum();
    }

    @Override
    public void add(FileSystemComponent component) {
        children.add(component);
    }

    @Override
    public void remove(FileSystemComponent component) {
        children.remove(component);
    }

    @Override
    public FileSystemComponent getChild(int index) {
        return children.get(index);
    }
}

3. Ví Dụ Thực Tế

3.1 Menu System

public interface MenuComponent {
    void display();
    double getPrice();
    String getName();
}

public class MenuItem implements MenuComponent {
    private String name;
    private String description;
    private double price;

    public MenuItem(String name, 
            String description, double price) {
        this.name = name;
        this.description = description;
        this.price = price;
    }

    @Override
    public void display() {
        System.out.println(name + " - " + 
            description + " ($" + price + ")");
    }

    @Override
    public double getPrice() {
        return price;
    }

    @Override
    public String getName() {
        return name;
    }
}

public class Menu implements MenuComponent {
    private String name;
    private List<MenuComponent> menuComponents = 
        new ArrayList<>();

    public Menu(String name) {
        this.name = name;
    }

    public void add(MenuComponent menuComponent) {
        menuComponents.add(menuComponent);
    }

    public void remove(MenuComponent menuComponent) {
        menuComponents.remove(menuComponent);
    }

    @Override
    public void display() {
        System.out.println("\n" + name);
        System.out.println("-------------------");

        for (MenuComponent menuComponent : menuComponents) {
            menuComponent.display();
        }
    }

    @Override
    public double getPrice() {
        return menuComponents.stream()
            .mapToDouble(MenuComponent::getPrice)
            .sum();
    }

    @Override
    public String getName() {
        return name;
    }
}

3.2 Organization Structure

public interface Employee {
    void showDetails();
    double getSalary();
    void add(Employee employee);
    void remove(Employee employee);
    Employee getChild(int index);
}

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

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

    @Override
    public void showDetails() {
        System.out.println("Developer: " + name + 
            " (Salary: $" + salary + ")");
    }

    @Override
    public double getSalary() {
        return salary;
    }

    @Override
    public void add(Employee employee) {
        throw new UnsupportedOperationException(
            "Cannot add to a developer");
    }

    @Override
    public void remove(Employee employee) {
        throw new UnsupportedOperationException(
            "Cannot remove from a developer");
    }

    @Override
    public Employee getChild(int index) {
        throw new UnsupportedOperationException(
            "Cannot get child from a developer");
    }
}

public class Manager implements Employee {
    private String name;
    private double salary;
    private List<Employee> subordinates = new ArrayList<>();

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

    @Override
    public void showDetails() {
        System.out.println("Manager: " + name + 
            " (Salary: $" + salary + ")");
        System.out.println("Team members:");
        for (Employee employee : subordinates) {
            employee.showDetails();
        }
    }

    @Override
    public double getSalary() {
        return salary + subordinates.stream()
            .mapToDouble(Employee::getSalary)
            .sum() * 0.1; // 10% bonus from team's salary
    }

    @Override
    public void add(Employee employee) {
        subordinates.add(employee);
    }

    @Override
    public void remove(Employee employee) {
        subordinates.remove(employee);
    }

    @Override
    public Employee getChild(int index) {
        return subordinates.get(index);
    }
}

4. Spring Framework Integration

4.1 Component Configuration

@Configuration
public class MenuConfig {
    @Bean
    public MenuComponent mainMenu() {
        Menu mainMenu = new Menu("Main Menu");
        mainMenu.add(appetizerMenu());
        mainMenu.add(mainCourseMenu());
        mainMenu.add(dessertMenu());
        return mainMenu;
    }

    @Bean
    public MenuComponent appetizerMenu() {
        Menu menu = new Menu("Appetizers");
        menu.add(new MenuItem("Salad", 
            "Fresh garden salad", 8.99));
        menu.add(new MenuItem("Soup", 
            "Soup of the day", 6.99));
        return menu;
    }

    @Bean
    public MenuComponent mainCourseMenu() {
        Menu menu = new Menu("Main Courses");
        menu.add(new MenuItem("Steak", 
            "Grilled ribeye steak", 29.99));
        menu.add(new MenuItem("Fish", 
            "Grilled salmon", 24.99));
        return menu;
    }

    @Bean
    public MenuComponent dessertMenu() {
        Menu menu = new Menu("Desserts");
        menu.add(new MenuItem("Ice Cream", 
            "Vanilla ice cream", 5.99));
        menu.add(new MenuItem("Cake", 
            "Chocolate cake", 7.99));
        return menu;
    }
}

4.2 Service Implementation

@Service
public class RestaurantService {
    private final MenuComponent menu;

    public RestaurantService(
            @Qualifier("mainMenu") MenuComponent menu) {
        this.menu = menu;
    }

    public void displayMenu() {
        menu.display();
    }

    public double calculateTotalPrice() {
        return menu.getPrice();
    }
}

@RestController
@RequestMapping("/api/menu")
public class MenuController {
    private final RestaurantService restaurantService;

    public MenuController(
            RestaurantService restaurantService) {
        this.restaurantService = restaurantService;
    }

    @GetMapping("/total")
    public ResponseEntity<Double> getTotalPrice() {
        double totalPrice = 
            restaurantService.calculateTotalPrice();
        return ResponseEntity.ok(totalPrice);
    }
}

5. Testing Composite Pattern

5.1 Unit Testing

@ExtendWith(MockitoExtension.class)
class MenuTest {
    @Test
    void whenCalculatePrice_thenSumAllItems() {
        // Given
        MenuItem item1 = new MenuItem(
            "Item 1", "Desc 1", 10.0);
        MenuItem item2 = new MenuItem(
            "Item 2", "Desc 2", 20.0);
        Menu menu = new Menu("Test Menu");

        // When
        menu.add(item1);
        menu.add(item2);

        // Then
        assertEquals(30.0, menu.getPrice());
    }

    @Test
    void whenNestedMenus_thenCalculateTotalPrice() {
        // Given
        MenuItem item1 = new MenuItem(
            "Item 1", "Desc 1", 10.0);
        MenuItem item2 = new MenuItem(
            "Item 2", "Desc 2", 20.0);

        Menu subMenu = new Menu("Sub Menu");
        subMenu.add(item2);

        Menu mainMenu = new Menu("Main Menu");
        mainMenu.add(item1);
        mainMenu.add(subMenu);

        // Then
        assertEquals(30.0, mainMenu.getPrice());
    }
}

5.2 Integration Testing

@SpringBootTest
class RestaurantServiceIntegrationTest {
    @Autowired
    private RestaurantService restaurantService;

    @Test
    void whenCalculateTotalPrice_thenSuccess() {
        // When
        double totalPrice = 
            restaurantService.calculateTotalPrice();

        // Then
        assertTrue(totalPrice > 0);
    }
}

6. Lợi Ích và Nhược Điểm

6.1 Lợi Ích

  1. Đơn giản hóa cấu trúc phân cấp
  2. Dễ dàng thêm mới component
  3. Code client đơn giản và thống nhất
  4. Tuân thủ Open/Closed Principle
  5. Tái sử dụng code tốt

6.2 Nhược Điểm

  1. Khó thiết kế interface chung
  2. Có thể tạo ra hệ thống quá tổng quát
  3. Khó xử lý các trường hợp đặc biệt
  4. Có thể ảnh hưởng đến performance với cây sâu

7. Best Practices

7.1 Type Safety

public abstract class MenuComponent {
    public void add(MenuComponent menuComponent) {
        throw new UnsupportedOperationException();
    }

    public void remove(MenuComponent menuComponent) {
        throw new UnsupportedOperationException();
    }

    public MenuComponent getChild(int index) {
        throw new UnsupportedOperationException();
    }

    public abstract void display();
    public abstract double getPrice();
}

public class MenuItem extends MenuComponent {
    private String name;
    private double price;

    @Override
    public void display() {
        System.out.println(name + " ($" + price + ")");
    }

    @Override
    public double getPrice() {
        return price;
    }
}

public class Menu extends MenuComponent {
    private String name;
    private List<MenuComponent> menuComponents = 
        new ArrayList<>();

    @Override
    public void add(MenuComponent menuComponent) {
        menuComponents.add(menuComponent);
    }

    @Override
    public void remove(MenuComponent menuComponent) {
        menuComponents.remove(menuComponent);
    }

    @Override
    public MenuComponent getChild(int index) {
        return menuComponents.get(index);
    }

    @Override
    public void display() {
        System.out.println(name);
        menuComponents.forEach(MenuComponent::display);
    }

    @Override
    public double getPrice() {
        return menuComponents.stream()
            .mapToDouble(MenuComponent::getPrice)
            .sum();
    }
}

7.2 Cache Implementation

public class CachedMenu extends Menu {
    private Double cachedPrice;
    private boolean isDirty = true;

    @Override
    public void add(MenuComponent menuComponent) {
        super.add(menuComponent);
        isDirty = true;
    }

    @Override
    public void remove(MenuComponent menuComponent) {
        super.remove(menuComponent);
        isDirty = true;
    }

    @Override
    public double getPrice() {
        if (isDirty || cachedPrice == null) {
            cachedPrice = super.getPrice();
            isDirty = false;
        }
        return cachedPrice;
    }
}

8. Common Issues và Solutions

8.1 Memory Management

public class WeakReferenceMenu extends Menu {
    private List<WeakReference<MenuComponent>> components = 
        new ArrayList<>();

    @Override
    public void add(MenuComponent component) {
        components.add(new WeakReference<>(component));
    }

    @Override
    public double getPrice() {
        return components.stream()
            .map(WeakReference::get)
            .filter(Objects::nonNull)
            .mapToDouble(MenuComponent::getPrice)
            .sum();
    }

    public void cleanup() {
        components.removeIf(ref -> ref.get() == null);
    }
}

8.2 Thread Safety

public class ThreadSafeMenu extends Menu {
    private final ReentrantReadWriteLock lock = 
        new ReentrantReadWriteLock();
    private final Lock readLock = lock.readLock();
    private final Lock writeLock = lock.writeLock();

    @Override
    public void add(MenuComponent component) {
        writeLock.lock();
        try {
            super.add(component);
        } finally {
            writeLock.unlock();
        }
    }

    @Override
    public double getPrice() {
        readLock.lock();
        try {
            return super.getPrice();
        } finally {
            readLock.unlock();
        }
    }
}

9. References

9.1 Books

  • Design Patterns: Elements of Reusable Object-Oriented Software
  • Head First Design Patterns
  • Clean Architecture by Robert C. Martin
  • Patterns of Enterprise Application Architecture

9.2 Online Resources