DEV Community

Cover image for 11 Key Design Patterns
Sukhpinder Singh
Sukhpinder Singh

Posted on • Originally published at Medium

11 Key Design Patterns

Unlock the secrets of software architecture with Mastering Software Architecture: 11 Key Design Patterns Explained.

Table of Contents

  1. Design Pattern — Abstract Factory
  • Learning Objectives

  • Getting Started

  • How to use an abstract factory provider?

  • Output

2. Design Pattern — Adapter

  • Use Case

  • Learning Objectives

  • Getting Started

3. Design Pattern — Builder

  • Use Case

  • Learning Objectives

  • Getting Started

  • How to use the builder pattern from the Main() method

  • Output

4. How to use the Chain of Responsibility Pattern

  • Use Case

  • Getting Started

  • How to use the Chain of Responsibility pattern?

  • Output

5. Design Pattern — Decorator

  • Use Case

  • Learning Objectives

  • Getting Started

  • Decorator Pattern in action

  • Complete Code

  • Output

6. Design Pattern — Factory Method

  • Learning Objectives

  • Getting Started

  • How to use the factory method?

  • Output

7. Design Pattern — Iterator

  • Use Case

  • Getting Started

  • Iterator Pattern in Action

  • Output

8. Design Pattern — Mediator

  • Use Case

  • Learning Objectives

  • Getting Started

  • How to use the mediator pattern from the main method

9. Design Pattern — Observer

  • Use Case

  • Learning Objectives

  • Getting Started

  • How to use an observer pattern?

  • Output

10. Advance Property Pattern C# 8.0

  • Let’s Start

  • Pattern matching program with new switch syntax

  • Test program

  • Console Output

11. Design Pattern — Singleton

  • Learning Objectives

  • Getting Started

  • Output

  • Thread Safety

Design Pattern — Abstract Factory

According to Gang of Four, abstract factory patterns can be assumed as the factory for creating factories.

Learning Objectives

  • What is the abstract factory design pattern?

  • How to write code using the abstract factory design pattern?

  • How to create a factory provider?

  • How to create a client application(from the Main method) that uses a factory provider

Prerequisites

Abstract factory pattern is purely an extension factory method; it’s recommended to go through the factory method before understanding abstract factory design.

  • Basic knowledge of OOPS concepts.

  • Any programming language knowledge.

Getting Started

Let’s consider the same example of any Bank with account types such as Savings and Current accounts. Now let’s implement the above example using the abstract factory design pattern.

Firstly, implement ISavingAccount and ICurrentAccount interfaces as follows:

    public interface ISavingAccount{  }
    public interface ICurrentAccount{  }
Enter fullscreen mode Exit fullscreen mode

Inherit the interface in the classes below

    public class CurrentAccount : ICurrentAccount
    {
       public CurrentAccount(string message)
       {
        Console.WriteLine(message);
       }
    }
    public class SavingsAccount : ISavingAccount
    {
       public SavingsAccount( string message)
       {
        Console.WriteLine(message);
       }
    }
Enter fullscreen mode Exit fullscreen mode

Let’s write an abstract class with abstract methods for each account type.

    public abstract class AccountTypeFactory
    {
      public abstract ISavingAccount SavingAccountFactory(string message);
      public abstract ICurrentAccount CurrentAccountFactory(string message);
    }
Enter fullscreen mode Exit fullscreen mode

Now, let’s create a factory implementation named “Bank1Factory,” which provides the implementation of abstract methods.

    public class Bank1Factory : AccountTypeFactory
    {
        public override ICurrentAccount CurrentAccountFactory(string message)
        {
            return new CurrentAccount(message);
        }

        public override ISavingAccount SavingAccountFactory(string message)
        {
            return new SavingsAccount(message);
        }
    }
Enter fullscreen mode Exit fullscreen mode

The **abstract factory design pattern **differs from the **factory method that it needs to implement a **factory provider, which returns factories as per definition.

Now that we have all the abstractions and factories created. Let us design the factory provider. Please find below the code snippet for the factory provider, where a static method will create a factory based on the account name.

    public class AccountFactoryProvider
    {
        public static AccountTypeFactory GetAccountTypeFactory(string accountName)
        {
          if (accountName.Contains("B1")) { return new Bank1Factory(); }
            else return null;
        }
    }
Enter fullscreen mode Exit fullscreen mode

How to use an abstract factory provider?

Let’s take an example of a list of account numbers where if an account name consists of “B1” literally, then it will use the Bank1Factory instance returned via the factory provider.

    static void Main(string[] args)
    {
        List<string> accNames = new List<string> { "B1-456", "B1-987", "B2-222" };
        for (int i = 0; i < accNames.Count; i++)
        {
            AccountTypeFactory anAbstractFactory = AccountFactoryProvider.GetAccountTypeFactory(accNames[i]);
            if (anAbstractFactory == null)
            {
                Console.WriteLine("Invalid " + (accNames[i]));
            }
            else
            {
                ISavingAccount savingAccount = anAbstractFactory.SavingAccountFactory("Hello saving");
                ICurrentAccount currentAccount = anAbstractFactory.CurrentAccountFactory("Hello Current");
            }
        }
        Console.ReadLine();
    }
Enter fullscreen mode Exit fullscreen mode

