Understand the decorator design pattern from the root with pictures and texts

decorative pattern

Also known as: decorator pattern, decorator pattern, Wrapper, Decorator

intention

The "Decorator Pattern" is a structural design pattern that allows you to bind new behavior to an original object by placing the object in a special wrapper object that contains the behavior.

decorative design pattern

question

Suppose you are developing a library that provides notification functionality that other programs can use to notify users about important events.

The initial version of the library was based on the Notifier class, which had only a few member variables, a constructor and a send method. This method can receive message parameters from the client and send the message to a series of mailboxes, the list of mailboxes is passed to the notifier through the constructor. A third-party program acting as a client creates and configures the notifier object only once, and then calls it when an important event occurs.

Library structure before decorating mode

Programs can use notifier classes to send notifications of important events to predefined mailboxes.

At some point later, you'll find that users of the library want to use functionality other than email notifications. Many users will want to receive mobile text messages about emergencies, some users will want to receive messages on WeChat, and corporate users will want to receive messages on QQ.

Library structure after implementing other types of notifications

Each notification type will be implemented as a subclass of Notifier.

What's so hard about this? First extend the Notifier class, then add additional notification methods in the new subclass. Now the client needs to initialize the corresponding class of the required notification form, and then use this class to send all subsequent notification messages.

But soon someone will ask: why not use multiple forms of notification at the same time? If the house is on fire, you probably want to get the same message on all channels.

You can try to create a special subclass to combine multiple notification methods to solve the problem. But this approach will quickly swell the amount of code, not only the library code, but also the client code.

Library structure after creating composite classes

The number of subclass combinations exploded.

You have to find other ways to structure the notification classes, or their numbers will inadvertently break the Guinness Book of Records.

solution

When you need to change the behavior of an object, the first thought that springs to mind is to extend the class it belongs to. However, you cannot ignore several serious problems that inheritance can raise.

  • Inheritance is static. You cannot change the behavior of an existing object at runtime, you can only replace the current entire object with an object created by a different subclass.

  • A subclass can only have one superclass. Most programming languages ​​do not allow a class to inherit the behavior of multiple classes at the same time.

One of the ways is to use aggregation or composition instead of inheritance. Both work almost exactly the same way: one object contains a reference to another object and delegates part of the work to the referenced object; objects in inheritance inherit the behavior of the parent class, and they can do the work themselves.

You can use this new method to easily replace various connected "helper" objects, which can change the behavior of the container at runtime. An object can use the behavior of multiple classes, contain multiple references to other objects, and delegate various work to the referenced objects. Aggregation (or Composition) Composition is a key principle behind many design patterns (including decoration). With this in mind, let's move on to the discussion of patterns.

Inheritance vs Aggregation

Wrapper is another name for the Decorator pattern, which clearly expresses the main idea of ​​the pattern. A "wrapper" is an object that can be linked to other "target" objects. The wrapper contains the same set of methods as the target object, and it delegates all received requests to the target object. However, the wrapper can process the request before and after it is delegated to the target, so it may change the final result.

So when can a simple wrapper be called a real decoration? As mentioned earlier, a wrapper implements the same interface as the object it wraps. So from the client's point of view, these objects are exactly the same. A reference member variable in a wrapper can be any object that conforms to the same interface. This allows you to put an object into multiple wrappers and add the combined behavior of all those wrappers to the object.

For example, in the message notification example, we could put the simple email notification behavior in the base class Notifier, but put all the other notification methods in the decorator.

Decorative Pattern Solutions

Put various notification methods into the decoration.

Client code must put the underlying notifier into a series of its own desired decorations. So the final object will form a stack structure.

Programs can configure complex stacks of notification decorations

Programs can configure complex stacks of notification decorations.

The object that actually interacts with the client will be the last decorated object to enter the stack. Since all decorations implement the same interface as the notification base class, other code on the client side doesn't care whether it interacts with the "pure" notifier object or the decorated notifier object.

We can use the same method to accomplish other actions (such as formatting a message or creating a recipient list). Clients can decorate objects with any custom decorator as long as all decorators follow the same interface.

real world analogy

Decorative Pattern Example

Wearing more than one piece of clothing will give you a combinatorial effect.

