When constructing objects in Java, the choice of design pattern heavily influences the readability, scalability, and maintainability of the code. Two prevalent creational design patterns in Java are the Factory Method and the Builder. Understanding these patterns and knowing when to use each can significantly enhance the robustness and flexibility of your code.
The Factory Method Pattern provides an interface for creating objects in a superclass but allows subclasses to alter the type of objects that will be created. It is particularly useful when the exact type of object that needs to be created varies based on some condition.
Product
that is instantiated by ConcreteFactory
.Let's consider a simple example where different types of buttons (WindowsButton and HTMLButton) need to be created, but the client code should remain unaware of the specific class of the button being instantiated.
// Product interface
interface Button {
void render();
void onClick();
}
// ConcreteProduct A
class WindowsButton implements Button {
public void render() {
// Render a Windows button
System.out.println("Rendering Windows Button");
}
public void onClick() {
System.out.println("Windows Button Clicked!");
}
}
// ConcreteProduct B
class HTMLButton implements Button {
public void render() {
// Render an HTML button
System.out.println("Rendering HTML Button");
}
public void onClick() {
System.out.println("HTML Button Clicked!");
}
}
// Factory class
abstract class Dialog {
public void renderWindow() {
Button okButton = createButton();
okButton.render();
}
protected abstract Button createButton();
}
// ConcreteFactory A
class WindowsDialog extends Dialog {
@Override
protected Button createButton() {
return new WindowsButton();
}
}
// ConcreteFactory B
class HTMLDialog extends Dialog {
@Override
protected Button createButton() {
return new HTMLButton();
}
}
// Client code
class Application {
private static Dialog dialog;
public static void initialize(String config) {
if (config.equals("Windows")) {
dialog = new WindowsDialog();
} else {
dialog = new HTMLDialog();
}
}
public static void main(String[] args) {
initialize("HTML");
dialog.renderWindow();
}
}
The Builder Pattern separates the construction of a complex object from its representation, allowing the same construction process to create various representations. This pattern is ideal for creating objects that require multiple steps to configure.
Product
object.Builder
interface and constructs and assembles parts of the product.Consider the construction of a House
object that has multiple parts like rooms, windows, doors, etc.
// Product class
class House {
private String foundation;
private String structure;
private String roof;
private String interior;
public void setFoundation(String foundation) { this.foundation = foundation; }
public void setStructure(String structure) { this.structure = structure; }
public void setRoof(String roof) { this.roof = roof; }
public void setInterior(String interior) { this.interior = interior; }
@Override
public String toString() {
return "House [Foundation=" + foundation + ", Structure=" + structure +
", Roof=" + roof + ", Interior=" + interior + "]";
}
}
// Builder interface
interface HouseBuilder {
void buildFoundation();
void buildStructure();
void buildRoof();
void buildInterior();
House getHouse();
}
// ConcreteBuilder A
class IglooHouseBuilder implements HouseBuilder {
private House house;
public IglooHouseBuilder() {
this.house = new House();
}
public void buildFoundation() {
house.setFoundation("Ice blocks");
System.out.println("Building Igloo Foundation with Ice blocks");
}
public void buildStructure() {
house.setStructure("Ice Bricks");
System.out.println("Building Igloo Structure with Ice Bricks");
}
public void buildRoof() {
house.setRoof("Ice dome");
System.out.println("Building Igloo Roof with Ice dome");
}
public void buildInterior() {
house.setInterior("Ice Carvings");
System.out.println("Building Igloo Interior with Ice Carvings");
}
public House getHouse() {
return this.house;
}
}
// ConcreteBuilder B
class ConcreteHouseBuilder implements HouseBuilder {
private House house;
public ConcreteHouseBuilder() {
this.house = new House();
}
public void buildFoundation() {
house.setFoundation("Concrete, brick, and stone");
System.out.println("Building Concrete House Foundation with concrete, brick, and stone");
}
public void buildStructure() {
house.setStructure("Concrete and Steel");
System.out.println("Building Concrete House Structure with Concrete and Steel");
}
public void buildRoof() {
house.setRoof("Concrete Slab");
System.out.println("Building Concrete House Roof with Concrete Slab");
}
public void buildInterior() {
house.setInterior("Electrical, Plumbing, and Interior Design");
System.out.println("Building Concrete House Interior with Electrical, Plumbing, and Interior Design");
}
public House getHouse() {
return this.house;
}
}
// Director class
class ConstructionEngineer {
private HouseBuilder houseBuilder;
public ConstructionEngineer(HouseBuilder houseBuilder) {
this.houseBuilder = houseBuilder;
}
public House constructHouse() {
this.houseBuilder.buildFoundation();
this.houseBuilder.buildStructure();
this.houseBuilder.buildRoof();
this.houseBuilder.buildInterior();
return this.houseBuilder.getHouse();
}
}
// Client code
class BuilderPatternClient {
public static void main(String[] args) {
HouseBuilder iglooBuilder = new IglooHouseBuilder();
ConstructionEngineer engineer = new ConstructionEngineer(iglooBuilder);
House house = engineer.constructHouse();
System.out.println("House is: " + house);
HouseBuilder concreteBuilder = new ConcreteHouseBuilder();
engineer = new ConstructionEngineer(concreteBuilder);
house = engineer.constructHouse();
System.out.println("House is: " + house);
}
}
Builder
class can ensure all required fields are set before construction.Both the Factory Method and Builder patterns are essential tools in a developer's arsenal for creating objects in Java. The Factory Method is ideal for cases where the exact type of the object to be created varies, while the Builder excels in situations where constructing an object is a complex, multi-step process. Understanding their differences, advantages, and use-cases helps in making informed decisions that enhance code quality, readability, and maintainability. By judiciously applying these patterns, developers can build robust and flexible applications that stand the test of time.