If the account name does not contain the “B1” literal, then the program will output an invalid accountName

Output

Please find below the output from the above code snippet.

    Hello saving B1-456
    Hello Current B1-456
    Hello saving B1-987
    Hello Current B1-987
Enter fullscreen mode Exit fullscreen mode

Design Pattern — Adapter

According to Gang of Four, the Adapter Pattern converts the interfaces of a class into interfaces that the client requires.

In other words, the adapter design pattern helps incompatible interfaces work collectively.

Use Case

Let’s consider an example of two organizations merging; X organization is taking over Y, but while combining code, the interfaces are not compatible. Assume that the interface that provides a list of transactions of organization Y is not compatible with X.

So the adapter design pattern helps solve this problem whose implementation is very straightforward.

Learning Objectives

  • How to code using an adapter design pattern?

Getting Started

Let’s create a list of transactions from organization Y that are converted to patterns that the client application of organization X requires. The above class is known as “Adaptee.”

    public class OrgYTransactions
    {
        public List<string> GetTransactionsList()
        {
            List<string> transactions = new List<string>();
            transactions.Add("Debit 1");
            transactions.Add("Debit 2");
            transactions.Add("Debit 3");
            return transactions;
        }
    }
Enter fullscreen mode Exit fullscreen mode

Secondly, let’s create a target interface.

    public interface ITransactions{
      List<string> GetTransactions();
    }
Enter fullscreen mode Exit fullscreen mode

Now finally, let’s implement the adapter class as follows.

    public class TransAdapter : OrgYTransactions, ITransactions
    {
        public List<string> GetTransactions()
        {
            return GetTransactionsList();
        }
    }
Enter fullscreen mode Exit fullscreen mode

After all the above implementations are done, let’s understand how to use the adapter class in a console application.

    class Program
    {
        static void Main(string[] args)
        {
            ITransactions adapter = new TransAdapter();
            foreach (var item in adapter.GetTransactions())
            {
                Console.WriteLine(item);
            }
        }
    }
Enter fullscreen mode Exit fullscreen mode

If you look closely at the below usage, we have used the target interface ITransactions and the adapter class TransAdapter without considering how third-party class OrgYTransactions interfaces look. That’s the power of the adapter design pattern it converts the interfaces of a class into interfaces that the client requires.

Design Pattern — Builder

According to Gang of Four, a creational pattern “Builder” allows one to separate and reuse a specific method to build something.

Use Case

Let us take an example of a Car, and the user wanted to build two models, i.e., SUV and Sedan.

Builder design pattern comes in handy in the above use case, and let’s see a step-by-step demonstration.

The Car class has the following properties.

    public class Car{
     public string Name { get; set; }
     public double TopSpeed { get; set; }
     public bool IsSUV { get; set; }
    }
Enter fullscreen mode Exit fullscreen mode

Learning Objectives

  • How to code using a builder design pattern?

Getting Started

Firstly, let’s implement an abstract class builder extended by different car models like SUVs or sedans as per the use case.

    public abstract class CarBuilder
    {
        protected readonly Car _car = new Car();
        public abstract void SetName();
        public abstract void SetSpeed();
        public abstract void SetIsSUV();
        public virtual Car GetCar() => _car;
    }
Enter fullscreen mode Exit fullscreen mode

The abstract class consists of the following methods

  • Abstract methods for each property of the Car class.

  • A virtual method that outputs the Car class instance.

Now, let’s create a factory that utilizes CarBuilder class to build different car models and returns the instance of the car made.

    public class CarFactory
    {
        public Car Build(CarBuilder builder)
        {
            builder.SetName();
            builder.SetSpeed();
            builder.SetIsSUV();
            return builder.GetCar();
        }
    }
Enter fullscreen mode Exit fullscreen mode

Finally, implement different models of cars.

ModelSuv.cs

    public class ModelSuv : CarBuilder
    {
        public override void SetIsSUV()
        {
            _car.IsSUV = true;
        }

        public override void SetName()
        {
            _car.Name = "Maruti SUV";
        }
        public override void SetSpeed()
        {
            _car.TopSpeed = 1000;
        }
    }
Enter fullscreen mode Exit fullscreen mode

ModelSedan.cs

    public class ModelSedan : CarBuilder
    {
        public override void SetIsSUV()
        {
            _car.IsSUV = false;
        }

        public override void SetName()
        {
            _car.Name = "Maruti Sedan";
        }
        public override void SetSpeed()
        {
            _car.TopSpeed = 2000;
        }
    }
Enter fullscreen mode Exit fullscreen mode

How to user builder pattern from the Main() method

Finally, let’s use design patterns to build different car models with the help of factory.Build() method.

    static void Main(string[] args)
    {
        var sedan = new ModelSedan();
        var suv = new ModelSuv();
        var factory = new CarFactory();
        var builders = new List<CarBuilder> { suv, sedan };
        foreach (var b in builders)
        {
            var c = factory.Build(b);
            Console.WriteLine($"The Car details" +
                $"\n--------------------------------------" +
                $"\nName: {c.Name}" +
                $"\nIs SUV: {c.IsSUV}" +
                $"\nTop Speed: {c.TopSpeed} mph\n");
        }
    }
Enter fullscreen mode Exit fullscreen mode

The above usage shows how gracefully we can build different car models using the builder design pattern.

