Discover the Design Patterns You're Already Using in the .NET Framework
This article discusses:
|
This article uses the following technologies: .NET Framework and ASP.NET Code download available at: DesignPatterns.exe (122KB) |
ecently, Microsoft has placed increasing emphasis on design patterns. If you are unfamiliar with patterns, suddenly being inundated with new terms and foreign-looking UML diagrams can be overwhelming. This emphasis on patterns, however, doesn't represent a shift in methodology so much as a change in vocabulary. The Microsoft® .NET Framework base class library (BCL) already makes extensive use of patterns, and you are probably familiar with the most common ones, even though you might not realize it yet.
In this article, I'll cover a basic overview of several common design patterns and how they are used in the BCL and other areas of the .NET Framework. In doing so, you can discover some of the motivation for why the Framework is designed the way it is, as well as make the abstract concepts of the patterns themselves more intuitively understandable.
Most of the patterns I'll be covering come from the canonical reference, Design Patterns by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides, (Addison-Wesley, 1995). These authors are collectively known as the Gang of Four. The Gang of Four didn't invent these patterns, but they documented and formalized the good work others had been doing since the beginning of software development.
If you are already familiar with some of these patterns, feel free to read about those you aren't familiar with, since each section is relatively independent. The section on ASP.NET-related patterns requires familiarity with the request pipeline, a basic overview of which is provided in that section.
Within the .NET Framework, the use of some patterns is so prevalent that they have been built into programming languages themselves, instead of just represented by the class libraries. The first two patterns I will discuss, the Observer and the Iterator, are supported by various language features of both C# and Visual Basic® .NET. As you will see, they are an integral part of many common programming tasks.
Observer Pattern
Good object-oriented design emphasizes both encapsulation and loose coupling. In other words, classes should keep internal details private and also minimize their strict dependencies. In most applications, classes don't work in isolation; they interact with many other classes. A common scenario of class interaction occurs when one class (the Observer) needs to be notified when something changes in another (the Subject). For example, several Windows® Forms controls might need to update their display after a button is clicked. A simple solution would be to have the Subject call a specific method of the Observer whenever a change in state occurs. This introduces a host of problems, however. Now, since the Subject needs to know which method to call, it is tightly coupled to that specific Observer. Furthermore, if you need to add more than one Observer, you have to continue to add code for each method call to the Subject. If the number of Observers changes dynamically, this gets even more complex. You'll quickly end up with a brittle mess that's difficult to maintain.
Applying the Observer pattern helps to resolve this problem efficiently. You can decouple the Subject from the Observers so that Observers of any variety can easily be added and removed, at both design time and run time. The Subject maintains a list of interested Observers. Each time the Subject's state changes, it calls the Notify method on each Observer. Figure 1 shows a sample implementation. All classes designed to act as Observers implement the ICanonicalObserver interface, and all Subjects must derive from CanonicalSubjectBase. If a new Observer wants to monitor a Subject, the Add method easily handles this without having to change any code in the Subject class. Note also that each Subject only directly depends on the ICanonicalObserver interface, not any specific Observer.
While the Gang of Four's Observer pattern solves some of these problems, there are still some roadblocks, since subjects must inherit from a specific base class and Observers must implement a special interface. Thinking back to the Windows Forms button example, a solution emerges. The .NET Framework introduces delegates and events to solve these problems. If you have done any programming for ASP.NET or Windows Forms, then you have probably worked with events and event handlers. Events act as the Subject, while delegates act as Observers. Figure 2 shows an example of the Observer pattern, making use of events.
The Windows Forms Button control exposes a Click event which gets raised whenever the button is clicked. Any class designed to react to this event just needs to register a delegate with that event. The Button class doesn't depend on any of the potential Observers, and each Observer only needs to know the correct type of the delegate for the event (EventHandler, in this case). Since EventHandler is a delegate type and not an interface, each Observer doesn't need to implement an extra interface. Assuming it already contains a method with a compatible signature, it only needs to register that method with the event of the Subject. Through delegates and events, the Observer pattern lets you decouple Subjects and their Observers.
Iterator Pattern
Many programming tasks involve manipulating collections of objects. Whether these collections are simple lists or something more complex such as binary trees, you'll often need some way to get access to each object in the collection. In fact, depending on the collection, you may want several ways to access each object such as front to back, back to front, preorder or postorder. To keep the collection simple, the traversal code itself is often in its own class.
One of the most basic ways to store a list of objects is in an array. Array types are built into both Visual Basic .NET and C#. Both languages also have a loop structure that aids in iterating over arrays: foreach in C# and For Each in Visual Basic .NET. Here's an easy example of iterating over arrays:
int[] values = new int[] {1, 2, 3, 4, 5}; foreach(int i in values) { Console.Write(i.ToString() + " "); }These statements make use of the iterator for the array behind the scenes. All you need to know is that you are guaranteed to have the loop run exactly once for each item in the array.
To make those statements work, the object referenced in the In expression must implement IEnumerable. Any collection of objects that implements the IEnumerable interface can be traversed (enumerated). This interface has a single method, GetEnumerator, that returns an object which implements IEnumerator. The IEnumerator class contains the code necessary to iterate through the collection. It has a property for the current object (Current) and methods to advance to the next object as well as start over (MoveNext, and Reset). All of the collection classes in the System.Collections namespace, as well as arrays, implement IEnumerable and can therefore be iterated over.
If you examine the Microsoft intermediate language (MSIL) generated by the C# compiler for any code that uses foreach, you can see that in most cases it just uses the IEnumerator to do the iteration (certain types, such as arrays and strings, are special cased by the compiler). Here you can see the IEnumerator approach for iterating over the same array shown previously:
int[] values = new int[] {1, 2, 3, 4, 5}; IEnumerator e = ((IEnumerable)values).GetEnumerator(); while(e.MoveNext()) { Console.Write(e.Current.ToString() + " "); }
The .NET Framework uses the IEnumerable and IEnumerator interfaces to implement the Iterator pattern. The Iterator pattern lets you easily traverse a collection without exposing the inner workings of that collection. An Iterator class, an implementer of IEnumerator, is a separate class from the collection, which implements IEnumerable. The Iterator class maintains the state of the traversal (including what the current item is and whether or not there are more items to be traversed) outside of the collection itself. The algorithm for the traversal is contained in the Iterator as well. This way you can simultaneously have multiple Iterators, each traversing the same collection in wildly different ways, without adding any complexity to the collection class itself.
Decorator Pattern
Any useful executable program involves either reading input, writing output, or both. Regardless of the source of the data being read or written, it can be treated abstractly as a sequence of bytes. .NET uses the System.IO.Stream class to represent this abstraction. Whether the data involves characters in a text file, TCP/IP network traffic, or something else entirely, chances are you will have access to it via a Stream. Since the class for working with file data (FileStream) and the class for working with network traffic (NetworkStream) both inherit from Stream, you can easily write code that processes the data independent of its origins. Here's a method for printing out some bytes from a Stream to the console:
public static void PrintBytes(Stream s) { int b; while((b = fs.ReadByte()) >= 0) { Console.Write(b + " "); } }
Reading a single byte at a time is typically not the most efficient way to access a stream. For example, hard drives are capable of (and optimized for) reading continuous blocks of data from the disk in a big chunk. If you know you are going to be reading several characters, it is better to read a chunk from the disk all at once and then consume the chunk from memory byte by byte. The Framework includes the BufferedStream class for doing just that. The constructor for BufferedStream takes as the parameter whatever stream you would like buffered access to. BufferedStream overrides the main methods of Stream, such as Read and Write, to provide more functionality. Since it is still a child class of Stream, you can use it the same as any other Stream (note that FileStream includes its own buffering capabilities). Similarly, you can use System.Security.Cryptography.CryptoStream to encrypt and decrypt Streams on the fly, without the rest of the application needing to know anything more than the fact that it is a Stream. Figure 3 shows several calls to my printing method using different Streams.
Figure 4 Using the Decorator Pattern
This ability to dynamically attach new functionality to objects transparently using composition is an example of the Decorator pattern, as shown in Figure 4. Given any instance of Stream, you can add the capability for buffered access by wrapping it in a BufferedStream, without changing the interface to the data. Since you are just composing objects, this can be done at run time, rather than using a technique like inheritance, which is a compile-time decision. The core functionality is defined either by an interface or by an abstract class (like Stream) from which all the Decorators derive. The Decorators themselves implement (or override) the methods in the interface (or base class) to provide the extra functionality. BufferedStream, for example, overrides Read to read from a buffer fed by the wrapped Stream, instead of reading from that Stream directly. As Figure 3 illustrates, any composition of Decorators, no matter how complex, can still be used as if it were only the base class.
Adapter Pattern
One of the strengths of the .NET Framework is backward compatibility. From .NET-based code you can easily call legacy COM objects and vice versa. In order to use a COM component in your project, all you have to do is add a reference to it via the Add Reference dialog in Visual Studio .NET. Behind the scenes, Visual Studio® .NET invokes the tlbimp.exe tool to create a Runtime Callable Wrapper (RCW) class, contained in an interop assembly. Once the reference has been added (and the interop assembly has been generated for you), the COM component can be used like any other class in managed code. If you were looking at code someone else had written without seeing the list of references (and without examining metadata associated with the classes or their implementation), you would be unable to tell which classes were written in a .NET-targeted language and which were COM components.
The magic that makes this happen is contained in the RCW. COM components have different error handling mechanisms and also make use of different data types. For example, strings in the .NET Framework use the System.String class while COM might use a BSTR. When calling a COM component with a string parameter from .NET-based code, though, you can pass in a System.String just like you would to any other similar managed code method. Inside the RCW, this System.String is converted into a format that the COM component expects, like a BSTR, before the COM call is made. Similarly, a method call on a COM component typically returns an HRESULT to indicate success or failure. When a COM method call returns an HRESULT that indicates that the call failed, the RCW turns this into an exception (by default), so it can be handled like all other managed code errors.
By allowing managed classes and COM components to interact despite their interface differences, RCWs are an example of the Adapter pattern. The Adapter pattern lets you adapt one interface to another. COM doesn't understand the System.String class, so the RCW adapts it to something that it can understand. Even though you can't change how a legacy component works, you can still interact with it. Adapters are frequently used like this.
The Adapter class itself wraps an Adaptee, translating all calls from the client into the appropriate format and sequence of calls. Though this sounds similar to the Decorator, there are several key differences. With a Decorator, the interfaces of the objects you're composing are the same, while the entire point of an Adapter is to allow you to change interfaces.Adapters also have a definite sequence to them; the Adaptee must be contained by the Adapter. A Decorator class doesn't need to know whether it is wrapped by 1 or 500 other classes, since the interfaces are all the same. As a result, the use of Decorators can be transparent to the application, while the use of Adapter cannot.
Factory Pattern
There are many cases in the Framework where you can obtain a new instance of a struct or class without calling its constructor yourself. The System.Convert class contains a host of static methods that work like this. To convert an integer to a Boolean, for example, you can call Convert.ToBoolean and pass in the integer. The return value of this method call is a new Boolean set to "true" if the integer was non-zero and "false" otherwise. The Convert class creates the Boolean for you with the correct value. Other type conversion methods work similarly. The Parse methods on Int32 and Double return new instances of those objects set to the appropriate value given only a string.
This strategy for creating new object instances is known as a Factory pattern. Rather than invoking the object's constructor, you can ask the object factory to create the instance for you. That way, the factory class can hide the complexity of object creation (like how to parse a Double out of a string). If you wanted to change the details of creating the object, you'd only have to change the factory itself; you would not have to change every single place in the code where the constructor is called.
These type conversion methods are one variant of this pattern, since you're not required to use the factory to create the objects in question. A more pure example of this pattern is the System.Net.WebRequest class, used to make a request and receive a response from a resource on the Internet. FTP, HTTP, and file system requests are supported by default. To create a request, call the Create method and pass in a URI. The Create method itself determines the appropriate protocol for the request and returns the appropriate subclass of WebRequest: HttpWebRequest, FtpWebRequest (new to the .NET Framework 2.0), or FileWebRequest. The caller doesn't need to know the specifics of each protocol, only how to invoke the factory and work with the WebRequest that gets returned. If the URI changes from an HTTP address to an FTP address, the code won't have to change at all. This is another common use of the Factory pattern. The parent class acts as a factory and returns a specific derived class based on parameters the client passes in. As in the WebRequest example, this hides the complexity of selecting an appropriate derived class from the caller.
Strategy Pattern
Both Array and ArrayList provide the capability to sort the objects contained in the collection via the Sort method. In fact, ArrayList.Sort just calls Sort on the underlying array. These methods use the QuickSort algorithm. By default, the Sort method will use the IComparable implementation for each element to handle the comparisons necessary for sorting. Sometimes, though, it is useful to sort the same list in different ways. For example, arrays of strings might be sorted with or without case sensitivity. To accomplish this, an overload of Sort exists that takes an IComparer as a parameter; IComparer.Compare is then used for the comparisons. This overload allows users of the class to use any of the built-in IComparers or any of their own making, without having to change or even know the implementation details of Array, ArrayList, or the QuickSort algorithm.
Leaving the choice of comparison algorithm up to the user of the class like this is an example of the Strategy pattern. The use of Strategy lets a variety of different algorithms be used interchangeably. QuickSort itself only requires a way to compare objects to each other. By calling Compare through a provided interface, the caller is free to substitute whatever particular comparison algorithm fits its specific needs. The code for the QuickSort can remain unchanged.
Figure 5 Strategy in Action
One of the new generic collections in version 2.0 of the .NET Framework, List<T>, also makes heavy use of the Strategy pattern, shown in Figure 5. In addition to the updated Sort method, the find-related methods, BinarySearch, and others all take parameters that allow parts of the respective algorithms to vary based on the needs of the caller. The use of a Predicate<T> delegate in the FindAll<T> method lets the caller use any method as a filter for the List<T> so long as it takes the appropriate object type and returns a Boolean. Combined with anonymous methods (another new C# language feature in version 2.0), clients can easily filter lists based on properties and methods of the objects in the list, without introducing dependencies into the List<T> class itself. Using the Strategy pattern lets complicated processes like sorting be easily modified to fit a caller's specific purpose, meaning you'll be able to write and maintain less code.
Composite Pattern in ASP.NET
The ASP.NET request/response pipeline is a complex system. Patterns are used in the design of the pipeline itself and in the control architecture to effectively balance its performance with extensibility and ease of programming. Before delving into the pipeline, however, I'll examine the patterns used in the programming model itself.
When dealing with collections of objects, there are often operations that are appropriate for both a single object and the entire collection. Think about an ASP.NET control. A control may be a simple single item like a Literal, or it could be composed of a complex collection of child controls, like a DataGrid is. Regardless, calling the Render method on either of these controls should still perform the same intuitive function.
When each item in the collection might itself contain collections of other objects, the use of the Composite pattern is appropriate. Composite is an easy way to represent tree-like collections without having to treat parent and leaf nodes differently.
The canonical example of Composite relies on an abstract base class, Component, that contains both methods for adding and removing children, and the operations common among parents and children. ASP.NET uses this formulation exactly with System.Web.UI.Control. Control represents the Component base class. It has operations for dealing with children (such as the child Controls property) as well as standard operations and properties like Render and Visible. Each object, whether a primitive object (like Literal) or a composite of several objects (like DataGrid), inherits from this base class.
Because the domain of controls is so diverse, there are several intermediate derived classes like WebControl and BaseDataList that serve as base classes for other controls. Though these classes expose additional properties and methods, they still retain the child management functions and core operations inherited from Control. In fact, the use of the Composite pattern helps to hide their complexity, if desired. Regardless of whether a control is a Literal or a DataGrid, the use of Composite means you can just call Render and everything will sort itself out.
Template Method Pattern
When the standard library of ASP.NET controls doesn't meet your needs, you have several options on how to create your own. For simple controls that only need to be used in a single project, a user control is often the best choice. When the control is to be used in several Web applications or requires more functionality, a custom server control may be a better fit.
When dealing with custom controls, there are two general types: controls that combine the functionality of several existing controls (called composite controls), and controls with a unique visual representation. The process for creating both of these types is very similar. For composite controls, you create a new class that inherits from one of the control base classes (like Control or WebControl) and then override the CreateChildControls method. In this method you add the controls whose functionality you are combining to the collection of child controls, called Controls. For other custom controls, you override Render instead and use the HtmlTextWriter parameter to output the HTML for your control directly.
Regardless of which style of custom control you choose, you don't have to write any code to handle the functionality that's common to all controls, like loading and saving ViewState at the right time, allowing PostBack events to be handled, and making sure the control lifecycle events are raised in the correct order. The main algorithm for how a control should be loaded, rendered, and unloaded is contained in the control base class.
The specifics of your particular control are handled at well-defined places in the control algorithm (the CreateChildControls or Render methods). This is an example of the Template Method pattern. The main algorithm skeleton is defined in a base class and subclasses can then plug in their own details without affecting the algorithm itself, as shown in Figure 6. A composite control and a custom control both share the same general lifecycle, but they can end up with drastically different visual representations.
Figure 6 Template Method Pattern
This pattern is similar to the Strategy pattern. These two patterns differ in scope and in methodology. Strategy is used to allow callers to vary an entire algorithm, like how to compare two objects, while Template Method is used to vary steps in an algorithm. Because of this, Strategy is more coarsely grained. There can be vast differences between different client implementations, while with Template Method the algorithm remains fundamentally the same. The other main difference is that Strategy uses delegation while Template Method uses inheritance. In the sorting example of Strategy, the comparison algorithm is delegated to the IComparer parameter, but with custom controls you subclass the base and override methods to make changes. Both, however, let you easily alter processes to fit your specific needs.
Patterns in the ASP.NET Pipeline
When a client requests an ASPX page from the Web server, the request travels through many steps before ultimately ending up as HTML displayed by the client's browser. First, the request is processed by IIS and routed to the appropriate ISAPI extension. The ISAPI extension for ASP.NET (aspnet_isapi.dll) routes the request to the ASP.NET worker process.
Figure 7 ASP.NET Request Pipeline
At this point, the request begins to interact with classes that you are used to dealing with. The request is passed to an HttpApplication. Usually this is the class created in the codebehind file for Global.asax. The HttpApplication then passes the request through any number of HTTP Modules. These classes implement the IHttpModule interface and have a chance to modify the request (or even halt the processing of it) before it gets passed on to the next module. ASP.NET provides some standard modules which provide functionality you're probably familiar with, including FormsAuthenticationModule, PassportAuthenticationModule, WindowsAuthenticationModule, and SessionStateModule, all of which provide exactly the functionality that their names imply.
Ultimately, the request ends up at an IHttpHandler, the most common of which is System.Web.UI.Page. Inside the IHttpHandler.ProcessRequest method, Page raises appropriate events (like Init, Load, and Render), handles ViewState, and provides the programming model for ASP.NET. Figure 7 shows an outline of this process.
Several of the patterns employed by this process are more thoroughly documented in another standard reference for patterns, Martin Fowler's Patterns of Enterprise Application Architecture (Addison-Wesley, 2002).
Intercepting Filter Pattern
Once a request has made it into an HttpApplication, it passes through any number of IHttpModules. Each module is independent and has only a limited amount of control over the order in which it is invoked. The HttpApplication class exposes a sequence of events that get raised as the request makes its way through processing. These events include BeginRequest, AuthenticateRequest, AuthorizeRequest, and EndRequest. When the HttpApplication loads a module, it calls the Init method of the IHttpModule interface, allowing the module to register for any of the events it cares about. As a given request is handled, the events are raised in the appropriate order and all registered modules get a chance to interact with the request. The module can therefore control the stage at which it gets invoked, but not the exact order within that stage.
These modules are an example of the Intercepting Filter pattern. This pattern represents a chain of filters that are each in turn given a chance to modify a request (or message) that passes through them. Figure 8 shows a simplified flow diagram of this process. The key ideas of this pattern are that the filters are independent; filters can modify the request as it passes through.
Figure 8 Request Flow
There are several different implementation variations of the Intercepting Filter pattern, one of which is the event-based model employed by ASP.NET. A simpler variant involves maintaining a list of filters and iterating over it, invoking a method on each one in turn. This is how the Web Services Enhancements (WSE) for ASP.NET Web services uses this pattern. Each filter either extends SoapInputFilter (for request messages) or SoapOutputFilter (for responses), overriding the ProcessMessage method to perform the work of the filter.
Another option is to implement Intercepting Filter via the Decorator pattern. Then each filter would wrap its successor, performing preprocessing, invoking the successor, and then performing post-processing. The chain would be built using recursive composition, from back to front. This pattern is used to implement .NET Remoting channel sinks.
Regardless of implementation, the result is a dynamically configurable chain of independent filters. Since they are independent, these filters can easily be reordered and reused in other applications. Common tasks like authentication or logging can be encapsulated in a filter and used over and over. These tasks can then be handled by the filter chain before a request even reaches the HttpHandler, keeping the handler code cleaner.
Page Controller Pattern
System.Web.UI.Page implements a core part of the programming model for ASP.NET. Whenever you want to add a logical page to a Web application, you can create a Web Form (represented by an ASPX file and its codebehind). You can then write code to handle the specific demands of the new page, whether through handling page-level events, displaying a set of controls, or loading and manipulating data. Each logical page in the application has a corresponding Web Form that controls its behavior and regulates its presentation.
The practice of having one controller for each logical page is an example of the Page Controller pattern. This idea is fundamental to ASP.NET. When a logical page is requested via its URI, the ASP.NET runtime resolves the address to the corresponding subclass of Page and uses that class to handle the request. All the details about what a page looks like, what user input it can handle, and how it responds to that input are contained in a single place. When a logical page in the application requires changes, no other pages are affected. It's an abstraction that's so common we don't even think about it.
One of the drawbacks usually associated with the pure implementation of Page Controller is that common code must be repeated for each page. ASP.NET avoids this drawback by including other patterns in the pipeline implementation, as well as providing System.Web.UI.Page as a common base class for all Page Controllers. Cross-cutting concerns like authentication and session state are handled by the HttpModule Intercepting Filters and raising page lifecycle events, and other activities, are handled by the base class.
Other Web Presentation Patterns in ASP.NET
In addition to Intercepting Filter and Page Controller, ASP.NET makes use of variations of several other Web presentation patterns. When ASP.NET determines which HttpHandler to pass a request to, it uses something similar to a Front Controller. Front Controller is characterized by a single handler for all requests (like System.Web.UI.Page). Once the request reaches the Page class, though, the Page Controller pattern takes over.
Within the ASP.NET implementation of Page Controller, there are elements of the Model View Controller pattern. Model View Controller separates the model (business objects, data, and processes) from the view (the display of information). The controller responds to user input and updates the model and view. Roughly speaking, an ASPX page represents the View while its codebehind file represents a hybrid Model-Controller. If you were to pull all business and data-related code out of the codebehind and leave only event handling code, this would turn the codebehind into a pure Controller while the new class containing the business logic would be the Model.
Since these variations diverge from the canonical form of the patterns, they won't be covered here. Martin Fowler's book and the Microsoft Patterns site are good sources for more discussion.
Conclusion
Now that I have examined instances of common patterns in use throughout the .NET Framework and BCL, it should be even easier to recognize those same patterns in code you work with every day. Hopefully highlighting the design patterns underlying common classes and functionality has given you a better sense of what those patterns are and the benefits they provide. Trying to imagine UI programming without the Observer pattern or collections without an Iterator shows how indispensable these frameworks really are. Once you understand what each does, it becomes another valuable tool to add to your toolbox.