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
- Đơn giản hóa cấu trúc phân cấp
- Dễ dàng thêm mới component
- Code client đơn giản và thống nhất
- Tuân thủ Open/Closed Principle
- Tái sử dụng code tốt
6.2 Nhược Điểm
- Khó thiết kế interface chung
- Có thể tạo ra hệ thống quá tổng quát
- Khó xử lý các trường hợp đặc biệt
- 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