The code pattern is highly maintainable & extensible. If in the future we need to develop a new model, just the new model needs to extend the CarBuilder class, and it's done.

Output

Image description

How to use the Chain of Responsibility Pattern

According to Gang of Four, it defines a chain of responsibilities to process a request. In other words, pass the request from one object to another until an object accepts its responsibility.

Use Case

Let’s consider an example of a claims system in any corporate company. Here is the list of the price range that can be approved and by whom.

    1001000 Rs => Junior/Senior Engineers => Approved by Manager
    100110000 Rs => Managers => Approved by Senior Manager
Enter fullscreen mode Exit fullscreen mode

If the amount is outside the 10000 range, exceptional approval is required from the senior manager.

The above use case can be easily implemented using the Chain of Responsibility design pattern. So, the claim class has the following properties.

    public class Claim{
      public int Id{get;set;}
      public double amount{get;set;}
    }
Enter fullscreen mode Exit fullscreen mode

Getting Started

Firstly, let’s define what functions a claim approver can perform and set a hierarchy for employees at different levels. Implement an abstract class as shown below

    public abstract class ClaimApprover
    {
        protected ClaimApprover claimApprover;
        public void SetHierarchy(ClaimApprover claimApprover)
        {
            this.claimApprover = claimApprover;
        }
        public abstract void ApproveRequest(Claim claim);
    }
Enter fullscreen mode Exit fullscreen mode

As per the use case, let’s drive the class “junior/senior” claim requestor. Notice that this class/designation of employees cannot approve any claims.

    public class Junior : ClaimApprover
    {
        public override void ApproveRequest(Claim claim)
        {
            System.Console.WriteLine("Cannot approve");
        }
    }
Enter fullscreen mode Exit fullscreen mode

Similarly, let’s define implementation for Manager and Senior Manager roles.

    public class Manager : ClaimApprover
    {
        public override void ApproveRequest(Claim claim)
        {
            if (claim.amount >= 100 && claim.amount <= 1000)
            {
                System.Console.WriteLine($"Claim reference {claim.Id} with amount {claim.amount} is approved by Manager");
            }
            else if (claimApprover != null)
            {
                claimApprover.ApproveRequest(claim);
            }
        }
    }
Enter fullscreen mode Exit fullscreen mode

Notice that based on the amount range, if within the Manager’s range, the claim can be approved by the Manager; otherwise, the request will be passed onto the Senior Manager.

    public class SeniorManager : ClaimApprover
    {
        public override void ApproveRequest(Claim claim)
        {
            if (claim.amount > 1000 && claim.amount <= 10000)
            {
                System.Console.WriteLine($"Claim reference {claim.Id} with amount {claim.amount} is approved by Senior Manager");
            }
            else
            {
                System.Console.WriteLine($"Exceptional approval for Claim reference {claim.Id} with amount {claim.amount} is approved by Senior Manager");
            }
        }
    }
Enter fullscreen mode Exit fullscreen mode

Similarly, if the amount range is within the Senior Manager range, the claim can be approved by the Manager; otherwise, being last in the hierarchy, an exceptional approval is done for an amount outside the range.

    ClaimApprover junior = new Manager();
    ClaimApprover sukhpinder = new Manager();
    ClaimApprover singh = new SeniorManager();
    junior.SetHierarchy(sukhpinder);
    sukhpinder.SetHierarchy(singh);

    Claim c1 = new Claim() { amount = 999, Id = 1001 };
    Claim c2 = new Claim() { amount = 10001, Id = 1002 };
    junior.ApproveRequest(c1);
    sukhpinder.ApproveRequest(c2);
Enter fullscreen mode Exit fullscreen mode

How to use the Chain of Responsibility pattern?

  1. Define claim approver: junior, although it cannot approve any claims.

  2. Define claim approver: manager “sukhpinder.”

  3. Define claim approver: senior manager “Singh.”

  4. Set up a hierarchy relationship for junior, i.e., the claims approver is the manager.

  5. Set up a hierarchy relationship for the manager, i.e., the claims approver is the senior manager.

  6. Create two different ranges of claims.

  7. Junior sends the claim request to the manager.

  8. The manager sends the claim request to the senior manager.

Output

    Claim reference 1001 with amount 999 is approved by Manager
    Exceptional approval for Claim reference 1002 with amount 10001 is approved by Senior Manager
Enter fullscreen mode Exit fullscreen mode

For line 1 output, the amount was within the range, so the manager approved it.

For line 2 output, although the senior manager approved it, the amount was outside the range.

Design Pattern — Decorator

According to Gang of Four, the pattern adds extra responsibilities to a class object dynamically.

Use Case

Let’s consider the example of buying a car worth ten lakhs; the company provides the following additional features

  • Sunroof

  • Advance Music System

  • and many more

With some additional features, the total price of the car increases. Let’s implement the above use case using the Decorator Pattern.

Learning Objectives

  • How to code using a decorator design pattern?

Getting Started

Let us implement the use case defined above. Firstly define an abstract class Car and its base methods.

    public abstract class Car{
      public abstract int CarPrice();
      public abstract string GetName();
    }
Enter fullscreen mode Exit fullscreen mode

Consider a small car which extends above the abstract class Car.

    public class SmallCar : Car{
      public override int CarPrice() => 10000;
      public override string GetName() => "Alto Lxi";
    }
