Skip to main content

Agile Software Development: Principles, Patterns,and Practices - The Abstract Server Pattern

June 11, 2004

{cs.r.title}









Contents
Design Patterns 101
Violating SOLID Principles
Insulating the Button from the
Light with Abstract Server
Static Vs. Dynamic Typing
A Look Back
References

Our first foray into the world of design patterns should involve a very simple pattern. This will allow us to talk about design patterns in general, rather than having to focus upon the intricacies of a particular pattern. Abstract Server is one of the simplest of the design patterns -- so it's a good place for us to start.

Design Patterns 101

Design patterns are common solutions to common problems. They are common solutions because in order to qualify as a pattern, they must have been used in at least three unrelated projects [Ref 1]. They are common problems because there's little point in creating common solutions to uncommon problems.

Not only are design patterns solutions to problems, but they also have a particular context in which they fit. The context describes the constraints upon the designer and the system. For example, some design patterns want you to change certain classes, others only add new classes. To change a class, you have to have the source code. Clearly, if you don't have the source code for the class that the pattern wants you to change, you can't use the pattern. Thus, a design pattern is a solution to a problem in a context.

One other thing that a design pattern has is a name. The name allows us to discuss the pattern with other developers. It provides a new vocabulary that allows a team of developers to talk about whether a problem could be solved with one pattern or another. Indeed, giving standard names to these common solutions is probably the single most important aspect of design patterns.

Most designers find the study of design patterns to be something of an anti-climax. If they are mature designers, they react to many of the patterns with familiarity and recognition. They've used those solutions before. They figured them out for themselves. They just didn't have a name for them. If you have that reaction, that's pretty normal, but don't let it convince you that the patterns aren't important; they are. By providing standard names, the patterns have increased our vocabulary and allow us to discuss our designs at a higher level of abstraction.

Thus a design pattern is a named solution to a problem in a context. [Ref 2]

Violating SOLID Principles

When we discuss design patterns, we always start with a problem. That problem is usually a bit of code that we don't like for some reason. In the case of the Abstract Server, the code we don't like is shown below.

We begin with a test case, just to ensure that there is no confusion. English is a fine language, but it cannot compare to code for precision.

public void testButtonControlsLight() throws Exception {
    Light l = new Light();
    Button b = new Button(l);
    b.press();
    assertTrue(l.isOn());
}

We have a class named Light, and another named Button. When you press the button, the light turns on. The code for Button and Light follows. As you read it, ask yourself what structural problems this code has. Don't be concerned with its trivial nature. Instead, think about what bad design decisions have been made while constructing it. Once you have an idea, read on.

public class Button {
  private Light light;

  public Button(Light light) {
    this.light = light;
  }

  public void press() {
    light.turnOn();
  }
}


public class Light {
  private boolean lightOn;

  public boolean isOn() {
    return lightOn;
  }

  public void turnOn() {
    lightOn = true;
  }
}

This code passes the testButtonControlsLight test case, but the design -- trivial as it is -- has a significant flaw. The Button class mentions the name of the Light class.

When one class mentions the name of another, it's known as a dependency. Button depends upon Light. This means that changes to Light can force Button to be recompiled and/or redeployed. It also means that if you wanted to use Button to control something other than a light -- such as a fan -- you would have to make changes to Button.

The current design violates one of the SOLID [Ref 3] design principles: the Dependency Inversion Principle (DIP). This principle recommends that we not depend on concrete volatile classes. A concrete class is a class that has no abstract methods -- all methods are fully implemented. A volatile class is a class that is likely to be changed or derived from; i.e., it is a part of ongoing active development. We don't want to depend upon volatile concrete classes, because they are the classes that are most likely to change and have significant impact upon their clients.

Also, clients that depend upon concrete classes are not easily extended. This violates another of the SOLID principles: the Open-Closed Principle (OCP), which says that classes should be easy to extend without having to modify them. Systems that violate the SOLID principles tend to be rigid, fragile, and non-reusable -- they are tightly coupled. On the other hand systems that conform to the SOLID principles, by not depending upon volatile concrete classes, tend to be flexible, robust, and reusable -- they are loosely coupled.

Insulating the Button from the Light with Abstract Server

In our example above, Button depends upon the concrete class Light. This makes Button hard to extend (violating OCP) and makes it vulnerable to changes in Light (violating DIP). The diagram below shows the structure of the system.

Figure 1

Remember that a design pattern is a named solution to a problem in a context. The problems we face here are the inability to extend Button and the vulnerability of Button to changes in Light. We can resolve these problems by employing a pattern known as Abstract Server. The structure of this pattern is shown in the following diagram.

Figure 2

By inserting an interface between Button and Light, we have insulated Button from changes to Light and provided a way to extend Button to use devices other than Light. The interface resolves the problems, and helps us conform to the OCP and DIP.
The code for this solution follows.

public interface Switchable {
  void turnOn();
}


public class Light implements Switchable {
  private boolean lightOn;

  public boolean isOn() {
    return lightOn;
  }

  public void turnOn() {
    lightOn = true;
  }
}


public class Button {
  private Switchable device;

  public Button(Switchable device) {
    this.device = device;
  }

  public void press() {
    device.turnOn();
  }
}

No change was made to the test case, and it still passes as before. Notice that Button no longer mentions Light. The dependency has been broken. We can now change the Light class all we like, and it will not force us to recompile or redeploy the Button class. Moreover, we can create new derivatives of Switchable, such as Fan, thereby extending the behavior of Button to new devices without having to change Button.

Why did we call the interface Switchable? There was a time, in the early days of OO, when we might have called it AbstractLight. In those days, we thought that the classes within inheritance hierarchies belonged together. [Ref 4] We might have packaged Light and AbstractLight together in the same .jar file (if there had been .jar files back then). Over the years, however, we have learned that it is more appropriate to package Button and Switchable together, separately from Light. This allows us to ship the Button .jar file with the Fan .jar file, without having to include Light.

Static Vs. Dynamic Typing

This brings up an interesting point. By adding an inheritance relationship, we made the call to turnOn() polymorphic. This made the system much more flexible. Ironically, the inheritance relationship is the tightest-coupled relationship in Java. This is more than just irony. There is a mismatch of language facilities here. The tool that gives us flexibility also gives us rigidity. To loosen coupling on one axis of the design, we must tighten it on another.

This is an unfortunate byproduct of compile-time type checking (i.e., static typing). Java is a statically typed language, meaning that it checks types at compile time. In statically typed languages such as Java, polymorphic relationships are created through inheritance. This is because the compiler needs to have some kind of declaration for the methods involved in the polymorphism. In our case, the compiler noted that Button called a method named turnOn(), and it needed to see some kind of declaration for that method. We gave it that declaration by creating the interface Switchable, and then deriving Light from it.

There are other languages, such as Ruby, Python, and Smalltalk, that use a different typing strategy called dynamic typing. These languages do not depend upon inheritance to enable polymorphism. Type checking is done at run time, not compile time, so the compiler does not need to see any declarations. In such a language, the Button object would simply send the turnOn() message to some other object. That other object would receive the message and respond to it if it could. Any object that implemented a turnOn() method could be used by Button, regardless of its base classes.

In dynamically typed languages, the Abstract Server pattern is unnecessary. All relationships between objects automatically conform to the DIP and have a good chance of also conforming to the OCP.

If this is so, then why has the industry made such a strong commitment to statically typed languages? Java, C++, C#, Eiffel, and Delphi are all statically typed. If dynamically typed languages are so flexible, why haven't they been uniformly adopted?

Simply, because we feel better about compile-time type checking than run-time type checking. If there is a type error we want to know about it before the program runs, not after. We don't want our customers informing us of type errors, by reporting crashes.

This rationale is a good one, and it has held sway for nearly two decades. However, there is a change in the wind. Test Driven Development (TDD) -- the act of writing unit tests and acceptance tests before writing the code that passes them -- is becoming ever more popular. Developers are finding that they get their programs written faster and with fewer bugs by following the TDD discipline. They are also finding that the tests they write find the majority of type errors, long before the system deploys. This makes static type checking redundant, thereby making the benefits of dynamic typing more attractive. So as more and more developers adopt TDD, we may see them shift their preferences towards dynamically typed languages, as well.

A Look Back

Getting back to the Abstract Server pattern, notice that in order to use this pattern we had to modify the Light class. This is part of the context of this pattern. In order to apply Abstract Server, you must be able to modify the target of the dependency. Next month, we'll study the pattern that we could use if Light could not be modified. In our case, however, we can modify Light, so Abstract Server is applicable.

All patterns are subject to a particular context that describes the constraints under which they may be used. Sometimes those constraints have to do with modifying certain modules, at other times they have to do with performance, or even build time. In each case, however, the context describes the conditions under which the pattern may be used. If those conditions are not obtained, then a different pattern must be used instead.

Finally, there is a cost to using the Abstract Server pattern. It introduces a new class. There may be a slight storage overhead for the interface, and possibly a bit of extra CPU time spent on the polymorphic dispatch. Then, of course, there is the cost of modifying the Light and Button classes. In this case these costs are quite low. However, this is another attribute common to all design patterns: they all have costs.

The use of a design pattern is an engineering tradeoff. When we consider using a pattern, we must decide whether the pattern solves the problem, whether it fits the context, and whether the benefits outweigh the costs. Patterns are not a universal good. It is not always "right" to use a pattern. Indeed, it is quite possible to create truly horrific designs out of badly misapplied patterns.

The Abstract Server pattern is one of the simplest of all the object-oriented design patterns. We use it when we want to break the dependency of a client upon a server, for the purpose of protecting the client from changes to the server, and to preserve the ability to extend the client to use other servers. The cost is low, and the context is common. Indeed, of all of the design patterns, this one is probably used more than any other.

References

1. The "rule of three" was the standard set by Gamma, et. al., in their definitive book Design Patterns: Elements of Reusable Object-Oriented Software, Addison Wesley, 1995.

2. This definition is often attributed to James O. Coplien.

3. SOLID is an acronym made from the names of the principles. We'll be discussing them many times while we investigate design patterns. You can read about them now by going to www.objectmentor.com and finding the articles on design principles. You can also read about them in the book Agile Software Development: Principles, Patterns, and Practices, Robert C. Martin, Prentice Hall, 2002.

4. See Object-Oriented Analysis and Design with Applications, Grady Booch, Addison-Wesley, 1993.

Robert C. Martin (Uncle Bob) has been a software professional since 1970 and an international software consultant since 1990.