Dressing is an example of using decoration. You can wear a sweater when you feel cold. If it's still cold in your sweater, you can put on another jacket. If it rains, you can also wear a raincoat. All of these garments "extend" your basic behavior, but they are not part of you, and can be easily removed at any time if you no longer need a certain garment.

Decorative Pattern Structure

Structure of the Decorative Pattern Example

  1. "Component" declares the wrapper and the public interface of the wrapped object.

  2. The Concrete Component class is the class to which the encapsulated object belongs. It defines basic behaviors, but decorator classes can change those behaviors.

  3. The "Base Decorator" class has a reference member variable that points to the encapsulated object. The variable's type should be declared as a generic widget interface so that it can refer to specific widgets and decorations. Decorating the base class will delegate all operations to the encapsulated object.

  4. Concrete Decorators define additional behaviors that can be dynamically added to widgets. The concrete decorated class overrides the method of the decorated base class and performs additional behavior before or after the parent class method is called.

  5. A "Client" can use multiple layers of decoration to encapsulate a widget, as long as it can interact with all objects using a common interface.

Fake code

In this case, the "decoration" mode is able to compress and encrypt sensitive data, thereby isolating the data from the code that uses the data.

Example of encryption and compression decorations

The program uses a pair of decorators to encapsulate the data source object. Both wrappers change the way data is read and written from disk:

  • The decoration encrypts and compresses the data just before it is "written to disk". Write encrypted protected data to a file without the original class noticing the change.

  • When the data is just "read from disk", the data is also decompressed and decrypted by decoration.

Decorator and data source classes implement the same interface and thus can be interchanged in client code.

//Decorations can change the operations defined by the component interface.
interface DataSource is
    method writeData(data)
    method readData():data

//Concrete components provide default implementations of operations. These classes may have several variants in the program.
class FileDataSource implements DataSource is
    constructor FileDataSource(filename) { ... }

    method writeData(data) is
        //Write data to a file.

    method readData():data is
        //Read data from a file.

//Decorated base classes and other components follow the same interface. The main task of this class is to define the envelope of all concrete decorations
//Install the interface. The encapsulated default implementation code may contain a member variable that holds the encapsulated component, and
//and is responsible for initializing it.
class DataSourceDecorator implements DataSource is
    protected field wrappee: DataSource

    constructor DataSourceDecorator(source: DataSource) is
        wrappee = source

    //Decorating the base class directly dispatches all work to the encapsulated component. Specific decoration can add some
    //additional behavior.
    method writeData(data) is
        wrappee.writeData(data)

    //A concrete decoration can call the operation implementation of its parent class instead of directly calling the encapsulated object. this way
    //It can simplify the extension work of the decoration class.
    method readData():data is
        return wrappee.readData()

//The concrete decoration must call the method on the encapsulated object, but it can also add some content to the result by itself.
//The decorator must perform additional behavior before or after calling the encapsulated object.
class EncryptionDecorator extends DataSourceDecorator is
    method writeData(data) is
        // 1. Encrypt the transmitted data.
        // 2. Pass the encrypted data to the writeData method of the encapsulated object.

    method readData():data is
        // 1. Obtain data through the readData method of the encapsulated object.
        // 2. Attempt to decrypt if the data is encrypted.
        // 3. Return the result.

//You can encapsulate objects in multiple layers of decorations.
class CompressionDecorator extends DataSourceDecorator is
    method writeData(data) is
        // 1. Compress the transfer data.
        // 2. Pass the compressed data to the writeData method of the encapsulated object.

    method readData():data is
        // 1. Obtain data through the readData method of the encapsulated object.
        // 2. If the data is compressed try to decompress it.
        // 3. Return the result.


//Option 1: Simple example of decoration component
class Application is
    method dumbUsageExample() is
        source = new FileDataSource("somefile.dat")
        source.writeData(salaryRecords)
        //Cleartext data has been written to the target file.

        source = new CompressionDecorator(source)
        source.writeData(salaryRecords)
        //Compressed data has been written to the destination file.

        source = new EncryptionDecorator(source)
        //The source variable now contains:
        // Encryption > Compression > FileDataSource
        source.writeData(salaryRecords)
        //Compressed and encrypted data has been written to the destination file.


