- vừa được xem lúc

OOP Design Patterns: Factory Pattern

0 0 3

Người đăng: Jimmy Nguyễn

Theo Viblo Asia

Xin chào anh em, lại là tôi - Jim đến từ Trà đá công nghệ đây!

Trong lĩnh vực kỹ thuật phần mềm, các mẫu thiết kế (design patterns) đại diện cho một kho tàng tri thức được đúc kết, cung cấp các giải pháp đã được kiểm chứng cho những vấn đề lặp đi lặp lại trong thiết kế hướng đối tượng. Chúng không chỉ là các đoạn mã, mà là những lược đồ tư duy giúp kiến tạo nên các hệ thống linh hoạt và bền vững.

Hôm nay, tôi sẽ cùng anh em đi vào một phân tích chuyên sâu về một trong những họ mẫu thiết kế nền tảng nhất: Factory Pattern. Bài viết này sẽ không dừng lại ở việc trình bày cú pháp, mà sẽ là một quá trình luận giải có hệ thống. Chúng ta sẽ bắt đầu từ một bài toán thực tiễn, phân tích các điểm yếu trong cách tiếp cận ban đầu, và từ đó xây dựng các giải pháp ngày càng tinh vi hơn, bao gồm Simple Factory, Factory Method, và Abstract Factory.

Thông qua các ví dụ cụ thể bằng Java và những phân tích chi tiết về ưu nhược điểm, mục tiêu là trang bị cho anh em một sự hiểu biết sâu sắc, cho phép anh em áp dụng các mẫu hình này một cách chính xác và hiệu quả trong các dự án của mình.

1. Khởi Nguồn Vấn Đề: Sự Phụ Thuộc Vào Việc Khởi Tạo Đối Tượng Cụ Thể

Mọi hệ thống phần mềm đều được cấu thành từ các đối tượng tương tác với nhau. Tuy nhiên, hành động khởi tạo đối tượng, tưởng chừng như đơn giản, lại là một trong những điểm có thể làm suy giảm nghiêm trọng chất lượng kiến trúc nếu không được quản lý cẩn thận.

1.1 Bối Cảnh: Hệ Thống Logistics

Để cụ thể hóa vấn đề, chúng ta hãy xét một ví dụ: xây dựng một module logistics với yêu cầu ban đầu là vận chuyển hàng hóa bằng xe tải.

Một triển khai trực tiếp sẽ có cấu trúc như sau:

// Lớp sản phẩm cụ thể (Concrete Product)
public class Truck { public void deliver() { System.out.println("Giao hàng bằng xe tải qua đường bộ."); }
} // Lớp client sử dụng sản phẩm
public class Logistics { public void planDelivery() { // Toán tử `new` được gọi trực tiếp bên trong logic nghiệp vụ Truck truck = new Truck(); truck.deliver(); }
}

Thiết kế này đáp ứng được yêu cầu ban đầu, nhưng nó chứa đựng một sự phụ thuộc cứng nhắc. Lớp Logistics (client) bị ràng buộc trực tiếp vào lớp Truck (sản phẩm cụ thể).

1.2 Sự Mong Manh Của Cấu Trúc Điều Kiện if-else

Khi yêu cầu phát triển, ví dụ như cần hỗ trợ thêm vận chuyển bằng đường biển, thiết kế ban đầu sẽ bộc lộ điểm yếu.

// Thêm một sản phẩm cụ thể khác
public class Ship { public void deliver() { System.out.println("Giao hàng bằng tàu thủy qua đường biển."); }
} // Lớp client bị sửa đổi
public class Logistics { public void planDelivery(String transportType) { // Logic lựa chọn đối tượng bị nhúng vào client code if (transportType.equalsIgnoreCase("road")) { Truck truck = new Truck(); truck.deliver(); } else if (transportType.equalsIgnoreCase("sea")) { Ship ship = new Ship(); ship.deliver(); } // Cấu trúc này sẽ ngày càng phình to và phức tạp }
}

Kiến trúc này đã vi phạm các nguyên tắc thiết kế phần mềm cốt lõi:

