Design Principles and Patterns for Software Engineering with Microsoft .NET

  • 10/15/2008

Object-Oriented Design

Before object orientation (OO), any program resulted from the interaction of modules and routines. Programming was procedural, meaning that there was a main stream of code determining the various steps to be accomplished.

OO is a milestone in software design.

OO lets you envision a program as the result of interacting objects, each of which holds its own data and behavior. How would you design a graph of objects to represent your system? Which principles should inspire this design?

We can recognize a set of core principles for object-oriented design (OOD) and a set of more advanced and specific principles that descend from, and further specialize, the core principles.

Basic OOD Principles

To find a broadly accepted definition of OOD, we need to look at the Gang of Four (Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides) and their landmark book Design Patterns: Elements of Reusable Object-Oriented Software, (Addison-Wesley, 1994). (We’ll make further references to this book as GoF, which is the universal acronym for “Gang of Four.”)

The entire gist of OOD is contained in this sentence:

  • You must find pertinent objects, factor them into classes at the right granularity, define class interfaces and inheritance hierarchies, and establish key relationships among them.

In GoF, we also find another excerpt that is particularly significant:

  • Your design should be specific to the problem at hand but also general enough to address future problems and requirements.

Wouldn’t you agree that this last sentence is similar to some of the guidelines resulting from the ISO/IEC 9126 standard that we covered in Chapter 1. "Architects and Architecture Today"? Its obvious similarity to that standard cannot be denied, and it is not surprising at all.

The basics of OOD can be summarized in three points: find pertinent objects, favor low coupling, and favor code reuse.

Find Pertinent Objects First

The first key step in OOD is creating a crisp and flexible abstraction of the problem’s domain. To successfully do so, you should think about things instead of processes. You should focus on the whats instead of the hows. You should stop thinking about algorithms to focus mostly on interacting entities. Interacting entities are your pertinent objects.

Where do you find them?

Requirements offer the raw material that must be worked out and shaped into a hierarchy of pertinent objects. The descriptions of the use cases you receive from the team of analysts provide the foundation for the design of classes. Here’s a sample use case you might get from an analyst:

  • To view all orders placed by a customer, the user indicates the customer ID. The program displays an error message if the customer does not exist. If the customer exists, the program displays name, address, date of birth, and all outstanding orders. For each order, the program gets ID, date, and all order items.

A common practice for finding pertinent objects is tagging all nouns and verbs in the various use cases. Nouns originate classes or properties, whereas verbs indicate methods on classes. Our sample use case suggests the definition of classes such as User, Customer, Order, and OrderItem. The class Customer will have properties such as Name, Address, and DateOfBirth. Methods on the class Customer might be LoadOrderItems, GetCustomerByID, and LoadOrders.

Note that finding pertinent objects is only the first step. As recommended in the statement that many consider to be the emblem of OOD, you then have to factor pertinent objects into classes and determine the right level of granularity and assign responsibilities.

In doing so, two principles of OOD apply, and they are listed in the introduction of GoF.

Favor Low Coupling

In an OO design, objects need to interact and communicate. For this reason, each object exposes its own public interface for others to call. So suppose you have a logger object with a method Log that tracks any code activity to, say, a database. And suppose also that another object at some point needs to log something. Simply enough, the caller creates an instance of the logger and proceeds. Overall, it’s easy and effective. Here’s some code to illustrate the point:

class MyComponent
{
  void DoSomeWork()
  {
    // Get an instance of the logger
    Logger logger = new Logger();

    // Get data to log
    string data = GetData();

    // Log
     logger.Log(data);
  }
}

The class MyComponent is tightly coupled to the class Logger and its implementation. The class MyComponent is broken if Logger is broken and, more importantly, you can’t use another type of logger.

You get a real design benefit if you can separate the interface from the implementation.

What kind of functionality do you really need from such a logger component? You essentially need the ability to log; where and how is an implementation detail. So you might want to define an ILogger interface, as shown next, and extract it from the Logger class:

interface ILogger
{
    void Log(string data);
}

class Logger : ILogger
{
    .
    .
    .
}

At this point, you use an intermediate factory object to return the logger to be used within the component:

