Skip to content

Instantly share code, notes, and snippets.

@DilanLivera
Last active April 6, 2023 01:34
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save DilanLivera/e6755b9140895c23526385696d369b05 to your computer and use it in GitHub Desktop.
Save DilanLivera/e6755b9140895c23526385696d369b05 to your computer and use it in GitHub Desktop.
Design patterns

Adapter

Problem

Incompatible interfaces between a client and a service provider.

Adapters convert the interface of one class into an interface a client expects.

Two Kinds of Adapters

characteristics of object and class adapters

One variation of class adapter that doesn't require multiple class inheritance simply implements the adapter interface. This variation can be used in languages like C# and Java. In C#, you'll mostly use object adapters. The reason is that C# doesn't support multiple inheritances, and a design principle of C# is to prefer composition over inheritance. - Steve Smith, "C# Design Patterns: Adapter"

object adapter class relationship

Please find the code samples from C# Design Patterns: Adapter course at ardalis/DesignPatternsInCSharp: Adapter.

Related Patterns

  • The Decorator has a similar structure, but the decorator intends to add functionality.
  • The Bridge pattern is very similar in structure, but its intent specifically is to allow interfaces and their implementations to vary independently from one another.
  • The Proxy is structurally similar as well, but it intends to control access to a resource, not to convert an incompatible interface.
  • The Repository pattern sometimes acts as an adapter, providing a common interface for persistence that can map various incompatible interfaces to a single common data access strategy.
  • The Strategy pattern is frequently used with the Adapter pattern to inject different implementations of behaviour into a particular client class. The Facade design pattern's intent is similar to the adapters in that it alters an interface to make it easier for a client to use. The difference, though, is that the facade often sits in front of multiple different types. Its goal is to simplify a complex set of operations, not necessarily to provide a way to swap between different incompatible operations easily.

Adapter for Results

The adapter pattern can also be applied to result types, not just the service providers. In this case, to apply the pattern, you must create or choose a common result type that your client expects to consume. Then you can inherit from this type and use composition to delegate calls made to the common type to the specific incompatible type. - Steve Smith, "C# Design Patterns: Adapter"

Please find a code samples of this at ardalis/DesignPatternsInCSharp: Adapter: ResultWrapper.

Resources

Return to the Table of contents

Flyweight

Flyweight is a structural design pattern that allows programs to support vast quantities of objects by keeping their memory consumption low.The pattern achieves it by sharing parts of object state between multiple objects. In other words, the Flyweight saves RAM by caching the same data used by different objects.

Identification

Flyweight can be recognized by a creation method that returns cached objects instead of creating new.

Note Since the same flyweight object can be used in different contexts, you have to make sure that its state can’t be modified. A flyweight should initialize its state just once, via constructor parameters. It shouldn’t expose any setters or public fields to other objects.

How

  1. Divide fields of a class that will become a flyweight into two parts:
    • The intrinsic state: the fields that contain unchanging data duplicated across many objects
    • The extrinsic state: the fields that contain contextual data unique to each object
  2. Leave the fields that represent the intrinsic state in the class, but make sure they’re immutable. They should take their initial values only inside the constructor.
  3. Go over methods that use fields of the extrinsic state. For each field used in the method, introduce a new parameter and use it instead of the field.
  4. Optionally, create a factory class to manage the pool of flyweights. It should check for an existing flyweight before creating a new one. Once the factory is in place, clients must only request flyweights through it. They should describe the desired flyweight by passing its intrinsic state to the factory.
  5. The client must store or calculate values of the extrinsic state (context) to be able to call methods of flyweight objects. For the sake of convenience, the extrinsic state along with the flyweight-referencing field may be moved to a separate context class.

Car.cs

public class Car
{
    public string Owner { get; set; }
    public string Number { get; set; }
    public string Company { get; set; }
    public string Model { get; set; }
    public string Color { get; set; }
}

Flyweight.cs

/*
 * The Flyweight stores a common portion of the state(also called intrinsic
 * state) that belongs to multiple real business entities.The Flyweight
 * accepts the rest of the state(extrinsic state, unique for each entity)
 * via its method parameters.
*/
public class Flyweight
{
    private readonly Car _sharedState;

    public Flyweight(Car car)
    {
        this._sharedState = car;
    }

    public void Operation(Car uniqueState)
    {
        string shared = JsonSerializer.Serialize(this._sharedState);
        string unique = JsonSerializer.Serialize(uniqueState);
        Console.WriteLine($"Flyweight: Displaying shared {shared} and unique {unique} state.");
    }
}

