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