Design patterns serve as blueprints for solving common design problems. The Decorator Pattern is great for extending the functionality of objects at runtime, without altering their structure. This pattern is particularly useful in C#, a language known for its robustness and versatility in object-oriented programming.

What is the Decorator Pattern?

The Decorator Pattern is a structural design pattern that allows you to attach additional responsibilities to an object dynamically. Unlike class inheritance, which extends object functionalities at compile time, the Decorator Pattern achieves this at runtime, making it a flexible choice for extending capabilities. It adheres to the Open/Closed Principle, one of the SOLID principles, which states that software entities should be open for extension but closed for modification.

Practical Examples

Let’s see an example of how we can expand on a coffee:

public static void CoffeExample()
{
    var coffee = new Coffee();
    Console.WriteLine(coffee.GetCost()); // 5
    Console.WriteLine(coffee.GetDescription()); // Coffee

    var milkCoffee = new MilkDecorator(coffee);
    Console.WriteLine(milkCoffee.GetCost()); // 7
    Console.WriteLine(milkCoffee.GetDescription()); // Coffee, Milk

    var sugarCoffee = new SugarDecorator(milkCoffee);
    Console.WriteLine(sugarCoffee.GetCost()); // 8
    Console.WriteLine(sugarCoffee.GetDescription()); // Coffee, Milk, Sugar
}

public interface ICoffee
{
    int GetCost();
    string GetDescription();
}

public class Coffee : ICoffee
{
    public int GetCost()
    {
        return 5;
    }

    public string GetDescription()
    {
        return "Coffee";
    }
}

public class CoffeeDecorator : ICoffee
{
    private readonly ICoffee _coffee;

    public CoffeeDecorator(ICoffee coffee)
    {
        _coffee = coffee;
    }

    public virtual int GetCost()
    {
        return _coffee.GetCost();
    }

    public virtual string GetDescription()
    {
        return _coffee.GetDescription();
    }
}

public class MilkDecorator : CoffeeDecorator
{
    public MilkDecorator(ICoffee coffee) : base(coffee)
    {
    }

    public override int GetCost()
    {
        return base.GetCost() + 2;
    }

    public override string GetDescription()
    {
        return $"{base.GetDescription()}, Milk";
    }
}

public class SugarDecorator : CoffeeDecorator
{
    public SugarDecorator(ICoffee coffee) : base(coffee)
    {
    }

    public override int GetCost()
    {
        return base.GetCost() + 1;
    }

    public override string GetDescription()
    {
        return $"{base.GetDescription()}, Sugar";
    }
}

This example demonstrates the Decorator Pattern applied to a simple coffee ordering scenario in C#. The pattern is used here to dynamically add ingredients (and their costs) to a coffee object at runtime without changing the coffee object’s class

Let’s create a more concrete example that showcases the use of the Decorator Pattern with streams in .NET, specifically focusing on decorating a FileStream with a BufferedStream and a CryptoStream for encrypted writing to a file. This example will demonstrate how to compose different stream decorators to add buffering and encryption functionalities to file I/O operations.

public void StreamExample()
{
    string filePath = "encryptedData.txt";
    string originalMessage = "Hello, world! This is a test message.";

    using (Aes aes = Aes.Create())
    {
        using (FileStream fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None))
        using (CryptoStream cryptoStream = new CryptoStream(fileStream, aes.CreateEncryptor(aes.Key, aes.IV), CryptoStreamMode.Write))
        using (StreamWriter streamWriter = new StreamWriter(cryptoStream))
        {
            streamWriter.Write(originalMessage);
        }

        using (FileStream fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read))
        using (CryptoStream cryptoStream = new CryptoStream(fileStream, aes.CreateDecryptor(aes.Key, aes.IV), CryptoStreamMode.Read))
        using (StreamReader streamReader = new StreamReader(cryptoStream))
        {
            string decryptedMessage = streamReader.ReadToEnd();
            Console.WriteLine($"Decrypted message: {decryptedMessage}");
        }
    }
}
  • Writing to the file:
    • A FileStream is created to write to a specific file.
    • A CryptoStream then decorates the FileStream, adding encryption capabilities. Data written to the CryptoStream is automatically encrypted before being written to the underlying FileStream.
    • A StreamWriter is used to write the plaintext message to the CryptoStream. The data flows through the StreamWriter -> CryptoStream (where it gets encrypted) -> FileStream (where it’s written to disk).
  • Reading from the file:
    • A FileStream is opened to read the previously written file.
    • A CryptoStream decorates the FileStream for decryption, using the same key and IV for AES decryption.
    • A StreamReader reads the decrypted data from the CryptoStream.

This example clearly demonstrates the flexibility and power of the Decorator Pattern in .NET’s I/O system, allowing for elegant composition of functionalities like buffering, encryption, and file access without tightly coupling these functionalities to the core logic of reading or writing data.

Conclusion

The Decorator Pattern is a powerful tool offering a flexible approach to extending object functionality. By understanding and implementing this pattern, developers can produce cleaner, more maintainable code that adheres to the Open/Closed Principle. Personally, I hope to see this pattern being used a lot more.

If you love learning new stuff and want to support me, consider buying a course like From Zero to Hero: SOLID Principles for C# Developers or by using this link to buy a course from dometrain.com ❤

Leave a comment

Trending