Exploring the Behavioral Strategy Design Model in Node.js

0

A design template is a reusable solution to a recurring problem. The term is really broad in its definition and can cover several areas of an application. However, the term is often associated with a well-known set of object-oriented models that were popularized in the 90s by the book, Design Templates: Reusable Object Elements- Software oriented, Pearson Education, by the almost legendary Strip of four (GoF): Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides.

This article is an excerpt from the book Node.js Design Templates, Third Edition by Mario Casciaro and Luciano Mammino – a comprehensive guide to learning proven patterns, techniques and tips for getting the most out of the Node.js platform.

In this article, we’ll look at the behavior of components in software design. We will learn to combine objects and define their mode of communication so that the behavior of the resulting structure becomes extensible, modular, reusable and adaptable. After presenting all of the behavioral design models, we will delve into the details of the strategy model.

Now it’s time to roll up your sleeves and get your hands dirty with some behavioral design patterns.

Types of behavioral design patterns

  • The strategy model allows us to extract the common parts of a family of closely related components in a component called the context and allows us to define policy objects that the context can use to implement specific behaviors.
  • The state model is a variation of the strategy model where strategies are used to model the behavior of a component when it is in different states.
  • The model model, instead, can be thought of as the “static” version of the Strategy model, where the different specific behaviors are implemented as subclasses of the model class, which models the common parts of the algorithm.
  • The Iterator model provides us with a common interface to browse a collection. It has now become a staple template in Node.js. JavaScript offers native support for the model (with iterator and iterable protocols). Iterators can be used as an alternative to complex asynchronous iteration patterns and even Node.js streams.
  • The Middleware model allows you to define a modular chain of processing steps. This is a very distinctive model born from the Node.js ecosystem. It can be used to preprocess and postprocess data and queries.
  • The order model materializes the information required to perform a routine, allowing this information to be easily transferred, stored and processed.

The strategy model

The Strategy pattern activates an object, called the the context, to support the variations of its logic by extracting the variable parts into separate and interchangeable objects called strategies. Context implements common logic for a family of algorithms, while strategy implements mutable parts, allowing context to adapt its behavior based on different factors, such as input value, system configuration or user preferences.

The strategies are generally part of a family of solutions and all implement the same interface expected by the context. The following figure shows the situation we have just described:


Figure 1: General structure of the Strategy pattern

Figure 1 shows you how the context object can integrate different strategies into its structure as if they were replaceable parts of a machine. Imagine a car; its tires can be considered as its strategy for adapting to different road conditions. Winter tires can be fitted for driving on snow-covered roads thanks to their studs, while we can decide to fit high performance tires to travel mainly on motorways for a long journey. On the one hand, we don’t want to change the whole car to make it possible, and on the other hand, we don’t want an eight-wheeled car so that it can run on all possible roads.

The Strategy model is particularly useful in any situation where supporting variations in the behavior of a component requires complex conditional logic (many if...else Where switch statements) or by mixing different components of the same family. Imagine an object called Order that represents an online order on an e-commerce site. The object has a method called pay() which, as it says, finalizes the order and transfers the user’s funds to the online store.

To support different payment systems, we have several options:

  • Use a ..elsestatement in the pay() method to complete the transaction depending on the payment option chosen
  • Delegate payment logic to a policy object that implements the logic for the specific payment gateway selected by the user

In the first solution, our Order the item cannot support other payment methods unless its code is changed. Also, it can get quite complex as the number of payment options increases. Instead, using the policy template allows the Order object to support a virtually unlimited number of payment methods and keeps its scope limited to managing user details, items purchased, and relative price while delegating the work of completing payment to another object.

Now let’s demonstrate this model with a simple and realistic example.

Multi-format configuration objects

Consider an object called Config which contains a set of configuration parameters used by an application, such as database URL, server listening port, etc. The Config object should be able to provide a simple interface to access these settings, but also a way to import and export the configuration using persistent storage, such as a file. We want to be able to support different formats for storing the configuration, for example JSON, INI or YAML.

By applying what we have learned about the Strategy model, we can immediately identify the variable part of the Config object, which is the feature that allows us to serialize and deserialize the configuration. This will be our strategy.

Creation of a new module

Let’s create a new module called config.js, and define the generic part of our configuration manager:

import { promises as fs } from 'fs'
import objectPath from 'object-path'

export class Config {
  constructor (formatStrategy) {                           // (1)
    this.data = {}
    this.formatStrategy = formatStrategy
  }

  get (configPath) {                                       // (2)
    return objectPath.get(this.data, configPath)
  }

