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

Creational Patterns

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

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

  • Hiểu 5 Creational Patterns: Singleton, Factory Method, Abstract Factory, Builder, Prototype
  • Biết cách implement thread-safe Singleton (enum, static holder, double-checked locking)
  • Nắm được Factory Method vs Abstract Factory — khi nào dùng cái gì
  • Áp dụng Builder pattern cho complex object construction
  • Phân biệt shallow copy vs deep copy trong Prototype pattern

Bài trước: Design Patterns: Giới thiệu — Đã tìm hiểu tổng quan về Design Patterns và SOLID. Bài này sẽ đi sâu vào nhóm Creational Patterns — các patterns tạo đối tượng.

Creational Patterns tập trung vào cách tạo object một cách linh hoạt và hiệu quả. Thay vì dùng new trực tiếp, các patterns này che giấu logic khởi tạo phức tạp, giúp code dễ bảo trì và mở rộng.

1. Singleton Pattern

Mục đích

Đảm bảo một class chỉ có một instance duy nhất trong toàn bộ ứng dụng và cung cấp global access point đến instance đó.

Khi nào dùng?

  • Configuration Manager: Quản lý cấu hình toàn cục
  • Logger: Ghi log tập trung
  • Database Connection Pool: Quản lý kết nối database
  • Cache Manager: Quản lý cache chung
Cẩn thận

Singleton có thể gây khó khăn cho testing (khó mock) và tạo hidden dependencies. Cân nhắc dùng Dependency Injection thay vì Singleton trong nhiều trường hợp.

Eager Initialization

Instance được tạo ngay khi class load.

public class DatabaseConnection {
// Instance được tạo ngay lập tức
private static final DatabaseConnection instance = new DatabaseConnection();

// Private constructor ngăn tạo instance từ bên ngoài
private DatabaseConnection() {
System.out.println("Database connection initialized");
}

public static DatabaseConnection getInstance() {
return instance;
}

public void executeQuery(String query) {
System.out.println("Executing: " + query);
}
}

// Sử dụng
DatabaseConnection db = DatabaseConnection.getInstance();
db.executeQuery("SELECT * FROM users");

Ưu điểm: Thread-safe, đơn giản Nhược điểm: Tạo instance ngay cả khi không dùng (lãng phí tài nguyên)

Lazy Initialization

Instance được tạo khi cần (lần đầu gọi getInstance()).

public class Logger {
private static Logger instance;

private Logger() {
System.out.println("Logger initialized");
}

public static Logger getInstance() {
if (instance == null) { // Tạo khi cần
instance = new Logger();
}
return instance;
}

public void log(String message) {
System.out.println("[LOG] " + message);
}
}

Vấn đề: Không thread-safe! Nhiều thread có thể tạo ra nhiều instance.

Thread-Safe Singleton

1. Synchronized Method (Chậm)

public class Logger {
private static Logger instance;

private Logger() {}

// synchronized đảm bảo thread-safe
public static synchronized Logger getInstance() {
if (instance == null) {
instance = new Logger();
}
return instance;
}
}

Vấn đề: Mỗi lần gọi getInstance() đều phải lock, rất chậm.

public class ConfigManager {
private static volatile ConfigManager instance; // volatile quan trọng!

private Map<String, String> config = new HashMap<>();

private ConfigManager() {
// Load configuration
config.put("app.name", "MyApp");
config.put("app.version", "1.0.0");
}

public static ConfigManager getInstance() {
if (instance == null) { // Check 1: Nhanh, không lock
synchronized (ConfigManager.class) {
if (instance == null) { // Check 2: Đảm bảo không tạo duplicate
instance = new ConfigManager();
}
}
}
return instance;
}

public String getConfig(String key) {
return config.get(key);
}
}

Giải thích:

  • volatile đảm bảo instance được update đúng trong multi-thread environment
  • Check 2 lần: lần 1 để tránh lock không cần thiết, lần 2 để đảm bảo thread-safe

