Beginner

Design Patterns Overview

Design patterns have fascinated Steve for years. They look like individual tools that you can add to your toolbox as a software developer. They don't take long to introduce, but they can...

GitHub repository: DesignPatternsInCSharp


Table of Contents

  1. Course Overview
  2. Introduction to Design Patterns
  3. Origins of Design Patterns
  4. Why learn Design Patterns?
  5. How to learn Design Patterns?
  1. What makes up a Design Pattern?
  2. Structure of a Design Pattern (UML)
  3. When to apply Design Patterns?
  4. Demonstration: Practicing with Patterns (Kata Gilded Rose)
  1. Some good Design Patterns to know
  2. Key Takeaways
  3. Additional code examples

1. Course Overview

Welcome to this course titled Design Patterns Overview, presented by Steve Smith (aka ardalis), an experienced .NET developer, architect and trainer. Its in-person workshops and Pluralsight courses have helped thousands of developers and teams write better code, faster. You can find it online under the name ardalis.

Design patterns have fascinated Steve for years. They look like individual tools that you can add to your toolbox as a software developer. They don’t take long to introduce, but they can take a lot of practice to master.

Topics covered in this course

  • What are design patterns, and where do they come from?
  • Why should you spend time learning them?
  • How to approach this vast subject?
  • What are some good patterns to start with?

By the end of this course, you will understand how design patterns can help you be a more effective software developer, and you will have a proven strategy for starting to learn more about them.


2. Introduction to Design Patterns

According to Wikipedia’s definition, a software design pattern is a general, reusable solution to a common problem in a given context.

This course provides an introduction and overview of design patterns — a topic that every software developer should strive to understand. At the end of this course, you will know:

  • What are design patterns
  • How they are discovered and defined
  • And above all, how and when to apply them in your software

The big questions this course answers are:

  1. What are patterns? Where do they come from, and why bother learning them?
  2. How ​​to learn them? There are many, and there are many different approaches.
  3. When to apply them? It is very common for developers new to patterns to be a little overzealous in their application.
  4. How ​​to combine them? The presentation ends by introducing some common patterns and demonstrating how they can be combined with each other.

3. Origins of Design Patterns

Timeline

YearEvent
1977Christopher Alexander, architect, introduces the concept of patterns in the context of city construction in his book A Pattern Language: Towns, Buildings, Construction
1987Ward Cunningham and Kent Beck decided to use the patterns as part of a client project, providing the client with a limited set of patterns to use. Impressed with the results, they present their findings at the OOPSLA conference in Orlando
1991Erich Gamma is working on a doctoral thesis on patterns, which will lead to the development of the book Design Patterns with three co-authors
1994Publication of Design Patterns: Elements of Reusable Object-Oriented Software (copyright 1995)

The founding book of physical architecture

A Pattern Language: Towns, Buildings, Construction by Alexander (1977) is the first book to identify the concept of design patterns. It documents 253 different patterns in the context of cities, towns and buildings. This book:

  • Defined a way to describe patterns
  • Organized them into different groups according to their characteristics
  • Provided an extensive catalog of patterns on cities and their structures

The founding book of object-oriented programming

Design Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides (often called the Gang of Four or GoF) has:

  • Established a language to describe object-oriented design patterns
  • Organized these patterns by type: creational, structural and behavioral
  • Detailed 23 individual design patterns

Since then, dozens of patterns have been identified and documented in many aspects of software development.


4. Why learn Design Patterns?

Aren’t they reserved for architects and professors?

This is a preconceived idea. There are very good reasons to learn design patterns, no matter your level or role.

Reason 1: Avoid reinventing the wheel

Many problems you will encounter in your career have already been solved before. Learning to recognize them and know the applicable patterns allows you to avoid suboptimal solutions when a well-known and proven solution already exists.

Reason 2: Improve communication

Patterns have names and associated constructs that help communicate more clearly. Understanding the patterns your team uses is essential to communicating effectively.

Example of communication WITHOUT patterns:

“We have strong coupling with the database here. We can probably fix it by applying some refactorings. Maybe start by extracting an interface. Then we could extract a method that contains the persistence code. After that, we can take this method and put it in its own class…”

Example of communication WITH patterns:

“We have strong coupling with the database here. We should introduce the Repository pattern. »

The second version is much more concise. It communicates the exact same intent, but in a single term that, once both people understand what a Repository is, says everything the first version describes in many words. Patterns enable higher-level conversations with your team and communicate your intentions more clearly in your code.

Reason 3: Be more efficient and deliver better software

Often difficult problems or duplicate code implementations can be resolved elegantly using a design pattern. Knowing this, and even just knowing to look for a pattern if you don’t know one right away, can greatly improve your ability to deliver quality software.

Reason 4: Advance in your career

All of these reasons combined can help you advance and grow in your career. Today, most senior developers, and certainly every architect, should be familiar with the concept of design patterns and know enough to implement a few without assistance.


5. How to learn Design Patterns?

5.1 Learning stages

Based on Steve’s personal experience with patterns, but also in other fields such as sports and martial arts, there are distinct stages of learning:

Stage 1: Ignorance

You do not know that a concept, technique or pattern exists. It’s where everyone starts, but it’s also the easiest place to progress from.

Stage 2: Awakening

In this stage you become aware of the thing you want to learn about. You might say, “Wow, I just learned that pattern XYZ could help my design. I’m not sure how to apply it, but I’m excited to find out. » At this point, you have probably never actually applied the pattern; you’ve only heard about it from others.

