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 20 download and individually named according to the filenames noted throughout this chapter.
Mixins are a mechanism for code reuse in which the properties of one object, the mixin, are utilized by another, the target. This sounds similar to the topic covered in Chapter 19, method-borrowing, and it is. Both techniques may be used to share method implementations from one object to another, but there are a few key differences:
Mixins are ideally suited for implementing functionality common to many object types, but not dependent upon the details of any of those types.
Suppose you’d like all of the objects in your system that are responsible for encapsulating data to expose an asJSON()
convenience method that returns the contents of the object as a JSON string. Creating such a function is not complicated, as modern browsers provide the JSON.stringify
method:
var dataObject1 = {
propertyA: "a property",
propertyB: "b property",
asJSON: function asJSON(){
return JSON.stringify(this);
}
};
dataObject1.asJSON(); // '{ propertyA: "a property", propertyB: "b property" }';
While the function itself is simple, adding it to each of the individual data objects in a large system would lead to hundreds of lines of identical code. Not very DRY at all.
Let’s continue the example and say that, against your better judgment, you’ve added the function to each of the data objects. What happens if a third-party JSON library that can generate a JSON string ten times faster than the browser’s native method comes along and you want to update all of your data objects to employ the library? A lot of Find and Replace, that’s what. There’s a better way.
Because the asJSON
method has no dependency upon the details of the object that contains it, it’s a prime candidate for implementation as a mixin:
var asJSONMixin = {
asJSON: function asJSON(){
return JSON.stringify(this);
}
};
This may be used to extend the data objects:
var dataObject1 = {
propertyA: "a property",
propertyB: "b property"
};
// Extend dataObject1 by adding the asJSONMixin to it (the details of the
// extend function will be covered later in the chapter)
extend(dataObject1, asJSONMixin);
// dataObject1 has the asJSON method, but didn't have to implement it itself
dataObject1.asJSON(); // '{ propertyA: "a property", propertyB: "b property" }';
By extending the data objects with a traditional mixin, one that has its properties copied into its target, which implements asJSON
rather than implementing asJSON
directly in each object, incorporation of the hypothetical JSON library is simple: Only the mixin needs to be updated.
In the coming sections, we show you how to create the extend
function used in the sample and explain how our implementation of extend
differs from those provided by libraries such as jQuery and underscore. We’ll also show you how to create a traditional mixin using test-driven development. Finally, we explore functional mixins and how they differ, both positively and negatively, from traditional mixins.
This section revisits the attendee
module that we used during the discussion of promises in Chapter 6. For reference, the attendee
module is shown in Listing 20-1.
Of the eight methods in the attendee
module shown in Listing 20-1, two stand out as generic and may be useful for other objects. Can you identify them? If you found getId()
and setId(id)
, we’re on the same page.
In Chapter 6, the id
-related methods were added to attendee
for the purpose of retaining an attendee
’s unique identifier. The unique identifier may be used to store and retrieve the attendee
via a web service. It’s not beyond the realm of possibility that other entities used by the JavaScript conference’s website will also have unique identifiers.
In other words, the concept of the unique identifier is not specific to attendee
, nor is it dependent upon the details of the attendee
. The implementation of the unique identifier is a great candidate for refactoring into a mixin. This section explores this refactoring.
As shown in the introductory example, traditional mixins rely upon a function to “mix” them into a target object. This function, which copies the mixin object’s properties into the target, is traditionally called mixin
or extend
. We prefer extend
, as it speaks to the action taking place: The target object is being extended by the mixin.
As you might imagine, the extend
function accepts two arguments:
target
mixin
As is our common practice, we’ll start by ensuring that errors are thrown in exceptional cases. For the extend
function, an error should be thrown if either the target
or the mixin
is not an object. The unit tests provided in Listing 20-2 ensure this functionality.
Each of the tests iterate through the notObjects
array whose elements, you might have guessed, are not objects. The tests provide each element as target
in the case of the first test, or mixin
in the case of the second test, and ensure that the appropriate error is thrown. A stub implementation of extend
doesn’t satisfy these tests, as Figure 20.1 shows.
Listing 20-3 implements the verification that target
and mixin
are, in fact, objects. The expected messages are also added to the messages
property of the extend
function.
The guard clauses highlighted in Listing 20-3 simply ensure that target
and mixin
are defined and are objects. Figure 20.2 shows that the clauses allow the unit tests to pass.
The next step is to ensure that the extend
method operates properly when the mixin
argument is an empty object. When mixin
is empty, target
shouldn’t be changed. Listing 20-4 provides the unit test to verify this functionality.
The test added in Listing 20-4 extends target
with an empty object. It then verifies that no properties have been added to or removed from target
by ensuring that target
’s keys haven’t changed. Finally, the test ensures that the functionality of target
’s properties haven’t changed.
Because the current implementation of extend
does nothing more than ensure target
and mixin
are provided, the new test passes, as shown in Figure 20.3.
The next test verifies that the opposite situation, a bare target
extended by a mixin
with properties, functions correctly. Listing 20-5 presents the test for this.
This test extends target
, which is initialized as a bare object, with mixin
, which contains multiple properties—both functions and data.
Because target
was initially bare, the test can ensure that the properties of mixin
were copied into target
by ensuring that target
and mixin
are equivalent after the extension has occurred. The statement:
expect(target).toEqual(mixin);
performs that test. This new test fails with the current implementation of extend
, as Figure 20.4 shows.
Listing 20-6 improves the implementation of extend
to accommodate this new test.
The code added in Listing 20-6 is simple but powerful. It iterates through the properties of mixin
and adds each property to target
. The new implementation of extend
satisfies all of the existing unit tests, as Figure 20.5 shows.
The for
loop introduced to extend
in Listing 20-6 copies the enumerable properties of mixin
to target
, including those properties that mixin
may inherit through its prototype. While mixins provide a convenient mechanism to reuse code, their use can introduce complexity when you’re attempting to determine the source of an object’s behavior.
In order to reduce this complexity, we prefer to copy only the properties that the mixin directly defines. This eliminates the need to traverse a prototype chain when you’re trying to find the source of a mixed-in property.
Listing 20-7 provides a test that ensures that properties that mixin
inherits aren’t copied to target
.
The test added in Listing 20-7 defines mixinBase
, which is then used as the prototype of mixin
. As such, mixin
inherits the two properties defined by mixinBase
, baseProperty
, and baseMethod
.
The test then defines two properties directly on mixin
: mixinProperty
and mixinMethod
. The extend
method is then used to extend target
with mixin
.
To ensure that the properties that mixin
has inherited from mixinBase
have not been copied to target
, the expectation
expect(Object.keys(target).sort()).toEqual(["mixinMethod", "mixinProperty"]);
verifies that target
has only the properties that were directly defined on mixin
. Figure 20.6 shows that the new test fails, proving that the extend
function doesn’t yet have the desired behavior.
Listing 20-8 presents a modified extend
function that only copies the properties directly defined on mixin
.
Listing 20-8 simply introduces the condition that a property is only copied from mixin
to target
if the property is directly defined on mixin
. Figure 20.7 shows that this added condition allows the unit test to pass.
One aspect of extend
that you may have noticed is that it blindly assigns the properties that it’s mixing in to properties of target
. If target
happens to already have a property that mixin
provides, target
’s property will be overwritten with the one provided by mixin
.
This is the behavior of the extend
function provided by both the jQuery library (http://api.jquery.com/jquery.extend/
) and the underscore library (http://underscorejs.org/#extend
).
We don’t believe this behavior promotes reliability. As an object can be extended by a mixin at any time in its lifespan, a mixin that changes an existing property of an object could have a negative impact on code that’s not aware the mixin has been applied.
As such, the implementation of extend
presented here will be altered so that an error is thrown if the application of mixin
would overwrite an existing property of target
. The tests in Listing 20-9 verify this behavior.
The two tests in Listing 20-9 ensure that the extend
function throws an appropriate error if mixin
replaces an existing property of target
. The first test ensures an error is thrown if the property replaced by mixin
is defined directly on target
. The second test ensures that an error is also thrown if the property replaced by mixin
is inherited by target
. Figure 20.8 shows that these tests fail with the current implementation of extend
.
The updated implementation of extend
that causes the appropriate errors to be thrown is provided in Listing 20-10.
The code highlighted in Listing 20-10 implements the necessary check to ensure that a mixin
that defines a property that already exists on target
causes an error to be thrown.
Checking for the presence of the property in target
via
if(!(item in target))
does so without regard to whether the property is directly defined or inherited. This ensures that an exception will be thrown in either case. The passing unit tests in Figure 20.9 prove this to be true.
With a fully implemented extend
function, we can turn our attention to creating a traditional mixin that implements the unique identifier functionality that’s used by the attendee
module.
In order to provide all the functionality that the attendee module requires, the idMixin
needs to provide both the property in which the identifier will be stored, along with the getId
and setId
methods. The first test, presented in Listing 20-11, ensures that idMixin
provides those properties to the object it extends.
The test suite starts with a beforeEach
that initializes target
to an empty object and mixin
to an instance of the idMixin
. The last step performed in the beforeEach
is the extension of target
by mixin
.
The test simply ensures that target
contains the properties id
, getId
, and setId
after it has been extended. With only a stub implementation of idMixin
, this test fails. Figure 20.10 shows the failure.
The test can be enticed to pass by returning an object with the requisite properties from the idMixin
module. Listing 20-12 updates idMixin
to do so, and Figure 20.11 shows that the update causes the unit test to pass.
As implemented by the attendee module, the getId
method returns undefined
if it is called before an attendee
’s ID has been set. Once setId
has been executed, subsequent execution of getId
should return the value provided to setId
. The tests in Listing 20-13 verify this functionality.
The first test in Listing 20-13 ensures that the value returned by getId
is undefined
if the ID hasn’t been previously set via a call to setId
. The second test in the listing verifies that getId
returns the ID that was provided to setId
. Figure 20.12 shows that these tests currently fail.
The implementation of idMixin
that allows the test to pass is straightforward and appears in Listing 20-14.
The updated implementation of idMixin
initializes the id
property to undefined
and provides the appropriate implementations of getId
and setId
.
Recall that the extend
function copies references to the properties of the mixin into properties of the target object. As such, when the methods provided to the target by the mixin are executed, this
will be bound to the target object (as long as the methods are executed via the target object; see Chapter 18 for more details). The idMixin
is guaranteed that the object containing the getId
and setId
methods will also contain the id
property because the mixin itself provides it.
The implementation of idMixin
presented in Listing 20-14 allows all the unit tests to pass, as shown in Figure 20.13.
With the idMixin
fully implemented, the final step is to integrate it into the attendee
module. Listing 20-15 contains an excerpt from the original unit tests for the attendee
module from Chapter 6.
As you can see, the test verifies that setId
and getId
function appropriately. Because the idMixin
should now be used to provide the getId
and setId
methods, they’re removed from the attendee
implementation, as is the attendeeId
variable used to store the ID. Unsurprisingly, doing so causes the unit test to fail, as shown in Figure 20.14.
Listing 20-16 shows how to incorporate the idMixin
into the attendee module to provide the getId
and setId
functions.
Rather than immediately returning an object, the attendee
module now assigns the object containing the properties specific to an attendee into newAttendee
. It then extends newAttendee
with the object returned by idMixin
, providing the getId
/setId
functionality that attendee
previously defined itself. Finally, the extended newAttendee
object is returned. Figure 20.15 shows that the attendee unit tests are once again passing.
There’s one small, but significant, difference between attendee
’s original implementation of getId
and setId
and the implementation of those methods as mixed in by the idMixin
object. In the original implementation, the value provided to setId
and returned from getId
was captured in a variable that wasn’t accessible to external code, attendeeId
. (Listing 20-1 shows the original version of attendee
.) By hiding the attendeeId
variable, the attendee
module could be certain that the variable could only be manipulated via the setId
method.
The idMixin
, however, adds id
as a property of the object being extended, and the getId
and setId
methods manipulate that property. Because it’s an exposed property of the object, id
could be changed directly; using setId
isn’t required:
// Create an attendee that's extended by idMixin
var extendedAttendee = Conference.attendee();
extendedAttendee.setId(12);
console.log(extendedAttendee.getId()); // 12
extendedAttendee.id = -1;
console.log(extendedAttendee.getId()); // -1 (!!!)
While exposing properties on the extended object may be appropriate and acceptable in some cases, it’s undesirable in this case. Does that mean the ID-related methods can’t be added to attendee
via a mixin? No, it doesn’t.
We were introduced to functional mixins by Angus Croll’s blog post, “A fresh look at JavaScript Mixins” (http://javascriptweblog.wordpress.com/2011/05/31/ a-fresh-look-at-javascript-mixins/
). Functional mixins appeal to us for many reasons. Chief among their appealing aspects is the ability of functional mixins to hide data, something that we’d like for our mixin to implement unique identifier functionality.
In this section, we’ll illustrate the creation of a functional mixin that provides getId
and setId
methods and stores the ID in a variable that’s protected from external manipulation.
Unlike traditional mixins, functional mixins are designed to add themselves to the object they’re extending; use of extend
(or a similar method) is not required. One drawback to this, however, is that each functional mixin must verify that the object being extended doesn’t already have a property it provides. Traditional mixins can rely on our implementation of the extend
method’s behavior of verifying that a mixin will not overwrite a property of the object being extended.
That knowledge in hand, the first tests that you need to write ensure that the functional mixin addId
doesn’t overwrite any existing properties, and that it throws errors instead. These tests appear in Listing 20-17.
The two tests are implemented as you might expect. The first attempts to extend a target that already implements getId
, and the second attempts to extend a function that already implements setId
. Both tests ensure that the correct error message is generated.
The mechanism used to extend target
with the addId
mixin should provide a hint about how addId
will be implemented (or, if you’ve read Angus’s blog post, you already know):
Conference.mixins.addId.call(target);
The tests execute addId
via call
, providing the object being extended as the context to use. The call
method, described in detail in Chapter 18, binds this
within the function to the context object provided, in this case target
. The addId
mixin will add itself to target
by manipulating this
.
With a stub implementation of addId
, the unit tests fail, as Figure 20.16 shows.
Listing 20-18 provides an initial implementation of addId
that performs the necessary property-checking.
Since this
has been bound to the object being extended, the addIn
function simply needs to examine to properties of this
to ensure existing properties won’t be overwritten.
Figure 20.17 shows that the checks allow the unit tests to pass.
Finally, tests that verify the following need to be written:
getId
and setId
are added to the object being extended.getId
returns undefined
if an ID hasn’t been set.getId
and setId
work correctly together.With the exception of the verification of properties, the tests are identical to those that were written for the traditional mixin. The tests are provided in Listing 20-19.
From the standpoint of verification of the functionality of setId
and getId
, the tests for the addId
functional mixin are identical to those for the idMixin
traditional mixin.
A difference in the tests is highlighted in Listing 20-20: The test that ensured the correct properties were added to the extended object for idMixin
needed to verify that the id
property was added to the extended object; the corresponding test for addId
does not. The current implementation of addId
doesn’t satisfy the new unit tests, as Figure 20.18 shows.
Listing 20-20 provides the full implementation of the addId
functional mixin.
The most notable aspects of final implementation of the addId
functional mixin are:
getId
and setId
are added to the object being extended by augmenting this
.id
, manipulated by getId
and setId
, is not externally accessible.Figure 20.19 shows that all of the unit tests for addId
now pass.
With a fully implemented addId
functional mixin in hand, the final step is to integrate it into the attendee
module. The first step is to remove the implementations of getId
and setId
, along with the variable used to store the ID, from attendee
.
This is the same first step that was performed when the idMixin
traditional mixin was added to the attendee
; you can refer to Figure 20.14 for proof that removal of the implementations of getId
and setId
causes an attendee
unit test to fail.
Listing 20-21 shows how to extend attendee
with the addId
mixin.
Once again, the attendee
module captures the object containing the properties specific to attendee
into newAttendee
. It then extends newAttendee
by call
ing addId
with newAttendee
provided as the object to which this
is bound. The addId
functional mixin adds the getId
and setId
methods to newAttendee
. Finally, the extended newAttendee
is returned. Because it isn’t added as a property of the extended object, the variable that is used to store the ID can only be changed via the setId
method.
Figure 20.20 shows that the unit tests for attendee are all now passing.
This chapter explored how mixins may be used to DRY out your JavaScript by isolating common functionality, such as the maintenance of unique identifiers, into mixin objects.
Once appropriately isolated into a mixin, the functionality may be added to an object that requires it via a function that extends one object with another, in the case of a traditional mixin, or directly by the mixin itself, in the case of a functional mixin.
The chapter also discussed identifying functionality appropriate for isolation into a mixin, namely functionality that has no dependency upon the implementation of the objects into which it will be mixed. We also explained a potential pitfall of the extend
methods provided by the jQuery and underscore libraries, the silent replacement of existing properties. We then avoided the pitfall in the version of extend
that was implemented in this chapter.
Also, we demonstrated extension of an object via both a traditional mixin and a functional mixin by refactoring the attendee
module.
When writing mixins, either traditional or functional, be sure to write unit tests to ensure:
In the next chapter, we tackle the unique and challenging aspects of ensuring the reliability of complex mediator-based and observer-based program architectures.