Click here to Skip to main content
15,878,959 members
Articles / Programming Languages / C#
Article

Illustrated GOF Design Patterns in C# Part II: Structural I

Rate me:
Please Sign up or sign in to vote.
4.66/5 (32 votes)
6 Nov 2002CPOL11 min read 339.6K   673   234   55
Part II of a series of articles illustrating GOF Design Patterns in C#

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 II of a series of articles illustrating the GOF Design Patterns in C#. We will discuss the Adapter, Bridge, Composite, and Decorator patterns. Part III of this series will cap off the rest of the Structural patterns not discussed in this article. 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 Structural design patterns, or how objects are composed.

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.

Structural Patterns

To quote the GOF, "Structural patterns are concerned with how classes and objects are composed to form larger structures. Structural class patterns use inheritance to compose interfaces or implementations. As a simple example, consider how multiple inheritance mixes two or more classes into one. The result is a class that combines the properties of its parent classes. This pattern is particularly useful for making independently developed class libraries work together."

Adapter

The Adapter structural design pattern is used to "Convert the interface of a class into another interface clients expect. Adapter lets classes work together that couldn't otherwise because of incompatible interfaces." (GOF) It's also known as a "Wrapper." There are two basic types of Adapters: class and object. The structure of a Class Adapter uses multiple inheritance to adapt one interface to another:

Class Adapter structure

C# only supports single inheritance of classes, but it does allow multiple inheritance of interfaces. This inheritance is always public, unlike C++, where you can inherit from classes and interfaces using differing access modifiers. In contrast, an Object Adapter relies on object composition:

Object Adapter structure

As an illustration (adapter.cs in the sample code) of a Class adapter, suppose we have an ICar interface:

C#
public interface ICar
{
    void Drive();
}

We create a concrete implementation in a class, CToyota. In a later project, we have a concrete CCessna class that needs to be adapted to be drivable:

C#
public class CCessna
{
    public void Fly()
    {
        Console.WriteLine("Static runup OK, " +
            "we're off in our C172...");
    }
}

To create a class Adapter for this, we would create a new class, CDrivableCessna, and inherit from both CCessna and ICar:

C#
public class CDrivableCessna : CCessna, ICar
{
    public void Drive() { base.Fly(); }
}

Using inheritance, we've made it possible to adapt a concrete CCessna to be used by clients the same way they'd use an ICar:

C#
ICar oCar = new CToyota();
Console.Write("Class Adapter:\nDriving an Automobile...");
oCar.Drive();

oCar = new CDrivableCessna();
Console.Write("Driving a Cessna...");

oCar.Drive();

As one can see, multiple inheritance [MI] is quite useful, but can lead to its own set of problems if not executed properly. The other solution is to create an object Adapter. Instead of using MI to make a drivable Cessna, we create a concrete CDrivableCessna2 which only inherits from ICar, and contains an instance of CCessna (the adaptee):

C#
public class CDrivableCessna2 : ICar
{
    private CCessna m_oContained;

    public CDrivableCessna2()
    {
        m_oContained = new CCessna();
    }

    public void Drive() { m_oContained.Fly(); }
}

The semantics for driving are exactly the same for the client (refer to the sample code,) however, I find that creating an Object Adapter is cleaner way to use the Adapter pattern because you are only exposing the expected interface to the client.

You'd use the Adapter pattern when (GOF):

  • you want to use an existing class, and its interface does not match the one you need.
  • you want to create a reusable class that cooperates with unrelated or unforeseen classes, that is, classes that don't necessarily have compatible interfaces.
  • (object adapter only) you need to use several existing subclasses, but it's impractical to adapt their interface by subclassing every one. An object adapter can adapt the interface of its parent class.

C# is quite useful for the Adapter Pattern. In fact, the example code shows creation of a pluggable adapter; that is, we use interface adoption, eliminating the assumption that other classes see the same interface. "Put another way, interface adaptation lets us incorporate our class into existing systems that might expect different interfaces to the class." (GOF) As you can see in the example code, we could swap out the class Adapter with the object Adapter and get the same results. C# was built for this!

Bridge

A Bridge pattern allows one to "Decouple an abstraction from its implementation so that the two can vary independently." (GOF) A prime example of this is the deferring of windowing operations in ATL (C++.) The Bridge pattern structure:

Bridge structure

An abstraction can be defined, which uses an underlying reference to an implementor. It may bit difficult to grasp at first, but the following example (bridge.cs) should help.

The act of flying airplanes differs from one type of airplane to the next. The underlying principles are the same to fly a single engine vs. a multiengine airplane, but the operations required to perform the flight may not be. A Bridge pattern allows us to implement flying an airplane for both single engine and multiengine aircraft.

We create an interface for the implementation of flying an airplane:

C#
public interface IFlyImpl
{
    void Fly();
}

We can then create different concrete classes that implement the interface:

C#
public class CSingleEngineFly : IFlyImpl
{
    public void Fly()
    {
        Console.WriteLine("SEL:  Mixture rich, throttle " +
            "smoothly to full...we're off!");
    }
}

