Recently, while studying Spring Security, I noticed a repetitive pattern in instance creation. I discovered that Java employed well-known design patterns following Object-Oriented Programming (OOP) principles. Today, I will explore the Builder Pattern sugin some exmaples.
The Builder Pattern is a design pattern that separates the construction of a complex object from its representation, allowing the same construction process to create different representations. This pattern is particularly useful when an object has many optional attribute, and it reduces the need for numerous constructors or complex parameter sets.
An example of using the Builder Pattern is when you have a class representing a sandwich. A sandwich can have various combinations of ingredients like bread, cheese, vegetable, and sause, Using the Builder Pattern, you can create different sandwich instances with different ingredients while following the same construction process
Key concepts of the Builder Pattern:
- Separate the construction of a complex object from its representation.
- Use the same construction process to create different representations.
- Reduce the need for multiple constructors or complex parameter sets.
- Simplify the creation of complex objects, making the code more maintainable and flexible.
To further illustrate the Builder Pattern's advantages, let's consider an example involving a "Car" class. A car can have different attributes such as color, engine type, number of doors, etc. Using the Builder Pattern, you can create car instances with different attributes while using the same construction process, making it easier to create different car variations without having to write separate constructors for each combination.
Implementing the Builder Pattern:
- Create the Builder class as a Static Nested Class. By convention, append "Builder" to the target class name. For example, the Builder class for a Car class would be named "CarBuilder".
- Make the Builder class's constructor public and accept essential values as parameters.
- Provide methods for optional values, ensuring that each method returns the Builder object itself.
- Define the build() method within the Builder class to provide the final product to the client program. Since object creation is provided only through build(), the target class's constructor should be private.
-
public class Car {
//required parameters
private String engine;
private int tires;
//optional paramters
private boolean ambientColor;
private boolean extraTire;
public String getEngine() {
return engine;
}
public int getTires() {
return tires;
}
public boolean isAmbientColor() {
return ambientColor;
}
public boolean isExtraTire() {
return extraTire;
}
private Car(CarBuilder builder) {
this.engine = builder.engine;
this.tires = builder.tires;
this.ambientColor = builder.ambientColor;
this.extraTire = builder.extraTire;
}
//Builder Pattern
public static class CarBuilder{
//required parameters
private String engine;
private int tires;
//optional paramters
private boolean ambientColor;
private boolean extraTire;
public CarBuilder(String engine, int tires) {
this.engine = engine;
this.tires = tires;
}
public void setAmbientColor(boolean ambientColor) {
this.ambientColor = ambientColor;
}
public void setExtraTire(boolean extraTire) {
this.extraTire = extraTire;
}
public Car build() {
return new Car(this);
}
}
}
Using the Builder Pattern improves the maintainability and flexibility of your code, making it easier to create complex objects with various attributes.
This example is a code sinppet from the SecurityFilterChain class, used to configure an HttpSecurity instance. Although the concept is more complex than the previous example, it still applies the Builder Pattern to create an HttpSecurity instances.
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.headers().frameOptions().sameOrigin()
.and()
.csrf().disable()
.formLogin()
.loginPage("/auths/login-form")
.loginProcessingUrl("/process_login")
.failureUrl("/auths/login-form?error")
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/")
.and()
.exceptionHandling().accessDeniedPage("/auths/access-denied")
.and()
.authorizeHttpRequests(authorize -> authorize
.antMatchers("/orders/**").hasRole("ADMIN")
.antMatchers("/members/my-page").hasRole("USER")
.antMatchers("⁄**").permitAll()
);
return http.build();
}
In this code snippet, the Builder Pattern is used to configure various security settings for an HttpSecurity instance, such as headers, CSRF protection, authentication, and authorization. The builder methods are chained together to create a concise and readable configuration. The build() method at the end is called to create the final HttpSecurity instance with the specified configuration. This example demonstrates how the Builder Pattern can improve the readability and maintainability of code, even in more complex scenarios.