Abstract
Design Patterns, Elements of Reusable Object-Oriented Software by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides [also known as the Gang of Four (GOF)] has been a de facto reference for any Object-Oriented software developer. This article is Part V of a series of articles illustrating the GOF Design Patterns in C#. We will discuss the Iterator, Mediator, Memento, Observer, and State patterns. The next article will conclude the C# illustrations of the GOF design patterns as well as provide some closing remarks. It is assumed the reader is familiar with basic C# syntax and conventions, and not necessarily details of the .NET Framework.
Background
In Design Patterns, each pattern is described with its name (and other well-known names); the motivation behind the pattern; its applicability; the structure of the pattern; class/object participants; participant collaborations; pattern consequences; implementation; sample code; known uses; and related patterns. This article will only give a brief overview of the pattern, and an illustration of the pattern in C#.
A design pattern is not code, per se, but a "plan of attack" for solving a common software development problem. The GOF had distilled the design patterns in their book into three main subject areas: Creational, Structural, and Behavioral. This article deals with the Behavioral design patterns, or how objects act together. The first three articles in this series dealt with the Creational and Structural patterns, and the last article introduced the Behavioral patterns. This article continues the illustration of the Behavioral patterns as described by the Gang of Four.
This article is meant to illustrate the design patterns as a supplement to their material. It is recommended that you are familiar with the various terms and object diagram methods used to describe the design patterns as used by the GOF. If you're not familiar with the diagrams, they should be somewhat self-explanatory once viewed. The most important terms to get your head around are abstract and concrete. The former is a description and not an implementation, while the latter is the actual implementation. In C#, this means an abstract class is an interface, and the concrete class implements that interface.
Behavioral Patterns
To quote the GOF, "Behavioral patterns are concerned with algorithms and the assignment of responsibilities between objects. Behavioral patterns describe not just patterns of objects or classes but also the patterns of communication between them. These patterns characterize complex control flow that's difficult to follow at run-time. They shift your focus away from flow of control to let you concentrate just on the way objects are interconnected."
Iterator
The intent of the Iterator pattern according to the GOF is to "Provide a way to access the elements of an aggregate object sequentially without exposing its underlying representation." C# is already rife with iterators, so I am only going to give an overview. The structure looks like this:
Many of the .NET System.Collections
assembly classes support all the functionality of the Iterator pattern through a call to GetEnumerator()
, which returns a concrete iterator, the IEnumerator
interface. I will not illustrate usage of the .NET "iterator," for there are plenty of examples that abound.
Mediator
When designing systems, a problem often occurs when there are many objects making many calls to each other. There is a great potential for things to get "lost in the shuffle." This is especially true months or years down the road when systems get refactored / updated. Things break rather easily because things are tightly coupled. The Mediator pattern is useful because it allows you to keep objects from referring to each other directly, and "it lets you vary their interaction independently." (
GOF) The structure of the Mediator pattern looks like this:
For our illustration (mediator.cs
,) suppose we consider an aviation manufacturer's robot control process. There are several robots, which report activity, errors, need for activity, etc. The robots need to communicate with others in order to change their state, for example, if an operational error occurs, or a subsystem has been completed. Because of the various number of robots and unrelated messages, the current system has become quite ugly to maintain. The Mediator pattern allows us to simplify the interactions. We'll define our base class robot, which knows it's "director":
public class BaseRobot
{
protected RobotDirector m_Director;
public BaseRobot(RobotDirector d)
{
m_Director = d;
}
public void OperationalError(string reason)
{
m_Director.ErrorOccurred(this, reason);
}
}
We now define our concrete Robots, and RobotDirector which contains Robot references (for notifications) and channels Robot interaction:
public class RobotDirector
{
protected WelderRobot m_Welder;
protected PainterRobot m_Painter;
protected AssemblyRobot m_Assembly;
public void Initialize()
{
m_Welder = new WelderRobot(this);
m_Painter = new PainterRobot(this);
m_Assembly = new AssemblyRobot(this);
}
public void DoWork()
{
m_Painter.DoWork();
m_Welder.DoWork();
}
public void ErrorOccurred(BaseRobot r, string why)
{
Console.WriteLine("Error Occurred from {0}: {1}",
r.GetType().ToString(), why);
m_Welder.Shutdown();
m_Painter.Shutdown();
m_Assembly.Shutdown();
}
}
The responsibility of inter-object communication has been moved to the director, which in turn mediates the object interaction. Objects involved need not "know" how to interact with other objects in the same director's arena, only that they need to communicate with the director. This allows a great deal of flexibility.
Memento
Object state is a tricky thing in software development. An object needs to save or retrieve its state, but in doing so may expose itself, revealing implementation details and breaking encapsulation. The Memento pattern is useful in that it stores a
snapshot of state from its originator, and only the originator can directly access the stored state. The memento is
opaque to other objects. The pattern structure:
The Caretaker knows nothing except how to store and manage Memento objects. The Originator can access or create a Memento to preserve its state. The Memento can contain as much or as little state as required by the Originator. Using this pattern, we can assign (recall) arbitrary states of an object. Unfortunately, in C#, there is no concept of friend access as there is in C++, so opacity to other objects is unreachable. A separate state object in C# must expose methods to the originator, and in doing so, it exposes these methods to all other objects. I will not illustrate this pattern due to this limitation. One could declare the state member fields or properties as internal
, but the members would still be exposed to all objects within a program.
Observer
Object state changes, and it is often necessary for other objects to be notified when this happens. The Observer behavioral design pattern is used for just that. Imagine a data structure which can be represented by several graphical views, when the data structure changes, so must the views. The pattern looks like this:
C# is well-suited for this pattern because of delegates and events, inherently supporting the Observer pattern. For our example (observer.cs
), a business has a purchase order editing system has several objects which watch for price changes on various products. Some of these objects may be user interface, some may be business objects which operate on open purchase orders. We define the delegate for the price change:
public delegate void PriceChanged(BaseProduct p);
In our base product class, we define the "observer" storage for event notification:
public event PriceChanged PriceChangedObservers;
We add notification of the observers to the price property of our base product:
public double price
{
get { return m_yPrice; }
set
{
m_yPrice = value;
PriceChangedObservers(this);
}
}
Our observers merely implement the event and add themselves to the "list" of observers for price:
public class BaseObserver
{
public void OnPriceChanged(BaseProduct p)
{
Console.WriteLine("{0} detected price change on {1}, ${2}",
this.GetType().ToString(), p.name, p.price);
}
public void ObserveProduct(BaseProduct p)
{
p.PriceChangedObservers += new PriceChanged(this.OnPriceChanged);
}
}
We now have an observer framework we can use in the purchase order system, which should output the following:
Concrete.UIObserver detected price change on Product A, $24.99
Concrete.UIObserver updating UI view
Concrete.POObserver detected price change on Product A, $24.99
Concrete.POObserver updating PO pricing
Concrete.UIObserver detected price change on Product B, $11.99
Concrete.UIObserver updating UI view
Concrete.POObserver detected price change on Product B, $11.99
Concrete.POObserver updating PO pricing
State
Objects often change their behavior when their internal state changes. Rather than using a plethora of conditional statements, the State pattern allows "plug in" behavior. For example, a vending machine may allow purchase and dispensing of an item depending on how much money has been given. The State pattern structure looks like this:
For our illustration (state.cs
), a business uses an Order object which can load order details "on the fly." Calculation of order totals depends on whether or not the details are loaded. We use the State pattern to define the behavior of this calculation.
We store a reference to a "base order state" in our Order context, and delegate order operations to the reference:
public class OrderContext
{
private BaseOrderState m_State;
private int m_nOrderID;
public OrderContext(int id)
{
m_State = new OrderUnloadedState();
m_nOrderID = id;
}
public int OrderID { get { return m_nOrderID; } }
public void ChangeState(BaseOrderState s)
{
m_State = s;
}
public void Load() { m_State.Load(this, m_nOrderID); }
public double CalcSubtotal()
{
if (!m_State.IsLoaded()) Load();
return m_State.CalcSubtotal(this);
}
public double CalcTax()
{
if (!m_State.IsLoaded()) Load();
return m_State.CalcTax(this);
}
public double CalcTotal()
{
if (!m_State.IsLoaded()) Load();
return m_State.CalcTotal(this);
}
}
Notice that we allow "just in time" loading of order details in the Order Context, while the State can perform this JIT loading, it's more desirable to have the Context test the state for whether or not the state "is loaded."
Our BaseOrderState defines the operations whose behavior changes according to state, and our derived classes implement the proper behavior, including changing the state of the OrderContext:
public class OrderLoadedState : BaseOrderState
{
public const double TAX = 0.07;
public const double SUBTOTAL = 99.25;
override public void Load(OrderContext c, int id)
{
OrderLoadedState s = new OrderLoadedState();
Console.WriteLine("Re-Loaded order details for order id {0}", id);
ChangeState(c, s);
}
override public bool IsLoaded() { return true; }
override public double CalcSubtotal(OrderContext c)
{ return SUBTOTAL; }
override public double CalcTax(OrderContext c)
{ return SUBTOTAL * TAX; }
override public double CalcTotal(OrderContext c)
{ return SUBTOTAL + (SUBTOTAL * TAX); }
}
Conclusions
Behavioral design patterns allow for great flexibility in how your software behaves:
The Iterator pattern simplifies the traversal of collections.
The Mediator pattern allows you to simplify interactions between large numbers of objects, decoupling them from each other; promotes better object reuse and customization by reducing explicit "links" to other objects. It also reduces subclassing and simplifies object protocols, easing maintenance; abstracts how objects communicate letting you focus on how objects interact independent from their behavior apart; and centralizes control.
The Memento pattern is useful in languages (such as C++) where the concept of friend access is supported, in order to provide a mechanism to store object state without breaking encapsulation rules.
The Observer pattern is powerful and easy to implement in C# compared to other languages because of the inherent support for delegates and events. It is easy to observe many subjects by adding event handlers to the observers. There are nuances to this pattern which you should review in the GOF Design Patterns book, such as ensuring the subject's state is consistent, or where the notification is triggered. The Observer pattern allows abstraction of subject/observer coupling and broadcast communication. A downside is that notifications may send a deluge of updates to observers, which is why you should use a well-defined notification event and handler instead of a broad "subject changed" notification.
The State pattern allows localization of state-specific behavior, allowing for easy addition of new states and transitions by adding subclasses. The transitions become explicit, and protects the context from inconsistent states because the transitions are atomic from the point of view of the context. The state objects can also be shared amongst contexts. The drawback is that you may end up with a lot of classes.
Stay tuned for future articles...
Building the Samples
Unzip the source files to the folder of your choice. Start a shell (cmd.exe) and type nmake
. You may have to alter the Makefile
to point to the correct folder where your .NET Framework libraries exist.
History
2003-04-01 Corrected introduction
2002-12-07 Initial Revision
References
- Design Patterns, Elements of Reusable Object-Oriented Software. Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides. Addison Wesley Longman, Inc. 1988. ISBN 0-201-63498-8.