Chapter 4. Common Design Patterns in OpenWhisk

This chapter and the next focus on designing OpenWhisk applications. When writing your code, you need to have a good grasp of programming languages and algorithms. In this chapter, we’ll apply many of the classic “Gang of Four” design patterns as described in Design Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides (Addison-Wesley). We also cover the ubiquitous Model-View-Controller pattern, although it is not part of this classic collection. We will go over an example for each pattern, and in the process, revamp our contact form from Chapter 2. To illustrate how to build and engineer a real serverless application using a design pattern, we will create a contact form handler.

Figure 4-1 shows the patterns covered in this chapter: Singleton, Facade, Prototype, Strategy, Chain of Responsibility, and Command. We’ll cover a few more patterns in Chapter 5.

Tip

The source code for the examples in this chapter and the next are available in the book’s GitHub repository.

laow 0401
Figure 4-1. A contact form using common design patterns

Built-in Patterns

OpenWhisk allows you to combine actions that implement some of the more common patterns out of the box. These patterns include:

Singleton

Available through triggers and named invocations (1 in Figure 4-1)

Facade

Provided by packages with web actions and feeds (2 in Figure 4-1)

Prototype

Used when you bind packages (3 in Figure 4-1)

Decorator

Available as the annotation feature (not shown in the diagram)

Singleton

The Singleton pattern (Figure 4-2) meets the challenge of locating services and components in an application with many parts, to coordinate the work across the different parts of the application. This is probably the most straightforward pattern. A singleton restricts the number of instances of a class to a single instance, and provides easily accessible methods to reach that instance, using some global name (see Figure 4-1).

laow 0402
Figure 4-2. The Singleton pattern

In general, you can implement this pattern using static methods or global variables to retrieve the unique instance of a class. Another option is to use libraries that do dependency injection and manage named instances of objects.

In OpenWhisk, the Singleton pattern is embedded in the API so you can retrieve actions by name. A trigger is a type of Singleton pattern, because it is used to locate a single point of access to multiple actions.

In a sense, the fact that actions are named and can be invoked using their name as an implementation of the Singleton pattern. You can consider each action as the unique instance of an (implicit) class containing main as a method.

In our contact form, we use a trigger as a unique entry point for processing the form submission (Figure 4-3). The trigger submit is in charge of receiving data coming from the frontend. It acts as a Singleton since the form processes the submission by name. What happens next depends on the configuration of the application. For example, as we are going to see, we can create rules to process a form submission, store it in the database, and send an email.

laow 0403
Figure 4-3. The Singleton pattern used to implement a trigger
Note

Another valid view of the pattern implemented by OpenWhisk when instantiating actions is the AbstractFactory pattern: there is indeed a single interface that instantiates different implementations of the actions as provided by runtimes.

Facade

The Facade pattern makes a complex subsystem easier to use by providing a more straightforward interface to it (see Figure 4-4). You use the Facade pattern when you have an application with many subsystems and you want to simplify their use. A facade usually provides only the subset of the functionalities of the subsystems that are of interest to users.

laow 0404
Figure 4-4. The Facade pattern

In OpenWhisk we can design an application using many interconnected actions. Those actions are, by themselves, hiding some complexity and providing a more straightforward interface to complex systems in the cloud, like GitHub or Slack. Thus, those actions could also be considered implementations of the Facade pattern.

OpenWhisk offers a few ways to define an external interface. You can mark actions as web actions, you can expose some actions as feeds, and you can define some actions as triggers (Figure 4-5).

laow 0405
Figure 4-5. The Facade pattern in OpenWhisk

Web actions define the external interfaces to be used by web clients. You can use them to define an externally accessible REST API. Defining a similar API is equivalent to providing the simplified access to the complex processing performed by other actions, as the Facade pattern requires.

But the interface of your Facade does not have to be web-based. You can use triggers as entry points to your application, and you can invoke them without having to expose them publicly. To use them you have to use one of the available client libraries to access the APIs of OpenWhisk. Note, however, that an essential feature of triggers is that you can connect them to feeds, so triggers offer an interface to other OpenWhisk applications.

A Feed is an action that must follow a documented pattern (we’ll see an example later in this chapter, when we discuss the Observer pattern). Using Feeds you can connect an OpenWhisk application to another web application, thus providing an interface to use it as a subsystem. While web actions expose a web interface, feeds invoke third-party web interfaces.

