DEV Community

Cover image for The Power of Template Design Pattern in JavaScript
jsmanifest
jsmanifest

Posted on • Originally published at jsmanifest.com on

The Power of Template Design Pattern in JavaScript

power-of-template-design-pattern-in-javascript

If you've used nodejs before then you know that packages are at the heart of this platform. Every day and every second there is either a new update or a new package published to the npm registry. The majority of these packages are reusable and extensible. The way they do this can be one of many ways but there's one common trait that they all share: They can be seen as templates that are waiting for you to execute them.

This post will go over the Template Design Pattern in JavaScript. We will understand more in detail the approach of this pattern and one scenario of when we should use it. We will also see a diagram of how the the structure look like "outside the box". And finally, we will implement the pattern in code so that by the end of this article you will be comfortable about templating in JavaScript.

How does the Template Pattern work?

When we are implementing this pattern a useful way to approach this is to think about the start phase of something and the end phase.

When we write functions the first thing we think about sometimes is to decide on its parameters and how variables will be initialized. Eventually we decide how to end that function.

What happens in the middle depends on the implementation.

This is similar to how the flow of the Template works.

In more official terms it's essentially a bare interface that is given to the consumer where they can implement one or more steps of the algorithm without changing the structure.

After they define those steps and follows execution, the "end" phase is reached, just like a basic function.

When is the Template Pattern needed?

It's most needed in scenarios where two functions have important similarities in an implementation or interface but share the same problem where they aren't able to reuse those similarities. This means that when there is an update to one of the function's implementation, the other function needs to update its implementation as well. This is a bad practice and eventually becomes unmaintainable if not dealt with.

This is where the Template Pattern comes in. It encapsulates those similarities in itself and delegates the responsibilities of the other parts out for those that derive and implement them themselves.

That way if there was a change to the implementation of the encapsulated parts all derived classes don't have to be involved in them.

How does the Template Pattern look like in code?

In this section we will be implementing the Template Pattern ourselves.

Like I mentioned before, this can be implemented in a lot of ways because the pattern in its implementation is closely relative to the problem it is addressing. However they all have the same objective when we look at it in a bigger perspective.

Let's pretend we are building a function that runs a series of "transform" functions on a collection of dates of any date format. These can look like this:

const dates = [
  357289200000,
  989910000000,
  'Tue Jan 18 2005 00:00:00 GMT-0800 (Pacific Standard Time)',
  new Date(2001, 1, 03),
  new Date(2000, 8, 21),
  '1998-02-08T08:00:00.000Z',
  new Date(1985, 1, 11),
  '12/24/1985, 12:00:00 AM',
  new Date(2020, 6, 26),
  'Tue May 15 2001 00:00:00 GMT-0700 (Pacific Daylight Time)',
  1652252400000,
  '2005-01-18T08:00:00.000Z',
  new Date(2022, 7, 14),
  '1999-02-01T08:00:00.000Z',
  1520668800000,
  504259200000,
  '4/28/1981, 12:00:00 AM',
  '2015-08-08T07:00:00.000Z',
]
Enter fullscreen mode Exit fullscreen mode

Our function will implement the Template Pattern and our task is to define the base skeleton holding these "empty" placeholders:

  1. reducer
  2. transformer
  3. finalizer
  4. sorter

When objects are created and derive from one of them they can provide their own algorithm that will be run when our function executes.

The consumer will have to implement the reducer as a function that takes an accumulator and a value and returns some accumulated result.

transformer is a function that transforms and returns a value of any data type.

finalizer takes in a value and also returns a value of any data type. But this time this value will be used to perform the final step.

The sorter is a function that takes in one item in the first argument and another item in the second argument. This function is the same as how you would implement the function in the native .Array.sort method.

Our function with the template implementation will be named createPipeline and takes in those functions if provided by the caller. If the caller doesn't provide one or more of them we must substitute them with a default implementation so that our algorithm can still run:

function createPipeline(...objs) {
  let transformer
  let reducer
  let finalizer
  let sorter

  objs.forEach((o) => {
    const id = Symbol.keyFor(_id_)
    if (o[id] === _t) transformer = o
    else if (o[id] === _r) reducer = o
    else if (o[id] === _f) finalizer = o
    else if (o[id] === _s) sorter = o
  })

  if (!transformer) transformer = { transform: identity }
  if (!reducer) reducer = { reduce: identity }
  if (!finalizer) finalizer = { finalize: identity }
  if (!sorter) sorter = { sort: (item1, item2) => item1 - item2 }

  return {
    into(initialValue, ...items) {
      return items
        .reduce((acc, item) => {
          return reducer.reduce(
            acc,
            finalizer.finalize(transformer.transform(item)),
          )
        }, initialValue)
        .sort((item1, item2) => sorter.sort(item1, item2))
    },
  }
}
Enter fullscreen mode Exit fullscreen mode