  • Vi phạm Nguyên tắc Mở/Đóng (Open/Closed Principle - OCP): Hệ thống phải được "đóng" để sửa đổi nhưng "mở" để mở rộng. Ở đây, mỗi khi có một phương thức vận tải mới, chúng ta buộc phải sửa đổi mã nguồn của lớp Logistics, thay vì mở rộng nó.
  • Vi phạm Nguyên tắc Trách nhiệm Đơn lẻ (Single Responsibility Principle - SRP): Lớp Logistics giờ đây đảm nhiệm hai trách nhiệm: (1) thực thi logic nghiệp vụ và (2) quyết định tạo ra đối tượng nào. Việc này làm tăng độ phức tạp và giảm khả năng bảo trì của lớp.
  • Liên kết Chặt chẽ (Tight Coupling): Lớp Logistics phụ thuộc trực tiếp vào các lớp cụ thể (Truck, Ship). Bất kỳ thay đổi nào về constructor hoặc cách khởi tạo của các lớp này đều sẽ ảnh hưởng đến client code.

Vấn đề cốt lõi nằm ở việc trộn lẫn logic sử dụng đối tượng và logic tạo đối tượng. Giải pháp là tách biệt hai mối quan tâm này. Đây là tiền đề cho sự ra đời của các mẫu thiết kế sáng tạo, mà Factory Pattern là đại diện tiêu biểu.

2. Giải Pháp Khởi Đầu: Simple Factory

Để giải quyết vấn đề trên, bước tiếp cận đầu tiên là tập trung hóa logic khởi tạo vào một điểm duy nhất. Kỹ thuật này được gọi là Simple Factory.

2.1 Định Nghĩa và Mục Đích

Cần lưu ý rằng Simple Factory không phải là một mẫu thiết kế chính thức trong danh sách 23 mẫu của "Gang of Four" (GoF). Nó được xem là một idiom lập trình (programming idiom) — một quy ước phổ biến.

Về bản chất, Simple Factory là một lớp có một phương thức chuyên dụng (thường là static) để đóng gói logic lựa chọn và khởi tạo đối tượng. Client sẽ gọi phương thức này thay vì sử dụng toán tử new trực tiếp.

2.2 Tái Cấu Trúc Kiến Trúc Logistics

Hãy cùng tôi áp dụng Simple Factory vào hệ thống.

Bước 1: Giới thiệu một interface chung cho sản phẩm.

// Product Interface
public interface Transport { void deliver();
}

Bước 2: Các lớp sản phẩm cụ thể triển khai interface này.

// Concrete Product 1
public class Truck implements Transport { @Override public void deliver() { /* ... */ }
} // Concrete Product 2
public class Ship implements Transport { @Override public void deliver() { /* ... */ }
}

Bước 3: Xây dựng lớp Factory.

// Simple Factory
public class TransportFactory { public static Transport createTransport(String transportType) { if (transportType == null || transportType.isEmpty()) { return null; } switch (transportType.toLowerCase()) { case "road": return new Truck(); case "sea": return new Ship(); default: throw new IllegalArgumentException("Loại phương tiện không xác định: " + transportType); } }
}

Bước 4: Client code được đơn giản hóa.

// Client đã được cải thiện
public class Logistics { public void planDelivery(String transportType) { // Ủy quyền việc khởi tạo cho Factory Transport transport = TransportFactory.createTransport(transportType); // Logic nghiệp vụ không còn bị ảnh hưởng bởi việc tạo đối tượng if (transport != null) { transport.deliver(); } }
}

2.3 Phân Tích Ưu và Nhược Điểm

  • Ưu điểm:

    • Tập trung hóa logic khởi tạo: Mã nguồn liên quan đến việc tạo đối tượng được gom về một nơi, giúp dễ quản lý hơn.
    • Giảm khớp nối (Decoupling): Client không còn phụ thuộc vào các lớp sản phẩm cụ thể, mà chỉ phụ thuộc vào interface Transport và lớp TransportFactory.
  • Nhược điểm:

