Builder Pattern

1. Giới Thiệu

1.1 Định Nghĩa

Builder Pattern là một creational pattern cho phép xây dựng các đối tượng phức tạp theo từng bước. Pattern này cho phép tạo ra các biểu diễn khác nhau của cùng một đối tượng sử dụng cùng một quy trình xây dựng.

1.2 Mục Đích

  • Tạo đối tượng phức tạp theo từng bước
  • Cho phép tạo nhiều biểu diễn khác nhau của cùng một đối tượng
  • Tách biệt quá trình xây dựng và biểu diễn
  • Kiểm soát quá trình xây dựng tốt hơn

2. Cấu Trúc Cơ Bản

2.1 Traditional Builder

public class Computer {
    private String cpu;
    private String ram;
    private String storage;
    private String gpu;

    private Computer() {}

    public static class Builder {
        private Computer computer;

        public Builder() {
            computer = new Computer();
        }

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

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

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

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

        public Computer build() {
            return computer;
        }
    }
}

// Usage
Computer computer = new Computer.Builder()
    .cpu("Intel i7")
    .ram("16GB")
    .storage("1TB SSD")
    .gpu("RTX 3080")
    .build();

2.2 Lombok Builder

@Builder
@Getter
public class Computer {
    private final String cpu;
    private final String ram;
    private final String storage;
    private final String gpu;
}

// Usage
Computer computer = Computer.builder()
    .cpu("Intel i7")
    .ram("16GB")
    .storage("1TB SSD")
    .gpu("RTX 3080")
    .build();

3. Ví Dụ Thực Tế

3.1 User Builder

public class User {
    private final String username;
    private final String email;
    private final String firstName;
    private final String lastName;
    private final String phone;
    private final String address;
    private final LocalDate birthDate;
    private final List<String> roles;

    private User(Builder builder) {
        this.username = builder.username;
        this.email = builder.email;
        this.firstName = builder.firstName;
        this.lastName = builder.lastName;
        this.phone = builder.phone;
        this.address = builder.address;
        this.birthDate = builder.birthDate;
        this.roles = builder.roles;
    }

    public static class Builder {
        private final String username;
        private final String email;
        private String firstName;
        private String lastName;
        private String phone;
        private String address;
        private LocalDate birthDate;
        private List<String> roles = new ArrayList<>();

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

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

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

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

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

        public Builder birthDate(LocalDate birthDate) {
            this.birthDate = birthDate;
            return this;
        }

        public Builder roles(List<String> roles) {
            this.roles = roles;
            return this;
        }

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

// Usage
User user = new User.Builder("john_doe", "john@example.com")
    .firstName("John")
    .lastName("Doe")
    .phone("1234567890")
    .address("123 Main St")
    .birthDate(LocalDate.of(1990, 1, 1))
    .roles(Arrays.asList("USER", "ADMIN"))
    .build();

3.2 Query Builder

public class QueryBuilder {
    private StringBuilder query;
    private List<String> conditions;
    private List<String> orderBy;
    private Integer limit;
    private Integer offset;

    public QueryBuilder() {
        query = new StringBuilder();
        conditions = new ArrayList<>();
        orderBy = new ArrayList<>();
    }

    public QueryBuilder select(String... columns) {
        query.append("SELECT ");
        if (columns.length == 0) {
            query.append("*");
        } else {
            query.append(String.join(", ", columns));
        }
        return this;
    }

    public QueryBuilder from(String table) {
        query.append(" FROM ").append(table);
        return this;
    }

    public QueryBuilder where(String condition) {
        conditions.add(condition);
        return this;
    }

    public QueryBuilder orderBy(String column, String direction) {
        orderBy.add(column + " " + direction);
        return this;
    }

    public QueryBuilder limit(int limit) {
        this.limit = limit;
        return this;
    }

    public QueryBuilder offset(int offset) {
        this.offset = offset;
        return this;
    }

    public String build() {
        if (!conditions.isEmpty()) {
            query.append(" WHERE ")
                .append(String.join(" AND ", conditions));
        }

        if (!orderBy.isEmpty()) {
            query.append(" ORDER BY ")
                .append(String.join(", ", orderBy));
        }

        if (limit != null) {
            query.append(" LIMIT ").append(limit);
        }

        if (offset != null) {
            query.append(" OFFSET ").append(offset);
        }

        return query.toString();
    }
}

// Usage
String sql = new QueryBuilder()
    .select("id", "name", "email")
    .from("users")
    .where("status = 'active'")
    .where("age >= 18")
    .orderBy("name", "ASC")
    .limit(10)
    .offset(0)
    .build();

4. Builder với Validation

4.1 Validation trong Build Step

public class UserBuilder {
    private String username;
    private String email;
    private String password;

    public UserBuilder username(String username) {
        if (username == null || username.trim().isEmpty()) {
            throw new IllegalArgumentException(
                "Username cannot be empty");
        }
        if (username.length() < 3) {
            throw new IllegalArgumentException(
                "Username must be at least 3 characters");
        }
        this.username = username;
        return this;
    }

