Is Inheritance Dead? A Detailed Look Into the Decorator Pattern
In the old days of OOP, inheritance was the way to go to extend your objects' functionality. Now, it's almost shunned. See why and learn a better way to handle it.
Join the DZone community and get the full member experience.
Join For FreeWhen object-oriented programming was introduced, inheritance was the main pattern used to extend object functionality. Today, inheritance is often considered a design smell. In fact, it has been shown that extending objects using inheritance often results in an exploding class hierarchy (see Exploding class hierarchy section). In addition, several popular programming languages such as Java and C# do not support multiple inheritance, which limits the benefits of this approach.
The decorator pattern provides a flexible alternative to inheritance for extending objects functionality. This pattern is designed in a way that multiple decorators can be stacked on top of each other, each adding new functionality. In contrast to inheritance, a decorator can operate on any implementation of a given interface, which eliminates the need to subclass an entire class hierarchy. Furthermore, the use of the decorator pattern leads to clean and testable code (see Testability and Other Benefits sections).
Sadly, a large portion of today’s software developers have a limited understanding of the decorator pattern. This is partially due to the lack of education but is also because programming languages have not kept up with the evolution of object-oriented design principles in a way that encourages developers to learn and use those patterns.
In this article, I will discuss the benefits of using the decorator pattern over inheritance and suggest that the decorator pattern should have native support in object-oriented programming languages. In fact, I believe that clean and testable code should have more occurrences of the decorator pattern than inheritance.
Exploding Class Hierarchy
An exploding class hierarchy occurs when the number of classes needed to add a new functionality to a given class hierarchy grows exponentially. To illustrate, let’s consider the following interface:
public interface IEmailService
{
void send(Email email);
Collection<EmailInfo> listEmails(int indexBegin, int indexEnd);
Email downloadEmail(EmailInfo emailInfo);
}
The default implementation of EmailService throws an exception if a request to the email server fails. We’d like to extend the EmailService implementation so that failed requests are retried few times before giving up. We’d also like to be able to choose whether the implementation is thread-safe or not.
We can achieve this by adding optional retries and thread-safety features to the EmailService class itself. The class would accept parameters in the constructor that enables/disables each feature. However, this solution violates both the Single Responsibility Principle (because the EmailService would have additional responsibilities) and the Open-Closed Principle (because the class itself would have to be modified for extension). Furthermore, the EmailService class could be part of a third-party library that we can’t modify.
A common approach to extend a class without modifying it, is to use inheritance. With inheritance, a derived class inherits the properties and behavior of its parent and can optionally extend or override some of its functionality. In the EmailService example, we can create three subclasses, one that adds retries, one that adds thread-safety and one that adds both features. The class hierarchy would look as follows:
Note that the ThreadSafeEmailServiceWithRetries could alternatively inherit from EmailServiceWithRetries or ThreadSafeEmailService (or both if multiple inheritance is supported). However, the number of classes and resulting functionality would be similar.
In addition to retries and thread-safety, we’d like to extend our email service API so that one can optionally enable logging. Once again, we use inheritance to extend the class hierarchy which becomes as follows:
Notice that the number of additional classes needed to add support for logging is equal to the total number of classes in the existing hierarchy (four in this case). To confirm this behavior, let’s add caching to the hierarchy and examine the result. The new hierarchy is shown below:
As you can see, the class hierarchy is growing exponentially and is quickly becoming unmanageable. This problem is known as exploding class hierarchy.
The Decorator Pattern to the Rescue
The decorator pattern extends object functionality using composition rather than inheritance. It eliminates the problem of exploding class hierarchy because only one decorator is needed for each new feature. To illustrate, let’s create a decorator for the retries feature. For simplicity, a simple for loop with three retries is used. The EmailServiceRetryDecorator is as follows:
public class EmailServiceRetryDecorator implements IEmailService
{
private final IEmailService emailService;
public EmailServiceRetryDecorator(IEmailService emailService) {
this.emailService = emailService;
}
@Override
public void send(Email email) {
executeWithRetries(() -> emailService.send(email));
}
@Override
public Collection<EmailInfo> listEmails(int indexBegin, int indexEnd) {
final List<EmailInfo> emailInfos = new ArrayList<>();
executeWithRetries(() -> emailInfos.addAll(emailService.listEmails(indexBegin, indexEnd)));
return emailInfos;
}
@Override
public Email downloadEmail(EmailInfo emailInfo) {
final Email[] email = new Email[1];
executeWithRetries(() -> email[0] = emailService.downloadEmail(emailInfo));
return email[0];
}
private void executeWithRetries(Runnable runnable) {
for(int i=0; i<3; ++i) {
try {
runnable.run();
} catch (EmailServiceTransientError e) {
continue;
}
break;
}
}
}
Notice that the constructor of EmailServiceRetryDecorator takes a reference to IEmailService, which could be any implementation of IEmailService (including the decorator itself). This completely decouples the decorator from specific implementations of IEmailService and increases its reusability and testability. Similarly, we can create decorators for thread-safety, logging and caching. The resulting class hierarchy is as follows:
As shown in the class diagram above, only one class is needed for each feature and the resulting class hierarchy is simple and scalable (grows linearly).
Decorators Queue
At first glance, it may seem that only one feature can be added to a given implementation using the decorator pattern. However, because decorators can be stacked on top of each other, the possibilities are endless. For instance, we can dynamically create an equivalent to the EmailServiceWithRetriesAndCaching that we created with inheritance as follows:
IEmailService emailServiceWithRetriesAndCaching = new EmailServiceCacheDecorator(
new EmailServiceRetryDecorator(new EmailService()));
In addition, by changing the order of decorators or using the same decorator at multiple levels, we can dynamically create new implementations that would be difficult to create with inheritance. For instance, we can add logging before and after retries as follows:
IEmailService emailService = new EmailServiceLoggingDecorator(new EmailServiceRetryDecorator(
new EmailServiceLoggingDecorator(new EmailService())));
With such combination, the status of the request before and after retries will be logged. This provides verbose logging that can be used for debugging purposes or to create rich dashboards.
Please also see the-decorator-builder pattern that makes composing decorators simpler and more readeable.
Testability
Another major benefit of the decorator over inheritance is testability. To illustrate, let’s consider writing a unit test for the retries feature.
The EmailServiceWithRetries that we created with inheritance cannot be tested in isolation of its parent class (EmailService) because there is no mechanism to replace a parent class with a stub (also known as mocking). Furthermore, because EmailService performs network calls to a backend server, all its subclasses become difficult to unit test (because network calls are often slow and unreliable). In such cases, it’s common to use integration tests instead of unit tests.
On the other hand, because the EmailServiceRetryDecorator takes a reference to IEmailService in its constructor, the decorated object can be easily replaced with a stub implementation (i.e. mock). This makes it possible to test the retry feature in isolation, which is not possible with inheritance. To illustrate, let’s write a unit test that verifies that at least one retry is performed (I used Mockito framework to create a stub in this example).
// Create a mock that fails the first time and then succeed
IEmailService mock = mock(IEmailService.class);
when(mock.downloadEmail(emailInfo))
.thenThrow(new EmailServiceTransientError())
.thenReturn(email);
EmailServiceRetryDecorator decorator = new EmailServiceRetryDecorator(mock);
Assert.assertEquals(email, decorator.downloadEmail(emailInfo));
In contrast to an integration test that would depend on the implementation of EmailService and remote service calls, this test is simple, fast and reliable.
Other Benefits
Besides simplifying the class hierarchy and improving testability, the decorator pattern encourages developers to write code that adheres to the SOLID design principles. In fact, using the decorator pattern, new features are added to new focused objects (Single Responsibility Principle) without modifying existing classes (Open-Closed Principle). In addition, the decorator pattern encourages the use of dependency inversion (which has many benefits such loose-coupling and testability) because decorators depend on abstractions rather than concretions.
Drawbacks
Even though the decorator pattern has many advantages over the alternative (inheritance or modifying existing classes), it has few drawbacks that are slowing its adoption.
One known drawback of this pattern is that all methods in the decorated interface must be implemented in the decorator class. In fact, methods that don’t add any additional behavior must be implemented as forwarding methods to keep existing behavior. In contrast, inheritance only requires subclasses to implement methods that change or extend the behavior of the base class.
To illustrate the problem of forwarding methods, let’s consider the following IProcess interface and create a decorator for it.
public interface IProcess
{
void start(String args);
void kill();
ProcessInfo getInfo();
ProcessStatus getStatus();
ProcessStatistics getStatistics();
}
The default implementation of the start method throws a FailedToStartProcessException if the process fails to start. We’d like to extend the default implementation so that starting the process is retried three times before giving up. Using the decorator pattern, the implementation would look as follows:
public class RetryStartProcess implements IProcess
{
private IProcess process;
public RetryStartProcess(IProcess process) {
this.process = process;
}
@Override
public void start(String args) {
for(int i=0; i<3; ++i) {
try {
process.start(args);
} catch (FailedToStartProcessException e) {
continue;
}
break;
}
}
@Override
public void kill() {
process.kill();
}
@Override
public ProcessInfo getInfo() {
return process.getInfo();
}
@Override
public ProcessStatus getStatus() {
return process.getStatus();
}
@Override
public ProcessStatistics getStatistics() {
return process.getStatistics();
}
}
Notice that this implementation contains a fair amount of boiler-plate code. In fact, the only part of the implementation that is relevant is the implementation of the start method. For interfaces that have many methods, such boiler-plate can be seen as a productivity and maintenance overhead.
Another drawback of the decorator pattern is its lack of popularity, especially among junior developers. In fact, less popular often means harder to understand which can lead to slower development time.
Native Support for the Decorator Pattern
Both drawbacks discussed in the previous section can be overcome if the decorator pattern benefited from native support in object-oriented programming languages (similar to what is provided today for inheritance). In fact, with such native support, forwarding methods would not be needed and the decorator pattern would be easier to use. In addition, native support for the decorator pattern would certainly increase its popularity and usage.
A good example of how programming languages can have an impact on the adoption of design patterns is the introduction of native support for the Observer pattern in C# (also known as events). Today C# developers (including junior ones) naturally use the Observer pattern to communicate events between loosely coupled classes. If events did not exist in C#, many developers would introduce direct dependencies between classes to communicate events, which will result in code that is less reusable and is harder to test. Similarly, native support for the decorator pattern would encourage developers to create decorators instead of modifying existing classes or inappropriately using inheritance, which will lead to better code quality.
The following implementation illustrates what native support for the decorator pattern would look like in Java:
public class RetryStartProcess decorates IProcess
{
@Override
public void start(String args) {
for(int i=0; i<3; ++i) {
try {
decorated.start(args);
} catch (FailedToStartProcessException e) {
continue;
}
break;
}
}
}
Notice the decorates keyword that is used instead of implements and the use of the decorated field to access the decorated object. For this to work, the default constructor of the decorator would require an IProcess parameter (which will be handled at the language level much like parameter-less default constructors are handled today). As you can see, such native support will make the decorator pattern boiler-plate free and as easy to implement as inheritance (if not easier).
Abstract Decorator
If, like me, you use the decorator pattern a lot and often end up with many decorators for each interface, there is a workaround that you could use to reduce the boiler-plate of forwarding methods (in the meantime until native support for the decorator pattern becomes available). The workaround consists of creating an abstract decorator that implements all methods as forwarding methods and to derive (inherit) all decorators from it. Because forwarding methods are inherited from the abstract decorator, only decorated methods will need to be reimplemented. This workaround takes advantage of the native support for inheritance and uses it to implement the decorator pattern. The following code illustrates this approach.
public abstract class AbstractProcessDecorator implements IProcess
{
protected final IProcess process;
protected AbstractProcessDecorator(IProcess process) {
this.process = process;
}
public void start(String args) {
process.start(args);
}
public void kill() {
process.kill();
}
public ProcessInfo getInfo() {
return process.getInfo();
}
public ProcessStatus getStatus() {
return process.getStatus();
}
public ProcessStatistics getStatistics() {
return process.getStatistics();
}
}
public class RetryStartProcess extends AbstractProcessDecorator
{
public RetryStartProcess(IProcess process) {
super(process);
}
@Override
public void start(String args) {
for(int i=0; i<3; ++i) {
try {
process.start(args);
} catch (FailedToStartProcessException e) {
continue;
}
break;
}
}
}
One downside of this approach is that decorators will not be able to inherit from other classes (for languages that don’t support multiple inheritance).
When to Use Inheritance
Although I believe that the decorator pattern should be chosen over inheritance when possible, inheritance is more adequate in some cases. A common situation where a decorator would be difficult to implement is when derived classes need to access non-public fields or methods in the parent class. Because decorators are only aware of the public interface, they do not have access to fields or methods that are specific to one implementation or another.
As a rule of thumb, if your subclass only depends on the public interface of its parent, it’s a hint that you could use a decorator instead. In fact, it would be great if static analysis tools suggested replacing inheritance with a decorator in such cases.
Takeaways
- The decorator pattern should be preferred over inheritance when possible.
- The decorator pattern eliminates the problem of exploding class hierarchy encountered with inheritance. In fact, using the decorator pattern, the resulting class hierarchy is simple and scales linearly.
- Decorators can be tested in isolation of decorated objects but subclasses cannot be tested in isolation of their parent. With inheritance, if a parent class is difficult to unit test (e.g. performs remote calls) its derived classes inherit this issue. However, because decorators only rely on the interface of decorated objects (injected through the constructor of the decorator class), decorators can be unit tested independently.
- The use of the decorator pattern encourages developers to write code that adheres to the SOLID design principles.
- Native support for the decorator pattern in object-oriented programming languages would make this pattern easier to use and increase its adoption.
Opinions expressed by DZone contributors are their own.
Comments