    • Vẫn vi phạm Nguyên tắc Mở/Đóng (OCP): Đây là hạn chế lớn nhất. Khi có thêm một loại sản phẩm mới (Airplane), chúng ta vẫn phải sửa đổi mã nguồn của TransportFactory.

Simple Factory là một bước cải tiến quan trọng, nhưng nó chưa giải quyết triệt để vấn đề. Để đạt được sự tuân thủ OCP, chúng ta cần một giải pháp ở cấp độ kiến trúc cao hơn: Factory Method Pattern.

3. Mẫu Thiết Kế Factory Method: Ủy Quyền Khởi Tạo Thông Qua Kế Thừa

Nếu Simple Factory là một giải pháp cấp lớp, thì Factory Method là một giải pháp ở cấp độ khung (framework), cho phép các lớp con định nghĩa đối tượng mà chúng sẽ tạo ra.

3.1 Định Nghĩa Chính Thức (GoF)

Factory Method được định nghĩa như sau:

"Define an interface for creating an object, but let subclasses decide which class to instantiate. Factory Method lets a class defer instantiation to subclasses."

Dịch nghĩa: "Định nghĩa một giao diện để tạo một đối tượng, nhưng để các lớp con quyết định lớp nào sẽ được khởi tạo. Factory Method cho phép một lớp ủy quyền việc khởi tạo cho các lớp con."

Điểm cốt lõi ở đây là sự ủy quyền (deferral) thông qua cơ chế kế thừa (inheritance).

3.2 Các Thành Phần Cấu Trúc

  1. Product: Interface hoặc lớp trừu tượng cho các sản phẩm. (Transport)
  2. ConcreteProduct: Các lớp cụ thể triển khai Product. (Truck, Ship)
  3. Creator: Lớp trừu tượng khai báo một factoryMethod() trừu tượng và chứa logic nghiệp vụ sử dụng sản phẩm.
  4. ConcreteCreator: Các lớp con kế thừa từ Creator và ghi đè factoryMethod() để tạo ra một ConcreteProduct cụ thể.

3.3 Ví Dụ Triển Khai Trong Java

Chúng ta sẽ tái cấu trúc hệ thống logistics một lần nữa.

Bước 1: Giữ nguyên Product và ConcreteProduct. Bước 2: Chuyển Logistics thành một lớp Creator trừu tượng.

// Creator (Abstract Class)
public abstract class Logistics { // Phương thức này chứa logic nghiệp vụ cốt lõi, không thay đổi. public void planDelivery() { Transport t = createTransport(); t.deliver(); } // The Factory Method - được khai báo là abstract public abstract Transport createTransport();
}

Bước 3: Tạo các ConcreteCreator.

// Concrete Creator 1
public class RoadLogistics extends Logistics { @Override public Transport createTransport() { return new Truck(); // Chịu trách nhiệm tạo Truck }
} // Concrete Creator 2
public class SeaLogistics extends Logistics { @Override public Transport createTransport() { return new Ship(); // Chịu trách nhiệm tạo Ship }
}

Bước 4: Cấu hình và sử dụng trong Client.

public class Application { private static Logistics logistics; public static void configure(String transportType) { if (transportType.equalsIgnoreCase("road")) { logistics = new RoadLogistics(); } else if (transportType.equalsIgnoreCase("sea")) { logistics = new SeaLogistics(); } } public static void main(String[] args) { configure("sea"); // Cấu hình dựa trên môi trường hoặc file config logistics.planDelivery(); }
}

3.4 Phân Tích Ưu và Nhược Điểm

  • Ưu điểm:

    • Tuân thủ triệt để Nguyên tắc Mở/Đóng (OCP): Để thêm phương tiện Airplane, chúng ta chỉ cần tạo lớp AirLogistics mới mà không cần chạm vào mã nguồn hiện có.
    • Tăng tính linh hoạt và khả năng mở rộng: Cung cấp "điểm móc nối" (hook) cho các lớp con.
    • Tách biệt rõ ràng trách nhiệm.
  • Nhược điểm:

    • Tăng độ phức tạp: Yêu cầu tạo ra một hệ thống phân cấp Creator song song với hệ thống phân cấp Product, có thể làm mã nguồn cồng kềnh hơn nếu bài toán đơn giản.

Factory Method giải quyết bài toán tạo một đối tượng. Nhưng nếu chúng ta cần tạo cả một họ các đối tượng liên quan và đảm bảo chúng tương thích với nhau thì sao?

4. Mẫu Thiết Kế Abstract Factory: Quản Lý Các Họ Đối Tượng Liên Quan

Abstract Factory là một mẫu thiết kế ở cấp độ cao hơn, nó không tạo ra một sản phẩm đơn lẻ, mà tạo ra cả một họ (family) các sản phẩm có liên quan.

4.1 Bài Toán Mở Rộng: Các Biến Thể Sản Phẩm

Hãy tưởng tượng hệ thống logistics cần hỗ trợ các "phong cách" dịch vụ khác nhau: Gói Cao cấp (Luxury) và Gói Tiết kiệm (Budget). Mỗi gói bao gồm một tập hợp các sản phẩm tương ứng:

