translation

This is an AI translated post.

제이온

[Effective Java] Item 2. Consider a Builder if Your Constructor Has Many Parameters

Select Language

  • English
  • 汉语
  • Español
  • Bahasa Indonesia
  • Português
  • Русский
  • 日本語
  • 한국어
  • Deutsch
  • Français
  • Italiano
  • Türkçe
  • Tiếng Việt
  • ไทย
  • Polski
  • Nederlands
  • हिन्दी
  • Magyar

Summarized by durumis AI

  • It introduces various patterns for efficiently managing constructors with many parameters, and compares and analyzes the advantages and disadvantages of the step-by-step constructor pattern, JavaBeans pattern, and builder pattern.
  • The builder pattern has the advantage of increasing code readability and stability, and is even more effective when used with hierarchically designed classes.
  • The builder pattern allows you to write code more clearly, but it has the drawbacks of builder object creation and code verbosity.

Telescoping Constructor Pattern

Both static factories and constructors have difficulty dealing with many parameters. For example, if a class has 6 fields and you want to divide the constructors when there are 2 parameters, 3 parameters, ... etc., you can use the telescoping constructor pattern as follows.


public class NutritionFacts {

    private final int servingSize;

    private final int servings;

    private final int calories;

    private final int fat;

    private final int sodium;

    private final int carbohydrate;

    public NutritionFacts(int servingSize, int servings, int calories, int fat, int sodium, int carbohydrate) {
        this.servingSize = servingSize;
        this.servings = servings;
        this.calories = calories;
        this.fat = fat;
        this.sodium = sodium;
        this.carbohydrate = carbohydrate;
    }

    public NutritionFacts(int servingSize, int servings) {
        this(servingSize, servings, 0, 0, 0, 0);
    }

    public NutritionFacts(int servingSize, int servings, int calories) {
        this(servingSize, servings, calories, 0, 0, 0);
    }

    public NutritionFacts(int servingSize, int servings, int calories, int fat) {
        this(servingSize, servings, calories, fat, 0, 0);
    }

    public NutritionFacts(int servingSize, int servings, int calories, int fat, int sodium) {
        this(servingSize, servings, calories, fat, sodium, 0);
    }


However, even with this, if there are many parameters, it is confusing to read the code to understand the meaning of each value, and parameters of the same type can be confused and values can be inserted.


Java Beans Pattern

The Java Beans pattern creates an object with a constructor that takes no parameters, and then calls setter methods to set the values of the desired parameters.


public class NutritionFactsWithJavaBeansPattern {

    private int servingSize = -1; // Required. No default value.

    private int servings = -1; // Required. No default value.

    private int calories;

    private int fat;

    private int sodium;

    private int carbohydrate;

    public void setServingSize(int servingSize) {
        this.servingSize = servingSize;
    }

    public void setServings(int servings) {
        this.servings = servings;
    }

    public void setCalories(int calories) {
        this.calories = calories;
    }

    public void setFat(int fat) {
        this.fat = fat;
    }

    public void setSodium(int sodium) {
        this.sodium = sodium;
    }

    public void setCarbohydrate(int carbohydrate) {
        this.carbohydrate = carbohydrate;
    }


The Java Beans pattern allows you to create an instance without confusing the values, even if there are many parameters. However, creating a single object requires multiple calls to setter methods, and consistency is broken until the object is fully complete. This makes it impossible to make the class immutable.


Builder Pattern

The builder pattern is mainly used, which combines the stability of the telescoping constructor pattern and the readability of the Java Beans pattern.

Instead of creating the object directly, the client calls the constructor with only the required parameters to obtain a builder object. Then, the builder object provides a kind of setter method to set the desired optional parameters. Finally, the build() method with no parameters is called to obtain the required object.


public class NutritionFactsWithBuilderPattern {

    private final int servingSize;

    private final int servings;

    private final int calories;

    private final int fat;

    private final int sodium;

    private final int carbohydrate;

    private NutritionFactsWithBuilderPattern(Builder builder) {
        servingSize = builder.servingSize;
        servings = builder.servings;
        calories = builder.calories;
        fat = builder.fat;
        sodium = builder.sodium;
        carbohydrate = builder.carbohydrate;
    }

    public static class Builder {

        private final int servingSize;

        private final int servings;

        private int calories;

        private int fat;

        private int sodium;

        private int carbohydrate;

        public Builder(int servingSize, int servings) {
            this.servingSize = servingSize;
            this.servings = servings;
        }

        public Builder calories(int val) {
            calories = val;
            return this;
        }

        public Builder fat(int val) {
            fat = val;
            return this;
        }

        public Builder sodium(int val) {
            sodium = val;
            return this;
        }

        public Builder carbohydrate(int val) {
            calories = val;
            return this;
        }

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


The constructor in the Builder class receives only the required parameters, and the remaining optional parameters are filled in with a kind of setter method. And finally, the build() method creates a complete NutritionFactsWithBuilderPattern object. The NutritionFactsWithBuilderPattern class is immutable, and the builder's setter methods return the builder itself, so they can be called in a chain. This method is called a fluent API or method chaining.


NutritionFactsWithBuilderPattern nutritionFacts = 
    new NutritionFactsWithBuilderPattern.Builder(240, 8)
        .calories(100)
        .sodium(35)


From the client's perspective, the builder pattern makes it easier to write and read code.


Builder Pattern Goes Well with Hierarchically Designed Classes

public abstract class Pizza {