This simple function is a template where callers can pass in their own algorithms. It allows them to choose not to pass in any implementation or allow them to pass in one or all of the 4 functions involved in the pipeline.

When they call the into function with a collection of items, the next step is to immediately run all of them through the pipeline and are eventually accumulated into a new collection.

Something we often see from libraries that provide some form of template interface to consumers is that they try to make it as easy as possible to be worked with.

For example, the createStore in the redux library provides several overloads that developers can work with for instantiation. This is a very useful thing to do and it improves their reusability but also demonstrates the nature of a template in practice.

Inside template pattern implementations when there is a strict flow that an algorithm requires it is usually hidden within the implementation like the createStore in redux.

When we go back to our previous example we noticed something in these lines:

objs.forEach((o) => {
  const id = Symbol.keyFor(_id_)
  if (o[id] === _t) transformer = o
  else if (o[id] === _r) reducer = o
  else if (o[id] === _f) finalizer = o
  else if (o[id] === _s) sorter = o
})
Enter fullscreen mode Exit fullscreen mode

This was not required or had anything to do with our pipeline but because we created a helper to distinguish them we allowed the caller to pass in any of the transformer,reducer, finalizer and sorter functions in any order even though they need to be in order when it runs the functions.

So any of these calls all return the same exact result even though they are ordered differently:

console.log(getResult(reducer, transformer, finalizer, sorter))
console.log(getResult(transformer, reducer, finalizer, sorter))
console.log(getResult(finalizer, sorter, transformer, reducer))
console.log(getResult(sorter, finalizer, transformer, reducer))
Enter fullscreen mode Exit fullscreen mode

In the internal implementation it doesn't work as expected if they were to be called in different orders because the sorter needs to be the final operation. The finalizer needs to be run before the final (the sorter) operation and the transformer needs to be run before the finalizer.

This is how the higher level implementation looks like:

function createFactories() {
  const _id_ = Symbol.for('__pipeline__')
  const identity = (value) => value

  const factory = (key) => {
    return (fn) => {
      const o = {
        [key](...args) {
          return fn?.(...args)
        },
      }

      Object.defineProperty(o, Symbol.keyFor(_id_), {
        configurable: false,
        enumerable: false,
        get() {
          return key
        },
      })

      return o
    }
  }

  const _t = 'transform'
  const _r = 'reduce'
  const _f = 'finalize'
  const _s = 'sort'

  return {
    createTransformer: factory(_t),
    createReducer: factory(_r),
    createFinalizer: factory(_f),
    createSorter: factory(_s),
    createPipeline(...objs) {
      let transformer
      let reducer
      let finalizer
      let sorter

      objs.forEach((o) => {
        const id = Symbol.keyFor(_id_)
        if (o[id] === _t) transformer = o
        else if (o[id] === _r) reducer = o
        else if (o[id] === _f) finalizer = o
        else if (o[id] === _s) sorter = o
      })

      if (!transformer) transformer = { transform: identity }
      if (!reducer) reducer = { reduce: identity }
      if (!finalizer) finalizer = { finalize: identity }
      if (!sorter) sorter = { sort: (item1, item2) => item1 - item2 }

      return {
        into(initialValue, ...items) {
          return items
            .reduce((acc, item) => {
              return reducer.reduce(
                acc,
                finalizer.finalize(transformer.transform(item)),
              )
            }, initialValue)
            .sort((item1, item2) => sorter.sort(item1, item2))
        },
      }
    },
  }
}
Enter fullscreen mode Exit fullscreen mode

One of several key parts of the internal implementation are these lines:

Object.defineProperty(o, Symbol.keyFor(_id_), {
  configurable: false,
  enumerable: false,
  get() {
    return key
  },
})
Enter fullscreen mode Exit fullscreen mode

This makes our template "official" because it hides the identifier from being seen from the outside and only exposes createTransformer, createReducer, createFinalizer, createSorter, and createPipeline to the consumer.

Another part that helps the template is the object above it:

const o = {
  [key](...args) {
    return fn?.(...args)
  },
}
Enter fullscreen mode Exit fullscreen mode

This helps to structure a fluent api that reads like english:

into(initialValue, ...items) {
    return items
        .reduce((acc, item) => {
            return reducer.reduce(
                acc,
                finalizer.finalize(transformer.transform(item)),
            )
        }, initialValue)
        .sort((item1, item2) => sorter.sort(item1, item2))
}
Enter fullscreen mode Exit fullscreen mode

Lets pretend we are the consumer and we want to use this template on this collection of dates as we've seen earlier:

const dates = [
  357289200000,
  989910000000,
  'Tue Jan 18 2005 00:00:00 GMT-0800 (Pacific Standard Time)',
  new Date(2001, 1, 03),
  new Date(2000, 8, 21),
  '1998-02-08T08:00:00.000Z',
  new Date(1985, 1, 11),
  '12/24/1985, 12:00:00 AM',
  new Date(2020, 6, 26),
  'Tue May 15 2001 00:00:00 GMT-0700 (Pacific Daylight Time)',
  1652252400000,
  '2005-01-18T08:00:00.000Z',
  new Date(2022, 7, 14),
  '1999-02-01T08:00:00.000Z',
  1520668800000,
  504259200000,
  '4/28/1981, 12:00:00 AM',
  '2015-08-08T07:00:00.000Z',
]
Enter fullscreen mode Exit fullscreen mode

We have some issues:

  1. They are in different data types. We want them all to be in ISO date format.
  2. They are not sorted. We want them all to be sorted in ascending order.

We can use the code that implements the template design pattern to solve these issues so that we can get an ordered collection of dates in ISO format:

const isDate = (v) => v instanceof Date
const toDate = (v) => (isDate(v) ? v : new Date(v))
const subtract = (v1, v2) => v1 - v2
const concat = (v1, v2) => v1.concat(v2)

const reducer = factory.createReducer(concat)
const transformer = factory.createTransformer(toDate)
const finalizer = factory.createFinalizer(toDate)
const sorter = factory.createSorter(subtract)

const getResult = (...fns) => {
  const pipe = factory.createPipeline(...fns)
  return pipe.into([], ...dates)
}

console.log(getResult(reducer, transformer, finalizer, sorter))
console.log(getResult(transformer, reducer, finalizer, sorter))
console.log(getResult(finalizer, sorter, transformer, reducer))
console.log(getResult(sorter, finalizer, transformer, reducer))
Enter fullscreen mode Exit fullscreen mode

It doesn't require much code and all of our executions return the same result:

[
  "1981-04-28T07:00:00.000Z",
  "1981-04-28T07:00:00.000Z",
  "1985-02-11T08:00:00.000Z",
  "1985-12-24T08:00:00.000Z",
  "1985-12-24T08:00:00.000Z",
  "1998-02-08T08:00:00.000Z",
  "1999-02-01T08:00:00.000Z",
  "2000-09-21T07:00:00.000Z",
  "2001-02-03T08:00:00.000Z",
  "2001-05-15T07:00:00.000Z",
  "2001-05-15T07:00:00.000Z",
  "2005-01-18T08:00:00.000Z",
  "2005-01-18T08:00:00.000Z",
  "2015-08-08T07:00:00.000Z",
  "2018-03-10T08:00:00.000Z",
  "2020-07-26T07:00:00.000Z",
  "2022-05-11T07:00:00.000Z",
  "2022-08-14T07:00:00.000Z"
]
Enter fullscreen mode Exit fullscreen mode

Here is a diagram depicting our template:

template-design-pattern-javascript-diagram.png

And there you go!

Another Example

I like to use snabbdom to demonstrate concepts in several of my posts because it is short, simple, powerful and uses several techniques that are relative to the topics I wrote about in the past. Snabbdom is a front end JavaScript library that lets you work with a virtual DOM to create robust web applications. They focus on simplicity, modularity and performance.

They provide a module api where developers can create their own modules. They do this by providing to consumers a template that provides hooks that hook onto the lifecycle of a "patching" phase where DOM elements are passed around to life cycles. This is a simple but powerful way to work with the virtual DOM. It is a great example of one variation of a template pattern.

This is their template:

const myModule = {
  // Patch process begins
  pre() {
    //
  },
  // DOM node created
  create(_, vnode) {
    //
  },
  // DOM node is being updated
  update(oldVNode, vnode: VNode) {
    //
  },
  // Patching is done
  post() {
    //
  },
  // DOM node is being directly removed from DOM via .remove()
  remove(vnode, cb) {
    //
  },
  // DOM node is being removed by any method including removeChild
  destroy(vnode) {
    //
  },
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

And that concludes the end of this post! I hope you got something out of it and look out for more posts from me in the future!

Top comments (0)