0

Effective Java - Builder Pattern cho Class Hierarchy: Khi Builder không còn đơn giản

Trong bài viết trước, chúng ta đã tìm hiểu lý do vì sao Builder Pattern là một giải pháp hiệu quả khi một class có quá nhiều constructor parameters.

Tuy nhiên, ví dụ Builder thông thường chỉ hoạt động tốt khi chúng ta làm việc với một class độc lập.

Mọi chuyện bắt đầu trở nên phức tạp khi xuất hiện kế thừa.

Đây cũng chính là lý do Joshua Bloch dành riêng một ví dụ khá đặc biệt trong Effective Java để giải quyết bài toán Builder Pattern cho class hierarchy.

Và thành thật mà nói, đây có lẽ là một trong những ví dụ khó đọc nhất của cả cuốn sách.

Bài toán

Giả sử chúng ta có một hierarchy như sau:

Pizza
├── NyPizza
└── Calzone

Tất cả các loại Pizza đều có chung tập hợp toppings.

public abstract class Pizza {
    private final Set<Topping> toppings;
}

Trong khi đó mỗi loại Pizza lại có thêm thuộc tính riêng.

Ví dụ:

public class NyPizza extends Pizza {
    private final Size size;
}
public class Calzone extends Pizza {
    private final boolean sauceInside;
}

Nếu áp dụng Builder Pattern thông thường, mọi thứ vẫn ổn.

Nhưng vấn đề xuất hiện khi chúng ta muốn hỗ trợ method chaining.

Ví dụ:

new NyPizza.Builder(Size.LARGE)
    .addTopping(HAM)
    .addTopping(ONION)
    .build();

hoặc:

new Calzone.Builder()
    .addTopping(HAM)
    .sauceInside()
    .build();

Làm thế nào để addTopping() luôn trả về đúng loại Builder tương ứng?

Đó chính là bài toán.


Cách tiếp cận đầu tiên

Một cách rất tự nhiên là viết:

abstract static class Builder {

    public Builder addTopping(Topping topping) {
        toppings.add(topping);
        return this;
    }
}

Thoạt nhìn có vẻ hợp lý.

Nhưng hãy xem điều gì xảy ra với Calzone.

new Calzone.Builder()
    .addTopping(HAM)
    .sauceInside();

Method:

addTopping()

trả về:

Pizza.Builder

chứ không phải:

Calzone.Builder

Do đó compiler sẽ không tìm thấy:

sauceInside()

Builder Pattern bị phá vỡ.


Giải pháp của Joshua Bloch

Joshua sử dụng kỹ thuật thường được gọi là:

Self-referential Generic Type

hay:

Curiously Recurring Template Pattern (CRTP)

Thông qua đoạn code:

abstract static class Builder<T extends Builder<T>>

Đây là phần khiến nhiều người cảm thấy khó hiểu nhất.


Phân tích từng thành phần

Bước 1: Generic T

Thông thường chúng ta thường thấy:

class Box<T> {
}

Ở đây:

T

là kiểu dữ liệu được truyền vào sau này.

Ví dụ:

Box<String>

thì:

T = String

Trong Builder:

Builder<T extends Builder<T>>

Joshua yêu cầu:

T phải là một Builder

Nói cách khác:

T extends Builder<T>

Bước 2: Builder tự tham chiếu tới chính nó

Hãy nhìn NyPizza.

public static class Builder
    extends Pizza.Builder<Builder> {
}

Thoạt nhìn khá kỳ lạ.

Nhưng thực tế compiler hiểu như sau:

public static class Builder
    extends Pizza.Builder<NyPizza.Builder> {
}

Nghĩa là:

T = NyPizza.Builder

Tương tự:

public static class Builder
    extends Pizza.Builder<Builder> {
}

trong Calzone được hiểu là:

T = Calzone.Builder

Bước 3: Ý nghĩa của addTopping()

Lúc này hãy quay lại method quan trọng nhất:

public T addTopping(Topping topping) {
    toppings.add(
        Objects.requireNonNull(topping)
    );

    return self();
}