  • Họ Cao cấp: LuxuryTruck, PremiumPackaging, PriorityLabel.
  • Họ Tiết kiệm: BudgetTruck, StandardPackaging, RegularLabel.

Thách thức ở đây là đảm bảo tính nhất quán (consistency). Chúng ta không thể chấp nhận một đơn hàng được vận chuyển bằng LuxuryTruck nhưng lại dùng StandardPackaging.

4.2 Định Nghĩa Chính Thức (GoF)

Abstract Factory giải quyết vấn đề này:

"Provide an interface for creating families of related or dependent objects without specifying their concrete classes."

Dịch nghĩa: "Cung cấp một giao diện để tạo ra các họ đối tượng có liên quan hoặc phụ thuộc lẫn nhau mà không cần chỉ định các lớp cụ thể của chúng."

Mẫu này hoạt động như một "nhà máy của các nhà máy", trong đó mỗi nhà máy cụ thể sản xuất một bộ sản phẩm đồng bộ.

4.3 Các Thành Phần Cấu Trúc

  1. AbstractFactory: Interface định nghĩa các phương thức tạo cho mỗi sản phẩm trong họ (ví dụ: createTransport(), createPackaging()).
  2. ConcreteFactory: Các lớp triển khai AbstractFactory để tạo ra một họ sản phẩm cụ thể.
  3. AbstractProduct: Các interface cho từng loại sản phẩm.
  4. ConcreteProduct: Các lớp triển khai AbstractProduct, được nhóm thành các họ.
  5. Client: Sử dụng các interface AbstractFactory và AbstractProduct.

4.4 Ví Dụ Triển Khai Trong Java

Bước 1: Định nghĩa các AbstractProduct.

public interface Transport { /*...*/ }
public interface Packaging { /*...*/ }

Bước 2: Định nghĩa các ConcreteProduct cho mỗi họ.

// Họ Luxury
public class LuxuryTruck implements Transport { /*...*/ }
public class PremiumPackaging implements Packaging { /*...*/ }
// Họ Budget
public class BudgetTruck implements Transport { /*...*/ }
public class StandardPackaging implements Packaging { /*...*/ }

Bước 3: Định nghĩa AbstractFactory.

// AbstractFactory
public interface ShippingKitFactory { Transport createTransport(); Packaging createPackaging();
}

Bước 4: Định nghĩa các ConcreteFactory.

public class LuxuryShippingKitFactory implements ShippingKitFactory { @Override public Transport createTransport() { return new LuxuryTruck(); } @Override public Packaging createPackaging() { return new PremiumPackaging(); }
} public class BudgetShippingKitFactory implements ShippingKitFactory { @Override public Transport createTransport() { return new BudgetTruck(); } @Override public Packaging createPackaging() { return new StandardPackaging(); }
}

Bước 5: Client được cấu hình với một ConcreteFactory.

public class LogisticsApp { private final Transport transport; private final Packaging packaging; public LogisticsApp(ShippingKitFactory factory) { this.transport = factory.createTransport(); this.packaging = factory.createPackaging(); } public void ship() { // Client không biết đến lớp cụ thể, chỉ làm việc với các interface // Tính nhất quán được đảm bảo bởi factory }
}

4.5 Phân Tích Ưu và Nhược Điểm

  • Ưu điểm:

    • Đảm bảo tính tương thích giữa các sản phẩm: Đây là lợi ích cốt lõi.
    • Tách biệt hoàn toàn client và các lớp cụ thể: Thay đổi một họ sản phẩm trở nên dễ dàng.
    • Tuân thủ OCP và SRP.
  • Nhược điểm:

    • Khó mở rộng với loại sản phẩm mới: Nếu cần thêm một sản phẩm mới vào họ (ví dụ, TrackingService), ta phải sửa đổi interface AbstractFactory và tất cả các lớp con của nó, vi phạm OCP.

5. Phân Tích So Sánh và Các Biến Thể

Để áp dụng hiệu quả, chúng ta cần phân biệt rõ ràng các mẫu hình này.

5.1 Bảng So Sánh Hệ Thống

Tiêu chí Simple Factory (Idiom) Factory Method (GoF Pattern) Abstract Factory (GoF Pattern)
Mục đích Đóng gói việc tạo đối tượng. Ủy quyền việc tạo đối tượng cho lớp con. Tạo ra các họ đối tượng liên quan.
Cơ chế Logic điều kiện trong một phương thức. Kế thừa (Inheritance). Thành phần (Composition).
Phạm vi Một sản phẩm duy nhất. Một sản phẩm duy nhất. Nhiều sản phẩm (một họ).

5.2 Biến Thể Hiện Đại Hóa với Java 8+

Với sự ra đời của lambda và functional interface, chúng ta có thể triển khai Factory một cách tinh gọn hơn, đặc biệt hữu ích để khắc phục nhược điểm OCP của Simple Factory.

import java.util.HashMap;
import java.util.Map;
import java.util.function.Supplier; public final class TransportFactory { private static final Map<String, Supplier<Transport>> REGISTRY = new HashMap<>(); public static void register(String type, Supplier<Transport> supplier) { REGISTRY.put(type, supplier); } public static Transport create(String type) { Supplier<Transport> supplier = REGISTRY.get(type); if (supplier == null) { throw new IllegalArgumentException("Type not supported: " + type); } return supplier.get(); }
}
// Client có thể đăng ký các loại mới trong thời gian chạy
// TransportFactory.register("AIR", Airplane::new);

Cách tiếp cận này vừa đơn giản vừa tuân thủ OCP.

6. Tổng Kết

Việc lựa chọn mẫu thiết kế phù hợp đòi hỏi sự cân nhắc kỹ lưỡng về bối cảnh và yêu cầu của bài toán.

Nguyên Tắc Lựa Chọn

  1. Khi logic tạo đối tượng đơn giản và không có khả năng thay đổi nhiều: Bắt đầu với một Simple Factory hoặc một phương thức static factory đơn thuần. Đây là nguyên tắc YAGNI (You Ain't Gonna Need It).
  2. Khi anh em đang xây dựng một thư viện hoặc framework và muốn client có khả năng mở rộng bằng cách cung cấp các triển khai riêng: Factory Method là lựa chọn phù hợp nhất.
  3. Khi hệ thống của anh em cần làm việc với nhiều họ sản phẩm và tính nhất quán giữa các sản phẩm trong một họ là yêu cầu bắt buộc: Abstract Factory là giải pháp tối ưu.

Lời khuyên quan trọng nhất mà tôi muốn gửi đến anh em là hãy luôn ưu tiên sự đơn giản. Chỉ áp dụng các mẫu thiết kế phức tạp khi vấn đề mà chúng giải quyết thực sự tồn tại trong hệ thống của anh em. Việc hiểu rõ bản chất và mục đích của từng mẫu hình sẽ giúp chúng ta sử dụng chúng như những công cụ mạnh mẽ, thay vì biến chúng thành gánh nặng kiến trúc.

Hẹn gặp lại anh em trong bài viết tiếp theo tại Trà đá công nghệ!

Bình luận

Bài viết tương tự

- vừa được xem lúc

Chương 5 Object oriented programming

Chương 5 Object oriented programming. Tôi lần đầu tiên được giới thiệu về lập trình hướng đối tượng ở trường cao đẳng nơi tôi đã có một giới thiệu tóm tắc về c++.

0 0 37

- vừa được xem lúc

SOLID trong OOP và ví dụ dễ hiểu bằng Python

Thế SOLID là gì? SOLID là cứng . Đùa tí Đây là các nguyên lý thiết kế trong OOP, được ghép lại từ các chữ cái đầu của Single Responsibility, Open Close Principle, Liskov Substitution Principle, Interf

0 0 43

- vừa được xem lúc

002: Object và Class trong OOP

Bài viết nằm trong series Object-Oriented Design from real life to software. Về mặt ý tưởng, OOP nói đến việc áp dụng từ thế giới thực vào thế giới lập trình.

0 0 47

- vừa được xem lúc

001: Procedural programming và Object-Oriented programming

Bài viết nằm trong series Object-Oriented Design from real life to software. 1) Procedural programming.

0 0 44

- vừa được xem lúc

003: Các tính chất cơ bản trong OOP P1

Bài viết nằm trong series Object-Oriented Design from real life to software. . . Abstraction.

0 0 60

- vừa được xem lúc

004: Các tính chất cơ bản trong OOP P2

Bài viết nằm trong series Object-Oriented Design from real life to software. . . Inheritance.

0 0 54