WHAT’S IN THIS CHAPTER?
WROX.COM CODE DOWNLOADS FOR THIS CHAPTER
You can find the wrox.com code downloads for this chapter at www.wrox.com/go/reliablejavascript
on the Download Code tab. The files are in the Chapter 13 download and individually named according to the filenames noted throughout this chapter.
The Strategy Pattern is used to isolate multiple algorithms, or strategies, that perform a specific task into modules that may be swapped in and out at run time. This chapter uses test-driven development to show how, through the use of strategies, algorithms may be added or removed in a way that’s independent from the client, or context, that uses them. The chapter will also describe how the Factory Pattern from Chapter 10 helps achieve this goal.
Additionally, the chapter will illustrate how programming to the interface of a strategy improves the testability, and thus reliability, of the code that consumes the strategy.
As an added convenience to attendees, the JavaScript conference organizers have negotiated discounted, fixed rates with three local cab companies for travel between the conference venue and the airport. The organizers are still trying to work out fixed rates with two limousine services for attendees that want to be transported in a bit more luxurious setting.
Even though deals with the limousine services are pending, the organizers would like to extend the conference website to add the ability to schedule transportation to the airport now, and add the ability to choose a limousine service once the deals have been made.
Because the conference is being held in a city in which the populace adopts technology early, each of the cab companies and limousine services offers a web service through which transportation may be scheduled. The conference’s transportation-scheduling feature will have to make the appropriate request to the company that the attendee has chosen, and return the reservation confirmation number provided by the company. So they can track it on the conference website’s dashboard, the conference organizers would also like to keep track of how many rides each of the companies provides.
Charlotte is assigned the task of creating the user interface that collects the following information from the attendee:
She is also creating the auditing service that will be used to keep track of the rides each company gives. You’re responsible for creating the module that will receive the data and schedule the reservation.
You begin designing the Conference.transportScheduler
module. It seems that it needs only a single method, requestTransportation(transportDetails)
, which will make the correct request for the chosen transport-company, interpret and return the results, and make an audit entry if a ride is successfully scheduled.
The first cab company you choose to implement support for is RediCab. The unit tests you write look like this:
describe("Conference.transportScheduler", function(){
'use strict';
describe("requestTransport(transportDetails)", function(){
describe("with RediCab", function(){
it("makes the correct request to the RediCab web service", function(done){
// test implementation
});
describe("when request successful", function(){
it("records the ride with the audit service", function(done){
// test implementation
});
it("resolves to the expected success value", function(done){
// test implementation
});
});
describe("when request unsuccessful", function(){
it("does not record the ride with the audit service", function(done){
// test implementation
});
it("returns the rejected promise", function(done){
// test implementation
});
});
});
});
});
Let’s skip the detail of making these tests pass one at a time; we’ve shown that process many times already (and we’re going to refactor the code anyway). The implementation that makes the tests pass looks like this:
var Conference = Conference || {};
Conference.transportScheduler = function(transportAuditService, httpService){
'use strict';
return {
requestTransport : function requestTransport(transportDetails){
var rediCabRequest;
switch(transportDetails.companyName){
case "RediCab":
rediCabRequest = {
passenger: transportDetails.passengerName,
pickUp: "Conference Center",
pickUpTime: transportDetails.departureTime,
dropOff: "Airport",
rateCode: "JavaScriptConference"
};
httpService.post("http://redicab.com/schedulepickup
", rediCabRequest)
.then(function resolved(status){
transportAuditService.recordRide(transportDetails.companyName);
return status.confirmationNumber;
});
break;
}
}
};
};
One aspect of the code that is immediately evident is that the requestTransport
function has quite a few responsibilities. It needs to
transportDetails
into a structure that the transport company’s web service expects.You have a feeling that transportScheduler
will just get bigger and more complex as additional cab companies (and eventually limousine services) are supported. You know that with increased complexity comes an increased potential for problems, reducing reliability. The thought of being responsible for an attendee missing a flight because the transportScheduler
doesn’t work properly prompts you to solicit feedback from Charlotte. She has a few suggestions for you.
“One thing you may consider is creating a separate module for each cab and limousine company, with the modules having a consistent interface,” she suggests. “That way, the logic for scheduling a ride with each of the companies will be isolated into individually testable units.”
“Also,” she continues, “think about creating a factory function that knows how to create the appropriate transport company module. Doing so will eliminate the switch
statement in transportScheduler
. You can even inject the factory into transportScheduler
so that transportScheduler
is easier to test.”
“Another benefit of isolating the functionality the way I’ve proposed,” continues Charlotte, “is that the transportScheduler
will no longer violate the open-closed principle the way it does now. It will be extendable by creating new transport company modules; it will not need to have its code changed at all in order to support new transport companies. The factory will be the only thing that needs to change when a new transport module is created.”
Unsurprisingly, everything Charlotte suggests makes a lot of sense, and you set off to rewrite your code.
Charlotte didn’t come right out and say it, but each of the transport company modules she suggested creating represents a strategy. Each one encapsulates a company’s scheduling algorithm, following a consistent interface. The interface is illustrated by the following code:
{
schedulePickup : function schedulepickup(transportDetails){
// Returns a Promise that resolves to a reservation confirmation
// number.
}
}
Because the interface will be implemented by each of the transport company modules, it’s possible to code the transportScheduler
against the interface rather than a concrete implementation. The transportScheduler
provides the context in which the strategies are executed.
Also, Charlotte’s suggestion of creating a factory to create the appropriate type of transport company module is a good one. As she suggested, and as illustrated in Chapter 10, the introduction of a factory will simplify the unit tests for transportScheduler
because it can be injected, and thus mocked.
Following the principles of Chapter 10, you implement Conference.transportCompanyFactory
, which follows in Listing 13-1.
Now that you’ve decided on the interface that each of the transport company modules will expose and developed a factory that creates instances of transport-company modules, you’re in a position to create the transportScheduler
.
In contrast to the list of responsibilities that it had when the Strategy Pattern was not being used, the transportScheduler
only needs to perform the following actions:
transportCompanyFactory.
schedulePickup
function.The reduced list of responsibilities translates into a simplified suite of unit tests, the start of which follows in Listing 13-2.
The tests in Listing 13-2 provide some basic coverage of negative cases that may occur as a result of incorrect use of the transportScheduler
. The tests
it("throws if audit service argument is not provided", function(){/*test*/});
it("throws if company factory argument is not provided", function(){/*test*/});
ensure that the module-creation function throws when it’s invoked without the arguments that it expects. The test
it("throws if transportDetails argument is not provided", function(){/*test*/});
performs a similar validation for the scheduleTransportation(transportDetails)
function. Finally, the test
it("doesn't swallow exceptions thrown by company factory", function(){/*test*/});
ensures that the scheduleTransportation(transportDetails)
function doesn’t suppress any errors that are thrown by the injected transport company factory.
As there’s no implementation for transportScheduler
, the tests fail spectacularly. The code in Listing 13-3, however, allows them to pass. The passing tests are shown in Figure 13.1.
Now that there’s some confidence that the transportScheduler
will report its misuse, the meat of its functionality can be implemented.
Recall that the interface each of the transport company modules will implement consists of a single function, schedulePickup(transportDetails)
, which returns a Promise
that will resolve to the reservation’s confirmation number. If the promise is resolved, the transportScheduler
should record the successful reservation with the audit service.
With that in mind, Listing 13-4 provides the rest of the unit tests for the transportScheduler
.
The tests in Listing 13-4 verify that transportScheduler
correctly coordinates interaction between the transportCompanyFactory
, the transport company module instance returned by the factory, and the transportCompanyAuditService
. The unit tests fail, as shown by Figure 13.2, because none of this coordination has been implemented.
Very little code is required to allow all of the unit tests to pass. The full implementation of transportScheduler
appears in Listing 13-5.
Notice that neither the unit tests nor the implementation have a direct dependency upon a specific transport company module; they only depend on the simple interface that transport company modules will implement. Because of this, the different logic, or strategies, required to schedule transportation with each company may be isolated into individual modules. The transportScheduler
doesn’t need to have any idea which company module is in use.
All the unit tests from Listing 13-4 now pass, as shown in Figure 13.3.
One of the benefits of correct implementation of the Strategy Pattern is that strategies may be added or removed with very little code change. In fact, the transportScheduler
will never have to change when a transport company is added or removed. Appropriately, only the module responsible for creating instances of transport company reservation strategies, the transportCompanyFactory
, will have to change when a company is added or removed.
To complete the functionality that was originally implemented without using the Strategy Pattern, a company module for RediCab needs to be implemented. Because the interface that needs to be exposed by transport company modules is so simple and well defined, there’s very little complexity in the module.
The unit tests for the module related to the implementation of the schedulePickup(transportDetails)
strategy appear in Listing 13-6.
And in Listing 13-7, the implementation of redicabTransportCompany
allows the unit tests from Listing 13-6 to pass.
Notice that the implementation of schedulePickup
is similar to the code that was originally written without the use of the Strategy Pattern. By using the Strategy Pattern, however, the code in redicabTransportCompany
is solely concerned with the details of interacting with the RediCab API. The passing unit tests are shown in Figure 13.4.
Any number of additional transport company modules may be created in the same manner, none of which will require any change to the transportScheduler
or any of the other transport-company modules.
In this chapter, you saw how the Strategy Pattern can be used to isolate different algorithms for performing a task, in this case scheduling a ride with a transport company, and to allow the appropriate algorithm, or strategy, to be dynamically determined at run time. As additional transport company modules are added, the transportCompanyFactory
may be extended to provide an instance of the appropriate module based on the type of transportation the user requires.
In addition, it described how the Factory Pattern from Chapter 10 can be used to create concrete instances of strategies. Doing so reduced testing complexity and ensured that the context in which the strategies were used didn’t have to change when strategies were added or removed.
When creating strategy modules, it’s important to write unit tests to verify that the implementations expose the correct interface. Chapter 16, “Conforming to Interfaces in an Interface-Free Language,” contains additional information about ensuring that a JavaScript module conforms to an interface.
The next chapter covers the Proxy Pattern, a mechanism through which one object can manage access to another.