3. Bill Pugh Singleton (Best Practice)

public class ConnectionPool {
private ConnectionPool() {
System.out.println("Connection pool initialized");
}

// Inner static class, chỉ load khi getInstance() được gọi
private static class SingletonHelper {
private static final ConnectionPool INSTANCE = new ConnectionPool();
}

public static ConnectionPool getInstance() {
return SingletonHelper.INSTANCE;
}

public void getConnection() {
System.out.println("Getting connection from pool");
}
}

Ưu điểm:

  • Lazy initialization
  • Thread-safe (JVM đảm bảo)
  • Không cần synchronized
  • Performance cao

4. Enum Singleton (Simplest & Best)

public enum AppConfig {
INSTANCE; // Chỉ có 1 instance

private Map<String, String> properties = new HashMap<>();

// Constructor
AppConfig() {
properties.put("db.host", "localhost");
properties.put("db.port", "5432");
}

public String getProperty(String key) {
return properties.get(key);
}

public void setProperty(String key, String value) {
properties.put(key, value);
}
}

// Sử dụng
AppConfig config = AppConfig.INSTANCE;
System.out.println(config.getProperty("db.host")); // localhost

Ưu điểm:

  • Đơn giản nhất
  • Thread-safe (JVM đảm bảo)
  • Chống Serialization attack
  • Chống Reflection attack
Khuyến nghị

Dùng Enum Singleton cho hầu hết trường hợp. Nếu cần lazy initialization phức tạp, dùng Bill Pugh Singleton.

2. Factory Method Pattern

Mục đích

Tạo object thông qua method thay vì gọi constructor trực tiếp (new). Cho phép subclass quyết định class nào sẽ được khởi tạo.

Simple Factory vs Factory Method

Simple Factory (Không phải GoF pattern)

// Product interface
interface Payment {
void processPayment(double amount);
}

// Concrete products
class CreditCardPayment implements Payment {
public void processPayment(double amount) {
System.out.println("Processing credit card payment: $" + amount);
}
}

class PayPalPayment implements Payment {
public void processPayment(double amount) {
System.out.println("Processing PayPal payment: $" + amount);
}
}

class CashPayment implements Payment {
public void processPayment(double amount) {
System.out.println("Processing cash payment: $" + amount);
}
}

// Simple Factory
class PaymentFactory {
public static Payment createPayment(String type) {
switch (type.toLowerCase()) {
case "credit":
return new CreditCardPayment();
case "paypal":
return new PayPalPayment();
case "cash":
return new CashPayment();
default:
throw new IllegalArgumentException("Unknown payment type: " + type);
}
}
}

// Sử dụng
Payment payment = PaymentFactory.createPayment("paypal");
payment.processPayment(100.0);

Vấn đề: Thêm payment type mới phải sửa PaymentFactory (vi phạm Open/Closed Principle).

Factory Method (GoF Pattern)

// Product interface
interface Notification {
void send(String message);
}

// Concrete products
class EmailNotification implements Notification {
public void send(String message) {
System.out.println("Email: " + message);
}
}

class SMSNotification implements Notification {
public void send(String message) {
System.out.println("SMS: " + message);
}
}

class PushNotification implements Notification {
public void send(String message) {
System.out.println("Push: " + message);
}
}

// Creator (abstract class hoặc interface)
abstract class NotificationService {
// Factory Method (để subclass override)
protected abstract Notification createNotification();

// Business logic sử dụng factory method
public void notifyUser(String message) {
Notification notification = createNotification();
notification.send(message);
}
}

// Concrete creators
class EmailNotificationService extends NotificationService {
protected Notification createNotification() {
return new EmailNotification();
}
}

class SMSNotificationService extends NotificationService {
protected Notification createNotification() {
return new SMSNotification();
}
}

