paint-brush
11 Key Design Patterns: An Essential Guideby@ssukhpinder
1,415 reads
1,415 reads

11 Key Design Patterns: An Essential Guide

by Sukhpinder SinghSeptember 8th, 2024
Read on Terminal Reader
Read this story w/o Javascript

Too Long; Didn't Read

Unlock the secrets of software architecture with *Mastering Software Architecture: 11 Key Design Patterns Explained*. The book is written in the C# programming language C# 8.0. There are 11 design patterns that can be used to create a factory for creating factories. The abstract factory pattern is an extension factory method; it’s recommended to go through the factory method.
featured image - 11 Key Design Patterns: An Essential Guide
Sukhpinder Singh HackerNoon profile picture

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{  }


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);
   }
}


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);
}


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);
    }
}


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;
    }
}

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();
}


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

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;
    }
}


Secondly, let’s create a target interface.


public interface ITransactions{
  List<string> GetTransactions();
}


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


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


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);
        }
    }
}


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; }
}

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;
}


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 the 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();
    }
}


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;
    }
}

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;
    }
}

How to User Builder Pattern From the Main() Method

Finally, let’s use design patterns to build different car models with the help of a factory.Build(<model>) 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");
    }
}

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

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.


100–1000 Rs => Junior/Senior Engineers => Approved by Manager
1001–10000 Rs => Managers => Approved by Senior Manager


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;}
}

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);
}


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");
    }
}


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);
        }
    }
}


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");
        }
    }
}


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);

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


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();
}


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


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


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();
}


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());


Now, let’s add additional features as shown below


var car1 = new Sunroof(car);
var car2 = new AdvanceMusic(car);

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());
}

Output

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; }
}


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";
 }
}


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);
}


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");
        }
    }
}


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);
    }
}

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

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();
}


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++);
    }
}


The car iterator is implemented over List<string> 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++];
    }
}


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


public interface IVehicleAggregate{
   IVehicleIterator CreateIterator();
}


Finally, write down the classes that 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);
    }
}

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);
    }
}

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());
    }
}

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.

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?'

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.


public abstract class AChatroom
{
    public abstract void Register(User user);
    public abstract void Post(string fromUser, string toUser, string msg);
}


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;
    }
}


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);
}

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();
}


  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?'

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);
}

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);
}


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}");
    }
}

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);
    }
}

How to Sse 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();
}

Output

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; }
  }


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
};

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)}");

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


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,

Pattern Matching — Taxi Entity

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

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,

Pattern Matching — Delivery Truck Entity

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

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,
  };

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)}");

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


“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;
}

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));
}
  • 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.



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()
    {
    }
}

Github Sample

https://github.com/ssukhpinder/DesignPatterns

Thank you for reading!

Sponsorships help me continue maintaining and building new projects like these.


🙏 If you use Pay, Noticed, or any of my other projects, a small contribution would mean A WHOLE LOT. On its own, open-source doesn’t pay the bills. Hopefully, with your help, continuing my work can be sustainable, and I won’t have to go get a real job 😛.

C# Programming🚀

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

Buymeacoffee