Skip to content

Design Patterns: Decorator

26 mayo, 2021

In the previous article on design patterns, we talked about the Adapter pattern. And with the intention of continuing to increase our catalog of patterns, we continue today with another simple pattern. Simple to understand and simple to apply, but just as useful. This is the Decorator pattern.

So as always, we are going to describe the pattern and, through a practical example, demonstrate how it is implemented.

Decorator pattern description

The main utility of the Decorator pattern is to dynamically equip objects with functionalities through composition. I mean, let’s decorate objects to give them more functionality than they originally have.

This is something really useful when we want avoid complex class hierarchies. Inheritance is a powerful tool, but it can make our design much less extensible.

Describing the problem

Our company is dedicated to making computers to sell. We are the developers in charge of implementing the business logic in our favorite language (in this case C #). The computers, facing the customer, have a certain configuration. For example, there is the Dummy User PC, which has an average processor, limited RAM, and an ordinary hard drive. There is also the VeryHard Gammer PC which is much more powerful. State-of-the-art processor, two graphics cards with a screamer of RAM, SSD hard drive. We are the latest and most expensive.

Our application must calculate the final price of the computer based on its components and a profit margin in euros that each configuration will have. The components as we see, will not always be the same, since they depend on the configuration. In addition, the configuration can also include peripherals such as monitors or printers.

To solve the problem, and since we are good programmers, we use object-oriented programming. First we create an abstract class:


    public abstract class Computer
    {        
        public abstract decimal CalculateCost();
    }

We use this class so that the classes that inherit it, are seen forced to implement their own CalculateCost method. Using that class, we would have the following implementation of the Dummy PC. The implementation of VeryHard Gammer PC it would be very similar.


    public class DummyComputer:Computer
    {
        private decimal processor = 56.00M;
        private decimal hdd = 30.00M;
        private decimal graphics = 41.99M;
        private decimal ram = 23.50M;       

        public override decimal CalculateCost()
        {
            var cost = this.processor + this.hdd + this.graphics + this.ram;

            return cost;
        }      
    }

As you can see, the implementation is very simple. The logical thing would be to search the database, or perform more complex calculations, but since it is not the objective of the article we have simplified it a lot.

It turns out that because of their customer service and great prices, the company starts to grow. To increase the volume of business they decided to allow customers to choose their own configuration. From the company’s online store, users will be able to add their favorite processor, a very fast hard drive, or huge amounts of RAM. And the price must be calculated so that the customer can check it before making the purchase.

Solution with the Decorator pattern

In the end, we decided to implement a solution using the decorator pattern. In this case, our class Computer it will not undergo changes. It is the one we want to extend so we are not going to touch it. But if we are going to create a new class, which inherits from it, which we will call ComponentDecorator.


    public abstract class ComponentDecorator:Computer
    {
        public override abstract decimal CalculateCost();
    }

The important thing about this class is that inherits from original class so the objects can be converted to Computer with a simple cast.

Ok, and how do we add functionality? In this case we have chosen to create a class for each hardware component. In turn, each of these classes inherits from ComponentDecorator. For example, for an SSD hard drive, we will have the following code:


    public class FastSSD:ComponentDecorator
    {
        private Computer currentComputer;
                
        public FastSSD(Computer computer)
        {          
            this.currentComputer = computer;
        }

        public override decimal CalculateCost()
        {
            return this.currentComputer.CalculateCost() + 255.20M;
        }
    }

While for the top-of-the-range processor, we will have the following code:


    public class BigProcessor:ComponentDecorator
    {
        private Computer currentComputer;      

        public BigProcessor(Computer computer)
        {            
            this.currentComputer = computer;
        }

        public override decimal CalculateCost()
        {
            return this.currentComputer.CalculateCost() + 120.00M;
        }
    }

You can see that what we are picking up an object Computer and we are extending it so that the method CalculateCost increment by 120.00 what the original method returns. In order to use the original method, we pass the object Computer as a constructor parameter.

For everything to work we need to create a base class from which to start, which would be the following:


    public class BaseComputer:Computer
    {
        public override decimal CalculateCost()
        {
            return 0M;
        }
    }

In this case, the base team has a cost of 0, but it could have any other value. We can also create other base configurations, for example, for laptops, barebones, netbooks, etc. We will go to the base configurations decorating with the different components, to calculate the total price.

And to finish let’s see how we would use the pattern, in this case creating a Gammer PC


    Computer gammerPC = new BaseComputer();
    gammerPC = new LotOfRAM(gammerPC);
    gammerPC = new FastSSD(gammerPC);
    gammerPC = new BigProcessor(gammerPC);

    var cost = gammerPC.CalculateCost();

    Console.WriteLine(string.Format("El Coste del Gammer PC es de {0} euros", cost));

Who will write the following message on the screen:

El Coste de la configuración Gammer Pc es de 464,75 euros

You see that it is very simple. We take advantage of the fact that our objects ComponentDecorator, namely LotOfRAM, FastSSD or BigProcessor, return objects Computer. These objects are passed in the constructor of each component. When we have finished decorating the class, we call the method to calculate the cost. This call will call the methods CalculateCost of all the objects created to finally calculate the final price.

Although we could have achieved something similar with a class hierarchy, we probably we would not have the flexibility and dynamism that provides us with this solution. And all in a very simple way.

The Decorator pattern in real life

The example we have given is simple, and also very typical. If you search online or browse a book on patterns, you will find similar examples with Pizza, coffee or similar things. But this pattern is also used in real life.

** For example .NET uses it to decorate the Streams **. In this case Stream is the abstract class, to which functionality is added with this pattern. If we need to handle a Stream in memory, we use a MemoryStream. If we need to deal with a text file, we use FileStream. And if we want to encode data we use a
CryptoStream. But we can also use several of these objects to decorate what in the end will be a Stream. For example, we can with a code similar to the following:


    FileStream fs = File.Open("archivo.txt", FileMode.Open);
    BufferedStream bs = new BufferedStream(fs);
    CryptoStream cs = new CryptoStream(bs, new CryptoAPITransform(), CryptoStreamMode.Read);
    this.PrintBytes(cs);

Summary for our pattern catalog

  • Pattern name: Decorator.
  • Kind: structural pattern.
  • We will use it when: we need to add functionalities to a class dynamically, avoiding class hierarchies that have to be built at compile time.
  • Advantage: we can add responsibilities to an object progressively and dynamically. More flexibility than with inheritance.
  • Disadvantages: the main one is that the Decorator object is not exactly the same as the class you are decorating, so we have to be careful. We can also find a design of very small classes, but in large numbers.
  • Similar or related patterns: Adapter or Facade.

In Genbeta Dev | Design Patterns: What They Are and Why You Should Use Them, Software Design Patterns

Image | Bon adrien