Chuyển tới nội dung chính

Structural Patterns

Mục tiêu bài học

Sau bài này, bạn sẽ:

  • Hiểu các Structural Patterns chính: Adapter, Decorator, Proxy, Facade, Composite
  • Biết cách Adapter chuyển đổi interface không tương thích
  • Nắm được Decorator pattern — thêm behavior động mà không sửa class gốc
  • Phân biệt Proxy types: virtual, protection, remote, caching
  • Áp dụng Facade để đơn giản hóa complex subsystems

Bài trước: Creational Patterns — Đã học về Singleton, Factory, Builder, Prototype. Bài này sẽ tìm hiểu Structural Patterns — cách tổ chức và kết hợp objects/classes.

Structural Patterns tập trung vào cách tổ chức class và object để tạo thành cấu trúc lớn hơn, linh hoạt và hiệu quả. Các patterns này giúp kết nối các components khác nhau, mở rộng chức năng, và đơn giản hóa interface phức tạp.

1. Adapter Pattern

Mục đích

Chuyển đổi interface của một class thành interface khác mà client mong đợi. Giúp các class không tương thích có thể làm việc với nhau.

Ví dụ thực tế: Adapter sạc điện thoại chuyển đổi 220V → 5V USB.

Class Adapter vs Object Adapter

Object Adapter (Khuyến nghị - dùng Composition)

// Target interface (interface mà client mong đợi)
interface MediaPlayer {
void play(String audioType, String fileName);
}

// Adaptee (class có sẵn nhưng interface không tương thích)
class AdvancedMediaPlayer {
public void playVlc(String fileName) {
System.out.println("Playing VLC file: " + fileName);
}

public void playMp4(String fileName) {
System.out.println("Playing MP4 file: " + fileName);
}
}

// Adapter (chuyển đổi interface)
class MediaAdapter implements MediaPlayer {
private AdvancedMediaPlayer advancedPlayer;

public MediaAdapter(String audioType) {
advancedPlayer = new AdvancedMediaPlayer();
}

@Override
public void play(String audioType, String fileName) {
if (audioType.equalsIgnoreCase("vlc")) {
advancedPlayer.playVlc(fileName);
} else if (audioType.equalsIgnoreCase("mp4")) {
advancedPlayer.playMp4(fileName);
}
}
}

// Client
class AudioPlayer implements MediaPlayer {
private MediaAdapter mediaAdapter;

@Override
public void play(String audioType, String fileName) {
// Built-in support for mp3
if (audioType.equalsIgnoreCase("mp3")) {
System.out.println("Playing MP3 file: " + fileName);
}
// Use adapter for other formats
else if (audioType.equalsIgnoreCase("vlc") || audioType.equalsIgnoreCase("mp4")) {
mediaAdapter = new MediaAdapter(audioType);
mediaAdapter.play(audioType, fileName);
}
else {
System.out.println("Invalid media format: " + audioType);
}
}
}

// Sử dụng
AudioPlayer player = new AudioPlayer();
player.play("mp3", "song.mp3"); // Playing MP3 file: song.mp3
player.play("mp4", "video.mp4"); // Playing MP4 file: video.mp4
player.play("vlc", "movie.vlc"); // Playing VLC file: movie.vlc
player.play("avi", "clip.avi"); // Invalid media format: avi

Ví dụ 2: Legacy System Integration

// Modern interface
interface PaymentProcessor {
boolean processPayment(String customerId, double amount);
}

// Legacy system (không thể sửa)
class OldPaymentGateway {
public void makePayment(int customerCode, int amountInCents) {
System.out.println("Processing payment via old gateway: " +
"Customer " + customerCode + ", Amount: " + amountInCents + " cents");
}
}

// Adapter
class PaymentAdapter implements PaymentProcessor {
private OldPaymentGateway oldGateway;

public PaymentAdapter(OldPaymentGateway oldGateway) {
this.oldGateway = oldGateway;
}

@Override
public boolean processPayment(String customerId, double amount) {
// Chuyển đổi String ID → int code
int customerCode = Integer.parseInt(customerId);
// Chuyển đổi dollars → cents
int amountInCents = (int) (amount * 100);

oldGateway.makePayment(customerCode, amountInCents);
return true;
}
}

// Sử dụng
OldPaymentGateway legacy = new OldPaymentGateway();
PaymentProcessor processor = new PaymentAdapter(legacy);
processor.processPayment("12345", 99.99); // Processed: 9999 cents
Khi nào dùng Adapter?
  • Legacy integration: Tích hợp hệ thống cũ với code mới
  • Third-party libraries: API của library không khớp với interface bạn cần
  • XML/JSON conversion: Chuyển đổi data formats
  • Multiple data sources: Thống nhất interface cho nhiều nguồn dữ liệu