    public UserBuilder email(String email) {
        if (email == null || !email.matches(
            "^[A-Za-z0-9+_.-]+@(.+)$")) {
            throw new IllegalArgumentException(
                "Invalid email format");
        }
        this.email = email;
        return this;
    }

    public UserBuilder password(String password) {
        if (password == null || password.length() < 8) {
            throw new IllegalArgumentException(
                "Password must be at least 8 characters");
        }
        if (!password.matches(".*[A-Z].*")) {
            throw new IllegalArgumentException(
                "Password must contain uppercase letter");
        }
        if (!password.matches(".*[a-z].*")) {
            throw new IllegalArgumentException(
                "Password must contain lowercase letter");
        }
        if (!password.matches(".*\\d.*")) {
            throw new IllegalArgumentException(
                "Password must contain digit");
        }
        this.password = password;
        return this;
    }

    public User build() {
        validateRequiredFields();
        return new User(username, email, password);
    }

    private void validateRequiredFields() {
        List<String> missingFields = new ArrayList<>();

        if (username == null) missingFields.add("username");
        if (email == null) missingFields.add("email");
        if (password == null) missingFields.add("password");

        if (!missingFields.isEmpty()) {
            throw new IllegalStateException(
                "Missing required fields: " + 
                String.join(", ", missingFields));
        }
    }
}

4.2 Bean Validation

@Builder
@Getter
public class User {
    @NotNull(message = "Username is required")
    @Size(min = 3, message = "Username must be at least 3 characters")
    private final String username;

    @NotNull(message = "Email is required")
    @Email(message = "Invalid email format")
    private final String email;

    @NotNull(message = "Password is required")
    @Size(min = 8, message = "Password must be at least 8 characters")
    @Pattern(
        regexp = "^(?=.*[A-Z])(?=.*[a-z])(?=.*\\d).*$",
        message = "Password must contain uppercase, lowercase and digit"
    )
    private final String password;

    public static class UserBuilder {
        public User build() {
            User user = new User(username, email, password);
            ValidatorFactory factory = Validation
                .buildDefaultValidatorFactory();
            Validator validator = factory.getValidator();
            Set<ConstraintViolation<User>> violations = 
                validator.validate(user);

            if (!violations.isEmpty()) {
                throw new ConstraintViolationException(
                    violations);
            }

            return user;
        }
    }
}

5. Builder với Spring Framework

5.1 Request Builder

@Builder
@Getter
public class UserRequest {
    @NotNull
    private final String username;

    @NotNull
    @Email
    private final String email;

    @NotNull
    @Size(min = 8)
    private final String password;

    private final String firstName;
    private final String lastName;
    private final List<String> roles;
}

@RestController
@RequestMapping("/api/users")
public class UserController {
    @PostMapping
    public ResponseEntity<UserResponse> createUser(
            @RequestBody @Valid UserRequest request) {
        User user = User.builder()
            .username(request.getUsername())
            .email(request.getEmail())
            .password(request.getPassword())
            .firstName(request.getFirstName())
            .lastName(request.getLastName())
            .roles(request.getRoles())
            .build();

        // Save user
        return ResponseEntity.ok(
            UserResponse.fromUser(user));
    }
}

5.2 Response Builder

@Builder
@Getter
public class UserResponse {
    private final String id;
    private final String username;
    private final String email;
    private final String fullName;
    private final List<String> roles;

    public static UserResponse fromUser(User user) {
        return UserResponse.builder()
            .id(user.getId())
            .username(user.getUsername())
            .email(user.getEmail())
            .fullName(user.getFirstName() + " " + 
                user.getLastName())
            .roles(user.getRoles())
            .build();
    }
}

6. Testing Builder Pattern

6.1 Unit Testing

@ExtendWith(MockitoExtension.class)
class UserBuilderTest {
    @Test
    void whenValidInput_thenCreateUser() {
        // When
        User user = User.builder()
            .username("john_doe")
            .email("john@example.com")
            .password("Password123")
            .build();

        // Then
        assertNotNull(user);
        assertEquals("john_doe", user.getUsername());
        assertEquals("john@example.com", user.getEmail());
    }

    @Test
    void whenInvalidEmail_thenThrowException() {
        // When/Then
        assertThrows(IllegalArgumentException.class, () ->
            User.builder()
                .username("john_doe")
                .email("invalid-email")
                .password("Password123")
                .build());
    }

    @Test
    void whenMissingRequiredField_thenThrowException() {
        // When/Then
        assertThrows(IllegalStateException.class, () ->
            User.builder()
                .username("john_doe")
                .password("Password123")
                .build());
    }
}

6.2 Integration Testing

@SpringBootTest
class UserControllerTest {
    @Autowired
    private MockMvc mockMvc;

