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.
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).
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.
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.
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).
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).
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).
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 \ -p dbname pattern ok: created binding patterndb
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.
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.
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 examplepassword
orarray
)
-
-
description
for packages, actions, and parameters -
sampleInput
andsampleOutput
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 \ | jq .annotations [ { "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" } ]
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).
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.
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) { if(value) return "" return "missing "+this.field; } // validate data, adding messages and values validate(data) { if (!data.message) data.message = []; if (!data.errors) data.errors = []; let value = data[this.field]; let err = this.validator(value); if (err) data.errors.push(err); else data.message.push(this.field + ": " + value); return data; } }
The replaceable validation logic; by default, it checks only if a field exists.
The validator gets some data, then applies the validation logic to the specified field.
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") function main(args) { return new Validator("name").validate(args) }
Import the
Validator
class from the library.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 { validator(value) { let error = super.validator(value); if (error) return error; var re = /\S+@\S+\.\S+/; if(re.test(value)) return ""; return value+" does not look like an email" } } function main(args) { return new EmailValidator("email").validate(args) }
Redefine the
validator
method, hence applying the Strategy pattern.Validation logic, matching the email field against this regular expression.
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) if (match && match.length >= 10) return ""; return value + " does not look like a phone number"; } } function main(args) { return new PhoneValidator("phone").validate(args); }
This validator extracts the numbers from the string, then verifies that there are at least 10 of them.
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
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).
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) { 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 = []; if (!data.errors) data.errors = []; let value = data[this.field]; let err = this.validator(value); if (err) data.errors.push(err); else data.message.push(this.field + ": " + value); return data; } }
Pass a parameter saying which part of the form we are going to validate.
Collect the result (the email message to send) and the errors in the data and pass them to the next element.
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 { "email": "michele@sciabarra.com", "errors": [ "missing phone" ], "message": [ "name: Michele", "email: michele@sciabarra.com" ], "name": "Michele" }
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:
-
Collecting all the information needed to perform a task in an instance of a Command interface
-
Sending that instance of the Command interface to an executor, for actually performing the task.
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).
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 key
s. 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") function main (args) { let command = args.command let data = { type: args.type, key: args.key, value: args.value } switch (command) { case 'CREATE': return kv.create(data) case 'LIST: return kv.list(data) case 'DELETE': return kv.delete_(data) } }
Use the library keyvalue.js.
Extract the
command
key.Create a record with the key and value.
List all the records.
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 \ -p command LIST -p type test -r {"list":[]} $ wsk action invoke pattern/command-database \ -p command CREATE -p type test \ -p key alpha -p value 1 -r { "activationId": "994212b782224eb58212b782226eb561" } $ wsk action invoke pattern/command-database \ -p command LIST -p type test -r {"list":[{"key":"alpha","value":1}]} $ wsk action invoke pattern/command-database \ -p command DELETE -p type test -p key alpha { "activationId": "a93f1a0ada0f43edbf1a0ada0f53ed90" } $ wsk action invoke pattern/command-database \ -p command LIST -p type test -r {"list":[]}
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.