FlyweightFactory.cs

/*
 * The Flyweight Factory creates and manages the Flyweight objects. 
 * It ensures that flyweights are shared correctly. 
 * When the client requests a flyweight, the factory either returns an existing instance or 
 * creates a new one, if it doesn't exist yet.
*/
public class FlyweightFactory
{
    private readonly IDictionary<string, Flyweight> flyweights =
        new Dictionary<string, Flyweight>();

    public FlyweightFactory(params Car[] cars)
    {
        Array.ForEach(cars, car => flyweights.Add(GetKey(car), new Flyweight(car)));
    }

    // Returns a Flyweight's string hash for a given state.
    public string GetKey(Car key)
    {
        List<string> elements = new List<string>
        {
            key.Model,
            key.Color,
            key.Company
        };

        if (key.Owner != null && key.Number != null)
        {
            elements.Add(key.Number);
            elements.Add(key.Owner);
        }

        elements.Sort();

        return string.Join("_", elements);
    }

    // Returns an existing Flyweight with a given state or creates a new one.
    public Flyweight GetFlyweight(Car car)
    {
        string key = GetKey(car);

        if (!flyweights.ContainsKey(key))
        {
            Console.WriteLine("FlyweightFactory: Can't find a flyweight, creating new one.");
            flyweights.Add(key, new Flyweight(car));
        }
        else
        {
            Console.WriteLine("FlyweightFactory: Reusing existing flyweight.");
        }

        return flyweights[key];
    }

    public void ListFlyweights()
    {
        Console.WriteLine($"\nFlyweightFactory: I have {flyweights.Count} flyweights:");

        foreach (var (key, _) in flyweights)
        {
            Console.WriteLine(key);
        }
    }
}

Program.cs

class Program
{
    static void Main()
    {
        // The client code usually creates a bunch of pre-populated
        // flyweights in the initialization stage of the application.
        var factory = new FlyweightFactory(
            new Car { Company = "Chevrolet", Model = "Camaro2018", Color = "pink" },
            new Car { Company = "Mercedes Benz", Model = "C300", Color = "black" },
            new Car { Company = "Mercedes Benz", Model = "C500", Color = "red" },
            new Car { Company = "BMW", Model = "M5", Color = "red" },
            new Car { Company = "BMW", Model = "X6", Color = "white" }
        );
        factory.ListFlyweights();

        AddCarToPoliceDatabase(factory, new Car
        {
            Number = "CL234IR",
            Owner = "James Doe",
            Company = "BMW",
            Model = "M5",
            Color = "red"
        });

        AddCarToPoliceDatabase(factory, new Car
        {
            Number = "CL234IR",
            Owner = "James Doe",
            Company = "BMW",
            Model = "X1",
            Color = "red"
        });

        factory.ListFlyweights();
    }

    public static void AddCarToPoliceDatabase(FlyweightFactory factory, Car car)
    {
        Console.WriteLine("\nClient: Adding a car to database.");

        var flyweight = factory.GetFlyweight(new Car
        {
            Color = car.Color,
            Model = car.Model,
            Company = car.Company
        });

        /*
         * The client code either stores or calculates extrinsic state and
         * passes it to the flyweight's methods.
         * 
         */

        flyweight.Operation(car);
    }
}

Resources

Return to the Table of contents

Null Object

Where a behaviour may be implemented in different ways, and one of the ways is to have no behaviour at all, treat the null case as one version.

Null Object by Scott Bain

//set-up the services
public interface INotificationService
{
    Task SendNotification(string notification);
}

public class PushNotificationService : INotificationService
{
    private readonly ILogger _logger;

    public PushNotificationService(ILogger<PushNotificationService> logger)
    {
        _logger = logger;
    }

    public Task SendNotification(string notification)
    {
        //Do the work
        _logger.LogInformation("{Notification} sent", notification);

        return Task.CompletedTask;
    }
}

public class NullNotificationService : INotificationService
{
    public Task SendNotification(string notification)
    {
        return Task.CompletedTask;
    }
}
//use the service
public class WeatherForecastsFetchedNotificationHandler
    : INotificationHandler<WeatherForecastsFetchedNotification>
{
    private readonly INotificationService _notificationService;
  
    public WeatherForecastsFetchedNotificationHandler(
        INotificationService notificationService)
    {
        _notificationService = notificationService;
    }
  
    public Task Handle(
        WeatherForecastsFetchedNotification notification, CancellationToken cancellationToken)
    {
        return _notificationService.SendNotification("Weather forecasts got fetched");
    }
}
//register the services
public class Startup
{
    //removed for brevity