    @Test
    void whenValidRequest_thenCreateUser() throws Exception {
        // Given
        UserRequest request = UserRequest.builder()
            .username("john_doe")
            .email("john@example.com")
            .password("Password123")
            .build();

        // When/Then
        mockMvc.perform(post("/api/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content(asJsonString(request)))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.username")
                .value("john_doe"))
            .andExpect(jsonPath("$.email")
                .value("john@example.com"));
    }

    @Test
    void whenInvalidRequest_thenReturnError() 
            throws Exception {
        // Given
        UserRequest request = UserRequest.builder()
            .username("john_doe")
            .email("invalid-email")
            .password("weak")
            .build();

        // When/Then
        mockMvc.perform(post("/api/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content(asJsonString(request)))
            .andExpect(status().isBadRequest());
    }
}

7. Best Practices

7.1 Immutable Objects

@Builder
@Getter
@ToString
@EqualsAndHashCode
public class ImmutableUser {
    private final String username;
    private final String email;
    private final String password;
    private final List<String> roles;

    private ImmutableUser(
            String username, 
            String email, 
            String password,
            List<String> roles) {
        this.username = username;
        this.email = email;
        this.password = password;
        this.roles = Collections
            .unmodifiableList(new ArrayList<>(roles));
    }

    public static class ImmutableUserBuilder {
        private List<String> roles = new ArrayList<>();

        public ImmutableUserBuilder role(String role) {
            this.roles.add(role);
            return this;
        }
    }
}

7.2 Required vs Optional Fields

public class User {
    private final String username;  // Required
    private final String email;     // Required
    private final String firstName; // Optional
    private final String lastName;  // Optional

    private User(RequiredBuilder builder) {
        this.username = builder.username;
        this.email = builder.email;
        this.firstName = builder.firstName;
        this.lastName = builder.lastName;
    }

    public static RequiredBuilder builder(
            String username, 
            String email) {
        return new RequiredBuilder(username, email);
    }

    public static class RequiredBuilder {
        private final String username;
        private final String email;
        private String firstName;
        private String lastName;

        private RequiredBuilder(
                String username, 
                String email) {
            this.username = username;
            this.email = email;
        }

        public RequiredBuilder firstName(String firstName) {
            this.firstName = firstName;
            return this;
        }

        public RequiredBuilder lastName(String lastName) {
            this.lastName = lastName;
            return this;
        }

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

// Usage
User user = User.builder("john_doe", "john@example.com")
    .firstName("John")
    .lastName("Doe")
    .build();

8. Common Issues và Solutions

8.1 Deep Copy trong Builder

public class Document {
    private final List<String> tags;
    private final Map<String, String> metadata;

    private Document(Builder builder) {
        // Deep copy collections
        this.tags = new ArrayList<>(builder.tags);
        this.metadata = new HashMap<>(builder.metadata);
    }

    public static class Builder {
        private List<String> tags = new ArrayList<>();
        private Map<String, String> metadata = 
            new HashMap<>();

        public Builder tags(List<String> tags) {
            this.tags = new ArrayList<>(tags);
            return this;
        }

        public Builder addTag(String tag) {
            this.tags.add(tag);
            return this;
        }

        public Builder metadata(
                Map<String, String> metadata) {
            this.metadata = new HashMap<>(metadata);
            return this;
        }

        public Builder addMetadata(
                String key, 
                String value) {
            this.metadata.put(key, value);
            return this;
        }

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

8.2 Builder Inheritance

public abstract class BaseEntity {
    protected final String id;
    protected final LocalDateTime createdAt;
    protected final LocalDateTime updatedAt;

    protected BaseEntity(Builder<?> builder) {
        this.id = builder.id;
        this.createdAt = builder.createdAt;
        this.updatedAt = builder.updatedAt;
    }

    public abstract static class Builder<T extends Builder<T>> {
        private String id;
        private LocalDateTime createdAt;
        private LocalDateTime updatedAt;

        @SuppressWarnings("unchecked")
        public T id(String id) {
            this.id = id;
            return (T) this;
        }

        @SuppressWarnings("unchecked")
        public T createdAt(LocalDateTime createdAt) {
            this.createdAt = createdAt;
            return (T) this;
        }

        @SuppressWarnings("unchecked")
        public T updatedAt(LocalDateTime updatedAt) {
            this.updatedAt = updatedAt;
            return (T) this;
        }

        public abstract BaseEntity build();
    }
}

public class User extends BaseEntity {
    private final String username;
    private final String email;

    private User(Builder builder) {
        super(builder);
        this.username = builder.username;
        this.email = builder.email;
    }

    public static class Builder 
            extends BaseEntity.Builder<Builder> {
        private String username;
        private String email;

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

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

        @Override
        public User build() {
            return new User(this);
        }
    }
}

9. References

9.1 Books

  • Design Patterns: Elements of Reusable Object-Oriented Software
  • Effective Java by Joshua Bloch
  • Clean Code by Robert C. Martin
  • Head First Design Patterns

9.2 Online Resources