s any seasoned object-oriented software developer knows, it is unthinkable to discuss software design and architecture without at least a rudimentary understanding of design patterns. Most, if not all, software applications, tools, and systems incorporate one or more design patterns. A design pattern is a description of a set of interacting classes that provide a framework for a solution to a generalized problem in a specific context or environment. In other words, a pattern suggests a solution to a particular problem or issue in object-oriented software development. Additionally, patterns take into account design constraints and other factors that limit their applicability to the solution in general. Together, the classes, the communication and interconnections among those classes, and the contextual specifics define a pattern that provides a solution to any problem in object-oriented software design that presents characteristics and requirements matching those addressed by the pattern context.
I must confess that I am an enthusiastic proponent of design patterns. Ever since I read the seminal book Design Patterns by Gamma, Helm, Johnson, and Vlissides (Addison Wesley, 1995), I have rarely designed a feature that does not use any patterns. In fact, I spend considerable time in the early stages of software design to identify patterns that would fit naturally in the feature architecture. After all, patterns are time-tested and field-tested solutions to problems that have been addressed by experienced architects, developers, and language specialists, and it behooves anyone designing software to make use of the available knowledge and expertise in this discipline. It is almost always a better idea to go with a solution that has been proven successful time and again than to invent one completely from scratch.
Few developers have the luxury of writing only small programs. Modern software applications and systems are complex, comprising hundreds of thousands of lines of code, and I know of code bases that are even larger. Programming demands a lot more than simple mastery of tools and languages—corporate software development typically requires a great deal of flexibility in design and architecture to accommodate the ever-changing needs of clients and users at various stages of product development, and often after the product has been released. Such dynamics dictate that software design not be brittle. It should be able to accept changes without any undesirable ripple effect that would necessitate the reworking of other, potentially unrelated, subsystems. It is frustrating and counterproductive to add features and components to modules that were never designed for extensibility. Sooner or later, closed, inflexible designs break under the weight of changes. Design patterns assist in laying the foundation for a flexible architecture, which is the hallmark of every good object-oriented design.
Design patterns have been cataloged to address a variety of design problems, from small issues to large, architecture-level problems. In this article, I will describe some of the popular design patterns that I have found useful in my own projects. The article does not assume any prior knowledge of design patterns, although familiarity with concepts of object-oriented design will help. While any programming language that facilitates object-oriented development could be used to illustrate patterns, I will present examples exclusively in C#, exposing some of the strengths of the language along the way. I will not discuss any Microsoft® .NET-specific classes or libraries—instead, I'll focus on C# as a vehicle for designing object-oriented software.
C# and Design Patterns
C# is a modern programming language that promotes object-oriented software development by offering syntactic constructs and semantic support for concepts that map directly to notions in object-oriented design. This is in contrast to C++, which supports procedural as well as object-oriented (and generic) programming. Nonetheless, if you are a C++ programmer, getting up to speed with C# should be a snap—the learning curve for C++ programmers is flat. Even if you haven't seen any C# code before, you should have no problem comprehending the example code in this article. In fact, I wouldn't be surprised if you find the C# implementation of the design patterns cleaner, especially if you have used or coded the patterns before. Books and articles that discuss design patterns typically explain the problem and the context in great detail, followed by a formal description of the solution. I will take a less rigorous approach in this article, focusing on the essence of the pattern instead, and illustrating an appropriate example with some sample code in C#.
Let's start with the simplest design pattern: Singleton.
Singleton
Anyone who has ever written an MFC application—no matter how small—knows what a singleton is. A singleton is the sole instance of some class. To use an MFC analogy, the global instance of the CWinApp-derived application class is the singleton. Of course, while it's imperative that no additional instances of the application class be created, there really is nothing preventing you from creating additional instances. In situations like these, when you need to enforce singleton behavior for a specific class, a better alternative is to make the class itself responsible for ensuring that one and only one instance of the class can be created. Back in the MFC analogy, you see that the responsibility for keeping track of the solitary instance of the application class rests with the developers of the application. They must not inadvertently instantiate another application object.
Now consider the class shown in Figure 1. Note how access to the singleton is controlled via the static method Instance. It is most often the case that the singleton should also be globally accessible, and this is achieved by making the creation method public. However, unlike the scenario in which a global variable is instantiated as the singleton, this pattern prevents creation of any additional instances, while simultaneously allowing global access. Note that the class constructor is private—there is no way to circumvent the static method and directly create an instance of the class.
There are additional benefits, too. Specifically, this pattern can be extended to accommodate a variable number of instances of an object. For instance, let's say you have an application with a dedicated worker thread that is dispatched whenever a particular task is required. In the interest of conserving system resources, you have implemented the thread as a singleton. At some point along the way, if you decide to scale up your application because the rate at which tasks arrive is too much for your singleton thread to handle, it will be fairly straightforward to increase the number of worker threads in the application because all the logic that creates the threads and grants access to them is confined to one class.
One other advantage to this pattern is that creation of the singleton can be delayed until it is actually needed, as shown in Figure 1. A variable declared at global scope will be created on startup regardless of whether it is needed—it may very well be that the object isn't always needed. C# doesn't allow variables at global scope anyway, but it is possible to create an object on the heap at the outset of a method and not use it until much later, if at all. The Singleton pattern offers an elegant solution in such cases.
Additionally, as an implementation vehicle, C# is superior to C++ for this design pattern in a subtle but important way. A C++-based implementation has to take into account some sticky issues related to lifetime management that are automatically taken care of by the C# runtime. This is a significant benefit, as all you need to do in the C# version is make sure you have a live reference to the singleton object for as long as it's needed.
Strategy
Applications are often written so that the way they perform a particular task varies, depending on user input, the platform it's running on, the environment it's been deployed in, and so on. An example is asynchronous I/O on disk files: Win32® APIs under Windows NT® and Windows® 2000 support asynchronous I/O natively. However, that's not the case with Windows 95 or Windows 98. An application that relies on asynchronous file I/O, therefore, has to execute two different algorithms, depending on the deployment platform—one that uses native Win32 APIs, and another that is built from scratch, perhaps using multiple threads. Clients of such a service will be oblivious to the fact that different algorithms are being executed; as far as they are concerned, the end result is the same and that's all they care about.
Another example is downloading a file from some remote server on the Internet. An application that offers a file download service that accepts a URL as input needs to examine the URL, identify the protocol (FTP or HTTP, for example), and then create an object that can communicate with the remote server using that protocol. Note that depending on user input, a different algorithm (protocol) will be used. However, again, the end result is the same—a file is downloaded.
Let's consider a more concrete example: testing for primality. The following code declares an interface, a C# construct, with just one method: IsPrime.
interface Strategy
{
bool IsPrime(int n);
}
An interface is like a contract. It is a specification that inheriting classes must follow. More specifically, it defines method signatures but no implementations—the latter must be provided by the concrete classes that implement the interface. C# is clearly superior to C++ in this regard because C++ lacks native language support for interfaces. C++ programmers typically create interfaces by defining abstract classes with pure virtual methods. In C#, all interface members are public, and classes adhering to an interface must implement all methods in the interface.
Now let's assume that I have three different algorithms for primality testing, each with its own performance/accuracy trade-off. One of them is very computation-intensive, but does a more thorough job of checking for factors, while another is faster but generates results that may be inaccurate for very large numbers. My application will ask the user for the desired performance and then invoke the appropriate algorithm. To this end, I will encapsulate my algorithms in classes that implement the Strategy interface. Here's an example.
class Fermat : Strategy
{
public bool IsPrime(int n)
{
bool result = false;
// use Fermat's Test to determine
// if n is prime; update �result'
Console.WriteLine("Using Fermat's test");
return result;
}
}
Having implemented all three algorithms in this manner, I can now design the client in a manner that decouples it from the implementation details of any specific algorithm. The client holds a reference to the interface, and does not need to know anything about the concrete implementation of the interface.
class Primality
{
private Strategy strategy;
public Primality(Strategy s)
{
strategy = s;
}
public bool Test(int n)
{
return strategy.IsPrime(n);
}
}
Finally, I create an instance of the Primality class and, depending on user input, initialize it with the appropriate algorithm. The Test method of the Primality class invokes the IsPrime method of the Strategy interface that every algorithm implements.
There are a number of advantages to structuring a family of algorithms in this manner, but the most important is that doing so decouples the client from the implementation details of any particular algorithm. This promotes extensibility in that additional algorithms can be developed and plugged in seamlessly, as long as they follow the base interface specification, thereby allowing algorithms to vary dynamically. Moreover, the Strategy pattern eliminates conditional statements that would otherwise litter client code.
Decorator
A client application often needs to augment the services provided by methods of some class, perhaps by inserting some preprocessing and post-processing tasks before and after the method calls, respectively. One way to accomplish this is to bracket each method invocation with calls to functions that achieve the desired effect. However, this approach is not only cumbersome, it also limits the framework's extensibility. For instance, if distinct pre- and post-processing tasks were to be carried out for different clients, the application logic would be obscured by conditional statements, leading to a maintenance nightmare. The question, then, is how to enhance the functionality offered by a class in a manner that does not cause repercussions in client code. The Decorator pattern is just what's needed.
Consider a class with methods that clients can invoke to transfer files to and from some remote server. Such a class might look like the one in Figure 2.
Next, let's consider a client interested in this functionality. In addition to being able to upload and download files, the client application would also like to log all file transfer requests and perform access checks for each invocation. An implementation based on the Decorator pattern would derive a class from FileTransfer and override the virtual methods, inserting the additional operations before and after calling the base methods (see Figure 3).
The client continues to work with the same interface. In fact, the solution can be improved if the FileTransfer class, as well as the Decorator class, implement a common interface with the Upload and Download methods. Doing so will allow the client to work exclusively in terms of the interface and decouple it completely from a concrete implementation.
The Decorator pattern thus allows dynamic and transparent addition and removal of responsibilities without affecting client code. It is particularly useful when a range of extensions or responsibilities can be applied to existing classes, and when defining subclasses to accommodate all those extensions is impractical.
Composite
The Composite pattern is useful when individual objects as well as aggregates of those objects are to be treated uniformly. An everyday example is enumerating the contents of a file folder. A folder may contain not only files, but subfolders as well. An application designed to recursively display the list of all files in some top-level folder can use a conditional statement to distinguish between files and directories, and traverse down the directory tree to display the names of files in the subfolders. A better approach is suggested by the Composite pattern. In this approach, every folder item, be it a file, a subfolder, a network printer, or a moniker for any directory element, is an instance of a class that conforms to an interface offering a method to display a user-friendly name of the element. In this case, the client application does not have to treat each element differently, thereby reducing the complexity of the application logic.
Another example, and one that I will present in C#, is a drawing application that pulls graphics primitives and blocks from an object database and paints them on a canvas. Let's assume that the database can contain lines, circles, and drawings made up of these primitives. Now consider the following interface.
interface Shape
{
void Draw();
}
The Shape interface contains one method, Draw. A simple graphics object such as a line can implement this interface and override the Draw method to paint a line on some canvas, as you can see in Figure 4.
The circle primitive can similarly override the Draw method in the Shape interface and produce the desired shape on the canvas. In order to treat aggregates as well as simple entities uniformly, however, objects of aggregates should also implement the Shape interface. A drawing is a collection of graphics objects, and its implementation of the Draw method will enumerate all primitives it contains and draw each one of them. Figure 5 shows how it works.
Note that the client couldn't care less about whether an object is an instance of a graphics primitive or a collection of such entities. A common interface across individual elements and aggregates streamlines processing and allows you to add new objects without triggering any modifications in the client application.
By presenting a shared interface across all components, the Composite pattern promotes code reuse and simplifies client logic considerably by removing the burden of having to make a distinction between individual elements and container objects.
Note that the implementation of Drawing.Draw uses the collection classes available in the System.Collections library. For more information on these and other libraries, check out the documentation in the .NET Framework SDK.
State
Every developer has implemented a finite state machine at least once in his or her career. You can't avoid them—they are everywhere, and not just limited to the world of software development. It's no wonder that literature on the design and implementation of deterministic finite automata is also readily available. Given the ubiquity of finite state machines, I am often surprised to see poor designs and buggy implementations with hardwired transitions and total disregard for extensibility. The ability to add more states to the design of a finite automaton is often an unwritten requirement, and implementations are frequently modified when requests for additional states and transitions arrive. If you've got a good design, you can anticipate and account for such changes. More importantly, behavior and operations specific to any state in a finite state machine should be confined and localized to the representation of that state. In other words, state-specific code lives in an object that implements that state. This, in turn, allows you to add new states and transitions easily.
A popular design for finite state machines is based on table lookup. A table maps all possible inputs for each state to transitions that would lead the machine to perhaps a different state. Needless to say, while this design is simpler, it is unable to accommodate changes without significant modifications to the existing implementation. A better alternative is the solution offered by the State design pattern.
Consider a software implementation of a soda vending machine that accepts nickels, dimes, and quarters only, and dispenses a can when the deposited amount reaches or exceeds 25 cents. With each coin that is inserted into the slot, the vending machine transitions to a different state, until the credit reaches the requisite amount, at which point the machine delivers the can of soda and resets to the start state. Figure 6 defines an abstract class to represent the base of all states that the vending machine can reach.
All five states will be subclasses of this base class and override its methods appropriately. For instance, when the vending machine is in the Start state and a nickel is inserted, the machine moves to the Five state. If another nickel is inserted, it moves to the Ten state. This isolates the transition logic specific to each state in the corresponding object. The implementation of two such states is shown in Figure 7.
The vending machine does not keep track of the transition logic—it forwards the operation to the instance of its current state, hence decoupling itself from state-specific behavior, as you can see in Figure 8.
I have already described how such a design is superior to a simple, table-based implementation. To summarize, the State design pattern helps localize state-specific behavior to classes that implement concrete states, which promotes reuse and extensibility. This removes the need for conditional statements that would otherwise be scattered throughout the code, making life difficult for maintenance programmers, who vastly outnumber implementors in the real world.
Conclusion
Design patterns distill years of experience in solutions to frequently encountered problems in the design of object-oriented software. They offer answers to questions that most software developers face regardless of the size or scope of the project. C# promises to enhance programmer productivity with its features that promote object-oriented design, while removing the onus of certain housekeeping chores from the developer. Together, they make a winning combination.
Complete C# code listings for examples in this article are available at the link at the top of this article.
|