    public void ConfigureServices(IServiceCollection services)
    {
        //removed for brevity

        //resolving any services within the ConfigureServices method is not recommended
        //this is done here for demo purposes
        ServiceProvider provider = services.BuildServiceProvider();

        IHostEnvironment hostEnvironment = provider.GetRequiredService<IHostEnvironment>();
        if (hostEnvironment.IsDevelopment())
        {
            services.AddScoped<INotificationService, NullNotificationService>();
        }
        else
        {
            services.AddScoped<INotificationService, PushNotificationService>();
        }
    }

    //removed for brevity
}

Resources

Return to the Table of contents

Proxy

Problem

Need to control access to a type for performance, security, and other reasons.

Example

The following is from C# Design Patterns: Proxy course.

sequenceDiagram
    participant Client
    participant Proxy
    participant Real Service
    Client->>Proxy: SomeMethod()
    activate Proxy
    Proxy->>Real Service: SomeMethod()
    activate Real Service
    Real Service-->>Proxy: SomeMethod() response
    deactivate Real Service
    Proxy-->>Client: SomeMethod() response
    deactivate Proxy

Typically proxy will mirror the interface of the real service. When the client makes a request to the proxy, the proxy has an opportunity to perform any logic needed before forwarding the request to the real service and, after receiving the response, before returning the response to the client (Eg. Caching the response if it is acting as a Cache).

proxy structure

Proxy Variants

Virtual Proxy

A virtual proxy is a stand-in for an object that is expensive to create for real. Its purpose is generally to optimize for performance. Instead of being the real object, the proxy knows how to get the real object when required, after which it delegates all calls back to that real object. Two common examples of this approach are placeholders within UI screens to allow the screen to render quickly, even if real data isn't yet available. And lazy‑loaded entity properties, which are only populated if the property is accessed or perhaps when it is first enumerated. - Steve Smith, "C# Design Patterns: Proxy"

Please find the code samples from C# Design Patterns: Proxy course at ardalis/DesignPatternsInCSharp: Proxy: Virtual Proxy.

Remote Proxy

Remote Proxy is used to hide the details of working with remote data or services.

The goal of a remote proxy is to act as a local resource while hiding the details of how to connect to a remote resource over a network. The remote proxy centralises all knowledge of the network details, and often these proxies can be generated automatically based on some service definition file, like WSDL, .proto, a Swagger, or open API specification. - Steve Smith, "C# Design Patterns: Proxy"

Please find the code samples from C# Design Patterns: Proxy course at ardalis/DesignPatternsInCSharp: Proxy: Remote Proxy.

Smart Proxy

Smart Proxy performs additional actions when a resource is accessed.

A smart proxy is used to add additional logic around access to a resource. These can be useful to perform resource counting, or to manage the caching of a resource, or to lock access to shared resources. - Steve Smith, "C# Design Patterns: Proxy"

Please find the code samples from C# Design Patterns: Proxy course at ardalis/DesignPatternsInCSharp: Proxy: Smart Proxy.

Protective Proxy

Protective Proxy controls access to a sensitive resource by checking whether or not the client is authorised to perform those operations.

A protective or protection proxy is used to control access to a resource based on certain rules. This can help to eliminate having these checks live either in the client code or in the resource itself. Generally, this helps with separation of concerns, the don't repeat yourself principle, and the single responsibility principle. You can think of the protective proxy as being a kind of gatekeeper around the resource. - Steve Smith, "C# Design Patterns: Proxy"

Please find the code samples from C# Design Patterns: Proxy course at ardalis/DesignPatternsInCSharp: Proxy: Protective Proxy.

Related Patterns

  • The Decorator pattern has a very similar structure, but the Decorator pattern intends to add functionality, whereas the Proxy intends to control access. In the case of a Smart Proxy, this difference is very slim.
  • The Prototype and the Proxy could be used to deal with an expensive object to create. Typically, a virtual proxy is used for this. However, the Proxy offers a stub or placeholder and fetches the real object on demand, while the Prototype pattern keeps a copy of the object on hand and can clone it when required.
  • The Adapter is structurally very similar to the Proxy, but its purpose is to convert an incompatible interface into one that works for the client. It's not concerned with access control.
  • The Flyweight pattern is also very similar to the Proxy pattern. However, the Flyweight pattern is designed to manage many references to a shared instance, while the Proxy is designed to wrap a single specific instance.

Resources

Return to the Table of contents

Rules Engine