public class CMultiEngineFly : IFlyImpl
{
    public void Fly()
    {
        Console.WriteLine("MEL:  Mixture rich, props high " +
            "RPM, throttles smoothly to full...we're off!");
    }
}

Our abstraction, CAirplane, will contain a reference to an object which implements IFlyImpl. This way, we can use either concrete implementation:

C#
public class CAirplane
{
    private IFlyImpl m_oFlyImpl;

    public IFlyImpl FlyImplementation
    {
        get { return m_oFlyImpl; }
        set { m_oFlyImpl = value; }
    }

    public CAirplane()
    {
        m_oFlyImpl = null;
    }

    public void FlyTheAirplane()
    {
        if(null == m_oFlyImpl)
            throw new Exception(
                "FlyImplementation is not set!");

        m_oFlyImpl.Fly();
    }
}

The client can then choose the implementation to use:

C#
CAirplane o = new CAirplane();

o.FlyImplementation = new CSingleEngineFly();
o.FlyTheAirplane();

o.FlyImplementation = new CMultiEngineFly();
o.FlyTheAirplane();

A Bridge pattern makes it very easy to create extensible software. You are not bound to one particular implementation of something that's abstracted, as in the case of the airplane in the example above. Switching implementations "on the fly" becomes trivial.

Composite

There is often a problem in software design where you need to create a hierarchy of objects and also need to treat each element of the hierarchy uniformly. The Composite structural design pattern accomplishes this be defining primitive and composite objects. Primitives can be composed into more complex systems, which can in turn be composed, and so on. The structure of the Composite pattern looks like this:

Composite structure

You would use the Composite pattern when (GOF):

  • you want to represent part-whole hierarchies of objects.
  • you want clients to be able to ignore the difference between compositions of objects and individual objects. Clients will treat all objects in the composite structure uniformly.

As an example (composite.cs), we're writing software for an airplane manufacturer, and want to be able to treat items on an equipment list equally. Some equipment is made up of other pieces of equipment. We begin the Composite pattern by defining a concrete equipment class CEquipment, and a concrete equipment composite CCompositeEquipment:

C#
public class CEquipment
{
    private string m_strName;
    private double m_yNetPrice;

    public CEquipment(string strName, double yNetPrice)
    {
        m_strName = strName;
        m_yNetPrice = yNetPrice;
    }

    public string Name { get { return m_strName; } }
    virtual public double NetPrice() { return m_yNetPrice; }
    virtual public IEnumerator GetEnumerator() { return null; }
}

public class CCompositeEquipment : CEquipment
{
    // m_yNetPrice is unused in this class
    private ArrayList m_aItems;

    public CCompositeEquipment(string strName) : base(strName, 0.0)
    {
        m_aItems = new ArrayList();
    }

    public void Add(CEquipment c) { m_aItems.Add(c); }

    override public double NetPrice()
    {
        double yTotal = 0.0;

        foreach (CEquipment c in m_aItems) 
            yTotal += c.NetPrice();

        return yTotal;
    }

    override public IEnumerator GetEnumerator()
    {
        return m_aItems.GetEnumerator();
    }
}

Notice that CCompositeEquipment derives from CEquipment. The GetEnumerator() method can be used to see if a CEquipment is a composite. The design decision could have been made to inherit from an IEquipment interface for this example, and it's a choice on what classes/interfaces implement what functionality whenever you create your Composite pattern in your design. We could, for example, derive both concrete classes from an IEquipment interface that supports Name, NetPrice, Add, and GetEnumerator. We would then just stub out Add for a single CEquipment vs. CCompositeEquipment where we would use the code above. We don't use interfaces here for simplicity.

Now it is just a matter of creating some derivative equipment classes from our base classes, and utilizing them in the client:

C#
// I wish these prices were true!
CEquipment oGPS = new CEquipment("Cheap GPS", 125.00);

CEquipment oComm = 
    new CEquipment("Communications Panel", 50.00);

CEquipment oTCASDisplay = 
    new CEquipment("TCAS Display", 200.00);

CEquipment oTCASSensors = 
    new CEquipment("TCAS Sensors", 195.00);

CCompositeEquipment oTCAS = 
    new CCompositeEquipment("TCAS Stack");

CCompositeEquipment oAvionicsStack = 
    new CCompositeEquipment("My Cool AVStack");

oTCAS.Add(oTCASDisplay);
oTCAS.Add(oTCASSensors);
oAvionicsStack.Add(oTCAS);
oAvionicsStack.Add(oGPS);
oAvionicsStack.Add(oComm);

Console.WriteLine("{0} Net Price ${1}", oAvionicsStack.Name,
    oAvionicsStack.NetPrice());

IEnumerator i = oAvionicsStack.GetEnumerator();

while(i.MoveNext())
{
    CEquipment cur = (CEquipment)i.Current;

    Console.WriteLine("  {0} - ${1}", cur.Name, cur.NetPrice());
}

The usefulness of the Composite pattern should be quite apparent.

Decorator