2. Decorator Pattern

Mục đích

Thêm behavior mới cho object một cách động, không cần sửa class gốc. Linh hoạt hơn inheritance.

Decorator vs Inheritance:

  • Inheritance: Static, định nghĩa lúc compile-time
  • Decorator: Dynamic, add behavior lúc runtime

Ví dụ 1: Coffee Shop

// Component interface
interface Coffee {
String getDescription();
double getCost();
}

// Concrete component (base coffee)
class SimpleCoffee implements Coffee {
@Override
public String getDescription() {
return "Simple Coffee";
}

@Override
public double getCost() {
return 2.0;
}
}

// Decorator abstract class
abstract class CoffeeDecorator implements Coffee {
protected Coffee decoratedCoffee;

public CoffeeDecorator(Coffee coffee) {
this.decoratedCoffee = coffee;
}

public String getDescription() {
return decoratedCoffee.getDescription();
}

public double getCost() {
return decoratedCoffee.getCost();
}
}

// Concrete decorators
class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) {
super(coffee);
}

@Override
public String getDescription() {
return decoratedCoffee.getDescription() + ", Milk";
}

@Override
public double getCost() {
return decoratedCoffee.getCost() + 0.5;
}
}

class SugarDecorator extends CoffeeDecorator {
public SugarDecorator(Coffee coffee) {
super(coffee);
}

@Override
public String getDescription() {
return decoratedCoffee.getDescription() + ", Sugar";
}

@Override
public double getCost() {
return decoratedCoffee.getCost() + 0.2;
}
}

class WhipDecorator extends CoffeeDecorator {
public WhipDecorator(Coffee coffee) {
super(coffee);
}

@Override
public String getDescription() {
return decoratedCoffee.getDescription() + ", Whip";
}

@Override
public double getCost() {
return decoratedCoffee.getCost() + 0.7;
}
}

// Sử dụng - có thể stack nhiều decorators!
Coffee coffee = new SimpleCoffee();
System.out.println(coffee.getDescription() + " - $" + coffee.getCost());
// Simple Coffee - $2.0

coffee = new MilkDecorator(coffee);
System.out.println(coffee.getDescription() + " - $" + coffee.getCost());
// Simple Coffee, Milk - $2.5

coffee = new SugarDecorator(coffee);
System.out.println(coffee.getDescription() + " - $" + coffee.getCost());
// Simple Coffee, Milk, Sugar - $2.7

coffee = new WhipDecorator(coffee);
System.out.println(coffee.getDescription() + " - $" + coffee.getCost());
// Simple Coffee, Milk, Sugar, Whip - $3.4

Ví dụ 2: Java I/O (Real-world example)

Java I/O sử dụng Decorator pattern rất nhiều:

import java.io.*;

// Base component: InputStream
// Decorators: BufferedInputStream, DataInputStream, etc.

try {
InputStream file = new FileInputStream("data.txt");

// Add buffering
InputStream buffered = new BufferedInputStream(file);

// Add data reading capabilities
DataInputStream data = new DataInputStream(buffered);

// Stack nhiều decorators!
int value = data.readInt();
} catch (IOException e) {
e.printStackTrace();
}

// Tương tự với OutputStream
try {
OutputStream file = new FileOutputStream("output.txt");
OutputStream buffered = new BufferedOutputStream(file);
DataOutputStream data = new DataOutputStream(buffered);

data.writeInt(123);
data.flush();
} catch (IOException e) {
e.printStackTrace();
}

Ví dụ 3: Text Formatting

interface Text {
String format();
}

class PlainText implements Text {
private String text;

public PlainText(String text) {
this.text = text;
}

@Override
public String format() {
return text;
}
}

abstract class TextDecorator implements Text {
protected Text decoratedText;

public TextDecorator(Text text) {
this.decoratedText = text;
}
}

class BoldDecorator extends TextDecorator {
public BoldDecorator(Text text) {
super(text);
}

@Override
public String format() {
return "<b>" + decoratedText.format() + "</b>";
}
}

class ItalicDecorator extends TextDecorator {
public ItalicDecorator(Text text) {
super(text);
}

@Override
public String format() {
return "<i>" + decoratedText.format() + "</i>";
}
}

class UnderlineDecorator extends TextDecorator {
public UnderlineDecorator(Text text) {
super(text);
}

@Override
public String format() {
return "<u>" + decoratedText.format() + "</u>";
}
}