Apache OpenWhisk includes many packages that can be considered facades for complex subsystems. The ones that are available out of the box in the IBM Cloud platform include:

  • /whisk.system/github, a facade over the GitHub API

  • /whisk.system/slack, a facade to access the Slack API

  • /whisk.system/weather, which allows access to the service giving information on the weather

  • /whisk.system/watson-translator, an IBM service for language translation

Prototype

The Prototype pattern makes constructing complex objects easier. It works by creating an instance to be used as a model, or “prototype,” for new instances. The creation of new instances of the model is performed using a clone method. Of course, generally, it is not that useful to have completely identical copies of a given prototype. It is therefore common to provide parameters to the clone method in order to create “customized” clones that are more fit for our purposes (see Figure 4-6).

laow 0406
Figure 4-6. The Prototype pattern

The Prototype pattern is used in OpenWhisk in package binding. Using packages, we can create collections of actions that can be shared and reused. Using the command wsk package bind, we can create a copy of a package, just like the Prototype pattern prescribes. All the actions are then available under the new package name.

Tip

A complete copy of a package is rarely useful. When binding a package, we can provide a set of parameters to customize the package for our needs.

For example, to use Cloudant in IBM cloud, you use a package binding after you provision your server instance. A server instance includes multiple databases. You can create a clone that can read and write in multiple databases by providing username and password. Or better still, you can create a clone that can read and write in a single database within the server instance by providing a username, password, and database name.

We use the Prototype pattern in our contact form, customizing it with a set of parameters. We download a JSON file that includes the parameters to access the server instance, and then use those at bind time to create a cloned package customized with some parameters, so we get a prototype package parametrized to read and write in only one database (see Figure 4-7).

laow 0407
Figure 4-7. The Prototype pattern creating an instance of the Cloudant package

A binding creates a copy of an existing package, but lets you change the parameters. This way you can create an instance of a package customized for your use. We already covered how to bind to a package using specific parameters—now let’s see how to bind to a database using a JSON file. This feature implements the Prototype pattern with a custom parameter.

Go into the Cloudant database, as shown in Figure 4-8, and copy the configuration file. Then open a text editor, paste the the JSON file, and save it as cloudant.json.

Using that file can you now bind the patterndb database as follows:

$ wsk package bind /whisk.system/cloudant patterndb \
   -P cloudant.json \                                 1
   -p dbname pattern                                  2
ok: created binding patterndb
1

The configuration file binds to a server instance.

2

We add a parameter to use a specific database.

Now you can access all the actions configured in the cloudant package. Since we provided a clone customized with our parameters, the actions actually use our database in the server instance.

laow 0408
Figure 4-8. Retrieving the Cloudant JSON configuration file

Decorator

The Decorator pattern adds functionalities to an existing system, without changing the interface of the system and while preserving all the old functionality. With this pattern, we take an instance of a class and wrap it with another instance of the same class, providing the same interface. The added functionality is performed by the “wrapper,” which will also use existing functionality to perform its work (see Figure 4-9).

In OpenWhisk, the Decorator pattern is implemented using annotations. Annotations are a way to add additional information to OpenWhisk entities, and to add tools, plugins, and custom implementations.

Most of the available annotations are for documentation only and change the behavior of an entity only when displaying the online help. However, some annotations change the behavior of an entity in a more “semantic” way.

laow 0409
Figure 4-9. The Decorator pattern

In general, you can add an annotation with the flag -a <name> <value> for all OpenWhisk entities. Furthermore, you can read the annotation of every entity using the get subcommand.

We can use the following annotations to add additional information to our entities:

  • parameters for packages and actions show fields:

    • doclink (link to documentation)

    • required (is required)

    • bindTime (was this parameter bound?)

    • type (for example password or array)

  • description for packages, actions, and parameters

  • sampleInput and sampleOutput for actions

Warning

Currently, these annotations are not checked, so it is up to the developer to respect (or ignore) them. At some point in time, however, they may become constraints.

As an example, let’s read the annotations to see what parameters are available for the cloudant package’s read action:

$ wsk action get /whisk.system/cloudant/read  \
   | tail +2 \                                   1
   | jq .annotations                             2
[
  {
    "key": "description",
    "value": "Read document from database"
  },
  {
    "key": "parameters",
    "value": [
      {
        "name": "dbname",
        "required": true
      },
      {
        "description": "The Cloudant document id to fetch",
        "name": "id",
        "required": true
      },
      {
        "name": "params",
        "required": false
      }
    ]
  },
  {
    "key": "exec",
    "value": "nodejs:6"
  }
]
1