Enter fullscreen mode Exit fullscreen mode

Now implement the CarDecorator class using the Car component.

    public class CarDecorator : Car
    {
        protected Car _car;
        public CarDecorator(Car car)
        {
            _car = car;
        }
        public override int CarPrice() => _car.CarPrice();
        public override string GetName() =>_car.GetName();
    }
Enter fullscreen mode Exit fullscreen mode

Now, let us create a separate class for each additional feature available for Car inheriting the CarDecorator class.

As per the use case, the additional features are a sunroof and an advanced music system.

AdvanceMusic.cs

Override the methods as

  • Add the additional cost of an “advanced music system” to the total car price.

  • Update car name with additional feature name.

    public class AdvanceMusic : CarDecorator
    {
        public AdvanceMusic(Car car) : base(car)
        {
        }
    
    public override int CarPrice() => _car.CarPrice() + 3000;
        public override string GetName()=> "Alto Lxi with advance music system";
    }
    

    Sunroof. cs

Override the methods as

  • Add the additional cost of a “sunroof” to the total car price.

  • Update car name with additional feature name.

    public class Sunroof : CarDecorator
    {
        public Sunroof(Car car) : base(car)
        {
        }
        public override int CarPrice() => _car.CarPrice() + 2000;
        public override string GetName() => "Alto Lxi with Sunroof";
    }
    

    Decorator Pattern in action

Create an instance of SmallCar and output the name and price of the car.

    Car car = new SmallCar();

    Console.WriteLine($"Price of car {car.GetName()} : " + car.CarPrice());
Enter fullscreen mode Exit fullscreen mode

Now, let’s add additional features as shown below

    var car1 = new Sunroof(car);
    var car2 = new AdvanceMusic(car);
Enter fullscreen mode Exit fullscreen mode

Complete Code

    static void Main(string[] args)
    {
        Car car = new SmallCar();
        Console.WriteLine($"Price of car {car.GetName()} : " + car.CarPrice());
        var car1 = new Sunroof(car);
        Console.WriteLine($"Price of car {car1.GetName()} : " + car1.CarPrice());
        var car2 = new AdvanceMusic(car);
        Console.WriteLine($"Price of car {car2.GetName()} : " + car2.CarPrice());
    }
Enter fullscreen mode Exit fullscreen mode

Output

Image description

Congratulations..!! You have successfully implemented the use case using the decorator pattern.

Design Pattern — Factory Method

According to the Gang of Four, the factory method allows the subclass to determine which class object should be created.

Learning Objectives

  • What is the factory method design pattern?

  • How to write code using the factory method?

Getting Started

Let’s consider an example of any Bank with account types as Savings and Current accounts. Now let’s implement the above example using the factory design pattern

Firstly, create an account-type abstract class.

    public abstract class AccoutType
    {
       public string Balance { get; set; }
    }
Enter fullscreen mode Exit fullscreen mode

Implement current and saving account classes inheriting the AccountType abstract class as shown below.

    public class SavingsAccount : AccoutType
    {
     public SavingsAccount()
     {
      Balance = "10000 Rs";
     }
    }
    public class CurrentAccount : AccoutType
    {
     public CurrentAccount()
     {
      Balance = "20000 Rs";
     }
    }
Enter fullscreen mode Exit fullscreen mode

Finally, let’s implement the factory interface, which will provide a contract that helps create a class object. This interface is also known as the Creator.

    public interface IAccountFactory
    {
      AccoutType GetAccoutType(string accountName);
    }
Enter fullscreen mode Exit fullscreen mode

At last, write an implementation of the creator interface method as shown below. The class that implements the creator is known as Concrete Creator.

    public class AccountFactory : IAccountFactory
    {
        public AccoutType GetAccoutType(string accountName)
        {
            if (accountName.Equals("SAVINGS", StringComparison.OrdinalIgnoreCase))
            {
                return new SavingsAccount();
            }
            else if (accountName.Equals("CURRENT", StringComparison.OrdinalIgnoreCase))
            {
                return new CurrentAccount();
            }
            else
            {
                throw new ArgumentException("Invalid account name");
            }
        }
    }
Enter fullscreen mode Exit fullscreen mode

That’s it. You have successfully implemented the factory method using the Bank example.

How to use the factory method?

A subclass will decide which “AccountType ” class object will be created based on the account name.

    class Program
    {
        static void Main(string[] args)
        {
            IAccountFactory accountFactory = new AccountFactory();
            var savingAccount = accountFactory.GetAccoutType("SAVINGS");
            Console.WriteLine("Saving account balance: " + savingAccount.Balance);
            var currentAccount = accountFactory.GetAccoutType("CURRENT");
            Console.WriteLine("Current account balance: " + currentAccount.Balance);
        }
    }
Enter fullscreen mode Exit fullscreen mode

For example, if the account name is “SAVINGS,” then the “SavingAccount” class object will be created and returned.

Similarly, if the account name is “CURRENT,” then the “CurrentAccount” class object will be instantiated and returned.

Output

    Saving account balance: 10000 Rs
    Current account balance: 20000 Rs
Enter fullscreen mode Exit fullscreen mode

Design Pattern — Iterator

According to Gang of Four, the iterator pattern provides a process to obtain the aggregator object without knowing its implementation.