Sometimes you need to attach greater responsibility to an object dynamically. This is known as the Decorator structural design pattern. The GOF tells us to use the Decorator pattern to:

  • to add responsibilities to individual objects dynamically and transparently, that is, without affecting other objects.
  • for responsibilities that can be withdrawn.
  • when extension by subclassing is impractical. Sometimes a large number of independent extensions are possible and would produce an explosion of subclasses to support every combination. Or a class definition may be hidden or otherwise unavailable for subclassing.

The Decorator Structure:

Decorator structure

To illustrate using the pattern (decorator.cs), we'll use the following scenario: Visual Flight Rules (VFR) is where you fly an airplane by looking out the window. Instrument Flight Rules (IFR) is where you fly an airplane by referencing instruments, which is required if you fly into clouds, or where certain visibility restrictions exist. Sometimes you start VFR, but end up flying someplace where the weather deteriorates and you must then fly IFR (of course you must be rated and current to do so.)

We can encapsulate dynamically switching from VFR to IFR. We begin with defining VFR flight:

C#
public class CVFRFlight
{
    public void Fly()
    {
        Console.WriteLine("Look outside.  " + 
            "Control the aircraft.");
    }
}

Suppose we enter bad weather and have to switch to IFR. We're already flying VFR and we need to continue to do all the things we do VFR, but now with the added twist of IFR. We "decorate" our VFR flight with IFR rules. Using the Decorator pattern, we come up with the decorator class:

C#
public class CIFRDecorator
{
    CVFRFlight m_oVFR; // what we're decorating

    public CIFRDecorator(CVFRFlight o)
    {
        m_oVFR = o;
    }

    public void Fly()
    {
        m_oVFR.Fly();
        Console.WriteLine("Do the scan, cross check, " + 
            "start again.");
    }
}

It's worthwhile to note that we still perform the VFR action, we defer to the action of the decorated object. So, here it is in action:

C#
CVFRFlight oFlight = new CVFRFlight();

Console.WriteLine("VFR, here we go.");
oFlight.Fly();
Console.WriteLine("Encountering IMC!");

CIFRDecorator oDecorated = new CIFRDecorator(oFlight);
oDecorated.Fly();

Note that this was a flight example, you certainly can use the Decorator pattern in graphics programs to add features to objects on the fly, such as to highlight a window or add scroll bars. Also note that we did not use an abstract Decorator in this example, though it may be wise to do so. The abstract Decorator would just simply defer to the contained object instance for actions, whilst the concrete implementation of a decorator would defer then add its functionality:

C#
// CMyDecorator is derived from CDecorator
public void Action()
{
    base.Action(); // call base class decorator 
                   //(will defer to contained object)
    // more stuff
}

Conclusions

Structural design patterns allow for great flexibility in how your software's objects are composed:

Using the Adapter Pattern allows you to "recast" objects to another interface a client expects. There are, as with all patterns benefits and trade offs. Some things to be aware of:

  • Class adapters commit you to a concrete class. A class adapter will not work if we need to adapt a class and all its subclasses.
  • Object adapters make it harder to override the behavior of the adaptee
  • The amount of work in adapting may vary depending on how similar the operations are
  • The adapter may not be transparent to all clients

A Bridge pattern allows you to decouple an interface and its implementation; the implementation is not permanently bound to an interface. This the consequence that you don't have to recompile the abstraction class nor its clients to use a new implementation. You can also extend the abstraction and implementors independently.

A Composite pattern has many benefits:

  • It makes the client simple. Items in the hierarchy can be treated uniformly.
  • It's easy to add new kinds of components into the hierarchy.

Unfortunately the Composite pattern has some drawbacks:

  • Overgeneralized design.
  • Transparency may supersede safety. Clients may do meaningless things like adding or removing from a composite node that doesn't support it.

It would be wise to read Design Patterns to familiarize yourself with all the implications of the Composite pattern.

Using the Decorator pattern gives us more flexibility than just straight inheritance. You can do things at runtime simply by attaching and detaching them. It simplifies the system. It helps us to avoid classes that are heavy on features high up in our class hierarchy. Using this pattern also can mean you cannot rely on the decorator and component being decorated as identical, so you probably should not rely on object identity when using them. Furthermore, you may get lots of "little" objects that look all the same, causing some confusion for maintenance or future development.

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

2002-11-06 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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Chief Technology Officer
United States United States
20+ years as a strategist at the intersection of business, design and technology.

Comments and Discussions

 
GeneralRe: What I never understood... Pin
ian mariano20-Oct-04 4:33
ian mariano20-Oct-04 4:33 
GeneralRe: What I never understood... Pin
zitniet21-Oct-04 0:51
zitniet21-Oct-04 0:51 
GeneralRe: What I never understood... Pin
Marc Clifton21-Oct-04 12:06
mvaMarc Clifton21-Oct-04 12:06 
GeneralRe: What I never understood... Pin
ian mariano22-Oct-04 3:24
ian mariano22-Oct-04 3:24 
AnswerRe: What I never understood... Pin
Zijian29-Jan-07 18:49
Zijian29-Jan-07 18:49 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.