Get rid of the first line to feed JSON to jq.

2

Extract only the annotations part of the JSON output.

Now let’s see how we can add annotations, for example, to document the apidemo/clock interface:

$ wsk action update apidemo/clock ok: updated action apidemo/clock
$ wsk action update apidemo/clock \
-a "description" "return the current date or \
time" -a "parameters" '[{"name":"time","description":"show time"}, \
{"name":"time","description":"show time"}']
ok: updated action apidemo/clock

Let’s check the results:

$ wsk action get apidemo/clock  | tail +2 | jq .annotations
[
  {
    "key": "description",
    "value": "return the current date or time"
  },
  {
    "key": "parameters",
    "value": [
      {
        "description": "show time",
        "name": "time"
      },
      {
        "description": "show time",
        "name": "time"
      }
    ]
  },
  {
    "key": "exec",
    "value": "nodejs:6"
  }
]

Note the exec annotation added automatically by the system.

Useful annotations are also added to activations. We can see them here:

$ wsk action invoke apidemo/clock
ok: invoked /_/apidemo/clock with id 17cb42c9fd2a45ad8b42c9fd2a45ad71
$ wsk activation get 17cb42c9fd2a45ad8b42c9fd2a45ad71 \
  | tail +2 | jq .annotations
[
  {
    "key": "path",
    "value": "openwhisk@example.com_dev/apidemo/clock"
  },
  {
    "key": "waitTime",
    "value": 45
  },
  {
    "key": "kind",
    "value": "nodejs:6"
  },
  {
    "key": "limits",
    "value": {
      "logs": 10,
      "memory": 256,
      "timeout": 60000
    }
  },
  {
    "key": "initTime",
    "value": 74
  }
]

Some useful information is provided by these annotations:

  • path is the action associated with this invocation.

  • limits are the execution constraints of the action.

  • kind is the runtime used.

  • waitTime is the time spent before the action is activated.

  • initTime is the time needed to initialize the action.

In addition to these documentation annotations, there are annotations for web actions, which will be discussed in “Advanced Web Actions”.

Note

You can also use sequences to implement the Decorator pattern. Consider an action that requires an authentication token to perform its work that must be retrieved automatically. You can “decorate” the token by using a sequence: the first action retrieves the authentication token and passes it to the second, which does its work.

Patterns Commonly Implemented with Actions

There are some other patterns that are used very frequently in the context of OpenWhisk and serverless applications. Hence, we gather them under the hat of “common” patterns. They are not part of OpenWhisk, but they are easily implemented with OpenWhisk actions

We’ll cover the following patterns in this section:

Strategy