Use Case

Let us take an example of a collection list of cars and string[] an array of motorcycles, we need to design an aggregator object so that one can iterate over the collection without knowing whether it's a list or an array.

The iterator design pattern helps solve this problem wherein a standard iterator will traverse different collection types.

Getting Started

Considering the above use case, let us define a custom iterator interface that acts as an abstract layer over the list and array iterator.

    public interface IVehicleIterator{
      void First();
      bool IsDone();
      string Next();
      string Current();
    }
Enter fullscreen mode Exit fullscreen mode

Now write car and motorcycle iterators that implement the above interface according to the use case.

CarIterator.cs

    public class CarIterator : IVehicleIterator
    {
        private List<string> _cars;
        private int _current;
        public CarIterator(List<string> cars)
        {
            _cars = cars;
            _current = 0;
        }
        public string Current()
        {
            return _cars.ElementAt(_current);
        }
        public void First()
        {
            _current = 0;
        }
        public bool IsDone()
        {
            return _current >= _cars.Count;
        }
        public string Next()
        {
            return _cars.ElementAt(_current++);
        }
    }
Enter fullscreen mode Exit fullscreen mode

The car iterator is implemented over List collection and provides an implementation of interface methods.

MotorcycleIterator.cs

The motorcycle iterator is implemented over string[] collection and provides an implementation of interface methods.

    public class MotercycleIterator : IVehicleIterator
    {
        private string[] _motercylces;
        private int _current;
        public MotercycleIterator(string[] motercylces)
        {
            _motercylces = motercylces;
            _current = 0;
        }
        public string Current()
        {
            return _motercylces[_current];
        }

        public void First()
        {
            _current = 0;
        }
        public bool IsDone()
        {
            return _current >= _motercylces.Length;
        }
        public string Next()
        {
            return _motercylces[_current++];
        }
    }
Enter fullscreen mode Exit fullscreen mode

After all the above iterators are defined, define a standard aggregator object interface that creates iterators.

    public interface IVehicleAggregate{
       IVehicleIterator CreateIterator();
    }
Enter fullscreen mode Exit fullscreen mode

Finally, write down the classes which implement the above aggregator interface. According to the use case, both Car and Motorcycle classes will implement the aggregator interface.

Car. cs

The method of the aggregator interface returns the relevant iterator as shown below.

    public class Car : IVehicleAggregate
    {
        private List<string> _cars;
        public Car()
        {
            _cars = new List<string> { "Car 1", "Car 2", "Car 3" };
        }

        public IVehicleIterator CreateIterator()
        {
            return new CarIterator(_cars);
        }
    }
Enter fullscreen mode Exit fullscreen mode

Motorcycle. cs

The method of the aggregator interface returns the relevant iterator as shown below.

    public class Motercycle : IVehicleAggregate
    {
      private string[] _motercycles;
        public Motercycle()
        {
            _motercycles = new[] { "Bike 1", "Bike 2", "Bike 3" };
        }
        public IVehicleIterator CreateIterator()
        {
            return new MotercycleIterator(_motercycles);
        }
    }
Enter fullscreen mode Exit fullscreen mode

Iterator Pattern in Action

The PrintVehicles methods check if !iterator.isDone then output the collection element. No matter what collection we’re dealing with, implement methods like First, IsDone, and Next.

    static void Main(string[] args)
    {
        IVehicleAggregate car = new Vehicles.Car();
        IVehicleAggregate motercycle = new Vehicles.Motercycle();
        IVehicleIterator carIterator = car.CreateIterator();
        IVehicleIterator motercycleIterator = motercycle.CreateIterator();
        PrintVehicles(carIterator);
        PrintVehicles(motercycleIterator);
    }
    static void PrintVehicles(IVehicleIterator iterator)
    {
        iterator.First();
        while (!iterator.IsDone())
        {
            Console.WriteLine(iterator.Next());
        }
    }
Enter fullscreen mode Exit fullscreen mode

Output

We don’t know the underlying collection type, but it is still iterated over via the Iterator Design Pattern. If you go ahead and run, it displays the following output.

Image description

Design Pattern — Mediator

According to Gang of Four, the Mediator pattern encapsulates the object interaction with each other.

The mediator design pattern helps us design loosely coupled applications by encapsulating object interactions.

Use Case

Let’s consider an example of a chatroom where participants register, and how to communicate efficiently.

Need to implement the following chatroom conversation using the Mediator Design Pattern.

    David to Scott: 'Hey'
    Scott to David: 'I am good how about you.'
    Jennifer to Ashley: 'Hey ashley... david is back in the group'
    Jennifer to David: 'Where have you been?'
    Ashley to David: 'How come you aren't active here anymore?'
Enter fullscreen mode Exit fullscreen mode

Learning Objectives

  • How to code using a mediator design pattern?

Getting Started

The primary step is to create a list of usernames that will be used inside a chatroom. A public enum for that is shown below.

    public enum Username{
    Ashley,
    David,
    Jennifer,
    Scott
    }
```=
Now first and foremost implement an abstract layer of the chatroom.
```csharp
    public abstract class AChatroom
    {
        public abstract void Register(User user);
        public abstract void Post(string fromUser, string toUser, string msg);
    }
Enter fullscreen mode Exit fullscreen mode

