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

Giới thiệu Design Patterns

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

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

  • Hiểu Design Patterns là gì và tại sao chúng quan trọng
  • Nắm được 3 nhóm patterns: Creational, Structural, Behavioral
  • Hiểu nguyên tắc SOLID và mối quan hệ với patterns
  • Biết khi nào nên và không nên áp dụng design patterns
  • Có cái nhìn tổng quan để chọn pattern phù hợp cho từng vấn đề

Design Pattern là gì?

Design Pattern (mẫu thiết kế) là các giải pháp chung, có thể tái sử dụng cho các vấn đề thường gặp trong thiết kế phần mềm. Đây không phải là code hoàn chỉnh có thể copy-paste, mà là template (khuôn mẫu) để giải quyết một vấn đề có thể áp dụng trong nhiều tình huống khác nhau.

Lợi ích của Design Patterns
  • Tái sử dụng: Giải pháp đã được kiểm chứng, không phải "phát minh lại bánh xe"
  • Giao tiếp: Tạo ngôn ngữ chung giữa các developer ("Dùng Singleton cho class này")
  • Tối ưu hóa: Cải thiện cấu trúc code, dễ bảo trì và mở rộng
  • Best Practices: Học từ kinh nghiệm của các chuyên gia

Ví dụ minh họa

Giả sử bạn cần đảm bảo một class chỉ có một instance duy nhất trong toàn bộ ứng dụng (ví dụ: class quản lý cấu hình). Thay vì tự nghĩ ra giải pháp, bạn có thể áp dụng Singleton Pattern - một giải pháp đã được chuẩn hóa và kiểm chứng.

Lịch sử: Gang of Four (GoF)

Design Patterns trở nên phổ biến nhờ cuốn sách "Design Patterns: Elements of Reusable Object-Oriented Software" xuất bản năm 1994 bởi 4 tác giả:

  • Erich Gamma
  • Richard Helm
  • Ralph Johnson
  • John Vlissides

Họ được gọi là Gang of Four (GoF). Cuốn sách này giới thiệu 23 Design Patterns cơ bản, được chia thành 3 nhóm.

Ba nhóm Design Patterns

1. Creational Patterns (Khởi tạo)

Liên quan đến cách tạo object. Thay vì dùng new trực tiếp, các patterns này cung cấp cơ chế tạo object linh hoạt hơn.

Mục đích: Che giấu logic khởi tạo phức tạp, tăng tính linh hoạt.

2. Structural Patterns (Cấu trúc)

Liên quan đến cách tổ chức class và object để tạo thành cấu trúc lớn hơn.

Mục đích: Đơn giản hóa mối quan hệ giữa các entities, dễ mở rộng.

3. Behavioral Patterns (Hành vi)

Liên quan đến giao tiếp giữa các object và cách phân chia trách nhiệm.

Mục đích: Quản lý thuật toán, quan hệ và trách nhiệm giữa các object.

23 GoF Design Patterns

PatternNhómMô tả ngắn
SingletonCreationalĐảm bảo một class chỉ có một instance duy nhất
Factory MethodCreationalTạo object thông qua method thay vì constructor
Abstract FactoryCreationalTạo các nhóm object liên quan mà không chỉ định class cụ thể
BuilderCreationalXây dựng object phức tạp từng bước một
PrototypeCreationalTạo object mới bằng cách clone object có sẵn
AdapterStructuralChuyển đổi interface này sang interface khác
BridgeStructuralTách abstraction khỏi implementation
CompositeStructuralTổ chức object thành cấu trúc cây (tree)
DecoratorStructuralThêm behavior mới cho object một cách động
FacadeStructuralCung cấp interface đơn giản cho subsystem phức tạp
FlyweightStructuralChia sẻ object để tiết kiệm bộ nhớ
ProxyStructuralCung cấp placeholder hoặc đại diện cho object khác
Chain of ResponsibilityBehavioralChuyển request qua chuỗi handler
CommandBehavioralĐóng gói request thành object
InterpreterBehavioralĐịnh nghĩa grammar cho ngôn ngữ và interpret nó
IteratorBehavioralTruy cập tuần tự các phần tử mà không lộ cấu trúc bên trong
MediatorBehavioralĐịnh nghĩa object trung gian để giảm coupling giữa các object
MementoBehavioralLưu và khôi phục trạng thái của object
ObserverBehavioralThông báo cho nhiều object khi có thay đổi
StateBehavioralThay đổi behavior khi internal state thay đổi
StrategyBehavioralĐóng gói các thuật toán và làm chúng có thể thay thế lẫn nhau
Template MethodBehavioralĐịnh nghĩa skeleton của thuật toán, để subclass override các bước cụ thể
VisitorBehavioralTách thuật toán khỏi cấu trúc object

