2. 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.
Note
OCP
and LSP are closely related. Any function using a class that violates
Liskov’s Principle violates the Open/Close Principle. Let’s reference
the preceding example again. The method DoSomeWork uses a hierarchy of classes (ProgrammerToy and CustomProgrammerToy) that violate LSP. This means that to work properly DoSomeWork must be aware of which type it really receives. Subsequently, it has to be modified each time a new class is derived from ProgrammerToy. In other words, the method DoSomeWork is not closed for modification.
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 2.) On the other hand, your purpose is to reuse the high-level module, not low-level components.
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.