Design Pattern
.NET .NET Core C# Decorators Patterns Visual Studio

Using Design Patterns in .NET Core – Part 10 – Decorator

Welcome to today’s post.

In today’s post I will be discussing the next software design pattern. The design pattern I will be discussing is the Decorator pattern, which is a structural pattern.

Unlike other structural design patterns that I have discussed in previous posts, including the Adapter and Façade patterns, the Decorator pattern does not modify the base classes internally. In this case it does not alter existing functionality of a base class, it implements classes by using interfacing (composition). The interfaces include the corresponding methods from our base class are decorator methods in our composed decorator classes.

We then implement the decorator methods within the decorator interfaces. Each decorator class will implement the contract in one of the decorator interfaces, with the resulting action or output varying from the equivalent method action or output from the base class interface.

In the following sections, I will go through the steps taken to decorate a class with properties from an interface.

Applying the Decorator Pattern to Classes

In a library system where we have multiple types of media (books, compact discs, DVD discs, magazines etc.). The base class for the book is implemented as a service class, BookService that allows us to store and retrieve books and other media from our library catalogue. Should we wish to implement objects with properties instanced from the service class without affecting the underlying BooKService class, then we create decorator interfaces IBookServiceDecorator, IDVDServiceDecorator and so on, with the additional properties. The interface of the underlying class is then extended through additional interfaces that contain additional properties that we wish to decorate objects of the underlying class. We then implement decorators BookServiceDecorator, DVDServiceDecorator and so on from the decorator interfaces.

The class diagram is shown below:

Notice that we had extend the interface IBookService with additional decorator interfaces or else we will not be able to use dependency injection on the BookService class and multiple interfaces. For this reason, I have extended the interface from the underlying class and registering those into the service collection.

The following registrations of our interfaces and concrete classes to the IOC container will achieve our purpose of injecting the decorator classes in our application:

services.AddTransient<IBookServiceDecorator, BookServiceDecorator>();
services.AddTransient<IMagazineServiceDecorator, MagazineServiceDecorator>();
services.AddTransient<IDVDServiceDecorator, DVDServiceDecorator>();
services.AddTransient<ICDServiceDecorator, CDServiceDecorator>();

Had we implemented our underlying class directly from the decorator interfaces as shown:

public class BookService: IBookServiceDecorator
{
    …
}

public class BookService: IDVDServiceDecorator
{
    …
}

Then the IOC container cannot resolve which concrete class to instance from a given decorator interface. The following would give an error and not possible in the .NET Core Dependency Injection framework:

services.AddTransient<IBookService, BookServiceDecorator>();
services.AddTransient<IBookService, MagazineServiceDecorator>();
services.AddTransient<IBookService, DVDServiceDecorator>();
services.AddTransient<IBookService, CDServiceDecorator>();

Given our underlying class, the BookService has the following interface contract:

namespace BookLoan.Services
{
    public interface IBookService
    {
        …
        DecoratorPropertyModel GetServiceProperties();
        string GetServiceDescription();
        …
    }
}

In the next section, I will show how to use interfaces for both the decorator and the base class to apply decorators to the extended base class.

Applying Interfaces to Decorate Base Classes

In the section I will show how to use interfaces for each of the entities that we wish to decorate the base class.

First, our decorator interfaces are defined as follows. We first have a service decorator interface to extend the book service interface:

using BookLoan.Services;

namespace BookLoan.Catalog.API.Decorator
{
    public interface IBookServiceDecorator: IBookService
    {
        public string GetServiceDescription();
        public string GetMediaType();
    }
}

The then define a decorator interface for each media type that extends the book service interface. Below is the decorator interface for DVD media types:

using BookLoan.Services;

namespace BookLoan.Catalog.API.Decorator
{
    public interface IDVDServiceDecorator: IBookService
    {
        public string GetServiceDescription();
        public string GetMediaType();
        public bool HasStorageCase();
    }
}

What we have here is the Book and DVD decorators both have the following methods:

public string GetServiceDescription();
public string GetMediaType();

and the DVD decorator has the additional method:

public bool HasStorageCase();

The underlying BookService class has an existing method, GetServiceProperties() that we can refer to from our decorators. It is shown below:

public DecoratorPropertyModel GetServiceProperties()
{
    return new DecoratorPropertyModel()
    {
       	MediaType = "",
        ServiceDescription = "Library {0} Catalogue Service"
        HasStorageCase = null
    };
}

The above structure that will contain the data returned from our decorator method is defined below:

namespace BookLoan.Catalog.API.Models.DecoratorModels
{
    public class DecoratorPropertyModel
    {
        public string ServiceDescription { get; set; }
        public string MediaType { get; set; }
        public bool? HasStorageCase { get; set; }
    }
}

The implementations of the decorator classes refer to the underlying class BookService through aggregation. The instance is stored as an internal variable:

private readonly IBookService _bookService;

The internal reference is assigned through injection of a book service instance through the decorator constructor:

public BookServiceDecorator(IBookService bookService)

In the next section I will show how to implement concrete classes with the above extended decorator interfaces, and IBookServiceDecorator and IDVDServiceDecorator.

Applying Decorations to Concrete Classes with Extended Decorator Interfaces

In this section, I will show how to apply the above decorator interfaces to create concrete classes that include decorated properties. Each of the interfaces IBookServiceDecorator and IDVDServiceDecorator are used to create concrete classes BookServiceDecorator and DVDServiceDecorator that are decorated with properties obtained from an instance of the base BookService instance.