class MyComponent
{
  void DoSomeWork()
  {
    // Get an instance of the logger
    ILogger logger = Helpers.GetLogger();
    // Get data to log
    string data = GetData();

    // Log
    logger.Log(data);
  }
}

class Helpers
{
  public static ILogger GetLogger()
  {
    // Here, use any sophisticated logic you like
    // to determine the right logger to instantiate.

    ILogger logger = null;
    if (UseDatabaseLogger)
    {
        logger = new DatabaseLogger();
    }
    else
    {
        logger = new FileLogger();
    }
    return logger;
  }
}
class FileLogger : ILogger
{
    .
    .
    .
}

class DatabaseLogger : ILogger
{
    .
    .
    .
}

The factory code gets you an instance of the logger for the component to use. The factory returns an object that implements the ILogger interface, and the component consumes any object that implements the contracted interface.

The dependency between the component and the logger is now based on an interface rather than an implementation.

If you base class dependencies on interfaces, you minimize coupling between classes to the smallest possible set of functions—those defined in the interface. In doing so, you just applied the first principle of OOD as outlined in GoF:

  • Program to an interface, not an implementation.

This approach to design is highly recommended for using with the parts of your code that are most likely to undergo changes in their implementation.

Favor Code Reuse

Reusability is a fundamental aspect of the object-oriented paradigm and one of the keys to its success and wide adoption. You create a class one day, and you’re happy with that. Next, on another day, you inherit a new class, make some changes here and there, and come up with a slightly different version of the original class.

Is this what code reuse is all about? Well, there’s more to consider.

With class inheritance, the derived class doesn’t simply inherit the code of the parent class. It really inherits the context and, subsequently, it gains some visibility of the parent’s state. Is this a problem?

For one thing, a derived class that uses the context it inherits from the parent can be broken by future changes to the parent class.

In addition, when you inherit from a class, you enter into a polymorphic context, meaning that your derived class can be used in any scenarios where the parent is accepted. It’s not guaranteed, however, that the two classes can really be used interchangeably. What if the derived class includes changes that alter the parent’s context to the point of breaking the contract between the caller and its expected (base) class? (Providing the guarantee that parent and derived classes can be used interchangeably is the goal of Liskov’s principle, which we’ll discuss later.)

In GoF, the authors recognize two routes to reusability—white-box and black-box reusability. The former is based on class inheritance and lends itself to the objections we just mentioned. The latter is based on object composition.

Object composition entails creating a new type that holds an instance of the base type and typically references it through a private member:

public CompositeClass
{
  private MyClass theObject;

  public CompositeClass()
  {
    // You can use any lazy-loading policy you want for instantiation.
    // No lazy loading is being used here ...
    theObject = new MyClass();
  }

  public object DoWork()
  {
    object data = theObject.DoSomeWork();

    // Do some other work
    return Process(data);
  }
  private object Process(object data)
  {
    .
    .
    .
  }
}

In this case, you have a wrapper class that uses a type as a black box and does so through a well-defined contract. The wrapper class has no access to internal members and cannot change the behavior in any way—it uses the object as it is rather than changing it to do its will. External calls reach the wrapper class, and the wrapper class delegates the call internally to the held instance of the class it enhances. (See Figure 3-2.)

Figure 3-2

Figure 3-2. Object composition and delegation

When you create such a wrapper object, you basically apply the second principle of OOD:

  • Favor object composition over class inheritance.

Does all this mean that classic class inheritance is entirely wrong and should be avoided like the plague? Using class inheritance is generally fine when all you do is add new functions to the base class or when you entirely unplug and replace an existing functionality. However, you should never lose track of the Liskov principle. (We’ll get to the details of the Liskov principle in a moment.)

In many cases, and especially in real-world scenarios, object composition is a safer practice that also simplifies maintenance and testing. With composition, changes to the composite object don’t affect the internal object. Likewise, changes to the internal object don’t affect the outermost container as long as there are no changes to the public interface.