The following are scratch notes from the C# Design Patterns: Rules Engine Pattern by Steve Smith.

Design Patterns are like individual tools you can add to your toolkit as a software developer. In this course, C# Design Patterns: Rules Pattern, you'll learn to build and use a simple rules engine. First, you'll explore examples of problems and code smells that may benefit from applying rules. Next, you'll discover how to build a simple rules engine. Finally, you'll learn how to apply the engine in real application code and extend the application with new functionality. When you're finished with this course, you'll have the skills and knowledge of the rules engine pattern needed to apply it in your own applications.

What is a rules engine Pattern

According to Steve Smith,

A rules engine processes a set of rules and applies them to produce a result.

A rule describes a condition and may calculate a value.

How to identify where to use

  • Places where the Open/Closed principle is violated(We need to modify the current implementation to extend its behaviour)
  • Methods with lots of conditional complexity(i.e. cyclomatic complexity).

Note: To calculate the cyclomatic complexity in Visual Studio, Go to Analyze -> Calculate Code Metrics -> Select the Solution/Project

Examples of use

  • Scoring games
  • Calculating discounts for customer purchases
  • Diagnosing health concerns

Considerations to keep in mind as you apply this pattern

The key to tackling the complexity in an extensive method is teasing out individual cases, which we'll define as individual rules. As you extract these rules, make sure they're as small as practical(I(Steve Smith) didn't say as small as possible, but each rule should enforce a single case) and have a single responsibility.

Now rules need to be evaluated somewhere in your system. When refactoring, you might first assess them by hand in your existing "method". But eventually, you'll want the responsibility of evaluating a result from a collection of rules to reside in its own type, and that will be the rules engine class. As you start to think about how you'll pull out individual rules, consider what it will take to evaluate them correctly. Some cases are,

  • The first rule that matches a condition is the only one that matters.
  • In others, you'll need to evaluate every rule and then somehow aggregate or filter the individual results.
  • In others, the order in which rules are evaluated may be necessary or offer performance advantages.

Rules engine collaborators

  • Rule collection - This is the collection of the rules to apply for the input/context. (E.g. First time customer, Loyal customer, Veteran, Senior)
  • Rules Engine - This is responsible for applying the rules to a given system context or scenario.
  • Input from the system - E.g. customer, game state

rules engine structure

Things to remember when creating rules

  • Keep individual rules simple
  • Allow for complexity through combinations of simple rules
  • Decide how rules will combine or be chosen
  • Consider whether rule ordering will matter in the evaluation

Implementing a Rules Engine

  • Accept rules collection in engine constructor
  • Allow adding/removing rules or swapping sets of rules via methods
  • Apply the rules to a given context or system state
  • Choose the correct rule to apply or aggregate rules

Steps to follow to apply Rules Engine pattern to an existing code

  • Follow refactoring fundamentals
  • Extract methods for individual conditions
  • Convert methods into Rule classes
  • Create Rule Engine and evaluate Rules
  • Replace original method logic with a call to Rules Engine

Code samples from this course

Return to the Table of contents

Template Method

What is Template Method?

A template method is a method in a superclass that defines the skeleton of an operation in terms of higher-level steps. Subclass implement these steps. - Steve Smith, "C# Design Patterns: Template Method"

Template Method class diagram from "C# Design Patterns: Template Method by Steve Smith" Pluralsight course

When to use Template Method?

  • Lockdown a process while allowing clients to alter certain steps in the process
  • Generalize duplicate behaviour among methods in several classes
  • Create and control extension points for future code implementations
  • Very simple cases where you want to use inheritance, but you need to ensure base functionality is preserved. By default, child types can override virtual members of base types. When they do, the new method is called instead of the original. If the original method needs to be called and if it specifically must be called either before or after the child type's additional behaviour, there's no built‑in way to enforce this behaviour.

Please find the code samples from C# Design Patterns: Template Method course at ardalis/DesignPatternsInCSharp: Template Method.

Related Patterns

  • The Factory Method is often called by the template method. Frequently, the result of the template method is a returned object and part of the process of producing that object may be to invoke an appropriate factory method, perhaps in addition to other steps.
  • The Strategy pattern provides a way to vary an entire algorithm used by a class by delegating it to another class via composition. The Template Method encapsulates an algorithm but allows it to vary among its child classes through inheritance.
  • The Rules Engine pattern frequently leverages the Template Method pattern in its implementation. Individual rule processing may follow a process defined in a base rule class. Similarly, the engine itself may inherit behaviour from a base rule engine class that defines how it should operate.

Resources

Return to the Table of contents

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment