Giới thiệu Abstract Factory Pattern

Bài viết được sự cho phép của tác giả Nguyễn Văn Minh

ABSTRACT FACTORY

Giống như ở bài trước về Singleton, bài này gồm 5 phần:

  • Ý tưởng chính
  • Vấn đề cần giải quyết
  • Cấu trúc
  • Code ví dụ
  • Lưu ý

Ý tưởng chính

  • Khai báo một interface với các phương thức tạo các đối tượng abstract.
  • Một hệ thống phân cấp với các “dòng xe” và các “bộ phận xe”.
  • Tránh sử dụng từ khoá new.

Vấn đề cần giải quyết

Khi bạn muốn phần mềm của mình có thể được triển khai trên nhiều nền tảng khác nhau, bạn phải tìm cách xử lý việc khởi tạo các đối tượng trên các nền tảng đó. “Nền tảng” ở đây có thể hiểu là hệ điều hành, cơ sở dữ liệu,… Trong ví dụ ở phần mở đầu, Azera, Sonata và Veloster chính là các nền tảng.

Tuy nhiên, nếu bạn xử lý chuyện này bằng những câu if ... else thì chẳng mấy chốc code của bạn sẽ thành một mớ bòng bong vì phần mềm sẽ dần nở to. Và đó là lý do mà Abstract Factory ra đời.

Cấu trúc

Ta cần có năm thành phần, đó là:

  • Abstract Platform: interface hoặc abstract class với các phương thức tạo ra các “sản phẩm”
  • Concrete Platforms: Khai báo cụ thể các “nền tảng” (PlatformOne, PlatformTwo trong hình)
  • Abstract Products: interface hoặc abstract class định nghĩa các loại “sản phẩm” (AbstractProductOne, AbstractProductTwo trong hình)
  • Concrete Products: Khai báo cụ thể các “sản phẩm” (ProductOnePlatformOne, ProductOnePlatformTwo, ProductTwoPlatformOne, ProductTwoPlatformTwo trong hình)
  • Client: Đối tượng sử dụng Abstract Platform và Abstract Product

Với ví dụ về việc sản xuất xe, biểu đồ có thể trở thành như sau.

  • Hãy xem xét liệu việc độc lập với các “nền tảng” và việc khởi tạo các đối tượng có phải là vấn đề của hệ thống hiện tại hay không.
  • Làm rõ đâu là “nền tảng”, đâu là “sản phẩm”, và mối quan hệ giữa chúng.
  • Định nghĩa một factory (interface) với đầy đủ các phương thức khởi tạo từng sản phẩm.
  • Định nghĩa các lớp dẫn xuất của interface trên, đảm bảo đã “bao gói” các phương thức khởi tạo (ứng với việc gọi new).
  • Client không được dùng new mà phải dùng các phương thức mà bạn đã khai báo ở interface.

Code ví dụ

Các “sản phẩm” (CPU, MMU)
// class CPU
abstract class CPU {}

// class EmberCPU
class EmberCPU extends CPU {}

// class EnginolaCPU
class EnginolaCPU extends CPU {}

// class MMU
abstract class MMU {}

// class EmberMMU
class EmberMMU extends MMU {}

// class EnginolaMMU
class EnginolaMMU extends MMU {}
Các “nền tảng” cụ thể
// class EmberFactory
class EmberToolkit extends AbstractFactory {
    @Override
    public CPU createCPU() {
        return new EmberCPU();
    }

    @Override
    public MMU createMMU() {
        return new EmberMMU();
    }
}

// class EnginolaFactory
class EnginolaToolkit extends AbstractFactory {
    @Override
    public CPU createCPU() {
        return new EnginolaCPU();
    }

    @Override
    public MMU createMMU() {
        return new EnginolaMMU();
    }
}
“Nền tảng” trừu tượng
enum Architecture {
    ENGINOLA, EMBER
}

abstract class AbstractFactory {
    private static final EmberToolkit EMBER_TOOLKIT = new EmberToolkit();
    private static final EnginolaToolkit ENGINOLA_TOOLKIT = new EnginolaToolkit();

    // Returns a concrete factory object that is an instance of the
    // concrete factory class appropriate for the given architecture.
    static AbstractFactory getFactory(Architecture architecture) {
        AbstractFactory factory = null;
        switch (architecture) {
            case ENGINOLA:
                factory = ENGINOLA_TOOLKIT;
                break;
            case EMBER:
                factory = EMBER_TOOLKIT;
                break;
        }
        return factory;
    }

    public abstract CPU createCPU();

    public abstract MMU createMMU();
}
Client
public class Client { public static void main(String[] args) { AbstractFactory factory = AbstractFactory.getFactory(Architecture.EMBER); CPU cpu = factory.createCPU(); } }

Lưu ý

Creational patterns có thể kết hợp với nhau hoặc thay thế cho nhau. Và để quyết định dùng pattern nào hoặc kết hợp như thế nào, bạn phải xem xét trường hợp của mình.

Abstract Factory thường được kết hợp với Factory Method, đôi khi là cả Protoype nữa.

Abstract Factory có thể thay thế cho Facade để che giấu những lớp được đặc tả riêng cho từng “nền tảng”.

Thông thường, việc thiết kế hệ thống sẽ đi từ Factory Method (dễ hiểu, dễ tuỳ biến) thành Abstract Factory, Prototype, hoặc Builder (mềm dẻo hơn, phức tạp hơn) nếu như người thiết kế cảm thấy tính mềm dẻo là cần thiết.

Như đã nói ở trên, các creational pattern có thể thay thế cho nhau hoặc kết hợp với nhau tuỳ hoàn cảnh, và việc thiết kế có thể đi từ Factory Method đến Abstract Factory. Tức là bạn phải xem xét thật kỹ càng bài toán của mình để biết cần lựa chọn giải pháp nào, thay vì vội vàng lựa chọn.

Các pattern được đưa ra rõ ràng là đem lại cái lợi lớn cho các lập trình viên nhưng bạn không nên lệ thuộc vào chúng. Theo tôi, đừng bao giờ nên giả định rằng: “Chắc tương lai hệ thống sẽ phải thế này, thế kia” hay “Có lẽ sau này hệ thống cần phải mềm dẻo hơn, flexible hơn nên phải chọn Abstract Factory”. Code ít hơn bao giờ cũng tốt hơn.

Tham khảo

Abstract Factory Design Pattern, Source Making

Hướng dẫn Java Design Pattern – Abstract Factory, GP Coder