Yazının Türkçe versiyonuna bu link ile ulaşabilirsiniz.
In this article, we will examine the Decorator Design Pattern and how the design pattern can be implemented with dependency injection on .Net Core.
The Decorator design pattern falls under the category of structural design patterns. This design pattern is used in two main situations.
- Where we need to add new features to an object
- Where we need to change the behavior of the object without modifying the code
In both cases, by packing an object, we get a new object that we can manage its behavior as we want. For this reason, we can compare this design pattern to matryoshka. After taking a brief look at the structure of the Decorator design pattern, we will continue with the sample scenarios and code pieces for the two situations mentioned above.
Note: The sample project is available on GitHub.
Elements of Decorator Desing Pattern
Component: It is an asset defined in our system to perform a specific task. It is an element defined as an abstract class or interface.
Concrete Component: An implemented version of the asset defined as a Component.
Decorator: It is the asset that takes on the properties of the Component and contains the additional properties to be defined. This asset can be an abstract class or interface like Component. The object we define as Decorator is defined as a Component and it also contains an object of the Component type. In other words, there is both IS A and HAS A relationship between Decorator and Component.
Concrete Decorator: An implemented version of the asset defined as a Decorator.
How to Add New Feature with Decorator Design Pattern?
Let’s consider an interface called IProductRepository. Let it have a feature such as presenting the information of the product with the Id value through its method called GetById. Let’s create an implementation of this interface called DbProductRepository. When we create the interface and prepare an instance of it, we prepare the Component and Concreate Component. Below is the UML diagram and code fragment of this structure.
After we have done our coding, let’s start talking about the existence of an admin user. Admin users can read product data like standard user as well as being able to add product data to the system. At the point where we want to add new features without making changes in our codes, the Decorator design pattern comes to our aid. Let’s start by designing an interface called IAdminProductRepository. This interface will inherit the IProductRepository interface. By doing so, it will have the ability to read the products. In addition, we define the method we want to add called CreateProduct, which is not in the interface it inherited. Consequently, we create our Decorator.
By designing a class that implements the IAdminProductRepository interface, we complete the implementation of our Decorator design pattern.
We can manage the dependencies using the Microsoft Dependency Injection package as shown in the following code snippet.
While our system continues to work as before by using the IProductRepository, we will be able to use the additional features by requesting an instance of IAdminProductRepository in our new services where we want to use the product create feature. Thanks to the decorator design pattern, we have developed in accordance with the Open-Close principle.
How to Change Object’s Behavior with Decorator Design Pattern?
When you look at the code snippet below, a product query was made with the Id value by requesting an object from the IoC container from the IProductRepository type. Due to the object we injected into the system, we know that this query was made from the database.
Let’s assume that our system needs to run faster. Let’s also assume that we have set up a caching mechanism for faster querying of product information. In this case, when we query over IProductRepository, we want to search in the cache first. If we cannot find the data we are looking for, we will ask for the query to be executed in the database. We can change the DbProductRepository class to fulfill this expectation. We can execute our queries on the cache before database query. However, we would have broken the Open-Close principle because we made changes to the existing codes in this approach. In addition, we would have broken the Single-Responsibility principle since we were managing both database and cache in the same class. Decorator design pattern will help us stick to SOLID rules.
Let’s design a class called CacheProductRepository. This class will be both an implementation of the IProductRepository interface and the owner of an object from IProductRepository interface. It will search the cache first, if it cannot find an answer, it will transfer the task to another IProductRepository object. When the response comes from the repository entity owned by the CacheProductRepository, it will cache the data for further queries.
After designing this class, we can edit the dependencies on CompositionRoot as follows.
After the dependencies are introduced to the system, we will provide the change we want in the behavior of the service we receive through IProductRepository.