// Sử dụng
NotificationService service = new EmailNotificationService();
service.notifyUser("Welcome to our app!"); // Email: Welcome to our app!

service = new SMSNotificationService();
service.notifyUser("Your code is 1234"); // SMS: Your code is 1234

Ưu điểm: Thêm notification type mới chỉ cần tạo subclass mới, không sửa code cũ.

3. Abstract Factory Pattern

Mục đích

Tạo các nhóm object liên quan mà không cần chỉ định class cụ thể. "Factory of factories".

Ví dụ: UI Component Factory

// Abstract products
interface Button {
void render();
}

interface TextField {
void render();
}

// Windows products
class WindowsButton implements Button {
public void render() {
System.out.println("Rendering Windows button");
}
}

class WindowsTextField implements TextField {
public void render() {
System.out.println("Rendering Windows text field");
}
}

// Mac products
class MacButton implements Button {
public void render() {
System.out.println("Rendering Mac button");
}
}

class MacTextField implements TextField {
public void render() {
System.out.println("Rendering Mac text field");
}
}

// Abstract Factory
interface UIFactory {
Button createButton();
TextField createTextField();
}

// Concrete factories
class WindowsUIFactory implements UIFactory {
public Button createButton() {
return new WindowsButton();
}

public TextField createTextField() {
return new WindowsTextField();
}
}

class MacUIFactory implements UIFactory {
public Button createButton() {
return new MacButton();
}

public TextField createTextField() {
return new MacTextField();
}
}

// Client code
class Application {
private Button button;
private TextField textField;

public Application(UIFactory factory) {
button = factory.createButton();
textField = factory.createTextField();
}

public void render() {
button.render();
textField.render();
}
}

// Sử dụng
String os = System.getProperty("os.name").toLowerCase();
UIFactory factory = os.contains("windows")
? new WindowsUIFactory()
: new MacUIFactory();

Application app = new Application(factory);
app.render();
Khi nào dùng Abstract Factory?
  • Hệ thống cần làm việc với nhiều family of products (Windows UI, Mac UI)
  • Muốn đảm bảo các products trong cùng family tương thích với nhau
  • Muốn che giấu implementation details của products

4. Builder Pattern

Mục đích

Xây dựng object phức tạp từng bước một (step-by-step). Tách riêng construction logic khỏi representation.

Vấn đề: Telescoping Constructor

// Anti-pattern: Quá nhiều parameters
public class User {
private String firstName;
private String lastName;
private int age;
private String phone;
private String address;
private String email;

// Constructor hell!
public User(String firstName, String lastName) {
this(firstName, lastName, 0);
}

public User(String firstName, String lastName, int age) {
this(firstName, lastName, age, null);
}

public User(String firstName, String lastName, int age, String phone) {
this(firstName, lastName, age, phone, null);
}

// ... rất nhiều constructors
}

Giải pháp: Builder Pattern

public class User {
// Required parameters
private final String firstName;
private final String lastName;

// Optional parameters
private final int age;
private final String phone;
private final String address;
private final String email;

// Private constructor, chỉ Builder gọi được
private User(Builder builder) {
this.firstName = builder.firstName;
this.lastName = builder.lastName;
this.age = builder.age;
this.phone = builder.phone;
this.address = builder.address;
this.email = builder.email;
}

// Getters
public String getFirstName() { return firstName; }
public String getLastName() { return lastName; }
// ... other getters

@Override
public String toString() {
return "User{" +
"firstName='" + firstName + '\'' +
", lastName='" + lastName + '\'' +
", age=" + age +
", phone='" + phone + '\'' +
", email='" + email + '\'' +
'}';
}

// Static nested Builder class
public static class Builder {
// Required parameters
private final String firstName;
private final String lastName;

// Optional parameters - default values
private int age = 0;
private String phone = "";
private String address = "";
private String email = "";

// Constructor với required parameters
public Builder(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}

// Fluent API: return this để chain methods
public Builder age(int age) {
this.age = age;
return this;
}

public Builder phone(String phone) {
this.phone = phone;
return this;
}

public Builder address(String address) {
this.address = address;
return this;
}

public Builder email(String email) {
this.email = email;
return this;
}

// Build method tạo User instance
public User build() {
// Validation (optional)
if (age < 0) {
throw new IllegalStateException("Age cannot be negative");
}
return new User(this);
}
}
}