Used to change implementations while keeping a standard interface (#4 in Figure 4-1)

Chain of Responsibility

Used to partition the solution of a problem into multiple steps (#5 in Figure 4-1)

Command

Used to encapsulate information needed to perform a task (#6 in Figure 4-1)

Strategy

The Strategy pattern provides different functionalities to a system while keeping a uniform interface. It works by defining a base class for an algorithm that acts as the interface. We can then extend this base class with other classes to customize the logic as needed. The client sees a standard interface, while the behavior is implemented by providing a different implementation (or “strategy”) for each subclass (see Figure 4-10).

laow 0410
Figure 4-10. The Strategy pattern

To illustrate the Strategy pattern, we will implement validation in our contact form. We will have a base class performing the validation, with a standard interface. Then we will customize the validation logic to behave in a different way when we validate an email address or a phone number, as illustrated in Figure 4-11.

laow 0411
Figure 4-11. A demonstration of the Strategy pattern

To validate the different fields in the form, we’ll create a library file lib/validator.js with the following contents. The interface is the same for each field (“validate”), but the implementation is different: an email address must be validated in a different way than a phone number. The Validator class looks like this:

class Validator {
  constructor(field) {
    this.field = field;
  }

  // simple validator - just check it not empty
  // return an error, or an empty string if ok
  validator(value) {                           1
    if(value)
        return ""
    return "missing "+this.field;
  }

  // validate data, adding messages and values
  validate(data) {                             2
    if (!data.message) data.message = [];
    if (!data.errors)  data.errors = [];

    let value = data[this.field];
    let err = this.validator(value);           3
    if (err) data.errors.push(err);
    else data.message.push(this.field + ": " + value);
    return data;
  }
}
1

The replaceable validation logic; by default, it checks only if a field exists.

2

The validator gets some data, then applies the validation logic to the specified field.

3

Here is where we apply the validation logic.

The validator can be used as it is to validate the existence of a single field, as shown here in name.js:

const Validator = require("./lib/validator.js") 1

function main(args) {
    return new Validator("name").validate(args) 2
}
1

Import the Validator class from the library.

2

Create a validator for the name field, returning the form data annotated with messages and errors.

Now let’s apply the Strategy pattern by providing different implementations of the validation logic. First we’ll create an email action (email.js) to validate the email field with a validator for email addresses:

const Validator = require("./lib/validator.js")

class EmailValidator extends Validator {      1
    validator(value) {
        let error = super.validator(value);
        if (error) return error;
        var re = /\S+@\S+\.\S+/;              2
        if(re.test(value))
            return "";
        return value+" does not look like an email"
    }
}

function main(args) {                         3
    return new EmailValidator("email").validate(args)
}
1

Redefine the validator method, hence applying the Strategy pattern.

2

Validation logic, matching the email field against this regular expression.

3

We validate in the same way as before, but now we use the EmailValidator.

Next we’ll use a different strategy, implementing a phone action to validate a phone number:

const Validator = require("./lib/validator.js");

class PhoneValidator extends Validator {
  validator(value) {
    let error = super.validator(value);
    if (error) return error;
    var match = value.toString().match(/\d/g)     1
    if (match && match.length >= 10) return "";
    return value + " does not look like a phone number";
  }
}

function main(args) {                             2
  return new PhoneValidator("phone").validate(args);
}
1

This validator extracts the numbers from the string, then verifies that there are at least 10 of them.

2

We can use this validator in the same way as before.

This validation was designed to be chained, which means we can use it to implement another pattern—Chain of Responsibility.

Chain of Responsibility

The Chain of Responsibility pattern splits large, complex processing into multiple smaller, separate steps that are assembled as a chain of execution.

It works by doing the following (see Figure 4-12):

  • Defining a standard interface for the task to be performed

  • Implementing each step of the processing with a separate class

  • Connecting the specific instances performing the processing in a chain—when one step is complete, the next is called to continue the processing

laow 0412
Figure 4-12. The Chain of Responsibility pattern

The Chain of Responsibility pattern in OpenWhisk can be implemented in a natural way by using sequences. Of course, to put actions in a sequence, the output of an action must be usable as the input of the next action in the sequence (Figure 4-13).

laow 0413
Figure 4-13. Chain of Responsibility pattern

Actually, the implementation of the Strategy pattern in the preceding section is also an implementation of the Chain of Responsibility pattern.

Let’s review the Validator class again, to see where the pattern is. The key concepts are:

  • We need to partition the problem to apply different instances of the chain.

  • We have to make sure the instances can be concatenated.

Here’s the lib/validator.js file again:

class Validator {
  constructor(field) {                         1
    this.field = field;
  }

  // simple validator - just check if not empty
  // return an error, or an empty string if OK
  validator(value) {
    if(value)
        return ""
    return "missing "+this.field;
  }

  // validate data, adding messages and values
  validate(data) {
    if (!data.message) data.message = [];      2
    if (!data.errors)  data.errors = [];

    let value = data[this.field];              3
    let err = this.validator(value);
    if (err) data.errors.push(err);
    else data.message.push(this.field + ": " + value);
    return data;
  }
}
1

Pass a parameter saying which part of the form we are going to validate.

2

Collect the result (the email message to send) and the errors in the data and pass them to the next element.

3

Select a field of the form to which to apply the logic of an element of the chain.

We can ultimately state the following:

  • The form to validate (the problem) is partitioned; we specify which field we want to validate for each element of the chain.

  • The result is built incrementally, so we get the form data, and we annotate it incrementally with the errors found or the final result (the email message we want to send).

We can then deploy our Chain of Responsibility reusing the actions we built previously with the simple command:

$ wsk action update pattern/validate --sequence \
  pattern/strategy-name,\
  pattern/strategy-email,\
  pattern/strategy-phone
  ok: updated action pattern/chainresp

Let’s try a simple test on the command line:

$ wsk action invoke pattern/validate \
  -p name Michele -p email michele@sciabarra.com -r   1
{
    "email": "michele@sciabarra.com",
    "errors": [                                       2
        "missing phone"
    ],
    "message": [                                      3
        "name: Michele",
        "email: michele@sciabarra.com"
    ],
    "name": "Michele"
}
1

Missing the phone number.

2

Error correctly detected.

3

Parts of the form passing the validation check.

Note

Here we show only a simple manual test case to be sure you have correctly deployed the code. We cover systematic testing in Chapter 6.

Command

The Command pattern provides a uniform interface to perform multiple tasks, with the task description encapsulated in a single object (see Figure 4-14). It works by:

  1. Collecting all the information needed to perform a task in an instance of a Command interface

  2. Sending that instance of the Command interface to an executor, for actually performing the task.

laow 0414
Figure 4-14. The Command pattern

In our contact form, we have a problem: managing a feed. A feed is, as we’ll see later, an action that must execute some operation according to requests, called a –0—. As a result, we need to be able to store, retrieve, and delete data in a database (Figure 4-15).

laow 0415
Figure 4-15. The Command pattern

We implement the data storage with the Command pattern. We define a command as a JSON object with the fields command, key, value, and type.

Note

Strictly speaking, the pattern prescribes that you define a class whose instances are used to keep the command information. However, to implement it correctly we have to share the code for the Command class between the clients and the receivers. A common practice with OpenWhisk is to instead share data with JSON messages. We are hence not going to create a Command class. Instead, instances of actions just share the knowledge of the structure of the JSON. In this case, it wasn’t necessary to enforce this, so our command is just a JSON structure with the three fields.

An object to represent the Command pattern in JavaScript is as follows:

{ "command": <CMD>,
  "type": <TYPE>
  "key": <KEY>,
  "value": <VALUE>,
}

We execute a different action for each value of <CMD>, most notably:

  • CREATE, which saves the <VALUE> of the given <TYPE> in the database accessible with the <KEY>.

  • DELETE, which deletes a record of the given <TYPE> accessible with the <KEY> (<VALUE> is ignored).

  • LIST, which returns a list of each <KEY> and <VALUE> we stored in the database of the given <TYPE>.

Tip

Basically, the command implements a very simple key/value store: we save with a key, we retrieve the value with the key, and we can get a list of all the keys. However, we added a field type allowing us to store multiple key/value stores in the same database.

The structure of the action is as follows:

var kv = require("./lib/keyvalue.js")   1

function main (args) {
  let command = args.command        2
  let data = {
    type: args.type,
    key: args.key,
    value: args.value
  }
  switch (command) {
    case 'CREATE':                  3
      return kv.create(data)
    case 'LIST:                     4
      return kv.list(data)
    case 'DELETE':                  5
      return kv.delete_(data)
  }
}
1

Use the library keyvalue.js.

2

Extract the command key.

3

Create a record with the key and value.

4

List all the records.

5

Delete the record with the given value.

Tip

We used the name delete_ with a final underscore to avoid conflicts with the delete JavaScript keyword.

Here we use a library called keyvalue.js, which is self-explanatory. In Chapter 5, we will go into more depth on database storage.

For now, let’s execute an action at the command line:

$ wsk action invoke pattern/command-database \    1
  -p command LIST -p type test -r
{"list":[]}
$ wsk action invoke pattern/command-database \    2
  -p command CREATE -p type test \
  -p key alpha -p value 1 -r
{ "activationId": "994212b782224eb58212b782226eb561" }
$ wsk action invoke pattern/command-database \    3
  -p command LIST -p type test -r
  {"list":[{"key":"alpha","value":1}]}
$ wsk action invoke pattern/command-database \    4
  -p command DELETE -p type test -p key alpha
{ "activationId": "a93f1a0ada0f43edbf1a0ada0f53ed90" }
$ wsk action invoke pattern/command-database \    5
  -p command LIST -p type test -r
{"list":[]}
1

Check that the key/value store is empty.

2

Create a key alpha=1.

3

Now there is a value in the store.

4

Delete the value using the key.

5

Check that the key/value store is empty again.

Summary

In this chapter, we explored some simple design patterns in OpenWhisk. In particular, we explored a few patterns that are built into the design of OpenWhisk, and therefore used implicitly (Singleton, Facade, Prototype, and Decorator). We also explored some simple patterns that are frequently used with OpenWhisk (Strategy, Chain of Responsibility and Command).

Get Learning Apache OpenWhisk now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.