// Sử dụng
Text text = new PlainText("Hello World");
text = new BoldDecorator(text);
text = new ItalicDecorator(text);
text = new UnderlineDecorator(text);

System.out.println(text.format());
// <u><i><b>Hello World</b></i></u>
Khi nào dùng Decorator?
  • Cần thêm behavior mà không sửa class gốc
  • nhiều combinations của behaviors (coffee toppings, text formatting)
  • Muốn add/remove behavior dynamically lúc runtime
  • Tránh explosion of subclasses (nếu dùng inheritance sẽ có quá nhiều subclasses)

3. Proxy Pattern

Mục đích

Cung cấp placeholder hoặc đại diện cho object khác để kiểm soát quyền truy cập.

Các loại Proxy

1. Virtual Proxy (Lazy Loading)

// Subject interface
interface Image {
void display();
}

// Real subject (expensive to create)
class RealImage implements Image {
private String filename;

public RealImage(String filename) {
this.filename = filename;
loadFromDisk();
}

private void loadFromDisk() {
System.out.println("Loading image: " + filename);
// Giả lập loading image từ disk (tốn thời gian)
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}

@Override
public void display() {
System.out.println("Displaying image: " + filename);
}
}

// Proxy (lazy loading)
class ImageProxy implements Image {
private String filename;
private RealImage realImage;

public ImageProxy(String filename) {
this.filename = filename;
}

@Override
public void display() {
// Chỉ tạo RealImage khi thực sự cần!
if (realImage == null) {
realImage = new RealImage(filename);
}
realImage.display();
}
}

// Sử dụng
Image image1 = new ImageProxy("photo1.jpg");
Image image2 = new ImageProxy("photo2.jpg");

// Chưa load ảnh
System.out.println("Images created (not loaded yet)");

// Load khi cần
image1.display(); // Loading... then display
image1.display(); // Không load lại, chỉ display

image2.display(); // Loading... then display

2. Protection Proxy (Access Control)

interface Document {
void view();
void edit();
void delete();
}

class RealDocument implements Document {
private String content;

public RealDocument(String content) {
this.content = content;
}

@Override
public void view() {
System.out.println("Viewing document: " + content);
}

@Override
public void edit() {
System.out.println("Editing document: " + content);
}

@Override
public void delete() {
System.out.println("Deleting document: " + content);
}
}

class DocumentProxy implements Document {
private RealDocument realDocument;
private String userRole;

public DocumentProxy(String content, String userRole) {
this.realDocument = new RealDocument(content);
this.userRole = userRole;
}

@Override
public void view() {
// Mọi user đều có thể xem
realDocument.view();
}

@Override
public void edit() {
// Chỉ EDITOR và ADMIN mới edit được
if (userRole.equals("EDITOR") || userRole.equals("ADMIN")) {
realDocument.edit();
} else {
System.out.println("Access denied: You cannot edit this document");
}
}

@Override
public void delete() {
// Chỉ ADMIN mới delete được
if (userRole.equals("ADMIN")) {
realDocument.delete();
} else {
System.out.println("Access denied: You cannot delete this document");
}
}
}

// Sử dụng
Document userDoc = new DocumentProxy("User Manual", "VIEWER");
userDoc.view(); // OK
userDoc.edit(); // Access denied
userDoc.delete(); // Access denied

Document editorDoc = new DocumentProxy("Article", "EDITOR");
editorDoc.view(); // OK
editorDoc.edit(); // OK
editorDoc.delete(); // Access denied

Document adminDoc = new DocumentProxy("Config", "ADMIN");
adminDoc.view(); // OK
adminDoc.edit(); // OK
adminDoc.delete(); // OK

3. Caching Proxy

interface DatabaseService {
String queryUser(int userId);
}

class RealDatabaseService implements DatabaseService {
@Override
public String queryUser(int userId) {
System.out.println("Querying database for user " + userId);
// Giả lập query chậm
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "User" + userId + " data from database";
}
}

class CachingDatabaseProxy implements DatabaseService {
private RealDatabaseService realService;
private Map<Integer, String> cache = new HashMap<>();

public CachingDatabaseProxy() {
this.realService = new RealDatabaseService();
}

@Override
public String queryUser(int userId) {
// Check cache trước
if (cache.containsKey(userId)) {
System.out.println("Returning cached data for user " + userId);
return cache.get(userId);
}

// Cache miss - query database
String result = realService.queryUser(userId);
cache.put(userId, result); // Lưu vào cache
return result;
}
}

