When to Use the Decorator Pattern?

Viewed 41959

I am going over my design patterns, and one pattern I have yet to seriously use in my coding is the Decorator Pattern.

I understand the pattern, but what I would love to know are some good concrete examples of times in the real world that the decorator pattern is the best/optimal/elegant solution. Specific situations where the need for the decorator pattern is really handy.

Thanks.

10 Answers

GOF definition :

Attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.

This pattern says that the class must be closed for modification but open for extension, a new functionality can be added without disturbing existing functionalities. The concept is very useful when we want to add special functionalities to a specific object instead of the whole class. In this pattern, we use the concept of object composition instead of inheritance.

Generic example :

public abstract class Decorator<T> {
    private T t;

    public void setTheKernel(Supplier<? extends T> supplier) {
        this.t = supplier.get();
    }

    public T decorate() {
        return Objects.isNull(t) ? null : this.t;
    }

}

The implementation

public interface Repository {
    void save();
}

public class RepositoryImpl implements Repository{
    @Override
    public void save() {
        System.out.println("saved successfully");
    }
}

public class EnhancedRepository<T> extends Decorator<T> {
    public void enhancedSave() {
        System.out.println("enhanced save activated");
    }
}

public class Main {
    public static void main(String[] args) {
        EnhancedRepository<Repository> enhanced = new EnhancedRepository<>();
        enhanced.setTheKernel(RepositoryImpl::new);
        enhanced.enhancedSave();
        enhanced.decorate().save();
    }
}

Decorator pattern is used by C# language itself. It is used to to decorate the Stream I/O class of C#. The decorated versions are BufferedStream, FileStrem, MemoryStrem, NetworkStream and CryptoStream classed.

These subclasses inherit from the Stream class and also contain an instance of the Stream class.

Read more here

When you want to add multiple functionality to a class/object and you want to have flexibility to add them whenever you need, Decorator comes handy. you can extend the base class and add your desired changes but this way you come up with lot of sub-classes that could blow your mind. but with decorator you can have your desired changes but still have a simple, understanding flow. your design is easily open for extension but close to modification with really simple logic. The best example could be the Stream objects that are implemented in Java and C#. for example you have a File Stream and in one use-case You want to Encrypt that, then Zip that, then Log that and finally do something fancy with it. then in another class you decide to do something else You want to Convert that, then Encrypt that, then get the timing, bluh, bluh, bluh. again you have another flow in other classes. if you want to use Inheritance you have to create at least 3 sub-classes and if any other requirement is needed you have to add more sub-classes that in this case (Stream) you will end up dozens of sub-classes for small changes.

class EncryptZipLogStream{}
class ConvertEncryptTimeStream{}
class MoreStreamProcess{}
class OtherMoreStreamProcess{}
...

And in each use-case you have to remember what class is needed and try to use it. But imagine you have used Composition rather than Inheritance and you have Decorator classes for each Stream process, you can easily combine your desired wrappers and have any desired processes with least amount of effort and max simplicity.

class WhereIWannaUseUseCaseOne {
    EncryptStream(ZipStream(LogStream(FileStream("file name)))));
    // rest of the code to use the combines streams.
}

then you come up with another use-case:

class WhereIWannaUseUseCaseSecond {
    ConvertStream(TimeStream(LogStream(FileStream("file name)))));
    // rest of the code to use the combines streams.
}

And so on and so forth, you have the flexibility to do what ever you want at run-time with a simple flow and understandable logic.

A very real example for me :

I had to update a class that was heavily used in a project, but that class was in a library and the source code was lost in the netherworld.

I could either decompile the entire library to create another version of it or use the decorator design pattern, which I did. This enabled me to add the lacking functionality and simply update the configuration.

It is a very useful pattern when used responsibly.

This particular case inspired me to make this video where I explain the pattern.

Related