Appendix |
A complete PDF version of the text book is now available. The PDF version is an almost complete subset of the HTML version (where only a few, long program listings have been removed). See here. |
In this appendix we present topics and examples that are taken out of the original context, typically because they involve programs of some length or complexity.
|
58.1. A Sample Observer
Contents Up Previous Next Slide Annotated slide Aggregated slides Subject index Program index Exercise index
The Observer design pattern was introduced in Section 24.1. In this section we present a simple version of the weather service and watcher program, as originally illustrated in Figure 24.1.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 | using System; using System.Collections; namespace Templates.Observer { // An observer public class TemperatureWatcher { private float currentTemperature; private WeatherCenter mySubject; private string watcherName; public TemperatureWatcher (WeatherCenter s, float initTemp, string name){ mySubject = s; currentTemperature = initTemp; watcherName = name; } public void Update(){ SubjectState state = mySubject.GetState(); currentTemperature = state.temperature; Console.WriteLine ("Temperature watcher {1}: the temperature is now {0}.", currentTemperature, watcherName); } } // A subject public class WeatherCenter { private float temperature, rainAmount, airPressure; public WeatherCenter(float temp, float rain, float pres){ temperature = temp; rainAmount = rain; airPressure = pres; } public void WeatherUpdate(float temp, float rain, float pres){ float oldTemperature = this.temperature, oldRainAmount = this.rainAmount, oldAirPressure = this.airPressure; this.temperature = temp; this.rainAmount += rain; this.airPressure = pres; if (Math.Abs(oldTemperature - this.temperature) > 2.0F || rain > 0.5F || Math.Abs(oldAirPressure - this.airPressure) > 3.0F) this.Notify(); } private ArrayList observers = new ArrayList(); public void Attach(TemperatureWatcher o){ observers.Add(o); } public void Detach(TemperatureWatcher o){ observers.Remove(o); } public void Notify(){ foreach(TemperatureWatcher o in observers) o.Update(); } public SubjectState GetState(){ return new SubjectState(temperature, rainAmount, airPressure); } } public class SubjectState { public float temperature, rainAmount, airPressure; public SubjectState(float temp, float rain, float pres){ temperature = temp; rainAmount = rain; airPressure = pres; } } } | |||
|
In Program 58.2 (only on web) we force a number of weather updates, which trigger notifications and observer updates. The output of the program is shown in Listing 58.3 (only on web).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | using Templates.Observer; class Client { public static void Main(){ WeatherCenter subj = new WeatherCenter(25.0F, 0.0F, 1020.0F); TemperatureWatcher o1 = new TemperatureWatcher(subj, 25.0F, "w1"), o2 = new TemperatureWatcher(subj, 25.0F, "w2"); subj.Attach(o1); subj.Attach(o2); subj.WeatherUpdate(23.0F, 0.0F, 1020.0F); subj.WeatherUpdate(23.0F, 0.0F, 1020.0F); subj.WeatherUpdate(23.0F, 0.0F, 1020.0F); subj.WeatherUpdate(24.0F, 0.0F, 920.0F); subj.WeatherUpdate(21.0F, 0.0F, 1050.0F); } } | |||
|
1 2 3 4 | Temperature watcher w1: the temperature is now 24. Temperature watcher w2: the temperature is now 24. Temperature watcher w1: the temperature is now 21. Temperature watcher w2: the temperature is now 21. | |||
|
A major weakness of Program 58.1 (only on web) is that the WeatherCenter class is bound to a single type of watchers, namely the TemperatureWatcher. In fact, the only important thing that the subject needs to know about observers is that the observers respond to the Update message. Therefore it would be beneficial if the Observer class implements an interface IObserver which prescribes an Update method. For details about interfaces, see Chapter 31.
58.2. A Sample Observer with Events
Contents Up Previous Next Slide Annotated slide Aggregated slides Subject index Program index Exercise index
In continuation of our interest for delegates and events from Section 24.2 we will show a version of Program 58.1 and Program 58.2 that makes use of events.
Each observer has an alarm method. The delegate WeatherNotification represents a special purpose signature that matches the need of this program. The alarm methods can be added to (or deleted) from the subject. Inside the subject, the alarm methods and their receiver objects are added to an event of (delegate) type WeatherNotification. When the subject needs to update the observers it triggers the event (simply by calling it).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 | using System; using System.Collections; namespace Templates.Observer { // Delegate type: public delegate void WeatherNotification(float temp, float rain, float pres); // An observer public class TemperatureWatcher { private float currentTemperature; private WeatherCenter mySubject; private string watcherName; public TemperatureWatcher (WeatherCenter s, float initTemp, string name){ mySubject = s; currentTemperature = initTemp; watcherName = name; } public void TemperatureAlarm(float temp, float rain, float pres){ currentTemperature = temp; Console.WriteLine( "Temperature watcher {1}: the temperature is now {0}.", currentTemperature, watcherName); } } public class RainWatcher { private float currentRainAmount; private WeatherCenter mySubject; private string watcherName; public RainWatcher (WeatherCenter s, float initAmount, string name){ mySubject = s; currentRainAmount = initAmount; watcherName = name; } public void RainAlarm(float temp, float rain, float pres){ currentRainAmount = rain; Console.WriteLine("Rain watcher {1}: Accumulated rain fall: {0}.", currentRainAmount, watcherName); } } // A subject public class WeatherCenter { private float temperature, rainAmount, airPressure; public WeatherCenter(float temp, float rain, float pres){ temperature = temp; rainAmount = rain; airPressure = pres; } public void WeatherUpdate(float temp, float rain, float pres){ float oldTemperature = this.temperature, oldRainAmount = this.rainAmount, oldAirPressure = this.airPressure; this.temperature = temp; this.rainAmount += rain; this.airPressure = pres; if (Math.Abs(oldTemperature - this.temperature) > 2.0F || rain > 0.5F || Math.Abs(oldAirPressure - this.airPressure) > 3.0F) this.Notify(); } private event WeatherNotification weatherNotifier; public void AddNotifier(WeatherNotification n){ weatherNotifier += n; } public void RemoveNotifier(WeatherNotification n){ weatherNotifier -= n; } public void Notify(){ weatherNotifier(temperature, rainAmount, airPressure); } } } | |||
|
Notice, in Program 58.4 that the alarm methods receive weather information via parameters. Thus, in this version, there is no need to call back the observer to obtain this information. This detail is not, however, due to the use of delegates and events.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | using Templates.Observer; class Client { public static void Main(){ WeatherCenter subj = new WeatherCenter(25.0F, 0.0F, 1020.0F); TemperatureWatcher o1 = new TemperatureWatcher(subj, 25.0F, "w1"), o2 = new TemperatureWatcher(subj, 25.0F, "w2"); RainWatcher o3 = new RainWatcher(subj, 0.0F, "w3"); subj.AddNotifier(o1.TemperatureAlarm); // Adding instance methods subj.AddNotifier(o2.TemperatureAlarm); // to an event in subj.AddNotifier(o3.RainAlarm); // the subject subj.WeatherUpdate(23.0F, 0.0F, 1020.0F); subj.WeatherUpdate(23.0F, 2.0F, 1020.0F); subj.WeatherUpdate(23.0F, 0.0F, 1020.0F); subj.WeatherUpdate(24.0F, 0.3F, 920.0F); subj.WeatherUpdate(21.0F, 3.7F, 1050.0F); } } | |||
|
1 2 3 4 5 6 7 8 9 | Temperature watcher w1: the temperature is now 23. Temperature watcher w2: the temperature is now 23. Rain watcher w3: Accumulated rain fall: 2. Temperature watcher w1: the temperature is now 24. Temperature watcher w2: the temperature is now 24. Rain watcher w3: Accumulated rain fall: 2,3. Temperature watcher w1: the temperature is now 21. Temperature watcher w2: the temperature is now 21. Rain watcher w3: Accumulated rain fall: 6. | |||
|
58.3. A Composite example: IntSequence
Contents Up Previous Next Slide Annotated slide Aggregated slides Subject index Program index Exercise index
In this appendix section we will study yet another example of a Composite. The example is similar to the MusicElement composite from Section 32.2. Below we elaborate the number interval, which we have worked with in Section 21.3 and Section 31.6. We now form the general concept of an integer sequence, in terms of the abstract class IntSequence, see Figure 58.1.
A concrete example of an integer sequence is 3, 4, 5, 17, 12, 11, 10, 9, 8, 7, 29. This sequence can be represented as the interval [3 - 5], the number 17, the interval [12 - 7], and finally 29. An object such as [3 - 5] is an interval, in the sense of Section 21.3. A single number, such as 17, is (for the sake of our example at least) represented as an instance of the class IntSingular. The composition of exactly two constituents is represented as an instance of class IntCompSeq. We illustrate the mentioned sequence in Figure 58.2. There are, of course, a lot of other ways to split the given sequence into pieces.
Figure 58.1 The class diagram of the IntSequence composite. |
Before we study each of the classes shown in Figure 58.1 we will illustrate how they can be used from a client program. In Program 58.7 we construct the integer sequence mentioned above, see line 7-12. In that way we actually construct the tree structure of Figure 58.2. In line 14 of Program 58.7 we show how to operate on the IntSequence objects with the operations Min and Max. Min and Max are IntSequence operations, which are provided uniformly by the classes IntSingular, IntInterval, and IntCompSeq. A client program should not care about which of the classes it accesses. Min and Max make sense on any IntSequence object! In line 16-17 we show how the integer sequence can be traversed with an iterator via a foreach loop. As we will see below, the provision of iterators on IntSequence and its subclasses will be our most serious challenge.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | using System; class SeqApp { public static void Main(){ IntSequence isq = new IntCompSeq( new IntCompSeq( new IntInterval(3,5), new IntSingular(17) ), new IntCompSeq( new IntInterval(12,7), new IntSingular(29) ) ); Console.WriteLine("Min: {0} Max: {1}", isq.Min, isq.Max); foreach(int i in isq) Console.Write("{0,4}", i); } } | |||
|
1 2 | Min: 3 Max:29 3 4 5 17 12 11 10 9 8 7 29 | |||
|
Figure 58.2 A possible tree of objects which represent the integer sequence 3, 4, 5, 17, 12, 11, 10, 9, 8, 7, 29. |
58.4. Implementation of the IntSequence classes
Contents Up Previous Next Slide Annotated slide Aggregated slides Subject index Program index Exercise index
In Section 58.3 we have discussed the class structure of the IntSequence pattern, and we have used it from a client class. Now it is time to program the four classes which are involved in the Composite. These are the abstract class IntSequence itself, and its three concrete subclasses IntSingular (Program 58.9) , IntInterval (Program 58.10) , and IntCompSeq (Program 58.11) .
1 2 3 4 5 | public abstract class IntSequence: IEnumerable { public abstract IEnumerator GetEnumerator(); public abstract int Min {get;} public abstract int Max {get;} } | |||
|
In Program 58.9 we show the abstract class called IntSequence with an abstract method GetEnumerator and two abstract properties Min and Max . Min and Max are intended to return the smallest and the largest integer in the sequence respectively. GetEnumerator is intended to return an enumerator, which can be used for traversal of the sequence.
If we follow the Composite design pattern of [Gamma96] strictly, we need an Add and Remove operation to add and remove components. We have not provided these in IntSequence. Instead, we rely on the constructor of the class IntCompSeq for construction of a composite integer sequences. Notice that such a composite integer sequence has exactly two sub-sequences, both of which are non-empty.
The class IntInterval in Program 58.10 below is taken almost directly from Section 21.3. It has trivial Min and Max operations. The enumerator stuff in line 22-48 has already been explained in Section 31.6 in the context of our discussion of the interfaces IEnumerable and IEnumerator.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | public class IntInterval: IntSequence{ private int from, to; public IntInterval(int from, int to){ this.from = from; this.to = to; } public override int Min{ get {return Math.Min(from,to);} } public override int Max{ get {return Math.Max(from,to);} } public override IEnumerator GetEnumerator (){ return new IntervalEnumerator(this); } private class IntervalEnumerator: IEnumerator{ private readonly IntInterval interval; private int idx; public IntervalEnumerator (IntInterval i){ this.interval = i; idx = -1; // position enumerator outside range } public Object Current{ get {return (interval.from < interval.to) ? interval.from + idx : interval.from - idx;} } public bool MoveNext (){ if ( idx < Math.Abs(interval.to - interval.from)) {idx++; return true;} else {return false;} } public void Reset(){ idx = -1; } } } | |||
|
The class IntSingular in Program 58.11 is almost trivial (and it could actually have been handled by a trivial interval). Notice however that we, despite the simplicity of a sequence with a single element, have programmed a lengthy and clumsy nested class SingularEnumerator (line 21-45).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | public class IntSingular: IntSequence{ private int it; public IntSingular(int it){ this.it = it; } public override int Min{ get {return it;} } public override int Max{ get {return it;} } public override IEnumerator GetEnumerator (){ return new SingularEnumerator(this); } private class SingularEnumerator: IEnumerator{ private readonly IntSingular ints; private int idx; public SingularEnumerator (IntSingular ints){ this.ints = ints; idx = -1; // position enumerator outside range } public Object Current{ get {return ints.it;} } public bool MoveNext (){ if (idx == -1) {idx++; return true;} else {return false;} } public void Reset(){ idx = -1; } } } | |||
|
The class IntCompSeq in Program 58.12 is the interesting one. The Min and Max properties traverse all component sequences (recursively) in order to find the smallest and the largest number in the sequences respectively. Be sure that you are able to spot the recursion! Seen in retrospect, it would probably have been better to implement Min and Max as methods, because they require some non-trivial computations on composite sequences.
The method GetEnumerator in the class IntCompSeq returns an instance of the private, nested class CompositeEnumerator, which is shown in line 22-64 of Program 58.12. This enumerator has access to its surrounding (composite) integer sequence (via the two instance variables s1 and s2 in line 24). It also keeps track of the current composite of a traversal (idx) and its enumerator (idxEnumerator). The most difficult aspect is the programming of MoveNext, which advances the position of the enumerator. It assumes that there are no empty sequences. (In fact, there cannot be such empty sequences. Why?) Basically, it keeps track of which sub enumerator to use. You are invited to research the details if you care. Be sure, again, to spot the recursion: The method GetEnumerator, in line 18 - 20, instantiates CompositeEnumerator, which in turn can call GetEnumerator on constituent IntSequence objects.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | public class IntCompSeq: IntSequence{ private IntSequence s1, s2; public IntCompSeq(IntSequence s1, IntSequence s2) { this.s1 = s1; this.s2 = s2; } public override int Min{ get {return Math.Min(s1.Min,s2.Min);} } public override int Max{ get {return Math.Max(s1.Max, s2.Max);} } public override IEnumerator GetEnumerator (){ return new CompositeEnumerator(this); } private class CompositeEnumerator: IEnumerator{ private IntSequence s1, s2; private int idx; // 0: not started. // 1: s1 is current. 2: s2 is current. private IEnumerator idxEnumerator; public CompositeEnumerator (IntCompSeq outerIntCompSeq){ this.s1 = outerIntCompSeq.s1; this.s2 = outerIntCompSeq.s2; idx = 0; // 0: outside. 1: at s1. 2: at s2 idxEnumerator = null; } public Object Current{ get {return idxEnumerator.Current;} } public bool MoveNext (){ if (idx == 0){ // At start position. idx = 1; idxEnumerator = s1.GetEnumerator(); return idxEnumerator.MoveNext(); } else if (idx == 1){ // At left sequence bool hasMoved1 = idxEnumerator.MoveNext(); if (hasMoved1) return true; else{ idxEnumerator = s2.GetEnumerator(); idx = 2; return idxEnumerator.MoveNext(); } } else if (idx == 2) { // At right sequence bool hasMoved2 = idxEnumerator.MoveNext(); if (hasMoved2) return true; else return false; } else return false; } public void Reset(){ idx = 0; } } } | |||
|
Taken all together - looking at the classes in Program 58.10, Program 58.11, and Program 58.12, the Min and Max operations are easy to program. The GetEnumerator methods, and the private classes behind them, were, however, unreasonably complicated to deal with. The reason is in part, the rather low-level interface requested by IEnumerator. Another (minor) reason is the recursive, composite structure we are dealing with. It turns out that there is a much easier and a more high-level way of programming enumerators, namely by use of C#'s yield return facility. This will be discussed in Section 48.2. In Section 48.2 we will re-program the IntSequence classes in order to illustrate the alternative approach. See Program 48.3, Program 48.4, and Program 48.5.
58.5. References
[Gamma96] | E. Gamma, R. Helm, R. Johnson and J. Vlissides, Design Patterns: Elements of Reusable Object-oriented Software. Addison Wesley, 1996. |