By combining the two principles of OOD, you can refer to the original object through an interface, thus further limiting the dependency between composite and internal objects. Composition doesn’t provide polymorphism even if it will provide functionality. If polymorphism is key for you, you should opt for a white-box form of reusability. However, keep the Liskov principle clearly in mind.

Advanced Principles

You cannot go to a potential customer and sing the praises of your software by mentioning that it is modular, well designed, and easy to read and maintain. These are internal characteristics of the software that do not affect the user in any way. More likely, you’ll say that your software is correct, bug free, fast, easy to use, and perhaps extensible. However, you can hardly write correct, bug-free, easy-to-use, and extensible software without paying a lot of attention to the internal design.

Basic principles such as low coupling, high cohesion (along with the single responsibility principle), separation of concerns, plus the first two principles of OOD give us enough guidance about how to design a software application. As you might have noticed, all these principles are rather old (but certainly not outdated), as they were devised and formulated at least 15 years ago.

In more recent years, some of these principles have been further refined and enhanced to address more specific aspects of the design. We like to list three more advanced design principles that, if properly applied, will certainly make your code easier to read, test, extend, and maintain.

The Open/Closed Principle

We owe the Open/Closed Principle (OCP) to Bertrand Meyer. The principle addresses the need of creating software entities (whether classes, modules, or functions) that can happily survive changes. In the current version of the fictional product “This World,” the continuous changes to software requirements are a well-known bug. Unfortunately, although the team is working to eliminate the bug in the next release, we still have to face reality and deal with frequent changes of requirements the best we can.

Essentially, we need to have a mechanism that allows us to enter changes where required without breaking existing code that works. The OCP addresses exactly this issue by saying the following:

  • A module should be open for extension but closed for modification.

Applied to OOD, the principle recommends that we never edit the source code of a class that works in order to implement a change. In other words, each class should be conceived to be stable and immutable and never face change—the class is closed for modification.

How can we enter changes, then?

Every time a change is required, you enhance the behavior of the class by adding new code and never touching the old code that works. In practical terms, this means either using composition or perhaps safe-and-clean class inheritance. Note that OCP just reinforces the point that we made earlier about the second principle of OOD: if you use class inheritance, you add only new code and do not modify any part of the inherited context.

Today, the most common way to comply with the OCP is by implementing a fixed interface in any classes that we figure are subject to changes. Callers will then work against the interface as in the first principle of OOD. The interface is then closed for modification. But you can make callers interact with any class that, at a minimum, implements that interface. So the overall model is open for extension, but it still provides a fixed interface to dependent objects.

Liskov’s Substitution Principle

When a new class is derived from an existing one, the derived class can be used in any place where the parent class is accepted. This is polymorphism, isn’t it? Well, the Liskov Substitution Principle (LSP) restates that this is the way you should design your code. The principle says the following:

  • Subclasses should be substitutable for their base classes.

Apparently, you get this free of charge from just using an object-oriented language. If you think so, have a look at the next example:

public class ProgrammerToy
{
    private int _state = 0;

    public virtual void SetState(int state)
    {
        _state = state;
    }

    public int GetState()
    {
       return _state;
    }
}

The class ProgrammerToy just acts as a wrapper for an integer value that callers can read and write through a pair of public methods. Here’s a typical code snippet that shows how to use it:

static void DoSomeWork(ProgrammerToy toy)
{
    int magicNumber = 5;
    toy.SetState(magicNumber);
    Console.WriteLine(toy.GetState());
    Console.ReadLine();
}

The caller receives an instance of the ProgrammerToy class, does some work with it, and then displays any results. So far, so good. Let’s now consider a derived class:

public class CustomProgrammerToy : ProgrammerToy
{
    public override void SetState(int state)
    {
        // It inherits the context of the parent but lacks the tools
        // to fully access it. In particular, it has no way to access
        // the private member _state.
        // As a result, this class MAY NOT be able to
        // honor the contract of its parent class. Whether or not, mostly
        // depends on your intentions and expected goals for the overridden
        // SetState method. In any case, you CAN'T access directly the private member
        // _state from within this override of SetState.

        // (In .NET, you can use reflection to access a private member,
        // but that's a sort of a trick.)
        .
        .
        .
     }
}

