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).
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.
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.
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.
Note
Should
you use an interface? Or should you perhaps opt for an abstract base
class? In object-oriented languages that do not support multiple
inheritance—such as Java, C#, and Visual Basic .NET—an interface is
always preferable because it leaves room for another base class of your
choice. When you have multiple inheritance, it is mostly a matter of
preference. You should consider using a base class in .NET languages in
all cases where you need more than just an interface. If you need some
hard-coded behavior along with an interface, a base class is the only
option you have. ASP.NET providers, for example, are based on base
classes and not on interfaces.
An interesting possibility beyond base classes and interfaces are
mixins, but they are an OOP feature not supported by .NET languages. A
mixin is a class that provides a certain functionality that other
classes can inherit, but it is not meant to be a standalone class. Put
another way, a mixin is like an interface where some of the members
might contain a predefined implementation. Mixins are supported in some
dynamic languages, including Python and Ruby. No .NET languages
currently support mixins, but mixins can be simulated using ad hoc
frameworks such as Castle. DynamicProxy. With this framework, you first
define a class that contains all the methods you want to inject in an
existing class—the mixin. Next, you use the framework to create a proxy
for a given class that contains the injected methods.
Castle.DynamicProxy uses Reflection.Emit internally to do the trick.
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 1.)
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.
Note
In addition to composition, another approach is frequently used to contrast class inheritance—aggregation. Both aggregation and composition refer to a has-a relationship between two classes, whereas inheritance implies an is-a
relationship. The difference between composition and aggregation is
that with composition you have a static link between the container and
contained classes. If you dispose of the container, the contained
classes are also disposed of. With aggregation, the link is weaker and
the container is simply associated with an external class. As a result,
when the container is disposed of, the child class blissfully survives.