DEV Community

Cover image for Forever Functional: Chaining Calls for Fluent Interfaces
OpenReplay Tech Blog
OpenReplay Tech Blog

Posted on • Edited on • Originally published at blog.openreplay.com

Forever Functional: Chaining Calls for Fluent Interfaces

by author Federico Kereki

Chaining calls is a way to design fluent interfaces, which in turn allow for clearer, more understandable code.

How do you design a clear API? More specifically, how can you make your API easier to use? The concept of a "fluent interface", based on "chaining", lets you provide a more streamlined development experience to developers. While in the two previous articles in this series (here and here) we looked at improving performance, now we'll focus on improving clarity and usability.

What is Chaining?

Composing and pipelining are common patterns to apply a sequence of transformations when you work functionally . However, when you work with arrays or objects, there's another way of linking those calls: by chaining them. A well known example of this is provided by map(...) or filter(...), each of which returns a new array, to which you can apply further calls. As a trivial (somewhat exaggerated!) example, let's see some code from my Mastering JavaScript Functional Programming book. We can start by defining some essentially nonsensical operations:

const testOdd = x => x % 2 === 1;
const testUnderFifty = x => x < 50;
const duplicate = x => x + x;
const addThree = x => x + 3;
Enter fullscreen mode Exit fullscreen mode

We can now apply those maps and filters, in sequence, to an array:

  • First, we'll drop even numbers;
  • then, we'll duplicate those that are left;
  • we'll filter out results that are 50 or more;
  • and we'll finish by adding 3 to whatever remains.
const myArray = [22, 9, 60, 24, 11, 63];

const a0 = myArray
  .filter(testOdd)
  .map(duplicate)
  .filter(testUnderFifty)
  .map(addThree);
// [ 21, 25 ]
Enter fullscreen mode Exit fullscreen mode

The .map(...) and .filter(...) operations return a new array, to which you can apply further operations. You could have done this in several lines as shown below, but (I hope) you'll agree that this isn't as clear. (A reader would wonder if x1, x2, and x3 would be used again, for example.) This is the alternative code:

const x1 = myArray.filter(testOdd);
const x2 = x1.map(duplicate);
const x3 = x2.filter(testUnderFifty);
const a0 = x3.map(addThree);
// [ 21, 25 ]
Enter fullscreen mode Exit fullscreen mode

So, chaining implies that operations will return some object to which you may apply further operations. This is interesting by itself, but it makes more sense in the context of "Fluent Interfaces", which we'll discuss below.

What are Fluent Interfaces?

What is a "fluent interface"? The term was invented by Martin Fowler back in 2005; the idea is chaining method calls, so code will be more legible, almost becoming a domain-specific language. A fluent API is primarily designed to be highly readable, using terms (methods) that are close to the domain you're working on. This explanation may be hard to understand, I admit, so let's see a couple of examples.

A well-known library that implements a fluent interface is jQuery. For example, you can write something like the following, that will access a DOM element, set some of its attributes, add CSS, and make the element visible, all in a single line.

$("#titlephoto")
  .attr({
    alt: "Street scene",
    title: "Pune, India",
  })
  .css("background-color", "black")
  .show();
Enter fullscreen mode Exit fullscreen mode

You could get the same result in several lines -- but this is cleaner. Another example: the testing framework Jest applies fluent interfaces everywhere. For example, you can define a mock function that will return true once, then false, and undefined from that point on, in a single line.

const fakeTest = jest
  .fn()
  .mockReturnValueOnce(true)
  .mockReturnValueOnce(false)
  .mockReturnValue(undefined);
Enter fullscreen mode Exit fullscreen mode

Let's go for a final example. The popular D3.js library also uses this style to join together several method calls.

d3.select("svg")
  .append("text")
  .attr("font-size", "20px")
  .attr("transform", "translate(100,0)")
  .attr("x", 150)
  .attr("y", 200)
  .text("Sample Chart Title");
Enter fullscreen mode Exit fullscreen mode

The key to the concept of a fluent interface is that it somehow mimics the way you think ("ok, select such element, append some text to it, change its font size, ...") making code more understandable. Chaining method calls is the way to achieve this, but don't just think of it as a way to allow to write shorter code or avoid needing some extra intermediate variables. Conciseness and terseness are convenient, but not necessarily the best reasons for adopting the chaining pattern. Instead, consider chaining as a means to develop a Fluent Interface.

With this out of the way, let's consider how we can implement this pattern by ourselves, for our own code.

DIY: Chaining method calls