  set (configPath, value) {                                // (2)
    return objectPath.set(this.data, configPath, value)
  }

  async load (filePath) {                                  // (3)
    console.log(`Deserializing from ${filePath}`)
    this.data = this.formatStrategy.deserialize(
      await fs.readFile(filePath, 'utf-8')
    )
  }

  async save (filePath) {                                  // (3)
    console.log(`Serializing to ${filePath}`)
    await fs.writeFile(filePath,
      this.formatStrategy.serialize(this.data))
  }
}

Here’s what happens in the previous code:

  1. In the constructor we create an instance variable called data to keep the configuration data. Then we also store formatStrategy, which represents the component that we will use to analyze and serialize the data.
  2. We offer two methods, set()and get(), to access configuration properties using dotted path notation (for example, property.subProperty) by using a library called object-path (nodejsdp.link/object-path).
  3. The load() and save() The methods are those where we delegate, respectively, the deserialization and serialization of data to our strategy. This is where the logic of Config the class is modified according to the formatStrategy passed as input in the constructor.

As we can see, this very simple and neat design allows the Config object to transparently support different file formats when loading and saving its data. The best part is that the logic to support these different formats isn’t coded anywhere, so the Config class can adapt to virtually any file format without modification, with the right strategy.

Creating format strategies

To demonstrate this feature, let’s now create some format strategies in a file called strategies.js. Let’s start with a strategy for parsing and serializing data using the INI file format, which is a widely used configuration format (more on that here: nodejsdp.link/ini-format). For the task we will use an npm package called ini (nodejsdp.link/ini):

import ini from 'ini'

export const iniStrategy = {
  deserialize: data => ini.parse(data),
  serialize: data => ini.stringify(data)
}

Nothing very complicated! Our strategy simply implements the agreed interface, so that it can be used by the Config object.

Likewise, the next strategy we’re going to create allows us to support the JSON file format, which is widely used in JavaScript and in the web development ecosystem in general:

export const jsonStrategy = {
  deserialize: data => JSON.parse(data),
  serialize: data => JSON.stringify(data, null, '  ')
}

Now to show you how it all fits together let’s create a file named index.js, and let’s try to load and save a sample configuration using different formats:

import { Config } from './config.js'
import { jsonStrategy, iniStrategy } from './strategies.js'

async function main () {
  const iniConfig = new Config(iniStrategy)
  await iniConfig.load('samples/conf.ini')
  iniConfig.set('book.nodejs', 'design patterns')
  await iniConfig.save('samples/conf_mod.ini')

  const jsonConfig = new Config(jsonStrategy)
  await jsonConfig.load('samples/conf.json')
  jsonConfig.set('book.nodejs', 'design patterns')
  await jsonConfig.save('samples/conf_mod.json')

}

main()

Our test module reveals the basic properties of the Strategy model. We only defined one Config class, which implements the common parts of our config manager, and then, using different data serialization and deserialization strategies, we created different Config Class instances that support different file formats.

The example we have just seen shows us only one of the possible alternatives that we had to select a strategy. Other valid approaches could have been the following:

  • Creation of two families of different strategies: One for deserialization and the other for serialization. This would have made it possible to read from one format and save in another.
  • Dynamic strategy selection: According to the extension of the supplied file; the Config the object could have held a map extension → strategy and used it to select the correct algorithm for the given extension.

As we can see, we have several options to select which strategy to use, and the right one depends only on your needs and the compromise in functionality and simplicity you want to achieve.

In addition, the implementation of the model itself can also vary widely. For example, in its simplest form, both context and strategy can be simple functions:

function context(strategy) {...}

While this may seem trivial, it shouldn’t be underestimated in a programming language like JavaScript, where functions are first-class citizens and used as much as full-fledged objects.

Among all these variations, however, what doesn’t change is the idea behind the pattern; as always, the implementation may change slightly, but the basic concepts behind the model are still the same.

Summary

In this article, we dig deeper into the details of the Strategy Model, one of the behavioral design models of Node.js. Read more in the book, Node.js Design Templates, Third Edition by Mario Casciaro and Luciano Mammino.

about the authors

Mario Casciaro is a software engineer and entrepreneur. Mario worked at IBM for several years, first in Rome, then at the Dublin Software Lab. He currently divides his time between Var7 Technologies – his own software company – and his role as chief engineer at D4H Technologies where he creates software for emergency response teams.

Luciano Mammino wrote his first line of code at the age of 12 on his father’s old i386. Since then, he has never stopped coding. He currently works at FabFitFun as a Senior Software Engineer where he builds microservices to serve millions of users every day.


Source link

Share.

Leave A Reply