// Sử dụng
DatabaseService db = new CachingDatabaseProxy();

db.queryUser(1); // Query database (slow)
db.queryUser(2); // Query database (slow)
db.queryUser(1); // From cache (fast!)
db.queryUser(2); // From cache (fast!)
Khi nào dùng Proxy?
  • Virtual Proxy: Object tốn kém để tạo, cần lazy loading
  • Protection Proxy: Kiểm soát quyền truy cập
  • Remote Proxy: Đại diện cho object ở remote server (RMI, web services)
  • Caching Proxy: Cache kết quả để tăng performance
  • Logging Proxy: Log mỗi lần method được gọi

4. Facade Pattern

Mục đích

Cung cấp interface đơn giản cho một subsystem phức tạp. Che giấu complexity, làm subsystem dễ sử dụng hơn.

Ví dụ: Home Theater System

// Subsystem classes (phức tạp)
class DVDPlayer {
public void on() { System.out.println("DVD Player ON"); }
public void play(String movie) { System.out.println("Playing: " + movie); }
public void stop() { System.out.println("DVD Player stopped"); }
public void off() { System.out.println("DVD Player OFF"); }
}

class Projector {
public void on() { System.out.println("Projector ON"); }
public void setInput(String input) { System.out.println("Projector input: " + input); }
public void wideScreenMode() { System.out.println("Projector: widescreen mode"); }
public void off() { System.out.println("Projector OFF"); }
}

class SoundSystem {
public void on() { System.out.println("Sound System ON"); }
public void setVolume(int level) { System.out.println("Volume set to " + level); }
public void setSurroundSound() { System.out.println("Surround sound ON"); }
public void off() { System.out.println("Sound System OFF"); }
}

class Lights {
public void dim(int level) { System.out.println("Lights dimmed to " + level + "%"); }
public void on() { System.out.println("Lights ON"); }
}

// Facade - simplified interface
class HomeTheaterFacade {
private DVDPlayer dvd;
private Projector projector;
private SoundSystem sound;
private Lights lights;

public HomeTheaterFacade(DVDPlayer dvd, Projector projector,
SoundSystem sound, Lights lights) {
this.dvd = dvd;
this.projector = projector;
this.sound = sound;
this.lights = lights;
}

// Simple method che giấu complexity
public void watchMovie(String movie) {
System.out.println("\n=== Get ready to watch a movie ===");
lights.dim(10);
projector.on();
projector.wideScreenMode();
projector.setInput("DVD");
sound.on();
sound.setSurroundSound();
sound.setVolume(50);
dvd.on();
dvd.play(movie);
}

public void endMovie() {
System.out.println("\n=== Shutting down theater ===");
dvd.stop();
dvd.off();
sound.off();
projector.off();
lights.on();
}
}

// Client code - rất đơn giản!
DVDPlayer dvd = new DVDPlayer();
Projector projector = new Projector();
SoundSystem sound = new SoundSystem();
Lights lights = new Lights();

HomeTheaterFacade homeTheater = new HomeTheaterFacade(dvd, projector, sound, lights);

homeTheater.watchMovie("Inception");
// ... xem phim ...
homeTheater.endMovie();

Ví dụ 2: Database Facade

// Subsystem
class DatabaseConnection {
public void connect(String url) {
System.out.println("Connecting to: " + url);
}
public void disconnect() {
System.out.println("Disconnected");
}
}

class QueryBuilder {
public String buildSelectQuery(String table, String[] columns) {
return "SELECT " + String.join(", ", columns) + " FROM " + table;
}
}

class ResultSetMapper {
public List<Map<String, Object>> mapResults(String query) {
System.out.println("Executing: " + query);
// Giả lập kết quả
return List.of(Map.of("id", 1, "name", "John"));
}
}

// Facade
class DatabaseFacade {
private DatabaseConnection connection;
private QueryBuilder queryBuilder;
private ResultSetMapper mapper;

public DatabaseFacade(String dbUrl) {
this.connection = new DatabaseConnection();
this.queryBuilder = new QueryBuilder();
this.mapper = new ResultSetMapper();
connection.connect(dbUrl);
}

public List<Map<String, Object>> fetchUsers() {
String query = queryBuilder.buildSelectQuery("users", new String[]{"id", "name", "email"});
return mapper.mapResults(query);
}

public void close() {
connection.disconnect();
}
}