// Sử dụng - Fluent API rất dễ đọc!
User user1 = new User.Builder("John", "Doe")
.age(30)
.email("[email protected]")
.phone("123-456-7890")
.build();

User user2 = new User.Builder("Jane", "Smith")
.age(25)
.build(); // Chỉ set required + age

System.out.println(user1);
System.out.println(user2);

Ví dụ 2: Pizza Builder

public class Pizza {
private final String size;
private final boolean cheese;
private final boolean pepperoni;
private final boolean mushrooms;
private final boolean olives;

private Pizza(Builder builder) {
this.size = builder.size;
this.cheese = builder.cheese;
this.pepperoni = builder.pepperoni;
this.mushrooms = builder.mushrooms;
this.olives = builder.olives;
}

@Override
public String toString() {
return "Pizza{size=" + size + ", cheese=" + cheese +
", pepperoni=" + pepperoni + ", mushrooms=" + mushrooms +
", olives=" + olives + "}";
}

public static class Builder {
private final String size; // required

private boolean cheese = false;
private boolean pepperoni = false;
private boolean mushrooms = false;
private boolean olives = false;

public Builder(String size) {
this.size = size;
}

public Builder cheese() {
this.cheese = true;
return this;
}

public Builder pepperoni() {
this.pepperoni = true;
return this;
}

public Builder mushrooms() {
this.mushrooms = true;
return this;
}

public Builder olives() {
this.olives = true;
return this;
}

public Pizza build() {
return new Pizza(this);
}
}
}

// Sử dụng
Pizza pizza = new Pizza.Builder("Large")
.cheese()
.pepperoni()
.mushrooms()
.build();

System.out.println(pizza);
Khi nào dùng Builder?
  • Object có nhiều parameters (>4-5), đặc biệt là optional parameters
  • Cần immutable objects (final fields)
  • Muốn fluent API dễ đọc
  • Cần validation phức tạp trước khi tạo object

5. Prototype Pattern

Mục đích

Tạo object mới bằng cách clone object có sẵn thay vì tạo từ đầu. Hữu ích khi khởi tạo object tốn kém (expensive).

Shallow Copy vs Deep Copy

// Cloneable interface
class Address implements Cloneable {
private String street;
private String city;

public Address(String street, String city) {
this.street = street;
this.city = city;
}

// Getters and setters
public String getStreet() { return street; }
public void setStreet(String street) { this.street = street; }
public String getCity() { return city; }

@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}

@Override
public String toString() {
return street + ", " + city;
}
}

class Person implements Cloneable {
private String name;
private int age;
private Address address; // Reference type

public Person(String name, int age, Address address) {
this.name = name;
this.age = age;
this.address = address;
}

// Shallow copy (default clone())
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone(); // Chỉ copy reference của address!
}

// Deep copy (custom)
public Person deepClone() throws CloneNotSupportedException {
Person cloned = (Person) super.clone();
cloned.address = (Address) address.clone(); // Clone address object
return cloned;
}

public String getName() { return name; }
public Address getAddress() { return address; }

@Override
public String toString() {
return "Person{name='" + name + "', age=" + age + ", address=" + address + "}";
}
}

// Sử dụng
Address address = new Address("123 Main St", "New York");
Person person1 = new Person("John", 30, address);

// Shallow copy
Person person2 = (Person) person1.clone();
person2.getAddress().setStreet("456 Oak Ave"); // Thay đổi ảnh hưởng cả person1!