How can we implement chainable methods? The first solution, doing it by hand, is possibly the simplest. We just have to modify each method to return this at the end -- excepting, obviously, methods that need to return something else! This is certainly a solution, but we may think of something that needs less work, and proxy objects are a possibility.

A proxy object can intercept calls to another object, redefining how it works. You define a proxy by providing an object (the one whose methods will be intercepted) and a handler, that will implement whichever changes you want to achieve. Note that we won't be able to "proxify" a class; we have to proxify each object by itself.

In our case, we'll want to intercept all method calls, and make them return a reference to the object itself... unless, of course, the method is returning something else. This poses a slight problem: how do we detect if a method isn't returning anything. JavaScript, by default, works as if a return undefined statement had been added to functions or methods that don't otherwise return anything - so if your class has a method that may actually return undefined, we're in trouble!

An implementation of this proxy may be as follows; the makeChainable(...) function will transform an object into a chainable alternative, all of whose methods (that don't return undefined) will be returning a reference to the object itself.

const makeChainable = (obj) =>
  new Proxy(obj, {
    get(target, property, receiver) {                        /* 1 */
      return typeof target[property] === "function"          /* 2 */
        ? (...args) => {                                     /* 3 */ 
            const result = target[property](...args);        /* 4 */
            return result === undefined ? receiver : result; /* 5 */
          }
        : target[property];                                  /* 6 */
    }
  });
Enter fullscreen mode Exit fullscreen mode

A handler provides multiple "traps" for different operations and types of access. We want to intercept "get" calls (1) that make target methods or attributes. If the target is a function (2) we'll return a modified function that will accept the same arguments (3) as the original method, invoke that method (4), and then test if the result was undefined in which case it will return a reference to the proxied object (5) instead of the original returned value. If the target wasn't a function, it was an access to an attribute; we return it (6).

Open Source Session Replay

Debugging a web application in production may be challenging and time-consuming. OpenReplay is an Open-source alternative to FullStory, LogRocket and Hotjar. It allows you to monitor and replay everything your users do and shows how your app behaves for every issue.
It’s like having your browser’s inspector open while looking over your user’s shoulder.
OpenReplay is the only open-source alternative currently available.

OpenReplay

Happy debugging, for modern frontend teams - Start monitoring your web app for free.

A gangland example

Let's say we are developing an application with data on mobsters from the thirties -- maybe we're setting up a new "Untouchables" movie? We could have a class allowing us to record the first and last names of a criminal, as well as its nickname or alias.

class Mobster {
  constructor(firstName, lastName, nickname) {
    this.firstName = firstName;
    this.lastName = lastName;
    this.nickname = nickname;
  }

  setFirstName(newFirst) {
    this.firstName = newFirst;
  }

  setLastName(newLast) {
    this.lastName = newLast;
  }

  setNickname(newNickname) {
    this.nickname = newNickname;
  }

  getFullName() {
    return `${this.firstName} "${this.nickname}" ${this.lastName}`;
  }
}
Enter fullscreen mode Exit fullscreen mode

As is, objects in this class aren't chainable. The three setters do not return this, so you cannot chain methods. Certainly, we could rewrite the class by adding some lines as follows... but we'll look for a better, functional way of doing this.

class Mobster {
  constructor(...) { ... }

  setFirstName(newFirst) {
    this.firstName = newFirst;
    return this;                 /* added */
  }

  setLastName(newLast) {
    this.lastName = newLast;
    return this;                 /* added */
  }

  setNickname(newNickname) {
    this.nickname = newNickname;
    return this;                 /* added */
  }

  getFullName() { ... }
}
Enter fullscreen mode Exit fullscreen mode

To simplify creating new chainable objects, we could write a helper or we'll first have to create an object and then make it chainable; it's better to do all this in a single step. The helper we need does those two steps at once.

const makeMobster = (...args) => makeChainable(new Mobster(...args));
Enter fullscreen mode Exit fullscreen mode

Now we can write code as follows.

const gangster = makeMobster("Alphonse", "Capone", "Al");
console.log(gangster.getFullName());
// Alphonse "Al" Capone

console.log(
  gangster
    .setFirstName("Benjamin")
    .setLastName("Siegel")
    .setNickname("Bugsy")
    .getFullName()
);
// Benjamin "Bugsy" Siegel
Enter fullscreen mode Exit fullscreen mode

Using chaining, you can design your code to be fluent, for more readable code -- by changing how you create an object!

Summary

When designing an API of your own, going for a fluent interface produces a more easily used library. Chaining is key in implementing this pattern, and functional programming lets you transform any object into a chainable equivalent, instead of having to do it by hand. With this technique, your code will become simpler and more understandable; try it out!

Top comments (0)