1. Trang chủ
  2. » Công Nghệ Thông Tin

Phát triển Javascript - part 27 doc

10 70 0

Đang tải... (xem toàn văn)

THÔNG TIN TÀI LIỆU

Thông tin cơ bản

Định dạng
Số trang 10
Dung lượng 2,4 MB

Nội dung

ptg 11.5 Error Handling 233 By 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); } addObserver now 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); } From the Library of WoweBook.Com Download from www.eBookTM.com ptg 234 The Observer Pattern Running 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- fyObservers is 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); From the Library of WoweBook.Com Download from www.eBookTM.com ptg 11.6 Observing Arbitrary Objects 235 observable.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 newsletter objects, 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: From the Library of WoweBook.Com Download from www.eBookTM.com ptg 236 The Observer Pattern Observable.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.observable is 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 observers array, which shouldn’t be too hard to remove. All the methods on Observable.prototype access 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. From the Library of WoweBook.Com Download from www.eBookTM.com ptg 11.6 Observing Arbitrary Objects 237 As seen in Listing 11.33, we already have tests that call addObserver and hasObserver before 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. From the Library of WoweBook.Com Download from www.eBookTM.com ptg 238 The Observer Pattern Listing 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 hasObserver and 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; } /* */ } From the Library of WoweBook.Com Download from www.eBookTM.com ptg 11.6 Observing Arbitrary Objects 239 11.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 () { From the Library of WoweBook.Com Download from www.eBookTM.com ptg 240 The Observer Pattern this.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 = { From the Library of WoweBook.Com Download from www.eBookTM.com ptg 11.7 Observing Arbitrary Events 241 observe: 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 observe as 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 () { From the Library of WoweBook.Com Download from www.eBookTM.com ptg 242 The Observer Pattern /* */ 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 From the Library of WoweBook.Com Download from www.eBookTM.com . 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. 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. 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

Ngày đăng: 04/07/2014, 22:20