System.out.println("Person 1: " + person1); // Address changed!
System.out.println("Person 2: " + person2);

// Deep copy
Person person3 = person1.deepClone();
person3.getAddress().setStreet("789 Pine Rd"); // Không ảnh hưởng person1

System.out.println("Person 1: " + person1); // Address unchanged
System.out.println("Person 3: " + person3);
Shallow vs Deep Copy
  • Shallow copy: Copy giá trị của các fields. Reference types vẫn trỏ đến object gốc.
  • Deep copy: Copy cả object mà reference trỏ đến. Object mới hoàn toàn độc lập.

Prototype Registry

import java.util.HashMap;
import java.util.Map;

// Abstract prototype
abstract class Shape implements Cloneable {
private String id;
protected String type;

public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getType() { return type; }

public abstract void draw();

@Override
public Object clone() {
try {
return super.clone();
} catch (CloneNotSupportedException e) {
return null;
}
}
}

// Concrete prototypes
class Circle extends Shape {
public Circle() {
type = "Circle";
}

public void draw() {
System.out.println("Drawing Circle");
}
}

class Rectangle extends Shape {
public Rectangle() {
type = "Rectangle";
}

public void draw() {
System.out.println("Drawing Rectangle");
}
}

// Prototype Registry (cache)
class ShapeCache {
private static Map<String, Shape> shapeMap = new HashMap<>();

public static Shape getShape(String shapeId) {
Shape cachedShape = shapeMap.get(shapeId);
return (Shape) cachedShape.clone(); // Clone từ cache
}

// Load prototypes vào cache
public static void loadCache() {
Circle circle = new Circle();
circle.setId("1");
shapeMap.put(circle.getId(), circle);

Rectangle rectangle = new Rectangle();
rectangle.setId("2");
shapeMap.put(rectangle.getId(), rectangle);
}
}

// Sử dụng
ShapeCache.loadCache();

Shape clonedCircle1 = ShapeCache.getShape("1");
System.out.println("Type: " + clonedCircle1.getType()); // Circle
clonedCircle1.draw();

Shape clonedCircle2 = ShapeCache.getShape("1");
clonedCircle2.draw();

System.out.println(clonedCircle1 == clonedCircle2); // false (different instances)

Tổng hợp Creational Patterns

PatternMục đíchKhi nào dùngVí dụ thực tế
SingletonĐảm bảo chỉ 1 instanceLogger, Config, Connection PoolRuntime.getRuntime()
Factory MethodTạo object qua methodKhông biết chính xác class nào sẽ tạoCalendar.getInstance()
Abstract FactoryTạo family of objectsCần tạo nhiều objects liên quanJDBC Connection factories
BuilderXây dựng object phức tạpObject có nhiều parametersStringBuilder, Stream.builder()
PrototypeClone objectKhởi tạo tốn kém, cần copyObject.clone(), ArrayList(Collection)

Bài tập

Bài 1: Singleton - Logger

Implement Logger class với:

  • Enum Singleton
  • Method log(Level level, String message) với Level = INFO, WARNING, ERROR
  • Ghi log ra console với timestamp

Bài 2: Builder - Computer

Tạo Computer class với Builder:

  • Required: CPU, RAM
  • Optional: GPU, SSD, HDD, Bluetooth, WiFi
  • Validation: RAM >= 4GB, SSD/HDD ít nhất 1 cái

Bài 3: Factory - Document

Tạo Document factory:

  • Document interface với open(), save()
  • Concrete: PDFDocument, WordDocument, ExcelDocument
  • DocumentFactory.createDocument(String type)

Bài 4: Prototype - Game Character

Clone game characters với:

  • Character class: name, health, mana, equipment (List)
  • Implement deep clone cho equipment
  • Cache các character templates

Trong bài tiếp theo, chúng ta sẽ học Structural Patterns để tổ chức và kết nối các class/object hiệu quả hơn.

Đọc thêm