    public enum Topping {
        HAM, MUSHROOM, ONION, PEPPER, SAUSAGE
    }

    final Set toppings;

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

    abstract static class Builder> {

        private EnumSet toppings = EnumSet.noneOf(Topping.class);

        public T addTopping(Topping topping) {
            toppings.add(topping);
            return self();
        }

        abstract Pizza build();

        protected abstract T self();
    }


The Pizza.Builder class is a generic type that uses recursive type bounds, and the self() abstract method is added to support method chaining without type casting in subclasses. Subclasses can simply give themselves the return value of this abstract method.

Now, let's look at the subclasses of Pizza, New York pizza and Calzone pizza, and experience the flexibility of the builder pattern.


public class NyPizza extends Pizza {

    public enum Size {
        SMALL, MEDIUM, LARGE
    }

    private final Size size; // Required parameter

    private NyPizza(Builder builder) {
        super(builder);
        size = builder.size;
    }

    public static class Builder extends Pizza.Builder {

        private final Size size;

        public Builder(Size size) {
            this.size = size;
        }

        @Override
        NyPizza build() {
            return new NyPizza(this);
        }

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

}

public class CalzonePizza extends Pizza {

    private final boolean sauceInside; // Optional parameter

    private CalzonePizza(Builder builder) {
        super(builder);
        sauceInside = builder.sauceInside;
    }

    public static class Builder extends Pizza.Builder {

        private boolean sauceInside = false;

        public Builder sauceInside() {
            sauceInside = true;
            return this;
        }

        @Override
        CalzonePizza build() {
            return new CalzonePizza(this);
        }

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


The build() class defined by each subclass builder returns a specific subclass. The ability of a subclass method to return its subtype rather than the type returned by the superclass method is called covariant return typing. This allows clients to avoid type casting.


NyPizza nyPizza = new NyPizza.Builder(Size.SMALL)
    .addTopping(Topping.SAUSAGE)
    .addTopping(Topping.ONION)
    .build();

CalzonePizza calzonePizza = new CalzonePizza.Builder()
    .addTopping(Topping.HAM)
    .sauceInside()


From the client's point of view, you can mix and match the Pizza's enum and each subclass's enum, and you can complete the object with each appropriate method.


Disadvantages of the Builder Pattern

  • You have to create a builder object.
  • Code is verbose.


Summary

Consider the builder pattern if the constructor or static factory method needs to handle many parameters.


Source

  • Effective Java
제이온
제이온
제이온
제이온
[Effective Java] Item 1: Consider Static Factory Methods Instead of Constructors Static factory methods provide a flexible and efficient way to create instances instead of constructors. They can have names, return instances that meet specific conditions, and improve performance through caching. Unlike the singleton pattern, they can c

April 27, 2024

[Effective Java] Item 5. Use Dependency Injection instead of Hard-Coding Resources If a class relies on external resources, it is not recommended to use singletons or static utility classes. Dependency injection can be used to improve the class's flexibility, reusability, and testability, and using the factory method pattern can lead to

April 28, 2024

[Effective Java] Item 3. Ensure Singleton with Private Constructor or Enum Type This article introduces three ways to implement the Singleton pattern in Java (public static member, static factory method, enum type), explains the pros and cons of each method, and the precautions to be taken during serialization. It presents the enum t

April 27, 2024

Logical Data Modeling Logical data modeling is the process of transforming a conceptual data model into the relational database paradigm based on mapping rules, handling 1:1, 1:N, and N:M relationships, and ensuring data integrity through normalization. It refines tables throu
제이의 블로그
제이의 블로그
제이의 블로그
제이의 블로그
제이의 블로그

April 9, 2024

Relational Data Modeling Relational data modeling is the process of dividing real-world information into tables and data, going through the stages of requirement analysis, conceptual data modeling, logical data modeling, and physical data modeling. Conceptual modeling is visualiz
제이의 블로그
제이의 블로그
제이의 블로그
제이의 블로그

April 8, 2024

[Non-majors, Survival as a Developer] 3. Why I Want to Become a Developer There are many reasons why you might want to become a developer, but to succeed, you need to have a clear goal and work consistently towards it. Problem-solving skills and continuous learning are essential for developers. It is important to constantly evo
투잡뛰는 개발 노동자
투잡뛰는 개발 노동자
투잡뛰는 개발 노동자
투잡뛰는 개발 노동자

March 28, 2024

Kanban Board Project 1 Conceptual Data Modeling The Kanban Board project was a Wanted Backend internship assignment. I implemented the project again for practice with relational data modeling and conducted conceptual data modeling. Through the process of separating entities, defining identifiers, and i
제이의 블로그
제이의 블로그
제이의 블로그
제이의 블로그
제이의 블로그

April 9, 2024

Mocking Prisma Client for Unit Testing in NestJS Removing external dependencies is essential for application unit testing. You can easily perform unit testing using Prisma ORM's Jest mocking method. By installing the jest-mock-extended package and mocking Prisma Client, you can build a convenient testin
제이의 블로그
제이의 블로그
제이의 블로그
제이의 블로그

April 2, 2024

Physical Data Modeling Physical data modeling is the process of designing tables in a relational database for actual use. It aims to optimize performance through storage space efficiency, data partitioning, and index design. Performance issues can be addressed through slow quer
제이의 블로그
제이의 블로그
제이의 블로그
제이의 블로그
제이의 블로그

April 9, 2024