//Option 2: The client uses an external data source.  The SalaryManager object doesn't care
//How the data is stored. They interact with pre-configured data sources, which are configured by
//obtained by the setter.
class SalaryManager is
    field source: DataSource

    constructor SalaryManager(source: DataSource) { ... }

    method load() is
        return source.readData()

    method save() is
        source.writeData(salaryRecords)
    //...other useful methods...


//A program can assemble different decoration stacks at runtime depending on the configuration or environment.
class ApplicationConfigurator is
    method configurationExample() is
        source = new FileDataSource("salary.dat")
        if (enabledEncryption)
            source = new EncryptionDecorator(source)
        if (enabledCompression)
            source = new CompressionDecorator(source)

        logger = new SalaryManager(source)
        salary = logger.load()
    // ...

Decoration mode is suitable for application scenarios

If you want to use an object without modifying your code, and you want to add additional behavior to the object at runtime, you can use the decorator pattern.

Decorators organize business logic into hierarchies, and you can create a decorator for each layer to combine various different logics into objects at runtime. Since these objects follow a common interface, client code can use these objects in the same way.

You can use this pattern if extending the behavior of an object with inheritance is difficult or impossible to implement.

Many programming languages ​​use the final keyword to restrict further extensions to a class. The only way to reuse the existing behavior of a final class is to use the decorator pattern: wrap it with a wrapper.

Method to realize

  1. Make sure that business logic can be represented by a base component with additional optional layers.

  2. A common approach to finding basic components and optional hierarchies. Create a component interface and declare these methods in it.

  3. Create a concrete component class and define its underlying behavior.

  4. Create a decorated base class that uses a member variable to store a reference to the encapsulated object. This member variable must be declared as the component interface type so that concrete components and decorations can be connected at runtime. The decorating base class must delegate all work to the encapsulated object.

  5. Make sure all classes implement the component interface.

  6. Extends the decorator base class to a concrete decorator. Concrete decorations must perform their own actions before or after calling the superclass method (which always delegates to the encapsulated object).

  7. Client code is responsible for creating decorations and combining them into the form required by the client.

Advantages and disadvantages of decoration mode

  • You can extend the behavior of an object without creating new subclasses.

  • You can add or remove the functionality of an object at runtime.

  • You can combine several behaviors by wrapping objects with multiple decorations.

  • Single Responsibility Principle. You can split a large class that implements many different behaviors into several smaller classes.

  • It is difficult to remove a specific wrapper from the wrapper stack.

  • It is difficult to implement decorations whose behavior is not affected by the order of the decoration stack.

  • The initial configuration code for each layer may look bad.

Relationship to other modes

  • The adapter mode can modify the interface of the existing object, and the decoration mode can enhance the function of the object without changing the object interface. In addition, decorators also support recursive composition, which adapters cannot.

  • Adapters can provide different interfaces for encapsulated objects, proxy patterns can provide the same interfaces for objects, and decorations can provide enhanced interfaces for objects.

  • The class structure of the Chain of Responsibility pattern and the Decorator pattern is very similar. Both rely on recursive composition to pass what needs to be done to a sequence of objects. However, there are several important differences between the two.

The managers of the chain of responsibility can perform all operations independently of each other, and can also stop passing requests at any time. On the other hand, various decorations can extend the behavior of an object while adhering to the base interface. Furthermore, decorations cannot interrupt the delivery of requests.

  • Composition patterns and decorations are similar in structure in that both rely on recursive composition to organize an infinite number of objects. A decoration is similar to a composition, but it has only one child component. There is also one notable difference: the decoration adds additional responsibilities to the encapsulated object, and the composition only "sums" the results of its children.

    However, patterns can also cooperate with each other: you can use decorations to extend the behavior of specific objects in the composition tree.

  • Designs that make heavy use of composition and decoration often benefit from the use of prototype patterns. You can use this pattern to replicate complex structures instead of rebuilding from scratch.

  • Decorations allow you to change the appearance of an object, and strategy mode allows you to change its essence.

  • Decorators and proxies have similar structures, but their intentions are very different. Both patterns are built on the principle of composition, which means that one object should delegate part of the work to another object. The difference between the two is that proxies usually manage the lifecycle of their service objects themselves, while the generation of decorations is always controlled by the client.

Tags: Java Design Pattern

Posted by Rayne on Sun, 22 May 2022 04:08:07 +0300