From a syntax point of view, ProgrammerToy and CustomProgrammerToy are just the same and method DoSomeWork will accept both and successfully compile.

From a behavior point of view, though, they are quite different. In fact, when CustomProgrammerToy is used, the output is 0 instead of 5. This is because of the override made on the SetState method.

This is purely an example, but it calls your attention to Liskov’s Principle. It doesn’t go without saying that derived classes (subclasses) can safely replace their base classes. You have to ensure that. How?

You should handle keywords such as sealed and virtual with extreme care. Virtual (overridable) methods, for example, should never gain access to private members. Access to private members can’t be replicated by overrides, which makes base and derived classes not semantically equivalent from the perspective of a caller. You should plan ahead of time which members are private and which are protected. Members consumed by virtual methods must be protected, not private.

Generally, virtual methods of a derived class should work out of the same preconditions of corresponding parent methods. They also must guarantee at least the same postconditions.

Classes that fail to comply with LSP don’t just break polymorphism but also induce violations of OCP on callers.

The Dependency Inversion Principle

When you create the code for a class, you represent a behavior through a set of methods. Each method is expected to perform a number of actions. As you specify these actions, you proceed in a top-down way, going from high-level abstractions down the stack to more and more precise and specific functions.

As an illustration, imagine a class, perhaps encapsulated in a service, that is expected to return stock quotes as a chunk of HTML markup:

public class FinanceInfoService
{
  public string GetQuotesAsHtml(string symbols)
  {
    // Get the Finder component
    IFinder finder = ResolveFinder();
    if (finder == null)
      throw new NullReferenceException("Invalid finder.");

    // Grab raw data
    StockInfo[] stocks = finder.FindQuoteInfo(symbols);

    // Get the Renderer component
    IRenderer renderer = ResolveRenderer();
    if (renderer == null)
       throw new NullReferenceException("Invalid renderer.");

    // Render raw data out to HTML
    return renderer.RenderQuoteInfo(stocks);
  }

  .
  .
  .
}

The method GetQuotesAsHtml is expected to first grab raw data and then massage it into an HTML string. You recognize two functionalities in the method: the finder and the renderer. In a top-down approach, you are interested in recognizing these functionalities, but you don’t need to specify details for these components in the first place. All that you need to do is hide details behind a stable interface.

The method GetQuotesAsHtml works regardless of the implementation of the finder and renderer components and is not dependent on them. (See Figure 3-3.) On the other hand, your purpose is to reuse the high-level module, not low-level components.

Figure 3-3

Figure 3-3. Lower layers are represented by an interface

When you get to this, you’re in full compliance with the Dependency Inversion Principle (DIP), which states the following:

  • High-level modules should not depend upon low-level modules. Both should depend upon abstractions. Abstractions should not depend upon details. Details should depend upon abstractions.

The inversion in the name of the principle refers to the fact that you proceed in a top-down manner during the implementation and focus on the work flow in high-level modules rather than focusing on the implementation of lower level modules. At this point, lower level modules can be injected directly into the high-level module. Here’s an alternative implementation for a DIP-based module:

public class FinanceInfoService
{
  // Inject dependencies through the constructor. References to such external components
  // are resolved outside this module, for example by using an inversion-of-control
  // framework (more later).
  IFinder _finder = null;
  IRenderer _renderer = null;

  public FinanceInfoService(IFinder finder, IRenderer renderer)
  {
    _finder = finder;
    _renderer = renderer;
  }

  public string GetQuotesAsHtml(string symbols)
  {
    // Get the Finder component
    if (_finder == null)
      throw new NullReferenceException("Invalid finder.");
    // Grab raw data
    StockInfo[] stocks = _finder.FindQuoteInfo(symbols);

    // Get the Renderer component
    if (_renderer == null)
      throw new NullReferenceException("Invalid renderer.");

    // Render raw data out to HTML
    return _renderer.RenderQuoteInfo(stocks);
  }

  .
  .
  .
}

In this case, the lower level modules are injected through the constructor of the DIP-based class.

The DIP has been formalized by Robert Martin. You can read more about it at http://www.objectmentor.com/resources/articles/dip.pdf.