Khi nào nên dùng Design Patterns?

Nên dùng khi:

  1. Vấn đề lặp đi lặp lại: Bạn gặp phải vấn đề tương tự nhiều lần
  2. Code phức tạp: Cần cấu trúc rõ ràng để quản lý độ phức tạp
  3. Dự đoán thay đổi: Biết trước một phần của hệ thống sẽ thay đổi thường xuyên
  4. Mở rộng: Cần thêm tính năng mà không sửa code hiện tại
Ví dụ thực tế
  • Strategy Pattern: Khi có nhiều cách thanh toán (credit card, PayPal, cash)
  • Observer Pattern: Khi cần cập nhật UI khi dữ liệu thay đổi
  • Factory Pattern: Khi cần tạo nhiều loại object tương tự nhưng khác implementation

KHÔNG nên dùng khi:

  1. Over-engineering: Áp dụng pattern khi vấn đề đơn giản
  2. Chưa cần thiết: "Might need it later" - vi phạm YAGNI principle
  3. Không hiểu rõ: Dùng pattern chỉ để "show off"
  4. Vấn đề nhỏ: Solution đơn giản (vài dòng code) đủ giải quyết
Cẩn thận với Over-engineering
// KHÔNG CẦN THIẾT - code đơn giản
public interface CalculatorStrategy {
int calculate(int a, int b);
}

public class AddStrategy implements CalculatorStrategy {
public int calculate(int a, int b) { return a + b; }
}

// Chỉ cần thế này thôi!
public int add(int a, int b) {
return a + b;
}

Anti-Patterns: Những điều nên tránh

1. Golden Hammer

"Tôi có một cái búa, mọi thứ đều là cái đinh"

Áp dụng một pattern quen thuộc cho mọi vấn đề, dù nó không phù hợp.

// Không phải mọi class đều cần phải là Singleton!
public class Calculator {
private static Calculator instance;

private Calculator() {} // Tại sao lại singleton?

public static Calculator getInstance() {
if (instance == null) {
instance = new Calculator();
}
return instance;
}

public int add(int a, int b) { return a + b; }
}

2. Over-engineering

Tạo cấu trúc phức tạp không cần thiết cho vấn đề đơn giản.

// Quá phức tạp cho một task đơn giản
public interface StringProcessorFactory {
StringProcessor createProcessor();
}

public abstract class AbstractStringProcessor {
protected abstract String process(String input);
}

// Trong khi chỉ cần:
public class StringUtil {
public static String toUpperCase(String input) {
return input.toUpperCase();
}
}

3. Premature Optimization

Tối ưu hóa quá sớm, làm code phức tạp khi chưa có proof về performance issue.

4. God Object

Một class làm quá nhiều việc, vi phạm Single Responsibility Principle.

UML Class Diagram Basics

Design Patterns thường được biểu diễn bằng UML Class Diagram. Hiểu UML giúp bạn đọc và áp dụng patterns hiệu quả.

Các ký hiệu cơ bản