Stage 3: Experimentation

As the saying goes, a little knowledge is a dangerous thing. The next stage is where experimentation and some of the real learning takes place. Knowing that there is a pattern that others have found useful, you can try to use it wherever you can. In general, it is less painful to practice new techniques in a controlled environment rather than in production.

Stage 4: Mastery

With enough practice, you will learn where the pattern applies and where it does not, until you reach the final stage. This stage requires you to actually apply the pattern yourself in a variety of contexts. You cannot hope to master a pattern that you have never applied. With mastery, you have internalized the skill or pattern. You’ve developed a kind of muscle memory for applying it that no longer requires you to break down the steps and consciously apply each one. When you review them, you no longer have to think about them one by one. When you see an opportunity to apply the pattern, you do it quickly and move on. You also recognize situations where the pattern might seem like a good choice, but you’ve tried it before and know better now.

5.2 T-shaped knowledge

When considering knowledge or skills, a common recommendation is to strive to be T-shaped, like the letter T. That is:

  • Have a broad understanding of general concepts (the horizontal bar of the T)
  • Have a deep practical understanding of a few specific things (the vertical bar of the T)

It is generally less useful to have only vague, encyclopedic knowledge of a wide range of topics, without the ability to actually deliver something of value using that knowledge. It’s also suboptimal to only have a deep understanding of a very small number of tools or techniques without understanding other ways of approaching things. We risk being a one-trick pony who applies his golden hammer to every problem.

Application to Design Patterns

You can apply the T-shaped concept of knowledge to design patterns:

  • You should be familiar with a wide range of patterns. When someone mentions observer or flyweight to attack a problem, you should immediately recognize that it is a specific pattern. It’s okay not to know it intimately, but your broad knowledge of patterns should allow you to at least participate in the discussion.
  • You’ll also know how to learn more about any specific pattern by quickly watching its associated Pluralsight course or finding a suitable book or online resource.
  • For the patterns most applicable to the type of software you deliver, it will be very useful to master at least a few. These are patterns you might use every day, and you should know them well enough to explain how they are used to new members of your team.
  • Eventually the number of patterns you have mastered is likely to grow, but there will probably always be patterns that you don’t use often enough to truly master.

Which patterns to explore? It depends on the type of applications you’re building, but Steve offers some suggestions at the end of this course.


6. What makes up a Design Pattern?

Typically, patterns are defined using a set of documentation that describes different aspects of the pattern. The Gang of Four book helped establish these well-defined sections that are still used today.

Sections of a pattern (according to GoF)

SectionDescription
Name and classificationThe main name of the pattern and its category (creational, structural, behavioral)
IntentWhat is the problem he is trying to solve? What is the purpose of the pattern?
Alias ​​(Also Known As)Other names this pattern is known by — important because different developers call it differently
MotivationA scenario or problem to which this pattern applies
ApplicabilityThe different situations in which this pattern could be used
StructureClass diagrams and interaction (sequence) diagrams, typically in UML, showing how different parts of the pattern interact
ParticipantsList of classes and objects used in the pattern, and their individual roles in its design
CollaborationDetailed view of how these classes and objects interact with each other — not only their individual responsibilities, but also their interactions
ConsequencesThe side effects or trade-offs that this pattern implies
ImplementationHow to concretely implement the pattern, with example code in a programming language
Known UsesReal world implementations of the pattern for future reference
Linked PatternsOther patterns related to this pattern, either because they can be used side by side or because they share a structure but differ in their intent

The minimum you need to know to achieve enlightenment

If you are simply aiming for breadth of knowledge, the minimum you should understand about a pattern to reach the awakening stage is:

  1. The names of the pattern — its main name and its aliases
  2. His purpose or intention — what he seeks to accomplish
  3. At least some scenarios — in what context this pattern is appropriate (covered by motivation and applicability)

This will help you recognize the pattern when you see it and remember that it might be applicable to a particular problem.


7. Structure of a Design Pattern (UML)

Design patterns in object-oriented programming are often described using UML diagrams.

Example: UML diagram of the Proxy pattern

+--------+       +-----------+       +-----------------+
| Client |------>| <<IService>> |<----| Proxy           |
|        |       | Service()  |       | - realService   |
|        |       +-----------+       | + Service()     |
|        |               ^           +-----------------+
|        |               |                    | appel
|        |         +-----------+     +-----------------+
|        |         | RealService|     |                 |
|        |         | + Service()|     |                 |
|        |         +-----------+     +-----------------+
+--------+

In this diagram:

  • The Client makes a call to a service that exposes a method
  • There is also a Proxy which calls a RealService
  • The implementation of this IService interface is carried out either by the Proxy or by the RealService
  • At runtime, we can have implementations for the Proxy as well as for the RealService that we can interchange in this pattern

UML diagram conventions

In these diagrams:

  • boxes typically represent classes, or sometimes interfaces
  • lines represent some kind of relationship: inheritance, interface implementation, composition, or invocation (call)
  • Each box can include details about the class members — typically important fields or properties at the top and methods at the bottom

Simple example: the Singleton pattern

Not all design patterns are complex. For example, the Singleton pattern diagram has only one class. This class has a private _instance field available and a static method called Instance which returns the singleton.

What you need to know about the structure

To really know a pattern:

  1. Understand the structure — be able to recognize it from its unlabeled diagram
  2. Be aware that multiple patterns often share the same structure and differ only in their intent and applicability
  3. Know the participants and how they interact
  4. Know how to implement the pattern in your programming language of choice
  5. Understand the consequences and trade-offs involved in using this particular pattern