Nhiều người đọc đến đây thường nhầm rằng:

T

là kiểu của topping.

Điều đó không đúng.

Trong đoạn code trên:

public T addTopping(Topping topping)
  • Topping là kiểu dữ liệu đầu vào
  • T là kiểu dữ liệu trả về

Đối với NyPizza:

T = NyPizza.Builder

Compiler sẽ nhìn method này như:

public NyPizza.Builder addTopping(...)

Đối với Calzone:

public Calzone.Builder addTopping(...)

Đây chính là chìa khóa giúp method chaining tiếp tục hoạt động.

Ví dụ:

new Calzone.Builder()
    .addTopping(HAM)
    .sauceInside()
    .build();

Sau khi gọi:

addTopping()

kiểu dữ liệu vẫn là:

Calzone.Builder

nên compiler vẫn nhìn thấy:

sauceInside()

Vai trò của self()

Một câu hỏi khác thường xuất hiện là:

return self();

dùng để làm gì?

Tại sao không viết:

return this;

?

Joshua muốn class cha không cần biết Builder cụ thể là gì.

Do đó:

protected abstract T self();

được khai báo trong lớp cha.

Và từng Builder con sẽ tự định nghĩa:

@Override
protected Builder self() {
    return this;
}

Điều này cho phép compiler biết chính xác kiểu trả về.


Constructor của Pizza hoạt động như thế nào?

Một đoạn khác cũng khá dễ gây nhầm lẫn:

Pizza(Builder<?> builder) {
    toppings = builder.toppings.clone();
}

Dấu:

?

ở đây là wildcard.

Có thể hiểu đơn giản là:

Tôi không quan tâm Builder cụ thể là loại nào

Có thể là:

NyPizza.Builder

hoặc:

Calzone.Builder

đều được.

Pizza chỉ cần lấy:

builder.toppings

để khởi tạo object.


Điều gì khiến ví dụ này khó đọc?

Theo quan điểm cá nhân, có ba lý do:

Thứ nhất, Builder được khai báo bên trong Pizza.

Pizza
 └── Builder

Thứ hai, mỗi class con lại có thêm một Builder riêng.

Pizza
 ├── Builder
 │
 ├── NyPizza
 │    └── Builder
 │
 └── Calzone
      └── Builder

Thứ ba, Generic đang tham chiếu ngược lại chính Builder của nó.

Builder
    extends Pizza.Builder<Builder>

Đây là kiểu code mà chúng ta hiếm khi gặp trong các ứng dụng thông thường.


Một hạn chế của ví dụ

Ví dụ trong sách giải quyết rất tốt bài toán:

Pizza
├── NyPizza
└── Calzone

Tuy nhiên nếu hierarchy tiếp tục phát triển:

Pizza
└── NyPizza
    └── StuffedCrustNyPizza

mọi thứ sẽ nhanh chóng trở nên phức tạp hơn.

Lúc đó chúng ta thường cần thêm nhiều tầng generic hoặc cân nhắc sử dụng composition thay vì mở rộng hierarchy bằng inheritance.

Đây cũng là một trong những lý do khiến nhiều dự án hiện đại hạn chế việc xây dựng các class hierarchy quá sâu.


Kết luận

Ví dụ Builder của Joshua Bloch không khó vì Builder Pattern.

Nó khó vì nó đang cố giải quyết đồng thời hai bài toán:

  • Builder Pattern
  • Inheritance

Thông qua kỹ thuật Self-referential Generic Type.

Nếu phải tóm tắt toàn bộ ví dụ trong một câu:

Joshua muốn mọi method trong Builder cha luôn trả về đúng loại Builder của lớp con để method chaining tiếp tục hoạt động ngay cả khi xuất hiện kế thừa.

Đó chính là mục đích của toàn bộ đoạn code:

Builder<T extends Builder<T>>

và cũng là lý do nó xuất hiện trong Effective Java như một ví dụ kinh điển về việc kết hợp Builder Pattern với Generics.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí