Cascade Design Pattern

Cascade Design Pattern

Introduction

A fluent Interface or Cascade Design Pattern is a design pattern that allows you to chain method calls together in a readable and fluent way. The idea behind the pattern is to create an object that has methods that return the object itself so that you can chain the calls together in a readable way.

Cascade Pattern prior to Java 8

Steps:

  1. Define a class that represents the object you want to cascade the method calls on. This class should have methods that return the object itself so that you can chain the calls together.

  2. In the class, define each method that you want to be able to cascade. Each of these methods should return the object itself.

For example, take this Mailer class. We need to set multiple properties (or perform operations) before sending mail. In this case, we can use the cascade pattern to call required methods and perform send operation in a readable/fluent way.

class Mailer {
  public Mailer from(String from) {
    System.out.println("from " + from);
    return this;
  }

  public Mailer to(String to) {
    System.out.println("to " + to);
    return this;
  }

  public Mailer subject(String subject) {
    System.out.println("subject " + subject);
    return this;
  }

  public Mailer body(String body) {
    System.out.println("body " + body);
    return this;
  }

  public void send() {
    System.out.println("sending ...");
  }

}

The client can use the Mailer class as below:

public static void main(String[] args) {
    new Mailer()
        .from("mailer@gmail.com") // return instance of Mailer
        .to("sandeep@gmail.com") // return instance of Mailer
        .subject("Message from mailer") // return instance of Mailer
        .body("...details of message...") // return instance of Mailer
        .send(); // perform send operation
}

Output:

From: mailer@gmail.com
To: sandeep@gmail.com
Subject: Message from mailer
Body: ...details of message...
Sending Mail ...

Cascade Pattern in Java 8 (Using Lambda):

In the above example, it's the client's responsibility to instantiate the Mailer object and then use it. Instead, the Mailer class itself can create an object and then use it to send the email.

We can provide Consumer to the send function. This consumer would be responsible for triggering other required methods.

class LambdaMailer {
// Make constructor private so that only way to instantiate is through send method
  private LambdaMailer(){}

  public LambdaMailer from(String from) {
    System.out.println("From: " + from);
    return this;
  }

  public LambdaMailer to(String to) {
    System.out.println("To: " + to);
    return this;
  }

  public LambdaMailer subject(String subject) {
    System.out.println("Subject: " + subject);
    return this;
  }

  public LambdaMailer body(String body) {
    System.out.println("Body: " + body);
    return this;
  }

// Make send method public and static, so class can be invoked without creating instance.
  public static void send(Consumer<LambdaMailer> mailerConsumer) {
// Now, instantiation is responsibility of Mailer class
    LambdaMailer instance = new LambdaMailer(); 
// Trigger methods required before sending the mail
    mailerConsumer.accept(instance);
// Send mail
    System.out.println("Sending Email...");
  }
}

The client can use the LambdaMailer class as below:

public static void main(String[] args) {
// Pass consumer to send method
    LambdaMailer.send( mailer -> mailer
        .from("lambda-mailer@linkedin.com")
        .to("sandeep@linkedin.com")
        .subject("Message from lambda mailer")
        .body("... details of lambda message ..."));
  }

Difference between Builder and Cascade design pattern

Builder Pattern

Cascade Pattern

Used to construct an object

Used to perform multiple operations on an existing object

Used to create an object by providing a set of parameters and then constructing the object based on those parameters

Used to perform multiple operations on an existing object without the need for explicit calls to each of the operations