// Client
DatabaseFacade db = new DatabaseFacade("jdbc:mysql://localhost:3306/mydb");
List<Map<String, Object>> users = db.fetchUsers();
System.out.println(users);
db.close();
Khi nào dùng Facade?
  • Subsystem phức tạp: Nhiều classes, nhiều dependencies
  • Simplify API: Client chỉ cần một số operations đơn giản
  • Layered architecture: Tạo entry point cho mỗi layer
  • Legacy code: Wrap legacy system với interface hiện đại

5. Composite Pattern

Mục đích

Tổ chức objects thành cấu trúc cây (tree structure). Client có thể xử lý individual objectgroup of objects một cách đồng nhất.

Ví dụ 1: File System

// Component interface
interface FileSystemComponent {
void showDetails();
int getSize();
}

// Leaf (file)
class File implements FileSystemComponent {
private String name;
private int size;

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

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

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

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

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

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

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

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

@Override
public int getSize() {
int total = 0;
for (FileSystemComponent child : children) {
total += child.getSize();
}
return total;
}
}

// Sử dụng
Folder root = new Folder("root");

Folder documents = new Folder("Documents");
documents.add(new File("resume.pdf", 50));
documents.add(new File("letter.docx", 30));

Folder photos = new Folder("Photos");
photos.add(new File("vacation.jpg", 200));
photos.add(new File("family.png", 150));

root.add(documents);
root.add(photos);
root.add(new File("readme.txt", 5));

root.showDetails();
System.out.println("\nTotal size: " + root.getSize() + " KB");

Output:

Folder: root
Folder: Documents
File: resume.pdf (50 KB)
File: letter.docx (30 KB)
Folder: Photos
File: vacation.jpg (200 KB)
File: family.png (150 KB)
File: readme.txt (5 KB)

Total size: 435 KB

Ví dụ 2: Organization Structure

interface Employee {
void showDetails();
double getSalary();
}

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);
}

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

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

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

public void addTeamMember(Employee employee) {
team.add(employee);
}

@Override
public void showDetails() {
System.out.println("Manager: " + name + " - $" + salary);
System.out.println(" Team:");
for (Employee emp : team) {
System.out.print(" ");
emp.showDetails();
}
}

@Override
public double getSalary() {
double total = salary;
for (Employee emp : team) {
total += emp.getSalary();
}
return total;
}
}

// Sử dụng
Developer dev1 = new Developer("Alice", 80000);
Developer dev2 = new Developer("Bob", 75000);
Developer dev3 = new Developer("Charlie", 70000);

Manager manager1 = new Manager("David", 100000);
manager1.addTeamMember(dev1);
manager1.addTeamMember(dev2);

Manager cto = new Manager("Eve", 150000);
cto.addTeamMember(manager1);
cto.addTeamMember(dev3);

cto.showDetails();
System.out.println("\nTotal payroll: $" + cto.getSalary());

Tổng hợp Structural Patterns

PatternMục đíchKhi nào dùngVí dụ thực tế
AdapterConvert interface A → BLegacy integration, third-party APIsArrays.asList(), InputStreamReader
DecoratorAdd behavior dynamicallyNhiều combinations, tránh subclass explosionJava I/O streams, Collections unmodifiable*()
ProxyControl access to objectLazy loading, access control, cachingHibernate lazy loading, Spring AOP
FacadeSimplify complex subsystemSubsystem phức tạp, cần simple interfacejava.net.URL, SLF4J logging
CompositeTree structure, treat uniformlyHierarchical data, part-whole relationshipsSwing/AWT components, XML DOM

Bài tập

Bài 1: Adapter - Temperature Converter

Implement adapter chuyển đổi:

  • FahrenheitSensor (interface cũ): readFahrenheit()
  • CelsiusSensor (interface mới): readCelsius()
  • Adapter để dùng FahrenheitSensor với code mới

Bài 2: Decorator - Pizza Toppings

Implement pizza với toppings:

  • Base: Margherita ($5)
  • Toppings: Cheese (+$1), Pepperoni (+$2), Mushrooms (+$1.5), Olives (+$1)
  • Method: getDescription(), getCost()

Bài 3: Proxy - Secure Document

Implement Protection Proxy:

  • 3 roles: VIEWER, EDITOR, ADMIN
  • VIEWER: view only
  • EDITOR: view + edit
  • ADMIN: view + edit + delete

Bài 4: Composite - Company Org Chart

Tạo org chart với:

  • Individual: Employee
  • Composite: Department (có nhiều employees/departments)
  • Method: showStructure(), getTotalHeadcount()

Tiếp theo, chúng ta sẽ học Behavioral Patterns để quản lý giao tiếp và trách nhiệm giữa các objects.

Đọc thêm