WHAT’S IN THIS CHAPTER?
WROX.COM CODE DOWNLOADS FOR THIS CHAPTER
The wrox.com code downloads for this chapter are found at www.wrox.com/go/reliablejavascript
on the Download Code tab. The files are in the Chapter 16 download and individually named according to the filenames noted throughout this chapter.
In a 2014 commercial for Google’s business products (https://www.youtube.com/watch?v=bA0Hmhnl1oE
), a videoconference is just finishing:
A conversation between a JavaScript programmer and a C# or Java programmer could go much the same way. The JavaScripter throws some objects together and the C# or Java developer keeps insisting, “You can’t do that! The program will fall apart without some interfaces to describe the objects.” The JavaScript developer says, “No, there are no next steps. We just solved the problem.”
Yet any JavaScripter who has worked on a team of more than one must admit that misunderstandings do occur. A member function that used to be called makeAWidget
is renamed to makeWidget
, breaking downstream code. An object literal’s member variable is named in the singular, objectID
, but someone puts an array of object IDs in it. (Hard to believe, but we have seen it happen.)
Strongly typed languages do their best to catch these misunderstandings at compile time, often by using interfaces. What is an interface, and why do programmers in these languages think they are worthwhile?
An interface describes a class, but has no executable code. Listing 16-1 shows an interface in C#.
With that interface defined, a class can inherit from it, thereby promising to implement it, as in Listing 16-2.
If the class fails to implement everything defined in the interface, the compiler will throw an error.
To a JavaScript programmer, this may seem like a lot of trouble. You’re saying everything twice: once in the interface and again in the class. And so it is, but the trouble is not without repayment.
The surface benefit is that an interface makes clear what consumers of the class can expect, and what programmers of similar classes should implement.
Interfaces also act as a sort of double-entry accounting. If the implementation goes out of balance with the interface, the compiler will tell you about it.
On a deeper level, interfaces open a whole world of possibilities to the software engineer, enabling him to solve problems more elegantly. The Strategy Pattern you met in Chapter 13 is one example. When implemented in a strongly typed language, each algorithm implements a common interface. There is no need for a factory method to create the chosen algorithm; dependency-injection software can wire up the desired one through runtime configuration.
Every time you have coded a Jasmine spy in the unit tests throughout this book, you have used the interface philosophy, with the spy implementing the same interface as the real object. Of course, in JavaScript the interface is only implied, but in a strongly typed language it would be explicit and therefore more reliable.
Of the five pillars of SOLID software design you met in Chapter 1, three of them are interface-oriented. The Liskov Substitution Principle (the L in SOLID) states that all implementers of an interface should adhere to the same semantics. The Dependency Inversion Principle (the D) had consumers of a class “own” the interface from which the class inherited. The Interface Segregation Principle (the I) is the subject of the next section.
As much as you may strive to make classes coherent and no larger than necessary, sometimes a consumer of a class needs only a fraction of its capabilities.
As an example, consider the attendee object from Chapter 6, reproduced here as Listing 16-3.
If JavaScript had interfaces, attendee
might implement two of them, which Listing 16-4 presents in pseudocode. One is designed for consumers who care only about an attendee’s personal information, the other for consumers oriented toward managing the check-in process.
Now suppose several functions in other modules each take an attendee
object as an argument. Suppose further that you wish to change attendee
’s getFullName
method. The functions in the other modules nicely show attendees as arguments (assuming the arguments were named helpfully), but you have no easy way to know which functions your change to getFullName
will affect. Sure, you could do a text search for “getFullName” but then you’d also turn up like-named methods in other objects (maybe presenter
or sponsor
).
This is one problem that the Interface Segregation Principle is designed to solve. The consuming functions, instead of having arguments called attendee
, could have attendeePersonalInfo
or attendeeCheckInManagement
arguments. Then, at a glance, you would know which functions your change might affect (again assuming the arguments are named in a non-deceptive manner).
Furthermore, if you needed to mock an argument for unit-testing, or inject another version of the object, you would be able to draw a tighter boundary around the attendee
’s members that you actually need to mock or implement.
Finally, a developer who really only cares about the attendeePersonalInfo
portion of an attendee
only has to master that small API. A properly segregated interface will make his life easier.
These benefits are enforced in strongly typed languages, but they are certainly available in JavaScript if you’re willing to program outside the box.
In the next section, you’ll build a module by which you can define and enforce interfaces. The module is called contractRegistry
because interfaces act as contracts between modules that define functionality and modules that consume it, and because in the next chapter you will use contractRegistry
’s capabilities for purposes that are not, strictly speaking, interface-related.
The contract registry as developed in this chapter will have the following capabilities, developed in this order:
Error
if it does not.Figure 16.1 gives an advance peek at the passing unit tests you’ll aim for.
A contract definition will consist of a name for the contract and a function that can evaluate an object and return true
or false
according to whether the object fulfills the contract. The define
method, then, has this signature:
function define(contractName, evaluator)
Incidentally, having an evaluator function act as an “interface” is quite different from other languages’ conception of interfaces as static data. This is not only necessary but welcome. It is necessary because JavaScript has no built-in way to evaluate conformance to an interface, and you must code it somewhere. It is welcome because a function can do much more than any data-oriented, interface-description language ever could. Plus, with a function riding along as a first-class object, it feels nice and JavaScripty, doesn’t it?
With several chapters of test-driven development behind you, there is no need to belabor every step toward creating this method. Listing 16-5 shows its tests.
As usual, test-driven development begins with the error conditions. Beyond that, there’s not much you can test when define
is the only method in the object. You can’t even test that it defines anything. However, until it does you won’t get very far with the fulfills
method (up next). Even so, you resist getting ahead of the tests and code only what’s necessary to make the above tests pass. The result is Listing 16-6.
The tests pass, as shown in Figure 16.2.
The fulfills
method will look like this:
function fulfills(contractName, obj)
It will return true
if obj
fulfills the named contract or false
if it does not. Before you get to that, you first test the obvious error condition: that the named contract is not in the registry (see Listing 16-7).
In previous chapters, you saw how exposing an object’s error messages allows unit tests to verify the exact errors. Sometimes, error messages include variable data. To keep the code DRY, the procedure for incorporating the variable data in a message is put in a function, getMessageForNameNotRegistered
.
You can make the test pass (see Figure 16.3) with the code highlighted in Listing 16-8.
The new object at the top, registry
, will hold the registrations, but so far define
isn’t putting anything there because no unit test has demanded it. This will finally catch up to you when you attempt to make the next tests (see Listing 16-9) pass.
The tests verify that fulfills
returns true
or false
appropriately. They use a contract whose name is stored in the isArray
variable. Near the top of the listing, note the following line:
registry.define(isArray,Array.isArray);
It uses JavaScript’s Array.isArray
method as isArray
’s contract-evaluator. Quick, lazy, and you know it works. Gotta love it, right?
The tests are in all kinds of hurt (see Figure 16.4) but are instantly set right (see Figure 16.5) when you add just one line to define
and one to fulfills
, to use the registry
object installed earlier. The working code appears in Listing 16-10.
Steps 1 and 2 of the outline at the top of this section are now complete. Next, it will be useful to have a method that asserts that a contract is fulfilled, throwing an error if it is not.
The assert
method is similar to fulfills
:
function assert(contractName, obj)
It should use the fulfills
method to determine whether the contract is fulfilled, rather than duplicating the code. A test that ensures this also ensures that the argument-checking you have already put in fulfills
will be carried into assert
. That is the first test of Listing 16-11. The other two verify the core functionality of assert
.
Just a few lines make the tests pass, as shown in Listing 16-12.
You might like the option to switch off all enforcement of contracts once your product has passed QA, particularly if your evaluator functions are time-consuming. (Keep in mind that you’re free to make them much more elaborate than Array.isArray
!)
The easiest, most efficient way to do this is to place an object’s contracts in a separate .js
file, and simply exclude that file from the production release. You will see an example of this in the next section.
With four of the requirements crossed off the list, only one remains.
You could use what has been developed so far to enforce a contract on any object you create. For example, you could define and assert the two attendee-related contracts in the attendee
module, as shown in Listing 16-13.
That works, but it suffers from the drawback that the contract-enforcement is embedded in the object itself, somewhat violating the Single Responsibility Principle. Many developers would prefer an aspect-oriented solution.
The attachReturnValidator
method that will be developed shortly attaches an aspect to a function that creates an object, where that object is supposed to conform to an interface. It looks like this:
function attachReturnValidator(funcName, funcObj, contractName)
With that function available, you can place the contract-related code outside of attendee
. If you put such code in a separate file, then after QA is complete you can exclude the file from the shipped version, eliminating the overhead from the release.
Listing 16-14 shows how the attendee
module from Listing 16-3, which has no injection or use of the registry, can be modified with an aspect thanks to attachReturnValidator
. The modification is done through a separate file, attendeeContracts.js
, which you would not include in the production release. It contains a function that creates the contracts for the two “interfaces” and then attaches aspects to Conference.attendee
that ensure that any object it returns conforms to those interfaces.
Now to develop the attachReturnValidator
method.
The contractRegistry_tests.js
download for this chapter contains the error-checking tests. Of more interest are the tests of the aspect functionality (see Listing 16-15).
The tests use an object, funcObj
, and its member called funcName
, which are set up in the beforeEach
. The call funcObj[funcName]()
returns an array (returnValue
), so it conforms to the Array.isArray
evaluator set up at the top of the test module, which you have already seen.
The first test is straightforward. After applying the aspect to the target function, it verifies that a call through the aspect to target returns the value unscathed.
In the second test, a different return validator is attached—one that expects the return value to be a number. When the return value turns out to be an array, the aspect should throw an error.
Skipping all the TDD steps with which you’re now very familiar, Listing 16-16 shows the completed attachReturnValidator
function. After some argument-checking, it uses the Aop.around
method introduced in Chapter 2 to capture the target function’s return value, assert that it fulfills the contract, and then return it.
The completed contractRegistry
allows you to assert anything you wish about a newly created object, either as an aspect (refer to Listing 16-14) or in the object’s code itself (refer to Listing 16-13).
If you prefer the aspect-oriented approach, you may wish to review the Module Pattern and Functional Inheritance in Chapter 3, as both of those object-creation patterns are aspect-friendly.
Interfaces, a staple of strongly typed languages, confer benefits that JavaScript programmers might secretly wish they could have. These include:
The Interface Segregation Principle further simplifies the work of an object’s consumers and helps the object’s developers scope out ramifications of any changes.
In this chapter, you worked through the development of the contractRegistry
, an object that can define and enforce interfaces JavaScript-style.
The next chapter takes this one step further, using the contractRegistry
to validate arguments passed to a function. With both the inputs and the output guarded by contracts, you will have taken a significant step toward creating reliable JavaScript.