Structural Design Patterns #03 - Decorator

Structural Design Patterns #03 - Decorator

(Image created by deepai)

This post is going to cover the decorator pattern. I tried imagining a situation where this pattern could be used. Unfortunately, after finishing this post imo the example feels a bit too theoretical. So please bear with me and do not question the problem scenario too much. :)

The problem

Let's continue with our example from the previous posts. We are working in a company that produces parts for an automotive company. Our company has different product lines, at the end of the product line a photo is taken of the produced part. An image-processing AI then determines if the produced part is of sufficient quality. For our model to be not dependent on various factors (e.g. lighting, type of camera, etc.) we are training the model additionally with edited pictures. So we have a function f_lightning that emulates different lighting, a function f_camera that emulates a different camera type, etc... To emulate a combination of these factors the edited result picture can be expressed as a function composition of the different functions:

res_picture = f_1(f_2(...f_n(picture)))

e.g: res_picture = f_lighting(f_camera(picture)) or res_picture = f_lighting(picture)

We also assume that the order of the different functions applied matters. Let's assume that we want to create a framework that enables us to create edited pictures with a various number of these "emulation functions" applied. Also, let's assume that we need to do basic editing of every picture before applying an "emulation function".

The solution

One of the most primitive approaches would be to create a class for each combination, like LightingCameraTypePicture, CameraTypePicture, etc... However, this approach would result in a lot of code and would make the introduction of another emulation function and the combinations with the already existing functions more difficult. A solution to this problem would be to create a structure that enables us to create the different combinations at runtime with the help of the decorator pattern. Additionally, this pattern be used to add additional functionality to already existing code without touching the existing code. Coming back to the previously described problem the decorator pattern would look like this:

interface IPictureService
{
    byte[] EditImage(byte[] picture);
}

This class represents the image with the basic editing that is needed before applying an "emulation function".

class BasicPictureService : IPictureService
{
    public byte[] EditImage(byte[] picture)
    {
        // Do basic image editing here e.g. preperation, basic editing, ...
        Console.WriteLine("Basic editing is done.");
        return picture;
    }
}

This abstract class enables one to wrap the BasicPictureService or any decorator (e.g. LightingService) and gives an instance the ability to call the EditImage method of the wrapped object.

abstract class PictureServiceDecorator : IPictureService
{
    IPictureService _pictureService;
    public PictureServiceDecorator(IPictureService pictureService)
    {
        _pictureService = pictureService;
    }

    public virtual byte[] EditImage(byte[] picture)
    {
        return _pictureService.EditImage(picture);
    }
}

These two classes represent two "emulation functions". Inside the EditImage method, before the actual code of the "emulation function", the EditImage method of the wrapped object is called.

class LightingService : PictureServiceDecorator
{
    public LightingService(IPictureService pictureService) 
        : base(pictureService)
    {
    }

    public override byte[] EditImage(byte[] picture)
    {
        var bytes = base.EditImage(picture);
        return ApplyLighting(bytes);
    }

    public byte[] ApplyLighting(byte[] picture)
    {
        // Change picture data to emulate lighting.
        Console.WriteLine("Applied lighting.");
        return picture;
    }
}

class CanonCameraTypeService: PictureServiceDecorator
{
    public CanonCameraTypeService(IPictureService pictureService) 
        : base(pictureService)
    {
    }

    public override byte[] EditImage(byte[] picture)
    {
        var bytes = base.EditImage(picture);
        return ApplyCanonCameraType(bytes);
    }

    public byte[] ApplyCanonCameraType(byte[] picture)
    {
        // Change picture data to emulate lighting.
        Console.WriteLine("Applied canon camera color scheme to picture data.");
        return picture;
    }
}

The decorator pattern can then be made use of by:

//Setup Lighting + CanonCamera combination
IPictureService pictureService = new BasicPictureSercie();
pictureService = new LightingService(pictureService);
pictureService = new CanonCameraTypeService(pictureService);

byte[] bytes = picture.GetBytes();
pictureService.EditImage(bytes);
/*
Console output:
----------------
Basic editing is done.
Applied lighting.
Applied canon camera color scheme to picture data.
----------------
*/

Decorator pattern in Java

Another example of this pattern in a not-so-constructed example are Streams in Java:

This enables a developer to create a custom decorator to filter an InputStream according to some custom requirements. This also enables the possibility to easily apply multiple filters.

That's it for this post, I hope you could learn something! Additionally, if you want to get informed about future posts: sUbsCr1be t0 mY n3wslet1er!1