Simplifying function arguments (the airport passport control)

Examples are in typescript: {} is like amap, type a|b means a or b,

Compare the two:

function f1(a: number, b: number, c: number): number
function f2(i: { a: number; b: number; c: number }): number

In func f1, there's complexity: args positionally depend on each other:

  • when we add a new argument, we need to update all clients
  • when we add an optional argument, we need to update all clients, or provide default value

As a consequence, clients are complected with the function.

Also, this positional complexity might lead to function proliferation:

function getChargePointByCityAndSerial(city, serial)
function getChargePointById(id)

If we remove the positional complexity, the function becomes:

function getChargePoint({city, serial} | {id})

semantically: give me a chargepoint, either by an id or city, serial.

This is exactly what the passport control does. You approach the control and they ask you: give me an ID or passport. And they will either let you pass or not. Passport control is a function

function checkPassenger(by: { id: IDCard } | { passport: Passport }): boolean

They are a boolean filter. In fact, it turns out, that in the airline industry, they indeed use the term "filter" for this.

Let's go back to the function. Having all parameters in one map, we can even name the input type if we want:

interface ChargePointFilter = {city: string, serial: string} | {id: serial}
function getChargePoint(f: ChargePointFiler)

But what if the arguments doesn't relate to each other? For example:

function getChargePointByCityAndSerial(db, city, serial)

The db, representing a db connection, doesn't fit well with the ChargePointFilter. It turns out, this might be a consequence of another complexity: complecting the filter with the db connection. What about decomplecting?

export default (db: Db) => ({
  getChargePoint: (f: ChargePointFilter) => ({
    /*...*/
  }),
})

If we have multiple queries:

// content of commonQueries.tsx
export default (db: Db) => ({
  getChargePoint: (f: ChargePointFilter) => ({
    /*...*/
  }),
  getLogs: (f: { chargePointId }) => ({
    /*...*/
  }),
  //...
})

which can be used as

import commonQueries from "commonQueries"
function f() {
  const db = getDb() //from somewhere
  const queries = commonQueries(db)
  console.log(queries.getChargePoint({ id: 12314 }))
}

All in all, we resolved:

  • complecting function arguments with each other by position
  • complecting semantically different groups of arguments (db vs filter)
  • complecting function name with its arguments (getChargePointByCityAndSerial(city, serial))
  • inconsitency of using multiple functions for semantically the same purpose, that is, retrieve a charge point from the database

See how a simple principle of complexity in fuction arguments leads us forward to orthogonal design? This orthgonality in design allowed us to:

  • add new filters without breaking clients
  • limit proliferation of functions
  • easier testing: we can plug in whatever db
  • less decisions to make when changing code
  • out of the box portability to other projects or open-source
  • more consistency with the domain model
  • and, be more consistent with the real world

-

If you need help with building the tech products get in touch.