Avoid subclassing using Decorator pattern
How to avoid subclassing using the decorator pattern explained using a text formatting example
Decorator pattern allows behaviour to be added to an individual object dynamically, without affecting the behaviour of other objects from the same class.
Decorator pattern is often used as an alternative to subclassing. Subclassing adds behaviour at compile time where as in decorating, behaviour can be added at run-time.
In the decorator pattern, a decorator object is created that wraps the original object. The decorator object provides the same interface as the original object, but it also adds its own behaviour.
Here are some examples
- Stream classes in java: FileInputStream can be wrapped in a BufferedInputStream to add buffering behaviour to the file input stream. It can again be wrapped in LineNumberInputStream to add the ability to track the current line number as the data is read.
- Logging: A decorator object might add logging functionality to an existing object. When a method is invoked on the decorator object, the decorator object would first log the method call, and then invoke the same method on the original object.
- File handling: Files can be written directly to disk or can be encrypted or compressed before writing to disk. Encryption and compression functionalities can be added via decorators.
- Text formatting: We can add bold, italic, underline and strike-through etc. formatting via the decorators. They can be applied in any combination we want.
Let’s understand the concept with a text formatting use case for a basic text editor.
Assume we have a class SimpleText in the backend to support simple text functionality.
public class SimpleText {
protected final String text;
public SimpleText(String text) {
this.text = text;
}
public String getText() {
return text;
}
}
Now, as the application gains popularity, we want to add support for different text formatting styles like Bold and Italic.
So one approach would be to use inheritance/subclassing to extend the SimpleText behaviour. We will create new classes that extend the SimpleText class for adding additional behaviour.
So to support bold and italic formatting, we need 3 new classes:
- BoldText (for supporting bold formatting)
- ItalicText (for supporting italic formatting)
- BoldItalicText (for handling the combination of bold and italic).
public class BoldText extends SimpleText {
public BoldText(String text) {
super(text);
}
public String getText() {
return "<b>" + this.text + "</b>";
}
}
public class ItalicText extends SimpleText {
public ItalicText(String text) {
super(text);
}
public String getText() {
return "<i>" + this.text + "</i>";
}
}
public class BoldItalicText extends SimpleText {
public BoldItalicText(String text) {
super(text);
}
public String getText() {
return "<b><i>" + this.text + "</i></b>";
}
}
Now, say we want to add ‘underline formatting’ as well.
That would mean we have to add another 4 classes for supporting underline formatting with all the existing bold and italic combinations: UnderlineText, UnderlineBoldText, UnderlineItalicText and UnderlineBoldItalicText. So now we have 7 classes in total.
Now say we want to add strike-through formatting as well. Now we need to add 8 more classes to cover all the possibilities.
What if we have more formatting types coming up in the future? We need to keep adding more and more classes for every new formatting type. That’s a maintenance nightmare!
Problems with the above approach:
- Classes grow exponentially for every new formatting type.
- If we want to change the way we maintain formatting, we have to update all the classes.
We can clearly can see that subclassing is not really the way to go here. We need to favour composition over inheritance. We will wrap the object (to which we want to add new behaviour) inside decorators using composition.
Let’s see how we can refactor the text formatting use-case to use the decorator pattern.
- We will define an interface for our text class and implement the interface for the SimpleText class. Let’s call it ‘component’.
// Component
public interface Text {
String getText();
}
// Concrete component
public class SimpleText implements Text {
private final String text;
public SimpleText(String text) {
this.text = text;
}
@Override
public String getText() {
return text;
}
}
2. Now we will define an interface for all the decorators. This interface should extend the interface of the original object, i.e., it should extend the component.
- This will allow us to re-use the decorator in place of the original object.
Decorator should implement the same interface as the original object.
/*
Common interface for all decorators.
This should extend the component interface.
*/
public interface TextDecorator extends Text {
}
3. We will implement the additional behaviours that we want as concrete decorator classes. In our example, we will implement bold, italic and underline formatting as decorators.
The decorators should hold a reference to the component and should use the reference for extending the functionality
Decorator should compose the component and use it for extending the behavior
This will allow us to wrap the original object into a decorator and also to wrap a decorator into another decorator.
/*
Concrete decorator implements the decorator interface, which in
turn extends the component.
*/
public class Bold implements TextDecorator {
private final Text text;
//Decorator should hold a reference to the component
public Bold(Text text) {
this.text = text;
}
//implement the methods using the component
@Override
public String getText() {
return "<b>" + this.text.getText() + "</b>";
}
}
public class Italic implements TextDecorator {
private final Text text;
public Italic(Text text) {
this.text = text;
}
@Override
public String getText() {
return "<i>" + this.text.getText() + "</i>";
}
}
public class Underline implements TextDecorator {
private final Text text;
public Underline(Text text) {
this.text = text;
}
@Override
public String getText() {
return "<u>" + this.text.getText() + "</u>";
}
}
Now in the future, if we want to add new formatting types like strike-through or links etc, we just have to add a new decorator class. There is no need to touch any existing object (open for extension, closed for modification).
We can wrap the original object with as many decorators as we want at run time.
public class Main {
public static void main(String[] args) {
Text formattedText = new Underline(new Italic(new Bold(new SimpleText("decorator pattern"))));
System.out.println(formattedText.getText());
// Output: <u><i><b>decorator pattern</b></i></u>
}
}
Below is the class diagram for the same example:
Here is the source code for the above example.
Points to note
- The decorator should extend/implement the component. This is needed for using the decorator in place of the original object in the code.
- The decorators should also compose the component. This is needed for wrapping the original object in a decorator and also for wrapping a decorator inside another decorator.
Advantages
- Maintenance- It is a flexible alternative to subclassing. The pattern allows new behaviour to be added to existing classes without changing the original code.
- Follows SRP- Functionality is encapsulated in different decorator classes.
- Follows OCP- New behaviour can be added in a new decorator. There is no need to update existing classes.
- Separation of concerns.
References
Resources
That’s all for now. Feel free to ask any questions or share any comments you have about the article. See you in the next article.
Happy Coding!