┌─────────────────┐
│ ClassName │ ← Class name (in đậm)
├─────────────────┤
│ - privateField │ ← Attributes
│ + publicField │ - : private
│ # protectedField│ + : public
├─────────────────┤ # : protected
│ + method() │ ← Methods
│ - privateMethod()│
└─────────────────┘

Mối quan hệ (Relationships)

Ký hiệuTênÝ nghĩa
───────▷Inheritance (Kế thừa)Class con extends class cha
- - - -▷Implementation (Triển khai)Class implements interface
───────>Association (Liên kết)Class A có quan hệ với class B
───────◇Aggregation (Tập hợp)Class A chứa class B (B có thể tồn tại độc lập)
───────◆Composition (Hợp thành)Class A chứa class B (B không tồn tại nếu thiếu A)
- - - ->Dependency (Phụ thuộc)Class A sử dụng class B tạm thời

Ví dụ đọc UML Diagram

      ┌─────────────┐
│ <<interface>>│
│ Vehicle │
├─────────────┤
│+ start() │
│+ stop() │
└─────────────┘

│ implements

┌─────────────┐
│ Car │
├─────────────┤
│- brand │
├─────────────┤
│+ start() │
│+ stop() │
└─────────────┘

Giải thích: Class Car implements interface Vehicle, phải cung cấp implementation cho start()stop().

Nguyên tắc khi học Design Patterns

  1. Hiểu vấn đề trước: Biết pattern giải quyết vấn đề gì
  2. Biết khi nào dùng: Nhận diện situation phù hợp
  3. Biết khi nào KHÔNG dùng: Tránh over-engineering
  4. Thực hành: Implement patterns trong project nhỏ
  5. Đọc code thực tế: Xem framework/library dùng patterns như thế nào
Cách học hiệu quả
  • Học 3-5 patterns phổ biến trước: Singleton, Factory, Observer, Strategy, Decorator
  • Tìm ví dụ trong Java API: java.io dùng Decorator, Collections dùng Iterator
  • Refactor code cũ: Áp dụng pattern vào code đã viết để cải thiện
  • Code review: Nhận diện patterns trong code của người khác

Tóm tắt

Khái niệmGiải thích
Design PatternGiải pháp tái sử dụng cho vấn đề thường gặp
GoF4 tác giả cuốn sách Design Patterns (1994)
23 GoF PatternsChia thành 3 nhóm: Creational, Structural, Behavioral
CreationalCách tạo object (Singleton, Factory, Builder...)
StructuralCách tổ chức class/object (Adapter, Decorator, Proxy...)
BehavioralCách giao tiếp giữa các object (Strategy, Observer, Command...)
Anti-patternCách làm SAI: Over-engineering, Golden Hammer
UML DiagramCông cụ biểu diễn patterns

Bài tập

Bài 1: Nhận diện vấn đề

Với mỗi tình huống sau, bạn nghĩ pattern nào phù hợp?

  1. Cần tạo nhiều loại báo cáo (PDF, Excel, HTML) với format khác nhau
  2. Một button cần thực hiện các action khác nhau tùy context
  3. Cần log thông tin, nhưng chỉ muốn 1 Logger instance duy nhất
  4. Cần thêm tính năng mã hóa cho file mà không sửa class File

Bài 2: Anti-pattern

Tìm vấn đề trong đoạn code sau:

public class DataManager {
private static DataManager instance;

private DataManager() {}

public static DataManager getInstance() {
if (instance == null) {
instance = new DataManager();
}
return instance;
}

public void saveUser(User user) { /* ... */ }
public void sendEmail(String email) { /* ... */ }
public void generateReport() { /* ... */ }
public void processPayment(Payment p) { /* ... */ }
public void uploadFile(File f) { /* ... */ }
}

Gợi ý: Có 2 anti-patterns ở đây!


Trong các bài tiếp theo, chúng ta sẽ đi sâu vào từng nhóm patterns với ví dụ code cụ thể và bài tập thực hành.

Đọc thêm