And a class defining abstract methods. The methods validate if the user exists in the dictionary. For example, the register method validates if the user already exists or not. If not exist, then only register the user in the chatroom.

    public class Chatroom : AChatroom
    {
        private Dictionary<string, User> _users = new Dictionary<string, User>();
        public override void Post(string fromUser, string toUser, string msg)
        {
            User participant = _users[toUser];
            if (participant != null)
            {
                participant.DM(fromUser, msg);
            }
        }
        public override void Register(User user)
        {
            if (!_users.ContainsValue(user))
            {
                _users[user.Name] = user;
            }
            user.Chatroom = this;
        }
    }
Enter fullscreen mode Exit fullscreen mode

Finally, let’s implement the actions the user can perform, like posting a message to a user in the chatroom or receiving a DM from another user.

    public class User
    {
        private Chatroom _chatroom;
        private string _name;
        public User(string name) => this._name = name;
        public string Name => _name;
        public Chatroom Chatroom
        {
            set { _chatroom = value; }
            get => _chatroom;
        }
        public void Post(string to, string message) => 
            _chatroom.Post(_name, to, message);
        public virtual void DM(string from, string message) => 
            Console.WriteLine("{0} to {1}: '{2}'", from, Name, message);
    }
Enter fullscreen mode Exit fullscreen mode

How to use the mediator pattern from the main method

    static void Main(string[] args)
    {
        Chatroom chatroom = new Chatroom();
        User Jennifer = new UserPersona(Username.Jennifer.ToString());
        User Ashley = new UserPersona(Username.Ashley.ToString());
        User David = new UserPersona(Username.David.ToString());
        User Scott = new UserPersona(Username.Scott.ToString());

        chatroom.Register(Jennifer);
        chatroom.Register(Ashley);
        chatroom.Register(David);
        chatroom.Register(Scott);

        David.Post(Username.Scott.ToString(), "Hey");
        Scott.Post(Username.David.ToString(), "I am good how about you.");
        Jennifer.Post(Username.Ashley.ToString(), "Hey ashley... david is back in the group");
        Jennifer.Post(Username.David.ToString(), "Where have you been?");
        Ashley.Post(Username.David.ToString(), "How come you aren't active here anymore?");
        Console.ReadKey();
    }
Enter fullscreen mode Exit fullscreen mode
  1. Chatroom class object is created.

  2. Four different users are created with unique names.

  3. Register each one of them in the chatroom.

  4. Users can now start posting messages to each other.

The program execution describes only the Post method of the user class.

Output: The chatroom history of the above program execution

    David to Scott: 'Hey'
    Scott to David: 'I am good how about you.'
    Jennifer to Ashley: 'Hey ashley... david is back in the group'
    Jennifer to David: 'Where have you been?'
    Ashley to David: 'How come you aren't active here anymore?'
Enter fullscreen mode Exit fullscreen mode

Design Pattern — Observer

According to Gang of Four, the observer pattern defines dependency b/w two or more objects. So when one object state changes, then all its dependents are notified.

In other words, a change in one object initiates the notification in another object.

Use Case

Let’s take an example of an Instagram celebrity influencer who has “x” number of followers. So the moment the celebrity adds a post, then all the followers are notified.

Let us implement the aforementioned use case using the Observer Design Pattern.

Learning Objectives

  • How to code using an observer design pattern?

Getting Started

According to the use case, the first implement an interface that contains what actions a celebrity can perform. It is known as “Subject.”

    public interface ICelebrityInstagram{
     string FullName { get; }
     string Post { get; set; }
     void Notify(string post);
     void AddFollower(IFollower fan);
     void RemoveFollower(IFollower fan);
    }
Enter fullscreen mode Exit fullscreen mode

The Subject contains the following member functions.

  • Notify: To notify all the followers.

  • AddFollower: Add a new follower to the celebrity list.

  • RemoveFollower: Remove a follower from the celebrity list.

Now implement the observer “IFollower” interface, which contains the “Update” member function for notification.

    public interface IFollower{
     void Update(ICelebrityInstagram celebrityInstagram);
    }
Enter fullscreen mode Exit fullscreen mode

Finally, it’s time to implement “Concrete Implementation” for both “Subject” and “Observer.”

ConcreteObserver named “Follower.cs”

It provides an implementation of the Update member function, which outputs the celebrity name & post to the console.

    public class Follower : IFollower
    {
        public void Update(ICelebrityInstagram celebrityInstagram)
        {
            Console.WriteLine($"Follower notified. Post of {celebrityInstagram.FullName}: " +
                $"{celebrityInstagram.Post}");
        }
    }
Enter fullscreen mode Exit fullscreen mode

ConcreteSubject named “Sukhpinder. cs”

    public class Sukhpinder : ICelebrityInstagram
    {
        private readonly List<IFollower> _posts = new List<IFollower>();
        private string _post;
        public string FullName => "Sukhpinder Singh";

        public string Post {
            get { return _post; }
            set
            {
                Notify(value);
            }
        }
        public void AddFollower(IFollower follower)
        {
            _posts.Add(follower);
        }
        public void Notify(string post)
        {
            _post = post;
            foreach (var item in _posts)
            {
                item.Update(this);
            }
        }
        public void RemoveFollower(IFollower follower)
        {
            _posts.Remove(follower);
        }
    }
Enter fullscreen mode Exit fullscreen mode

How to use an observer pattern?

The following use case shows that whenever the below statement is executedsukhpinder.Post = “I love design patterns.”; The update method is triggered for each follower, i.e., each follower object is notified of a new post from “Sukhpinder.”

    static void Main(string[] args)
    {
        var sukhpinder = new Sukhpinder();

        var firstFan = new Follower();
        var secondFan = new Follower();
        sukhpinder.AddFollower(firstFan);
        sukhpinder.AddFollower(secondFan);
        sukhpinder.Post = "I love design patterns.";
        Console.Read();
    }
Enter fullscreen mode Exit fullscreen mode

Output

Image description

Advance Property Pattern C# 8.0

The article describes how pattern matching provides an effective way to utilize and process that data in forms that weren’t part of the primary system.

Let’s Start

Let’s take an example of Toll Calculator and see how pattern matching helps to write an algorithm for that.

Entity class used throughout the article

    public class Car
      {
          public int PassengerCount { get; set; }
      }
      public class DeliveryTruck
      {
          public int Weight { get; set; }
      }
      public class Taxi
      {
          public int Fare { get; set; }
      }
      public class Bus
      {
          public int Capacity { get; set; }
          public int RidersCount { get; set; }
      }
Enter fullscreen mode Exit fullscreen mode

Example 1: Calculate toll fare as per following conditions:

  • If the vehicle is Car => 100 Rs

  • If the vehicle is DeliveryTruck => 200 Rs

  • If the vehicle is Bus => 150 Rs

  • If the vehicle is a Taxi => 120 Rs

Pattern matching program with new switch syntax

If the vehicle type matches with Car 100 is returned & so on. Notice that null & {} are default cases for the object type.

Also, “_” can be used to program the default scenario. **Refer new switch syntax.**

It’s a much more clean & efficient way of coding & also recommended the use of single-letter variable names inside the switch syntax.

    public static int TollFare(Object vehicleType) => vehicleType switch
    {
     Car c => 100,
     DeliveryTruck d => 200,
     Bus b => 150,
     Taxi t => 120,
     null => 0,
     { } => 0
    };
Enter fullscreen mode Exit fullscreen mode

Test above program

Test examples from a console application standpoint. The below code illustrates how to call the above pattern-matching function from the main method.

    var car = new Car();
    var taxi = new Taxi();
    var bus = new Bus();
    var truck = new DeliveryTruck();

    Console.WriteLine($"The toll for a car is {TollFare(car)}");
    Console.WriteLine($"The toll for a taxi is {TollFare(taxi)}");
    Console.WriteLine($"The toll for a bus is {TollFare(bus)}");
    Console.WriteLine($"The toll for a truck is {TollFare(truck)}");
Enter fullscreen mode Exit fullscreen mode

Console Output

    The toll for a car is 100
    The toll for a taxi is 120
    The toll for a bus is 150
    The toll for a truck is 200
Enter fullscreen mode Exit fullscreen mode

Example 2: Add occupancy pricing based upon vehicle type

  • Cars & taxis with “NO” passengers pay an extra 10 Rs.

  • Cars & taxis with two passengers get a 10 Rs discount.

  • Cars & taxis with three or more passengers get a 20 Rs discount.

  • Buses that are less than 50% of passengers pay an extra 30 Rs.

  • Buses that have more than 90% of passengers get a 40 Rs discount.

  • Trucks over 5000 lbs are charged an extra 100 Rs.

  • Light trucks under 3000 lbs, given a 20 Rs discount.

Pattern Matching Switch

Refer to pattern-matching syntax with single & multiple property classes. **Link**

Pattern Matching — Car Entity

    Car { PassengerCount: 0 } => 100 + 10,
    Car { PassengerCount: 1 } => 100,
    Car { PassengerCount: 2 } => 100 - 10,
    Car c => 100 - 20,
Enter fullscreen mode Exit fullscreen mode

Pattern Matching — Taxi Entity

    Taxi {Fare:0 }=>100+10,
    Taxi { Fare: 1 } => 100,
    Taxi { Fare: 2 } => 100 - 10,
    Taxi t => 100 - 20,
Enter fullscreen mode Exit fullscreen mode

Pattern Matching — Bus Entity

    Bus b when ((double)b.RidersCount / (double)b.Capacity) < 0.50 => 150 + 30,

    Bus b when ((double)b.RidersCount / (double)b.Capacity) > 0.90 => 150 - 40,

    Bus b => 150,
Enter fullscreen mode Exit fullscreen mode

Pattern Matching — Delivery Truck Entity

    DeliveryTruck t when (t.Weight > 5000) => 200 + 100,
    DeliveryTruck t when (t.Weight < 3000) => 200 - 20,
    DeliveryTruck t => 200,
Enter fullscreen mode Exit fullscreen mode

Combining all entities

The below example highlights the advantages of pattern matching: the pattern branches are compiled in order. The compiler also warns about the unreachable code.

    public static int OccupancyTypeTollFare(Object vehicleType) => vehicleType switch
      {
          Car { PassengerCount: 0 } => 100 + 10,
          Car { PassengerCount: 1 } => 100,
          Car { PassengerCount: 2 } => 100 - 10,
          Car c => 100 - 20,
          Taxi { Fare: 0 } => 100 + 10,
          Taxi { Fare: 1 } => 100,
          Taxi { Fare: 2 } => 100 - 10,
          Taxi t => 100 - 20,
          Bus b when ((double)b.RidersCount / (double)b.Capacity) < 0.50 => 150 + 30,
          Bus b when ((double)b.RidersCount / (double)b.Capacity) > 0.90 => 150 - 40,
          Bus b => 150,
          DeliveryTruck t when (t.Weight > 5000) => 200 + 100,
          DeliveryTruck t when (t.Weight < 3000) => 200 - 20,
          DeliveryTruck t => 200,
          null => 0,
          { } => 0,
      };
Enter fullscreen mode Exit fullscreen mode

Test above program

Test examples from a console application standpoint. The below code illustrates how to call the above pattern-matching function from the main method.

    var car1 = new Car{ PassengerCount=2};
    var taxi1 = new Taxi { Fare = 0 };
    var bus1 = new Bus { Capacity = 100, RidersCount = 30 };
    var truck1 = new DeliveryTruck { Weight = 30000 };

    Console.WriteLine($"The toll for a car is {OccupancyTypeTollFare(car1)}");
    Console.WriteLine($"The toll for a taxi is {OccupancyTypeTollFare(taxi1)}");
    Console.WriteLine($"The toll for a bus is {OccupancyTypeTollFare(bus1)}");
    Console.WriteLine($"The toll for a truck is {OccupancyTypeTollFare(truck1)}");
Enter fullscreen mode Exit fullscreen mode

Console Output

    The toll for a car is 90
    The toll for a taxi is 110
    The toll for a bus is 180
    The toll for a truck is 300
Enter fullscreen mode Exit fullscreen mode

“Pattern matching makes code more readable and offers an alternative to object-oriented techniques when you can’t add code to your classes.”

Design Pattern — Singleton

Gang of Four — Singleton design pattern ensures that a particular class has only one instance/object and a global access point.

Learning Objectives

  • How to code using a singleton design pattern?

Getting Started

Singleton classes are used to eliminate instantiating of more than one object of a particular class.

    public class SingletonExample
    {
        private string Name { get; set; } = "Hello from singleton";
        private static SingletonExample _instance;
        public static SingletonExample Instance
        {
            get
            {
                if (_instance == null)
                {
                    _instance = new SingletonExample();
                }
                return _instance;
            }
        }
        public SingletonExample()
        {
        }
        public string GetName() => Name;
    }
Enter fullscreen mode Exit fullscreen mode

Breakdown

  1. Iteration 1 _instance==null means that only instances will be created.

  2. Iteration 2, as now _intance !=null So previously created instances will be returned.

Test using a Console application

Let’s call the singleton class twice and assign the returned instance to two different variables. Finally, check if both objects are equal using theObject.Equals function.

    static void Main(string[] args)
    {
        var response = SingletonExample.Instance;
        Console.WriteLine(response);

        var response1 = SingletonExample.Instance;
        Console.WriteLine(response1);
        Console.WriteLine(Object.Equals(response1, response));
    }
Enter fullscreen mode Exit fullscreen mode
  • If it returns true, it means a single instance is produced every time.

  • If it returns false, it means the class is not following the singleton pattern.

Output

The console output returns true; congratulations. You have successfully implemented the Singleton Pattern.

Image description

Thread Safety

The above class is known as the singleton class, but currently, it’s not thread-safe. In a multi-threaded environment, two threads can hit if (_instance == null) statement at the same time, and we will end up having multiple instances of a singleton class.

One way for a safer thread is to use a lock mechanism, and the other way is to make a read-only instance for a cleaner and more efficient approach.

    public class ThreadSafeSingleton
{
private static readonly ThreadSafeSingleton _instance = new ThreadSafeSingleton();
public static ThreadSafeSingleton Instance
{
get
{
return _instance;
}
}
public ThreadSafeSingleton()
{
}
}
Enter fullscreen mode Exit fullscreen mode




Github Sample

https://github.com/ssukhpinder/DesignPatterns

Thank you for reading!

Tips help me continue maintaining and building new projects like these and do share your thoughts in the comments.

C# Programming🚀

Thank you for being a part of the C# community!

Follow me on Medium

Please consider supporting me by clicking on "Buy me a coffee"

Top comments (5)

Collapse
 
martinbaun profile image
Martin Baun

I generally consider design patterns more of guidelines than a fixed solution. Just because it says this is done in X way shouldn't mean you can't tweak it to match your current idea/requirement. Regardless, this piece is gold!

Collapse
 
peter_truchly_4fce0874fd5 profile image
Peter Truchly

That's true, some people take this too seriously. Especially those with an anti-pattern agenda are annoyingly funny sometimes.
My alternative understanding of patterns (apart of them being a cookbook recipes) is simple extension of our vocabulary. Name of the pattern is a short way how to describe a complex idea within architectural discussion.

Collapse
 
ssukhpinder profile image
Sukhpinder Singh

Thanks a lot @martinbaun

Collapse
 
syedmuhammadaliraza profile image
Syed Muhammad Ali Raza

nice

Collapse
 
ssukhpinder profile image
Sukhpinder Singh

Thanks @syed

Some comments may only be visible to logged-in visitors. Sign in to view all comments.