Listing 11.31 Various ways to share observable behavior var Observable = tddjs.util.Observable; // Extending the object with an observable object tddjs.extendnewsletter, new Observable;
Trang 1By throwing an exception already when adding the observers we don’t need
to worry about invalid data later when we notify observers Had we been
pro-gramming by contract, we could say that a precondition for the addObserver
method is that the input must be callable The postcondition is that the observer
is added to the observable and is guaranteed to be called once the observable calls notifyObservers
The test fails, so we shift our focus to getting the bar green again as quickly
as possible Unfortunately, there is no way to fake the implementation this time—
throwing an exception on any call to addObserver will fail all the other tests
Luckily, the implementation is fairly trivial, as seen in Listing 11.27
Listing 11.27 Throwing an exception when adding non-callable observers
function addObserver(observer) {
if (typeof observer != "function") { throw new TypeError("observer is not function");
} this.observers.push(observer);
} addObservernow checks that the observer is in fact a function before adding
it to the list Running the tests yields that sweet feeling of success: All green
11.5.2 Misbehaving Observers
The observable now guarantees that any observer added through addObserver
is callable Still, notifyObservers may still fail horribly if an observer throws
an exception Listing 11.28 shows a test that expects all the observers to be called even if one of them throws an exception
Listing 11.28 Expecting notifyObservers to survive misbehaving observers
"test should notify all even when some fail": function () { var observable = new tddjs.util.Observable();
var observer1 = function () { throw new Error("Oops"); };
var observer2 = function () { observer2.called = true; };
observable.addObserver(observer1);
observable.addObserver(observer2);
observable.notifyObservers();
assertTrue(observer2.called);
}
Trang 2Running the test reveals that the current implementation blows up along with the first observer, causing the second observer not to be called In effect,
noti-fyObserversis breaking its guarantee that it will always call all observers once
they have been successfully added To rectify the situation, the method needs to be
prepared for the worst, as seen in Listing 11.29
Listing 11.29 Catching exceptions for misbehaving observers
function notifyObservers() {
for (var i = 0, l = this.observers.length; i < l; i++) { try {
this.observers[i].apply(this, arguments);
} catch (e) {}
} }
The exception is silently discarded It is the observers responsibility to ensure that any errors are handled properly, the observable is simply fending off badly
behaving observers
11.5.3 Documenting Call Order
We have improved the robustness of the Observable module by giving it proper
error handling The module is now able to give guarantees of operation as long as it
gets good input and it is able to recover should an observer fail to meet its
require-ments However, the last test we added makes an assumption on undocumented
features of the observable: It assumes that observers are called in the order they
were added Currently, this solution works because we used an array to implement
the observers list Should we decide to change this, however, our tests may break
So we need to decide: Do we refactor the test to not assume call order, or do we
simply add a test that expects call order, thereby documenting call order as a
fea-ture? Call order seems like a sensible feature, so Listing 11.30 adds the test to make
sure Observable keeps this behavior
Listing 11.30 Documenting call order as a feature
"test should call observers in the order they were added":
function () {
var observable = new tddjs.util.Observable();
var calls = [];
var observer1 = function () { calls.push(observer1); };
var observer2 = function () { calls.push(observer2); };
observable.addObserver(observer1);
Trang 3observable.addObserver(observer2);
observable.notifyObservers();
assertEquals(observer1, calls[0]);
assertEquals(observer2, calls[1]);
} Because the implementation already uses an array for the observers, this test succeeds immediately
11.6 Observing Arbitrary Objects
In static languages with classical inheritance, arbitrary objects are made observable
by subclassing the Observable class The motivation for classical inheritance
in these cases comes from a desire to define the mechanics of the pattern in one place and reuse the logic across vast amounts of unrelated objects As discussed
in Chapter 7, Objects and Prototypal Inheritance, we have several options for code
reuse among JavaScript objects, so we need not confine ourselves to an emulation
of the classical inheritance model
Although the Java analogy helped us develop the basic interface, we will now break free from it by refactoring the observable interface to embrace JavaScript’s object model Assuming we have a Newsletter constructor that creates newsletterobjects, there are a number of ways we can make newsletters observ-able, as seen in Listing 11.31
Listing 11.31 Various ways to share observable behavior
var Observable = tddjs.util.Observable;
// Extending the object with an observable object tddjs.extend(newsletter, new Observable());
// Extending all newsletters with an observable object tddjs.extend(Newsletter.prototype, new Observable());
// Using a helper function tddjs.util.makeObservable(newsletter);
// Calling the constructor as a function Observable(newsletter);
// Using a "static" method:
Trang 4Observable.make(newsletter);
// Telling the object to "fix itself" (requires code on
// the prototype of either Newsletter or Object)
newsletter.makeObservable();
// Classical inheritance-like
Newspaper.inherit(Observable);
In the interest of breaking free of the classical emulation that constructors provide, consider the examples in Listing 11.32, which assume that tddjs
util.observableis an object rather than a constructor
Listing 11.32 Sharing behavior with an observable object
// Creating a single observable object
var observable = Object.create(tddjs.util.observable);
// Extending a single object
tddjs.extend(newspaper, tddjs.util.observable);
// A constructor that creates observable objects
function Newspaper() {
/* */
}
Newspaper.prototype = Object.create(tddjs.util.observable);
// Extending an existing prototype
tddjs.extend(Newspaper.prototype, tddjs.util.observable);
Simply implementing the observable as a single object offers a great deal of flexibility To get there we need to refactor the existing solution to get rid of the
constructor
11.6.1 Making the Constructor Obsolete
To get rid of the constructor we should first refactor Observable such that
the constructor doesn’t do any work Luckily, the constructor only initializes the
observersarray, which shouldn’t be too hard to remove All the methods on
Observable.prototypeaccess the array, so we need to make sure they can all
handle the case in which it hasn’t been initialized To test for this we simply need to
write one test per method that calls the method in question before doing anything
else
Trang 5As seen in Listing 11.33, we already have tests that call addObserver and hasObserverbefore doing anything else
Listing 11.33 Tests targeting addObserver and hasObserver
TestCase("ObservableAddObserverTest", {
"test should store functions": function () { var observable = new tddjs.util.Observable();
var observers = [function () {}, function () {}];
observable.addObserver(observers[0]);
observable.addObserver(observers[1]);
assertTrue(observable.hasObserver(observers[0]));
assertTrue(observable.hasObserver(observers[1]));
}, /* */
});
TestCase("ObservableHasObserverTest", {
"test should return false when no observers": function () { var observable = new tddjs.util.Observable();
assertFalse(observable.hasObserver(function () {}));
} });
The notifyObservers method however, is only tested after addObserver has been called Listing 11.34 adds a test that expects it to be possible to call this method before adding any observers
Listing 11.34 Expecting notifyObservers to not fail if called before
addObserver
"test should not fail if no observers": function () { var observable = new tddjs.util.Observable();
assertNoException(function () { observable.notifyObservers();
});
} With this test in place, we can empty the constructor as seen in Listing 11.35
Trang 6Listing 11.35 Emptying the constructor
function Observable() {
}
Running the tests shows that all but one is now failing, all with the same message:
“this.observers is not defined.” We will deal with one method at a time Listing 11.36
shows the updated addObserver method
Listing 11.36 Defining the array if it does not exist in addObserver
function addObserver(observer) {
if (!this.observers) { this.observers = [];
} /* */
}
Running the tests again reveals that the updated addObserver method fixes all but the two tests that do not call it before calling other methods, such as
hasObserverand notifyObservers Next up, Listing 11.37 makes sure to
return false directly from hasObserver if the array does not exist
Listing 11.37 Aborting hasObserver when there are no observers
function hasObserver(observer) {
if (!this.observers) { return false;
} /* */
}
We can apply the exact same fix to notifyObservers, as seen in Listing 11.38
Listing 11.38 Aborting notifyObservers when there are no observers
function notifyObservers(observer) {
if (!this.observers) { return;
} /* */
}
Trang 711.6.2 Replacing the Constructor with an Object
Now that the constructor doesn’t do anything, it can be safely removed We will then add all the methods directly to the tddjs.util.observable object, which can then be used with, e.g., Object.create or tddjs.extend to create observable objects Note that the name is no longer capitalized as it is no longer a constructor
Listing 11.39 shows the updated implementation
Listing 11.39 The observable object
(function () { function addObserver(observer) { /* */
} function hasObserver(observer) { /* */
} function notifyObservers() { /* */
} tddjs.namespace("util").observable = { addObserver: addObserver,
hasObserver: hasObserver, notifyObservers: notifyObservers };
}());
Surely, removing the constructor will cause all the tests so far to break Fixing them is easy, however; all we need to do is to replace the new statement with a call
to Object.create, as seen in Listing 11.40
Listing 11.40 Using the observable object in tests
TestCase("ObservableAddObserverTest", { setUp: function () {
this.observable = Object.create(tddjs.util.observable);
}, /* */
});
TestCase("ObservableHasObserverTest", { setUp: function () {
Trang 8this.observable = Object.create(tddjs.util.observable);
}, /* */
});
TestCase("ObservableNotifyObserversTest", {
setUp: function () { this.observable = Object.create(tddjs.util.observable);
}, /* */
});
To avoid duplicating the Object.create call, each test case gained a setUp method that sets up the observable for testing The test methods have to be updated
accordingly, replacing observable with this.observable
For the tests to run smoothly on any browser, the Object.create
imple-mentation from Chapter 7, Objects and Prototypal Inheritance, needs to be saved in
lib/object.js
11.6.3 Renaming Methods
While we are in the game of changing things we will take a moment to reduce the
ver-bosity of the interface by renaming the addObserver and notifyObservers
methods We can shorten them down without sacrificing any clarity Renaming
the methods is a simple case of search-replace so we won’t dwell on it too long
Listing 11.41 shows the updated interface, I’ll trust you to update the test case
accordingly
Listing 11.41 The refurbished observable interface
(function () {
function observe(observer) { /* */
} /* */
function notify() { /* */
} tddjs.namespace("util").observable = {
Trang 9observe: observe, hasObserver: hasObserver, notify: notify
};
}());
11.7 Observing Arbitrary Events
The current observable implementation is a little limited in that it only keeps
a single list of observers This means that in order to observe more than one event, observers have to determine what event occurred based on heuristics on the data they receive We will refactor the observable to group observers by event names Event names are arbitrary strings that the observable may use at its own discretion
11.7.1 Supporting Events in observe
To support events, the observe method now needs to accept a string argument
in addition to the function argument The new observe will take the event as its first argument As we already have several tests calling the observe method, we can start by updating the test case Add a string as first argument to any call to observeas seen in Listing 11.42
Listing 11.42 Updating calls to observe
TestCase("ObservableAddObserverTest", { /* */
"test should store functions": function () { /* */
this.observable.observe("event", observers[0]);
this.observable.observe("event", observers[1]);
/* */
}, /* * });
TestCase("ObservableNotifyObserversTest", { /* */
"test should call all observers": function () {
Trang 10/* */
this.observable.observe("event", observer1);
this.observable.observe("event", observer2);
/* */
},
"test should pass through arguments": function () { /* */
this.observable.observe("event", function () { actual = arguments;
});
/* */
},
"test should notify all even when some fail": function () { /* */
this.observable.observe("event", observer1);
this.observable.observe("event", observer2);
/* */
},
"test should call observers in the order they were added":
function () { /* */
this.observable.observe("event", observer1);
this.observable.observe("event", observer2);
/* */
}, /* */
});
Unsurprisingly, this causes all the tests to fail as observe throws an exception, because the argument it thinks is the observer is not a function To get tests back to
green we simply add a formal parameter to observe, as seen in Listing 11.43
Listing 11.43 Adding a formal event parameter to observe
function observe(event, observer) {
/* */
}
We will repeat this exercise with both hasObserver and notify as well, to make room for tests that describe actual functionality I will leave updating these
Trang 11other two functions (and their tests) as an exercise When you are done you will note that one of the tests keep failing We will deal with that last test together
11.7.2 Supporting Events in notify
While updating notify to accept an event whose observers to notify, one of the existing tests stays in the red The test in question is the one that compares arguments sent to notify against those received by the observer The problem is that because notifysimply passes along the arguments it receives, the observer is now receiving the event name in addition to the arguments it was supposed to receive
To pass the test, Listing 11.44 uses Array.prototype.slice to pass along all but the first argument
Listing 11.44 Passing all but the first argument to observers
function notify(event) { /* */
var args = Array.prototype.slice.call(arguments, 1);
for (var i = 0, l = this.observers.length; i < l; i++) { try {
this.observers[i].apply(this, args);
} catch (e) {}
} } This passes the test and now observable has the interface to support events, even if it doesn’t actually support them yet
The test in Listing 11.45 specifies how the events are supposed to work It registers two observers to two different events It then calls notify for only one
of the events and expects only the related observer to be called
Listing 11.45 Expecting only relevant observers to be called
"test should notify relevant observers only": function () { var calls = [];
this.observable.observe("event", function () { calls.push("event");
});
this.observable.observe("other", function () {