Fluent interfaces

All you need to know to make your own!
Introduction

About me

Daniël Beeke,

Software engineer at OM MediaWorks

Dutch, living in Stockerau

Introduction

My background

Full stack software engineer

Working on a headless CMS that:

  • Uses an RDF graph database
  • Outputs media catalog PWAs with ease
  • Up next: sharing of data between instances
  • This is when I learned about fluent interfaces

Chapters

  • What are fluent interfaces?
  • Use cases
  • My use case
  • Core concepts
  • Different types of chains
  • Considerations

What are fluent interfaces?

You probably have been working with them.

What are fluent interfaces?

A definition

In software engineering,
a fluent interface is an object-oriented API
whose design relies extensively on
method chaining.

What are fluent interfaces?

Example: jQuery

jQuery('.my-button').fadeOut().remove()
What are fluent interfaces?

Example: Entity builder

new Person()
  .firstName('John')
  .lastName('Doo')
  .dateOfBirth('05-09-1883')
  .save()
What are fluent interfaces?

Example: Fluent Snake

await api
  .fetch('https://en.wikipedia.org/wiki/Linux')
  .querySelector('.infobox tr:nth-child(4) a')
  .href()
  .fetch()
  .querySelector('#firstHeading')
  .text()

// Unix-like
  
What are fluent interfaces?

Example: LDflex on top of a Solid pod

await solid.data['https://danielbeeke.nl/']['foaf:img']

// https://danielbeeke.nl/images/daniel.jpg
  
What are fluent interfaces?

What are fluent interfaces?

Good developer experience

  • Abstractions on top of complexity
  • Provide easy access to data
  • Feels like objects
  • TypeScript autocompletion

Use cases

  • Libraries
  • Data for the template
  • Query builders
Use cases

Libraries

moment('20111031', 'YYYYMMDD')
  .subtract(6, 'days')
  .fromNow()
Use cases

Libraries

d3.select('svg')
  .append('text')
  .attr('font-size', '20px')
  .attr('transform', 'translate(100,0)')
  .attr('x', 150)
  .attr('y', 200)
  .text('Sample Chart Title')

Use cases

Data for the template

Omni prototype
Uses LDflex and uhtml/async similar to React

const source = 'https://ruben.verborgh.org/profile/'
const omni = await new Omni(source, { context })
const person = omni.get(source + '/#me')

render(document.body, html`
  

${person.name}

    ${person.friends.givenName.map(name => html`
  • ${name}
  • `)}
`);
Use cases

Query builders

knex.select('id').from('users')

My use case

A bit of background.

What made me use and learn about fluent interfaces?

My use case

We are building an headless CMS
with frontends that support:

  • Multilingual media items
  • (all metadata of books, authors, videos etc. in multiple languages)
  • Online and offline usage
  • (read eBooks / listen podcasts while traveling etc)
My use case

We wanted

  • Easy integration of the CMS in frontends
  • While enabling (new) features
  • A library that abstracts away complexity
My use case

It is mostly data display

const eBook = get('ebook', id)

return html`
  

${ebook.title}

${ebook.authors.map(author => author.name)} `
My use case

Also lists

const offset = state.page * this.pageSize
const limit = this.pageSize

const overviewQuery = query(queryTypes)
  .sort({ name: 'asc' })
  .filter(filters)
  .addSidetrack('authors')
  .addSidetrack('category')
  .paginate({ offset, limit })

Core concepts

The mechanics that make it work.

Core concepts

Methods must return 'this'

class Thing {
  methodA () { return this }

  methodB () {
    console.log('b')
    return this
  }
}

const item = new Thing().methodA().methodB()

// b
Core concepts

A getter example

Core concepts

getters

You can use getter methods in chains

class Thing {
  get fruit () {
    return 'pear'
  }
}

console.log(item.fruit)

// pear
Core concepts

You can use .then() to have async chains

class Thing {
  async then (resolve) {
    // Do async things here ...
    resolve('Hello World')
  }
}

const item = new Thing()
console.log(await item)

// Hello World
Core concepts

await vs. .then()

await triggers the .then method

Core concepts

Queue

class Calculator {
  #commands = []

  add (number) { 
    this.#commands.push((total) => total + number)
    return this
  }

  minus ...
}
    
Core concepts

Queue

temperatureForCity (city) { 

  this.#commands.push(async (total) => {

    const { lat, lon } = await getLatLonByCityName(city)
    const temperature = await getTempByLatLon(lat, lon)

    return total + temperature
  })

  return this
}
    
Core concepts

Queue

class Calculator {
  #commands = []
  
  ...

  async then(resolve) {
    let result = 0
    for (command of this.#commands)
      result = await command(result)
    resolve(result)
  }
}

    
Core concepts

Queue

const calculator = new Calculator()
const number = await calculator
  .add(10)
  .temperatureForCity('Vienna') 
  .minus(4)
  .temperatureForCity('Amsterdam') 
  
// ~ 56

    
Core concepts

Promise chain

These can be used to return the value. Every segment of the chain appends a new Promise to the previous Promise.


// A Promise chain

const person = await fetch('/user.json')
  .then(response => response.json())
  .then(response => new Person(response))
Core concepts

Proxy

A layer on top of an object which can intercept and
redefine fundamental operations on that object.

We might want to have segments with properties we do not know beforehand.

Core concepts

Proxy example

const myObject = { hello: 'world' }

const proxy = new Proxy(myObject, {
  get: function(target, prop, receiver) {
    return 'Vienna'
  }
})

proxy.hello // Vienna
proxy.gutenTag // Vienna

Different types of chains

  • Builder chain
  • Async Queue chain
  • Async Proxy chain
Different types of chains

Builder chain

One class instance, returned from each method.
Keeps state in the instance.
Great for building queries or entities.

Different types of chains

Async queue chain

One class instance, returned from each method.
Methods add commands to queue.
.then() executes them all.

Different types of chains

Async proxy promise chain

Each chunk is a new Proxy.
.then() resolves the Promise chain.
Allows for unknown keys in the chain.

Considerations

What things could you consider?

Considerations

Diverging paths

One array of commands will not be enough.
Promises are better in this area.


Considerations

Diverging paths

Each chunk is its own Promise. With a queue we would have to clone the queue a lot of times.


Considerations

TypeScript

What about type autocompletion?

Considerations

TypeScript

TypeScript does not understand a Proxy.
But, we can instruct TypeScript it is dealing with something else.

It start with Generics...

const items: Array<​string> = []
Considerations

TypeScript Generics

function myFunction<​Type>(arg: Type) {}

const example1 = myFunction<​string>()
const example2 = myFunction<​number>()
const example3 = myFunction<​Array<​​string>>()

const example4 = myFunction('Hello World')
Considerations

TypeScript Generics

function myFunction<​Type>(arg: Type): Promise<​Type> {
  return new Promise(resolve => resolve(arg))
}
Considerations

TypeScript: as unknown as Type

We can use this trick to tell TypeScript something acts as a certain type.

function myFunction(arg: string) {
  return arg as unknown as number
}

Summary

  • Fluent interfaces enable DX
  • Core concepts
    • return this
    • getter methods
    • Proxies for unknown properties
    • then() for async chains
  • There are multiple types of chains
    • async vs. sync
    • queue vs. promise
  • Considerations
    • Use a Promise chain for templating
    • TypeScript can give autocompletion

What are you going to create?

Questions, thoughts? Thanks!

Interested? Here are some links:


This presentation:

https://danielbeeke.nl/fluent-interfaces