Clean Code: Optional Parameters

Bubu Tripathy
5 min readFeb 26, 2024

When dealing with optional parameters in methods, consider avoiding a long list of parameters or the use of null values. Instead, opt for method overloading or the builder pattern to enhance code readability and maintainability. This practice provides a cleaner interface for calling methods with varying sets of parameters.

⭐️ Utilize method overloading to create multiple versions of a method with different parameter sets. This allows for clear and explicit invocation without relying on optional parameters.

⭐️ If the number of optional parameters becomes substantial, consider implementing the builder pattern. This approach involves creating a separate builder class to construct instances with optional parameters.

public class Person {
private final String firstName;
private final String lastName;
private final int age;
private final String address;
private final String phoneNumber;

private Person(Builder builder) {
this.firstName = builder.firstName;
this.lastName = builder.lastName;
this.age = builder.age;
this.address = builder.address;
this.phoneNumber = builder.phoneNumber;
}
// Getters for Person properties

public static class Builder {
private final String firstName;
private final String lastName;
private int age;
private String address;
private String phoneNumber;

public Builder(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
public Builder age(int age) {
this.age = age;
return this;
}
public Builder address(String address) {
this.address = address;
return this;
}
public Builder phoneNumber(String phoneNumber) {
this.phoneNumber = phoneNumber;
return this;
}
public Person build() {
return new Person(this);
}
}
// Other methods or behaviors specific to Person class
}

Now, you can create Person instances using the Builder class:

public class PersonExample {
public static void main(String[] args) {
// Creating a Person with mandatory fields only
Person person1 = new Person.Builder("John", "Doe").build();
// Creating a Person with some optional fields
Person person2 = new Person.Builder("Jane", "Smith")
.age(30)
.address("123 Main St")
.build();
// Creating a Person with all fields
Person person3 = new Person.Builder("Bob", "Johnson")
.age(25)
.address("456 Oak St")
.phoneNumber("555-1234")
.build();
}
}

⭐️ Discourage the use of null values for optional parameters, as they can introduce ambiguity and lead to NullPointerExceptions. Instead, rely on method overloading or the builder pattern for a more explicit approach.

public class Car {
private String make;
private String model;
private int year;
private String color;

// Constructors with mandatory parameters
public Car(String make, String model, int year) {
this.make = make;
this.model = model;
this.year = year;
}

// Method overloading for optional parameters
public void setColor(String color) {
this.color = color;
}

// Getters for Car properties
public static void main(String[] args) {
// Creating a Car with mandatory parameters only
Car car1 = new Car("Toyota", "Camry", 2022);
// Creating a Car with an optional color
Car car2 = new Car("Honda", "Civic", 2021);
car2.setColor("Blue");
// Accessing Car properties
System.out.println("Car 1: " + car1.getMake() + " " + car1.getModel() + " " + car1.getYear());
System.out.println("Car 2: " + car2.getMake() + " " + car2.getModel() + " " + car2.getYear() +
" Color: " + (car2.getColor() != null ? car2.getColor() : "Not specified"));
}
}

⭐️ If you’re using Java 8 or later, you can leverage default parameter values in interfaces to provide default values for optional parameters. However, be cautious, as this may reduce code clarity in some cases.

interface Email {
void send(String recipient, String body);

default void sendWithSubject(String recipient, String body, String subject) {
System.out.println("Sending email to " + recipient + " with subject: " + subject);
// Actual implementation for sending email with subject
}

default void sendWithPriority(String recipient, String body, Priority priority) {
System.out.println("Sending email to " + recipient + " with priority: " + priority);
// Actual implementation for sending email with priority
}

enum Priority {
HIGH, MEDIUM, LOW
}
}

class GmailEmail implements Email {
@Override
public void send(String recipient, String body) {
System.out.println("Sending email to " + recipient);
// Actual implementation for sending email
}
}

public class EmailExample {
public static void main(String[] args) {
Email gmailEmail = new GmailEmail();
// Sending a basic email
gmailEmail.send("john.doe@example.com", "Hello, John!");
// Sending an email with a subject
gmailEmail.sendWithSubject("jane.smith@example.com", "Meeting tomorrow", "Important Meeting");
// Sending an email with priority
gmailEmail.sendWithPriority("bob@example.com", "Urgent task", Email.Priority.HIGH);
}
}

When using method overloading or the builder pattern, maintain consistency in naming to make it clear how methods differ. This enhances the readability of your code.

⭐️ Instead of numerous parameters, you can use a configuration object to encapsulate optional parameters. This can be particularly helpful when dealing with a large number of optional settings.

public class DatabaseConnection {
private String url;
private String username;
private String password;
private int timeout;
private boolean autoCommit;
// ... other parameters

// Constructor using a configuration object
public DatabaseConnection(DatabaseConfig config) {
this.url = config.getUrl();
this.username = config.getUsername();
this.password = config.getPassword();
this.timeout = config.getTimeout();
this.autoCommit = config.isAutoCommit();
// ... initialize other parameters
}

// Methods for database operations
public static void main(String[] args) {
// Creating a DatabaseConfig with only the necessary parameters
DatabaseConfig config1 = new DatabaseConfig.Builder("jdbc:mysql://localhost:3306/mydb", "user", "password")
.build();
// Creating a DatabaseConnection with the configuration object
DatabaseConnection connection1 = new DatabaseConnection(config1);
// Performing database operations using connection1
// Creating another DatabaseConfig with different parameters
DatabaseConfig config2 = new DatabaseConfig.Builder("jdbc:postgresql://localhost:5432/otherdb", "admin", "secret")
.setTimeout(60)
.setAutoCommit(false)
.build();
// Creating another DatabaseConnection with the new configuration
DatabaseConnection connection2 = new DatabaseConnection(config2);
// Performing database operations using connection2
}
}

class DatabaseConfig {
private final String url;
private final String username;
private final String password;
private final int timeout;
private final boolean autoCommit;
// ... other parameters
private DatabaseConfig(Builder builder) {
this.url = builder.url;
this.username = builder.username;
this.password = builder.password;
this.timeout = builder.timeout;
this.autoCommit = builder.autoCommit;
// ... initialize other parameters
}
// Getters for parameters
public String getUrl() {
return url;
}
public String getUsername() {
return username;
}
public String getPassword() {
return password;
}
public int getTimeout() {
return timeout;
}
public boolean isAutoCommit() {
return autoCommit;
}

// Builder pattern for DatabaseConfig
public static class Builder {
private final String url;
private final String username;
private final String password;
private int timeout = 30; // Default value
private boolean autoCommit = true; // Default value
// ... other parameters
public Builder(String url, String username, String password) {
this.url = url;
this.username = username;
this.password = password;
}
public Builder setTimeout(int timeout) {
this.timeout = timeout;
return this;
}
public Builder setAutoCommit(boolean autoCommit) {
this.autoCommit = autoCommit;
return this;
}
// Other setters for additional parameters
public DatabaseConfig build() {
return new DatabaseConfig(this);
}
}
}

⭐️ When implementing method overloading or the builder pattern, ensure that all possible combinations of parameters are tested. This guarantees that each version of the method behaves as expected and avoids unexpected interactions between parameters.

Regardless of the approach chosen, prioritize clarity and readability. Ensure that developers using your methods can easily understand the purpose and usage of each version.

--

--