The book service decorator is shown below:

using BookLoan.Catalog.API.Models.DecoratorModels;
using BookLoan.Models;
using BookLoan.Services;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace BookLoan.Catalog.API.Decorator
{
    public class BookServiceDecorator: IBookServiceDecorator
    {
        private readonly IBookService _bookService;
        
        public BookServiceDecorator(IBookService bookService)
        {
            _bookService = bookService;
        }

        public DecoratorPropertyModel GetServiceProperties()
        {
            return new DecoratorPropertyModel()
            {
                MediaType = this.GetMediaType(),
                ServiceDescription = string.Format(
                    _bookService.GetServiceProperties().ServiceDescription, GetMediaType())
            };
        }

        public string GetServiceDescription()
        {
            return "Library Book Catalogue Service";
        }

        public string GetMediaType()
        {
            return "Book";
        }
        …
      }
}

The DVD service decorator is shown below:

using BookLoan.Catalog.API.Models.DecoratorModels;
using BookLoan.Models;
using BookLoan.Services;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace BookLoan.Catalog.API.Decorator
{
    public class DVDServiceDecorator: IDVDServiceDecorator
    {
        private readonly IBookService _bookService;

        public DVDServiceDecorator(IBookService bookService)
        {
            _bookService = bookService;
        }

        public DecoratorPropertyModel GetServiceProperties()
        {
            return new DecoratorPropertyModel()
            {
                MediaType = this.GetMediaType(),
                ServiceDescription = string.Format(
                    _bookService.GetServiceProperties().ServiceDescription, GetMediaType()),
                HasStorageCase = this.HasStorageCase()                
            };
        }

        public string GetServiceDescription()
        {
            return "Library DVD Catalog Service";
        }
        
        public string GetMediaType()
        {
            return "DVD";
        }

        public bool HasStorageCase()
        {
            return true;
        }
        …
    }
}

Each subsequent decorator follows the above pattern, with potentially additional properties augmented through the decorator interfaces.

In the next section I will show how to instantiate the above decorator interfaces within a controller class.

Injection of Decorators into a Controller Class

The decorated classes are instantiated through dependency injection as shown in an API controller:

public class BookController : Controller
{
        private readonly ApplicationDbContext _db;
        private readonly IBookService _bookService;
        private readonly IBookServiceDecorator _bookServiceDecorator;
        private readonly IMagazineServiceDecorator _magazineServiceDecorator;
        private readonly IDVDServiceDecorator _dvdServiceDecorator;
        private readonly ICDServiceDecorator _cdServiceDecorator;
        private readonly ILogger _logger;

        public BookController(ApplicationDbContext db,
            ILogger<BookController> logger,
            IBookServiceDecorator bookServiceDecorator,
            IMagazineServiceDecorator magazineServiceDecorator,
            IDVDServiceDecorator dvdServiceDecorator,
            ICDServiceDecorator cdServiceDecorator,
            IBookService bookService)
        {
            _db = db;
            _logger = logger;
            _bookService = bookService;
            _bookServiceDecorator = bookServiceDecorator;
            _magazineServiceDecorator = magazineServiceDecorator;
            _dvdServiceDecorator = dvdServiceDecorator;
            _cdServiceDecorator = cdServiceDecorator;
        }
        
        ..
}

Typical implementations of the decorators are shown:

[HttpGet("api/[controller]/GetBookServiceProperties")]
public DecoratorPropertyModel GetBookServiceProperties()
{
    return _bookServiceDecorator.GetServiceProperties();
}

[HttpGet("api/[controller]/GetDVDServiceProperties")]
public DecoratorPropertyModel GetDVDServiceProperties()
{
    return _dvdServiceDecorator.GetServiceProperties();
}

The API calls to each of the decorated classes yields different properties.

A call to the HTTP GET method:

http://localhost:25183/api/Book/GetBookServiceProperties

Returns the following sample result for a Book:

{
    "serviceDescription": "Library Book Catalogue Service",
    "mediaType": "Book",
    "hasStorageCase": null
}

A call to the HTTP GET method:

http://localhost:25183/api/Book/GetDVDServiceProperties

Returns the following sample result for a DVD:

{
    "serviceDescription": "Library DVD Catalogue Service",
    "mediaType": "DVD",
    "hasStorageCase": true
}

Each of the decorator classes could also call methods from the underlying class in addition to the decorator methods and properties without affecting the existing behavior of the underlying class.

When we consider how decorators apply to SOLID design principles, especially where we are applying the open-closed principle to ensure that we create new decorators for each new responsibility for our underlying class, thus preserving the separation of concerns principle. The danger of not using decorators is a phenomenon known as exploding class hierarchy, where there is a combinatorial explosion of inherited classes that are incrementally being extended to do one additional behavior. We you can imagine, maintaining or extending an ever-growing tree of inherited classes is less preferable than maintaining classes implemented from a limited set of interfaces. The benefit we have of implemented classes over inherited classes is of the benefit of unit tests which are easier to implement for interfaced classes.

I hope this has given you an understanding of the usefulness of the decorator design pattern and how to apply in an application.

That is all for today’s post.

I hope you found this post useful and informative.

Social media & sharing icons powered by UltimatelySocial