8. When to apply Design Patterns?

Start in a controlled practice environment

When thinking about how to apply a pattern, it’s a good idea to start in a controlled practice environment:

  1. Do a coding exercise or coding kata — write tests to check your understanding of how the pattern works
  2. Write tests before applying the pattern, then apply the pattern and verify that these tests continue to pass
  3. Repeat several times with different variations — try applying a different pattern you already know, compare and contrast different behaviors
  4. Also practice on real code in own branch, but then probably delete it

Best practices when refactoring in production

If you are working on real code, make sure you follow the fundamentals of refactoring (Refactoring Fundamentals or Refactoring for C# Developers course on Pluralsight):

  • Have test coverage — whenever you do a refactoring, it’s a great idea to have tests in place that will verify that your code hasn’t been broken
  • Work in a separate branch, then use a pull request or similar tool to merge
  • Ideally have someone review the code before merging it into the main branch
  • Check that behavior is still consistent after refactoring

Accepting failure in practice

If you run into problems, if you’re stuck because you’re not very familiar with this pattern yet, be prepared to simply delete and start again. It’s better to throw away the code and come away with the experience and knowledge you’ve gained than to try to force a pattern into your production code where it really doesn’t fit.


9. Demonstration: Practicing with Patterns (Kata Gilded Rose)

9.1 Context of the kata

For this demonstration, Steve uses a programming kata — more precisely the Gilded Rose kata, one of his favorite katas, available in his kata-catalog on GitHub.

The Gilded Rose kata is a very flexible refactoring kata. What sets it apart from many other exercises is that it comes with seed code. The kata includes the requirements for a system that will update a store’s items. Items have a Quality property and a SellIn property which will be changed every night by a scheduled task.

The original code (starter version) includes a GildedRose class with an UpdateQuality method containing complex if code. The goal of the exercise is to clean up this code so that it is easier to follow.

The Item class is as follows — it cannot be modified (kata rule):

namespace DesignPatternsInCSharp.KataWithPatterns
{
    /// <summary>
    /// Source: https://github.com/emilybache/GildedRose-Refactoring-Kata/tree/master/csharpcore
    /// Cannot touch this code (except to make properties virtual, if desired)
    /// </summary>
    public class Item
    {
        public string Name { get; set; }
        public int SellIn { get; set; }
        public int Quality { get; set; }
    }
}

Steve has already written tests. Some of them fail because it hasn’t yet implemented the last item type called ConjuredItems. The full repository is available on GitHub under the name DesignPatternsInCSharp.

9.2 Pattern Proxy — ItemProxy

The problem to be solved

Looking at the original complex if code, we see a lot of places where it says: “if Quality is greater than 0” or “if Quality is less than 50”. The goal is to provide a way to avoid decrementing below 0 or incrementing above 50.

The solution: introduce a Proxy

The pattern Proxy wraps the target object and controls access to it. We cannot manipulate the object directly — we must go through the proxy, which provides controlled access to the underlying object.

In this case, the Name, the SellIn and the Quality are exposed directly for reading, but they cannot simply be defined. To modify the values, we must call a method — and these methods encode the validation rules:

namespace DesignPatternsInCSharp.KataWithPatterns
{
    public class ItemProxy
    {
        private Item _item;
        public ItemProxy(Item item)
        {
            _item = item;
        }

        public string Name => _item.Name;
        public int SellIn => _item.SellIn;
        public int Quality => _item.Quality;

        public void IncrementQuality()
        {
            if (_item.Quality < 50)
            {
                _item.Quality++;
            }
        }

        public void DecrementQuality()
        {
            if (_item.Quality > 0)
            {
                _item.Quality--;
            }
        }

        public void ResetQuality()
        {
            _item.Quality = 0;
        }

        public void DecrementSellIn()
        {
            _item.SellIn--;
        }
    }
}

Profit obtained

With this proxy in place, all tests continue to pass. Additionally, anywhere in the code where there were range checks (if Quality > 0, if Quality < 50), we can remove them. The proxy is now responsible for these checks.

9.3 Pattern Rules Engine — RuleBase and rules

The following problem

After introducing the proxy, business rules are still encoded in the UpdateQuality method as nested if statements and duplicate code.

The solution: extract rules into classes

Steve first shows the extraction of helper methods, then these helpers are transformed into independent rule classes. Each rule is a first class which:

  • Knows if it applies to an item (IsMatch method)
  • Contains logic to update the item (UpdateItem method)

Here is the abstract base class:

using System;

namespace DesignPatternsInCSharp.KataWithPatterns
{
    public abstract class RuleBase
    {
        public abstract bool IsMatch(ItemProxy item);
        public void UpdateItem(ItemProxy item)
        {
            AdjustQuality(item);

            AdjustSellIn(item);

            if (item.SellIn < 0)
            {
                AdjustQualityForNegativeSellIn(item);
            }
        }

        public abstract void AdjustQuality(ItemProxy item);
        public virtual void AdjustSellIn(ItemProxy item)
        {
            item.DecrementSellIn();
        }
        public abstract void AdjustQualityForNegativeSellIn(ItemProxy item);
    }
}

NormalItemRule — the default rule

namespace DesignPatternsInCSharp.KataWithPatterns
{
    public class NormalItemRule : RuleBase
    {
        public override bool IsMatch(ItemProxy item)
        {
            // default rule — corresponds à tout article qui ne correspond à rien d'autre
            return true;
        }

        public override void AdjustQuality(ItemProxy item)
        {
            item.DecrementQuality();
        }

        public override void AdjustQualityForNegativeSellIn(ItemProxy item)
        {
            item.DecrementQuality();
        }
    }
}

AgedBrieRule — quality increases over time

namespace DesignPatternsInCSharp.KataWithPatterns
{
    public class AgedBrieRule : RuleBase
    {
        public override void AdjustQuality(ItemProxy item)
        {
            item.IncrementQuality();
        }

        public override void AdjustQualityForNegativeSellIn(ItemProxy item)
        {
            item.IncrementQuality();
        }

        public override bool IsMatch(ItemProxy item)
        {
            return item.Name == "Aged Brie";
        }
    }
}

BackstagePassesRule — quality increases, then drops to 0

namespace DesignPatternsInCSharp.KataWithPatterns
{
    public class BackstagePassesRule : RuleBase
    {
        public override bool IsMatch(ItemProxy item)
        {
            return item.Name == "Backstage passes to a TAFKAL80ETC concert";
        }

        public override void AdjustQuality(ItemProxy item)
        {
            item.IncrementQuality();
            if (item.SellIn < 11)
            {
                item.IncrementQuality();
            }

            if (item.SellIn < 6)
            {
                item.IncrementQuality();
            }
        }

        public override void AdjustQualityForNegativeSellIn(ItemProxy item)
        {
            item.ResetQuality();
        }
    }
}

SulfurasRule — quality never changes

namespace DesignPatternsInCSharp.KataWithPatterns
{
    public class SulfurasRule : RuleBase
    {
        public override bool IsMatch(ItemProxy item)
        {
            return item.Name == "Sulfuras, Hand of Ragnaros";
        }

        public override void AdjustQuality(ItemProxy item)
        {
            // ne rien faire
        }

        public override void AdjustSellIn(ItemProxy item)
        {
            // ne rien faire — Sulfuras ne vieillit jamais
        }

        public override void AdjustQualityForNegativeSellIn(ItemProxy item)
        {
            // ne rien faire
        }
    }
}

ConjuredItemRule — quality drops twice as fast

namespace DesignPatternsInCSharp.KataWithPatterns
{
    public class ConjuredItemRule : RuleBase
    {
        public override bool IsMatch(ItemProxy item)
        {
            return item.Name == "Conjured Mana Cake";
        }

        public override void AdjustQuality(ItemProxy item)
        {
            item.DecrementQuality();
            item.DecrementQuality();
        }

        public override void AdjustQualityForNegativeSellIn(ItemProxy item)
        {
            item.DecrementQuality();
            item.DecrementQuality();
        }
    }
}

9.4 Pattern Builder — ItemQualityRuleEngine

The problem

After extracting the rules into separate classes, the GildedRose class still needs to know how to create and order these rules. The NormalItemRule must always be in last position in the list — this important detail is not forced by the code, which can lead to errors.

The solution: Builder Pattern

Steve creates an ItemQualityRuleEngine with a private constructor and an inner class Builder (nested class) which forces the correct construction of the engine. This guarantees:

  • That the engine can only be created with the Builder
  • That the NormalItemRule is always added last automatically in Build()
using System.Collections.Generic;
using System.Linq;

namespace DesignPatternsInCSharp.KataWithPatterns
{
    public class ItemQualityRuleEngine
    {
        private List<RuleBase> _rules = new List<RuleBase>();
        
        // Constructeur privé — ne peut être créé qu'avec le Builder
        private ItemQualityRuleEngine(List<RuleBase> rules)
        {
            _rules = rules;
        }

        public void ApplyRules(ItemProxy item)
        {
            foreach (var rule in _rules)
            {
                if (rule.IsMatch(item))
                {
                    rule.UpdateItem(item);
                    break; // une seule règle par article
                }
            }
        }

        public class Builder
        {
            private List<RuleBase> _builderRules = new List<RuleBase>();

            public Builder WithAgedBrieRule()
            {
                _builderRules.Add(new AgedBrieRule());
                return this;
            }

            public Builder WithSulfurasRule()
            {
                _builderRules.Add(new SulfurasRule());
                return this;
            }

            public Builder WithConjuredItemRule()
            {
                _builderRules.Add(new ConjuredItemRule());
                return this;
            }

            public Builder WithBackstagePassesRule()
            {
                _builderRules.Add(new BackstagePassesRule());
                return this;
            }

            public ItemQualityRuleEngine Build()
            {
                // La NormalItemRule est TOUJOURS ajoutée en dernier — aucune exception possible
                _builderRules.Add(new NormalItemRule());

                return new ItemQualityRuleEngine(_builderRules);
            }
        }
    }
}

The final GildedRose class

using System.Collections.Generic;

namespace DesignPatternsInCSharp.KataWithPatterns
{
    public class GildedRose
    {
        IList<Item> Items;
        public GildedRose(IList<Item> Items)
        {
            this.Items = Items;
        }

        public void UpdateQuality(ItemProxy item)
        {
            var engine = new ItemQualityRuleEngine.Builder()
                .WithAgedBrieRule()
                .WithBackstagePassesRule()
                .WithConjuredItemRule()
                .WithSulfurasRule()
                .Build();
            engine.ApplyRules(item);
        }

        public void UpdateQuality()
        {
            for (var i = 0; i < Items.Count; i++)
            {
                UpdateQuality(new ItemProxy(Items[i]));
            }
        }
    }
}

The UpdateQuality method is now very simple. It delegates to the ItemQualityRuleEngine and calls its ApplyRules method. The order of the rules is protected by the Builder.

9.5 Pattern Template Method — RuleBase

The problem

Looking at AgedBrieRule and NormalItemRule, we see that they both follow the same skeleton:

  1. Adjust Quality
  2. Decrement the SellIn
  3. If SellIn < 0, adjust the Quality again

This sequencing is duplicated in each rule.

The solution: Template Method Pattern

The pattern Template Method is applied in the base class RuleBase. The UpdateItem method is the template method — it is:

  • Non-virtual (we cannot overload it)
  • Non-abstract (it has a concrete implementation)

It calls three methods that subclasses must implement:

  • AdjustQualityabstract, every rule must implement it
  • AdjustSellInvirtual with default behavior (decrement), only SulfurasRule overrides it
  • AdjustQualityForNegativeSellInabstract, every rule must implement it
public abstract class RuleBase
{
    public abstract bool IsMatch(ItemProxy item);
    
    // Template Method — séquence fixe, ne peut pas être surchargée
    public void UpdateItem(ItemProxy item)
    {
        AdjustQuality(item);
        AdjustSellIn(item);
        if (item.SellIn < 0)
        {
            AdjustQualityForNegativeSellIn(item);
        }
    }

    public abstract void AdjustQuality(ItemProxy item);
    
    // Comportement par défaut : décrémenter SellIn
    public virtual void AdjustSellIn(ItemProxy item)
    {
        item.DecrementSellIn();
    }
    
    public abstract void AdjustQualityForNegativeSellIn(ItemProxy item);
}

By doing so, the sequencing logic (if SellIn < 0) is now encoded only once in the base class and does not need to be replicated in each individual rule. Only SulfurasRule overrides AdjustSellIn because it is the only rule that never changes its SellIn.

9.6 Kata unit tests

Tests for normal items

using System.Collections.Generic;
using Xunit;

namespace DesignPatternsInCSharp.KataWithPatterns
{
    public class GildedRoseUpdateQualityGivenNormalItem
    {
        private List<Item> _items = new List<Item>();
        private Item _item;
        private GildedRose _service;
        private const int INITIAL_QUALITY = 10;
        private const int INITIAL_SELL_IN = 20;

        public GildedRoseUpdateQualityGivenNormalItem()
        {
            _service = new GildedRose(_items);
            _item = GetNormalItem();
            _items.Add(_item);
        }

        private Item GetNormalItem()
        {
            return new Item { Name = "Normal Item", Quality = INITIAL_QUALITY, SellIn = INITIAL_SELL_IN };
        }

        [Fact]
        public void ReducesNormalItemQualityBy1GivenPositiveSellIn()
        {
            _service.UpdateQuality();

            Assert.Equal(INITIAL_QUALITY - 1, _item.Quality);
        }

        [Fact]
        public void ReducesNormalItemQualityBy2GivenNonPositiveSellIn()
        {
            _item.SellIn = 0;
            _service.UpdateQuality();

            Assert.Equal(INITIAL_QUALITY - 2, _item.Quality);
        }

        [Fact]
        public void DoesNotReduceQualityBelowZero()
        { 
            _item.Quality = 0;
            _service.UpdateQuality();

            Assert.Equal(0, _item.Quality);
        }
    }
}

Tests for Aged Brie

using System.Collections.Generic;
using Xunit;

namespace DesignPatternsInCSharp.KataWithPatterns
{
    public class GildedRoseUpdateQualityGivenAgedBrie
    {
        private List<Item> _items = new List<Item>();
        private Item _item;
        private GildedRose _service;
        private const int INITIAL_QUALITY = 10;
        private const int INITIAL_SELL_IN = 20;

        public GildedRoseUpdateQualityGivenAgedBrie()
        {
            _service = new GildedRose(_items);
            _item = GetAgedBrie();
            _items.Add(_item);
        }

        private Item GetAgedBrie()
        {
            return new Item { Name = "Aged Brie", Quality = INITIAL_QUALITY, SellIn = INITIAL_SELL_IN };
        }

        [Fact]
        public void IncreasesAgedBrieQualityBy1GivenPositiveSellIn()
        {
            _service.UpdateQuality();

            Assert.Equal(INITIAL_QUALITY + 1, _item.Quality);
        }

        [Fact]
        public void IncreasesAgedBrieQualityBy2GivenNonPositiveSellIn()
        {
            _item.SellIn = 0;
            _service.UpdateQuality();

            Assert.Equal(INITIAL_QUALITY + 2, _item.Quality);
        }

        [Fact]
        public void DoesNotIncreaseQualityBeyond50()
        {
            _item.Quality = 50;
            _service.UpdateQuality();

            Assert.Equal(50, _item.Quality);
        }

        [Theory]
        [InlineData(48)]
        [InlineData(49)]
        [InlineData(50)]
        public void DoesNotIncreaseQualityAbove50GivenNonPositiveSellIn(int initialQuality)
        {
            _item.SellIn = 0;
            _item.Quality = initialQuality;
            _service.UpdateQuality();
            // La quality ne peut jamais dépasser 50
        }
    }
}

Summary of patterns applied in the demo

In this demonstration, we saw how to use:

PatternApplication in kata
ProxyItemProxy controls access to Item, enforces range rules (0-50)
Rules EngineEach rule is an independent class that says whether it applies and how to update the article
BuilderItemQualityRuleEngine.Builder builds the engine flexibly and ensures that NormalItemRule is always last
Template MethodRuleBase.UpdateItem() defines the sequence of operations; subclasses customize every step

10. Some good Design Patterns to know

Steve promised to recommend some patterns before the end of the course. Here are good patterns for C# developers looking for a place to start.

1. Strategy

The Strategy pattern is one of Steve’s favorites. If you work with systems that use dependency injection, you are already somewhat familiar with the Strategy pattern. It’s definitely worth becoming more expert with this pattern, recognizing where it applies and how to apply it with different variations.

2. Repository

The Repository pattern is a data access pattern that works very well with the Strategy pattern. There are many ways to apply it (and some incorrect ways), but overall it tends to do a very good job of decoupling your system from its persistence concerns.

A particularly powerful example is Cached Repository: take Repository and combine it with other patterns like Decorator and Strategy, to easily achieve huge performance gains in your applications.

3. Adapt

The Adapter pattern is one of the most common patterns. You probably already know it, even if you don’t necessarily recognize its name or structure. Once you understand how the Adapter pattern works, you’ll see opportunities to use it everywhere in your code, even after you get out of the overzealous learning stage.

4. Factory (and IoC Container)

Objects in our systems can sometimes be difficult to create. The Factory pattern is an excellent pattern to recognize and understand. This pattern is closely related to the concept of an IoC (Inversion of Control) container, which Steve likes to describe as “a factory on steroids”. If you have a good understanding of the Factory pattern, it will also help you better understand dependency injection and IoC containers.

5. Proxy and Decorator

These two patterns are very closely related but have different intentions. Structurally, they are almost identical, but how they are used and why they are used differ. Both are very common and very powerful patterns, and they are extremely useful for stacking with other patterns.

6. Singleton

The Singleton pattern was almost absent from the Gang of Four book because they didn’t think it was a good pattern to discuss. Eventually it was included, but many today consider it an anti-pattern. You should understand how to apply the singleton the right way because it’s easy to get it wrong, but you should also understand how to solve the problem that the singleton solves in other ways that might be more appropriate.

Additional Tips

  • For those interested in Domain-Driven Design (DDD), the DDD Fundamentals course also introduces the basic patterns used in DDD.
  • Always look for ways to combine patterns with each other. Often the most powerful and elegant solutions result from applying a few different patterns in combination with each other.

11. Key Takeaways

Here are the key points of this Design Patterns Overview course:

1. Design patterns are general solutions to existing problems

They apply to a certain context, and initially, it wasn’t even a software concept — the idea was borrowed from urban planning and building architecture.

2. Patterns are worth knowing for several reasons

  • Avoid reinventing the wheel — a well-known and proven solution often already exists
  • Have higher-level conversations with your team
  • Communicate your intentions more clearly in your code

3. T-shaped knowledge

Although the topic of design patterns is quite broad, the value of having a T-shaped knowledge is significant:

  • Be familiar with a wide range of patterns (width)
  • Delve deeper into the patterns most relevant to the type of software you are writing (depth)

4. Practice before applying in production

When it comes time to apply patterns, especially while you’re still learning them:

  • Make sure you practice
  • Make sure you follow good refactoring techniques
  • There are several refactoring courses available on Pluralsight

5. Seek to combine patterns

The most powerful and elegant patterns often come from applying several different patterns in combination.


12. Additional code examples

12.1 Singleton Pattern — evolution in C#

The Singleton is a pattern that ensures that a class has only one instance and provides a global access point to that instance. Here is the evolution of its implementation in C#, from the most naive to the most robust.

Version 1: Naive (thread safety problem)

namespace DesignPatternsInCSharp.Singleton.v1
{
    // Bad code — non thread-safe
    public sealed class Singleton
    {
        private static Singleton? _instance;

        public static Singleton Instance
        {
            get
            {
                Logger.Log("Instance called.");
                return _instance ??= new Singleton();
            }
        }

        private Singleton()
        {
            // ne peut pas être créé en dehors de cette classe
            Logger.Log("Constructor invoked.");
        }
    }
}

Problem: In a multi-threaded environment, two threads can check _instance == null simultaneously and create two different instances.

Version 2: With lock (but ineffective)

namespace DesignPatternsInCSharp.Singleton.v2
{
    // Bad code — lock sur CHAQUE accès
    public sealed class Singleton
    {
        private static Singleton _instance = null;
        private static readonly object padlock = new object();

        public static Singleton Instance
        {
            get
            {
                Logger.Log("Instance called.");
                lock (padlock) // ce lock est utilisé sur CHAQUE référence au Singleton
                {
                    if (_instance == null)
                    {
                        _instance = new Singleton();
                    }
                    return _instance;
                }
            }
        }

        private Singleton()
        {
            Logger.Log("Constructor invoked.");
        }
    }
}

Problem: The lock is acquired on each access, even after the instance has been created. This is costly in performance.

Version 3: Double-checked locking

namespace DesignPatternsInCSharp.Singleton.v3
{
    // Bad code — problèmes de visibilité mémoire potentiels
    public sealed class Singleton
    {
        private static Singleton _instance = null;
        private static readonly object padlock = new object();

        public static Singleton Instance
        {
            get
            {
                Logger.Log("Instance called.");
                if (_instance == null) // lock uniquement si l'instance est null
                {
                    lock (padlock)
                    {
                        if (_instance == null)
                        {
                            _instance = new Singleton();
                        }
                    }
                }
                return _instance;
            }
        }

        private Singleton()
        {
            Logger.Log("Constructor invoked.");
        }
    }
}

Version 4: Static initialization (Less Lazy, but safe)

namespace DesignPatternsInCSharp.Singleton.v4
{
    // Source: https://csharpindepth.com/articles/singleton
    public sealed class Singleton
    {
        private static readonly Singleton _instance = new Singleton();

        // Lire ceci va initialiser _instance
        public static readonly string GREETING = "Hi!";

        // Indique au compilateur C# de ne pas marquer le type comme beforefieldinit
        static Singleton()
        {
        }

        public static Singleton Instance
        {
            get
            {
                Logger.Log("Instance called.");
                return _instance;
            }
        }

        private Singleton()
        {
            Logger.Log("Constructor invoked.");
        }
    }
}

Version 5: Nested Lazy Inner Class

namespace DesignPatternsInCSharp.Singleton.v5
{
    // Source: https://csharpindepth.com/articles/singleton
    public sealed class Singleton
    {
        public static readonly string GREETING = "Hi!";
        
        public static Singleton Instance
        {
            get
            {
                Logger.Log("Instance called.");
                return Nested._instance;
            }
        }

        private class Nested
        {
            // Empêche beforefieldinit
            static Nested()
            {
            }
            internal static readonly Singleton _instance = new Singleton();
        }

        private Singleton()
        {
            Logger.Log("Constructor invoked.");
        }
    }
}
using System;

namespace DesignPatternsInCSharp.Singleton.v6
{
    // Source: https://csharpindepth.com/articles/singleton
    public sealed class Singleton
    {
        // Lire ceci initialisera l'instance
        public static readonly Lazy<Singleton> _lazy = new Lazy<Singleton>(() => new Singleton());
        
        public static Singleton Instance
        {
            get
            {
                Logger.Log("Instance called.");
                return _lazy.Value;
            }
        }

        private Singleton()
        {
            Logger.Log("Constructor invoked.");
        }
    }
}

Lazy<T> is thread-safe by default and ensures that the constructor is only called once, no matter how many threads attempt to access Instance simultaneously.

Singleton testability issue

Direct static access to the Singleton is problematic for unit tests (anti-pattern Static Cling):

namespace DesignPatternsInCSharp.Singleton
{
    public class SingletonTesting
    {
        private readonly v6.Singleton _singleton;
        private readonly IServiceThatSingletonImplements _singleton2;

        // L'injection de dépendances est correcte
        public SingletonTesting(v6.Singleton singleton)
        {
            _singleton = singleton;
        }

        // Encore mieux : utiliser une interface
        public SingletonTesting(IServiceThatSingletonImplements singleton)
        {
            _singleton2 = singleton;
        }

        // L'accès statique est PROBLÉMATIQUE pour les tests
        public void DoSomething2()
        {
            // Ceci rend très difficile de tester cette méthode unitairement
            // Ceci est un exemple du code smell "Static Cling"
            // v6.Singleton.Instance.SaveToDatabase(data);
        }
    }
}

Key Lesson: If you use a Singleton, inject it via an interface rather than accessing it directly via Singleton.Instance in your business logic. This allows a different implementation to be substituted during testing.

12.2 Pattern Adapter — display of Star Wars characters

The Adapter pattern allows you to adapt an incompatible interface for use by a client. In this example, several different data sources (Star Wars API, local JSON file, third-party API) are adapted to work with a display service that always expects the same type of data.

The common interface (abstract class)

using System.Collections.Generic;
using System.Threading.Tasks;

namespace DesignPatternsInCSharp.Adapter
{
    public abstract class PeopleDataAdapter
    {
        public abstract Task<List<Person>> GetPeople();
    }
}

Adapter for Star Wars API (SWAPI)

using Newtonsoft.Json;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;

namespace DesignPatternsInCSharp.Adapter
{
    public class StarWarsApiPeopleAdapter : PeopleDataAdapter
    {
        public override async Task<List<Person>> GetPeople()
        {
            var people = new List<Person>();

            using (var client = new HttpClient())
            {
                string url = ApiConstants.SWAPI_PEOPLE_ENDPOINT;
                string result = await client.GetStringAsync(url);
                people = JsonConvert.DeserializeObject<ApiResult<Person>>(result).Results;
            }

            return people;
        }
    }
}

Adapt for a local JSON file

using Newtonsoft.Json;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;

namespace DesignPatternsInCSharp.Adapter
{
    public class LocalFilePeopleAdapter : PeopleDataAdapter
    {
        public override async Task<List<Person>> GetPeople()
        {
            string filePath = @"Adapter/People.json";
            return JsonConvert.DeserializeObject<List<Person>>(await File.ReadAllTextAsync(filePath));
        }
    }
}

The display service — source independent

using System.Text;
using System.Threading.Tasks;

namespace DesignPatternsInCSharp.Adapter
{
    public class StarWarsCharacterDisplayService
    {
        private readonly PeopleDataAdapter _peopleDataAdapter;

        public StarWarsCharacterDisplayService(PeopleDataAdapter peopleDataAdapter)
        {
            _peopleDataAdapter = peopleDataAdapter;
        }

        public async Task<string> ListCharacters()
        {
            var people = await _peopleDataAdapter.GetPeople();

            var sb = new StringBuilder();
            int nameWidth = 20;
            sb.AppendLine($"{"NAME".PadRight(nameWidth)}   {"GENDER"}");
            foreach (Person person in people)
            {
                sb.AppendLine($"{person.Name.PadRight(nameWidth)}   {person.Gender}");
            }

            return sb.ToString();
        }
    }
}

Tests demonstrating interchangeability

using System.Threading.Tasks;
using Xunit;

namespace DesignPatternsInCSharp.Adapter
{
    public class AdapterTests
    {
        [Fact]
        public async Task ListCharactersGivenStarWarsApiAdapterShouldReturnSomething()
        {
            var swAdapter = new StarWarsApiPeopleAdapter();
            var service = new StarWarsCharacterDisplayService(swAdapter);

            await CallListCharactersAndAssertContainsExpectedCharacters(service);
        }

        [Fact]
        public async Task ListCharactersGivenLocalFileAdapterShouldReturnSomething()
        {
            var localAdapter = new LocalFilePeopleAdapter();
            var service = new StarWarsCharacterDisplayService(localAdapter);

            await CallListCharactersAndAssertContainsExpectedCharacters(service);
        }

        [Fact]
        public async Task ListCharactersGivenThirdPartyApiAdapterShouldReturnSomething()
        {
            var adapter = new ThirdPartyApiAdapter();
            var service = new StarWarsCharacterDisplayService(adapter);

            await CallListCharactersAndAssertContainsExpectedCharacters(service);
        }

        private static async Task CallListCharactersAndAssertContainsExpectedCharacters(
            StarWarsCharacterDisplayService service)
        {
            var result = await service.ListCharacters();

            Assert.Contains("Luke Skywalker", result);
            Assert.Contains("Darth Vader", result);
        }
    }
}

Benefit: The StarWarsCharacterDisplayService has no knowledge of the data source. New adapters can easily be added without changing the service.

12.3 Pattern Proxy Protector — document access control

The Protective Proxy controls access to an object by checking permissions before allowing operations.

using System;
using System.Collections.Generic;

namespace DesignPatternsInCSharp.Proxy.ProtectiveProxy
{
    public class Document
    {
        public static Document CreateDocument(string name, string content)
        {
            // La factory method retourne toujours un ProtectedDocument
            return new ProtectedDocument(name, content);
        }

        protected Document(string name, string content)
        {
            Name = name;
            Content = content;
        }

        public int Id { get; private set; }
        public string Name { get; private set; }
        public IEnumerable<string> Tags { get; private set; }
        public string Content { get; private set; }
        public DateTime DateCreated { get; private set; } = DateTime.UtcNow;
        public DateTime? DateReviewed { get; private set; }

        internal virtual void CompleteReview(User editor)
        {
            DateReviewed = DateTime.UtcNow;
        }

        internal virtual void UpdateName(string newName, User user)
        {
            Name = newName;
        }
    }
}
using System;

namespace DesignPatternsInCSharp.Proxy.ProtectiveProxy
{
    public class ProtectedDocument : Document
    {
        public ProtectedDocument(string name, string content) : base(name, content)
        {
        }

        internal override void UpdateName(string newName, User user)
        {
            if (user.Role != Roles.Author)
            {
                throw new UnauthorizedAccessException(
                    "Cannot update name unless in Author role.");
            }
            base.UpdateName(newName, user);
        }

        internal override void CompleteReview(User editor)
        {
            if (editor.Role != Roles.Editor)
            {
                throw new UnauthorizedAccessException(
                    "Cannot review documents unless you are an Editor.");
            }
            base.CompleteReview(editor);
        }
    }
}

Benefit: The client still receives a Document (via CreateDocument), but in reality it obtains a ProtectedDocument which imposes the authorization rules.

12.4 Virtual Proxy Pattern — lazy loading

The Virtual Proxy delays the instantiation of an expensive object until it is actually needed.

using System;
using System.Collections.Generic;

namespace DesignPatternsInCSharp.Proxy.VirtualProxy
{
    public class LazyExpensiveToFullyLoad : BaseClassWithHistory
    {
        private Lazy<IEnumerable<ExpensiveEntity>> _homeEntities;
        public IEnumerable<ExpensiveEntity> HomeEntities 
        { 
            get { return _homeEntities.Value; } 
        }

        private Lazy<IEnumerable<ExpensiveEntity>> _awayEntities;
        public IEnumerable<ExpensiveEntity> AwayEntities 
        { 
            get { return _awayEntities.Value; } 
        }

        public LazyExpensiveToFullyLoad()
        {
            History.Add("Constructor called.");
            // Les données ne sont PAS chargées ici
            _homeEntities = new Lazy<IEnumerable<ExpensiveEntity>>(
                () => ExpensiveDataSource.GetEntities(this));
            _awayEntities = new Lazy<IEnumerable<ExpensiveEntity>>(
                () => ExpensiveDataSource.GetEntities(this));
        }
    }
}

Benefit: The construction of the object is fast. ExpensiveEntities are only loaded from the data source when .HomeEntities or .AwayEntities are accessed for the first time.


13. Resources and references

ResourceLink
Trainer GitHub repositoryhttps://github.com/ardalis/DesignPatternsInCSharp
Kata Catalog (ardalis)https://github.com/ardalis/kata-catalog
Kata Gilded Rose (Emily Bache)https://github.com/emilybache/GildedRose-Refactoring-Kata
Personal websitehttps://ardalis.com
Advice Podcasthttps://weeklydevtips.com
Course on Singletons (reference)https://csharpindepth.com/articles/singleton
Recommended Pluralsight coursesRefactoring for C# Developers, DDD Fundamentals


Search Terms

design · patterns · testing · architecture · c# · .net · development · pattern · proxy · version · lazy · quality · reason · singleton · stage · tests · class · kata · know · uml · adapt · adapter · additional · book

Interested in this course?

Contact us to book it or get a custom training plan for your team.