11

Design Patterns and .NET 5 Implementation

Design patterns can be defined as ready-to-use architectural solutions for common problems you encounter during software development. They are essential for understanding the .NET Core architecture and useful for solving ordinary problems that we face when designing any piece of software. In this chapter, we will look at the implementation of some design patterns. It is worth mentioning that this book does not explain all the known patterns we can use. The focus here is to explain the importance of studying and applying them.

In this chapter, we will cover the following topics:

  • Understanding design patterns and their purpose
  • Understanding the available design patterns in .NET 5

By the end of this chapter, you will have learned about some of the use cases from WWTravelClub that you can implement with design patterns.

Technical requirements

For completing this chapter, you will need the free Visual Studio 2019 Community Edition or better, with all the database tools installed, and a free Azure account. The Creating an Azure account subsection of Chapter 1, Understanding the Importance of Software Architecture, explains how to create one.

You can find the sample code for this chapter at https://github.com/PacktPublishing/Software-Architecture-with-C-9-and-.NET-5.

Understanding design patterns and their purpose

Deciding the design of a system is challenging, and the responsibility associated with this task is enormous. As software architects, we must always keep in mind that features such as great reusability, good performance, and good maintainability are important to deliver a good solution. This is where design patterns help and accelerate the design process.

As we mentioned previously, design patterns are solutions that have already been discussed and defined so that they can solve common software architectural problems. This approach grew in popularity after the release of the book Design Patterns – Elements of Reusable Object-Oriented Software, where the Gang of Four (GoF) divided these patterns into three types: creational, structural, and behavioral.

A little bit later, Uncle Bob introduced the SOLID principles to the developer community, giving us the opportunity to efficaciously organize the functions and data structures of each system. The SOLID design principles indicate how the components of software should be designed and connected. It is worth mentioning that, compared to the design patterns presented by GoF, the SOLID principles do not deliver code recipes. Instead, they give you the basic principles to follow when you design your solutions, keeping the software's structure strong and reliable. They can be defined as follows:

  • Single Responsibility: A module or function should be responsible for a single purpose
  • Open-Closed: A software artifact should be open for extension but closed for modification
  • Liskov Substitution: The behavior of a program needs to remain unchanged when you substitute one of its components for another component that has been defined by a supertype of the primer object
  • Interface Segregation: Creating huge interfaces will cause dependencies to occur while you are building concrete objects, but these are harmful to the system architecture
  • Dependency Inversion: The most flexible systems are the ones where object dependencies only refer to abstractions

As technologies and software problems change, more patterns are conceived. The advance of cloud computing has brought a bunch of them, all of which can be found at https://docs.microsoft.com/en-us/azure/architecture/patterns/. The reason why new patterns emerge is related to the challenges we face when new solutions are developed. Today, availability, data management, messaging, monitoring, performance, scalability, resiliency, and security are aspects we must deal with when delivering cloud solutions.

The reason why you should always consider developing using design patterns is quite simple—as a software architect, you cannot spend time reinventing the wheel. However, there is another great reason for using and understanding them: you will find many of these patterns already implemented in .NET 5.

In the next few subsections, we will cover some of the most well-known patterns. However, the idea of this chapter is to let you know that they exist and need to be studied so that you can accelerate and simplify your project. Moreover, each pattern will be presented with a C# code snippet so that you can easily implement them in your projects.

Builder pattern

There are cases where you will have a complex object with different behaviors due to its configuration. Instead of setting this object up while using it, you may want to decouple its configuration from its usage, using a customized configuration that is already built. This way, you have different representations of the instances you are building. This is where you should use the Builder pattern.

The following class diagram shows the pattern that has been implemented for a scenario from this book's use case. The idea behind this design choice is to simplify the way rooms from WWTravelClub are described:

Figure 11.1: Builder pattern

As shown in the following code, the code for this is implemented in a way where the configurations of the instances are not set in the main program. Instead, you just build the objects using the Build() method. This example is simulating the creation of different room styles (a single room and a family room) in WWTravelClub:

using DesignPatternsSample.BuilderSample;
using System;
namespace DesignPatternsSample
{
    class Program
    {
        static void Main()
        {
          #region Builder Sample
          Console.WriteLine("Builder Sample");
          var simpleRoom = new SimpleRoomBuilder().Build();
          simpleRoom.Describe();
          
          var familyRoom = new FamilyRoomBuilder().Build();
          familyRoom.Describe();
          #endregion
          Console.ReadKey();
        }
    }
}

The result of this implementation is quite simple but clarifies the reason why you need to implement a pattern:

Figure 11.2: Builder pattern sample result

As soon as you have the implementation, evolving this code becomes simpler and easier. For example, if you need to build a different style of room, you must just create a new builder for that type of room, and you will be able to use it.

The reason why this implementation becomes quite simple is related to the usage of chaining methods, as we can see in the Room class:

    public class Room
    {
        private readonly string _name;
        private bool wiFiFreeOfCharge;
        private int numberOfBeds;
        private bool balconyAvailable;
        public Room(string name)
        {
            _name = name;
        }
        public Room WithBalcony()
        {
            balconyAvailable = true;
            return this;
        }
        public Room WithBed(int numberOfBeds)
        {
            this.numberOfBeds = numberOfBeds;
            return this;
        }
        public Room WithWiFi()
        {
            wiFiFreeOfCharge = true;
            return this;
        }
    ...
    }

Fortunately, if you need to increase the configuration settings for the product, all the concrete classes you used previously will be defined in the Builder interface and stored there so that you can update them with ease.

We will also see a great implementation of the Builder pattern in .NET 5 in the Understanding the available design patterns in .NET 5 section. There, you will be able to understand how Generic Host was implemented using HostBuilder.

Factory pattern

The Factory pattern is useful in situations where you have multiple objects from the same abstraction, and you do not know which needs to be created by the time you start coding. This means you will have to create the instance according to a certain configuration or according to where the software is living now.

For instance, let us check out the WWTravelClub sample. Here, there is a user story that describes that this application will have customers from all over the world paying for their trips. However, in the real world, there are different payment services available for each country. The process of paying is similar for each country, but this system will have more than one payment service available. A good way to simplify this payment implementation is by using the Factory pattern. The following diagram shows the basic idea of its architectural implementation:

Figure 11.3: Factory pattern

Notice that, since you have an interface that describes what the payment service for the application is, you can use the Factory pattern to change the concrete class according to the services that are available:

static void Main()
{
    #region Factory Sample
    ProcessCharging(PaymentServiceFactory.ServicesAvailable.Brazilian,
        "[email protected]", 178.90f, EnumChargingOptions.CreditCard);
    
    ProcessCharging(PaymentServiceFactory.ServicesAvailable.Italian,
        "[email protected]", 188.70f, EnumChargingOptions.DebitCard);
    #endregion
    Console.ReadKey();
}
private static void ProcessCharging
    (PaymentServiceFactory.ServicesAvailable serviceToCharge,
    string emailToCharge, float moneyToCharge, 
    EnumChargingOptions optionToCharge)
{
    PaymentServiceFactory factory = new PaymentServiceFactory();
    var service = factory.Create(serviceToCharge);
    service.EmailToCharge = emailToCharge;
    service.MoneyToCharge = moneyToCharge;
    service.OptionToCharge = optionToCharge;
    service.ProcessCharging();
}

Once again, the service's usage has been simplified due to the implemented pattern. If you had to use this code in a real-world application, you would change the instance's behavior by defining the service you need in the Factory pattern.

Singleton pattern

When you implement Singleton in your application, you will have a single instance of the object implemented in the entire solution. This can be considered as one of the most used patterns in every application. The reason is simple—there are many use cases where you need some classes to have just one instance. Singletons solve this by providing a better solution than a global variable does.

In the Singleton pattern, the class is responsible for creating and delivering a single object that will be used by the application. In other words, the Singleton class creates a single instance:

Figure 11.4: Singleton pattern

To do so, the object that is created is static and is delivered in a static property or method. The following code implements the Singleton pattern, which has a Message property and a Print() method:

public sealed class SingletonDemo
{
    #region This is the Singleton definition
    private static SingletonDemo _instance;
    public static SingletonDemo Current => _instance ??= new 
        SingletonDemo();
    #endregion
    public string Message { get; set; }
    public void Print()
    {
        Console.WriteLine(Message);
    }
}

Its usage is simple—you just need to call the static property every time you need to use the Singleton object:

SingletonDemo.Current.Message = "This text will be printed by " +
  "the singleton.";
SingletonDemo.Current.Print();

One of the places where you may use this pattern is when you need to deliver the app configuration in a way that can be easily accessed from anywhere in the solution. For instance, let us say you have some configuration parameters that are stored in a table that your app needs to query at several decision points. Instead of querying the configuration table directly, you can create a Singleton class to help you:

Figure 11.5: Singleton pattern usage

Moreover, you will need to implement a cache in this Singleton, thus improving the performance of the system, since you will be able to decide whether the system will check each configuration in the database every time it needs it or if the cache will be used. The following screenshot shows the implementation of the cache where the configuration is loaded every 5 seconds. The parameter that is being read in this case is just a random number:

Figure 11.6: Cache implementation inside the Singleton pattern

This is great for the application's performance. Besides, using parameters in several places in your code is simpler since you do not have to create configuration instances everywhere in the code.

It is worth mentioning that due to the dependency injection implementation in .NET 5, Singleton pattern usage became less common, since you can set dependency injection to handle your Singleton objects. We will cover dependency injection in .NET 5 in later sections of this chapter.

Proxy pattern

The Proxy pattern is used when you need to provide an object that controls access to another object. One of the biggest reasons why you should do this is related to the cost of creating the object that is being controlled. For instance, if the controlled object takes too long to be created or consumes too much memory, a proxy can be used to guarantee that the largest part of the object will only be created when it is required.

The following class diagram is of a Proxy pattern implementation for loading pictures from Room, but only when requested:

Figure 11.7: Proxy pattern implementation

The client of this proxy will request its creation. Here, the proxy will only gather basic information (Id, FileName, and Tags) from the real object and will not query PictureData. When PictureData is requested, the proxy will load it:

static void Main()
{
    Console.WriteLine("Proxy Sample");
    ExecuteProxySample(new ProxyRoomPicture());
}
private static void ExecuteProxySample(IRoomPicture roomPicture)
{
    Console.WriteLine($"Picture Id: {roomPicture.Id}");
    Console.WriteLine($"Picture FileName: {roomPicture.FileName}");
    Console.WriteLine($"Tags: {string.Join(";", roomPicture.Tags)}");
    Console.WriteLine($"1st call: Picture Data");
    Console.WriteLine($"Image: {roomPicture.PictureData}");
    Console.WriteLine($"2nd call: Picture Data");
    Console.WriteLine($"Image: {roomPicture.PictureData}");
}

If PictureData is requested again, since image data is already in place, the proxy will guarantee that image reloading will not be repeated. The following screenshot shows the result of running the preceding code:

Figure 11.8: Proxy pattern result

This technique can be referred to as another well-known pattern: lazy loading. In fact, the Proxy pattern is a way of implementing lazy loading. Another approach for implementing lazy loading is the usage of the Lazy<T> type. For instance, in Entity Framework Core 5, as discussed in Chapter 8, Interacting with Data in C# – Entity Framework Core, you can turn on lazy loading using proxies. You can find out more about this at https://docs.microsoft.com/en-us/ef/core/querying/related-data#lazy-loading.

Command pattern

There are many cases where you need to execute a command that will affect the behavior of an object. The Command pattern can help you with this by encapsulating this kind of request in an object. The pattern also describes how to handle undo/redo support for the request.

For instance, let us imagine that, on the WWTravelClub website, users might have the ability to evaluate the packages by specifying whether they like, dislike, or even love their experience.

The following class diagram is an example of what can be implemented to create this rating system with the Command pattern:

Figure 11.9: Command pattern

Notice the way this pattern works—if you need a different command, such as Hate, you do not need to change the code and classes that use the command. The Undo method can be added in a similar way to the Redo method. The full code sample for this is available in this book's GitHub repository.

It might also help to mention that ASP.NET Core MVC uses the command pattern for its IActionResult hierarchy. Besides, the business operation described in Chapter 12, Understanding the Different Domains in Software Solutions, will make use of this pattern to execute business rules.

Publisher/Subscriber pattern

Providing information from an object to a group of other objects is common in all applications. The Publisher/Subscriber pattern is almost mandatory when there is a large volume of components (subscribers) that will receive a message containing the information that was sent by the object (publisher).

The concept here is quite simple to understand and is shown in the following diagram:

Figure 11.10: Publisher/Subscriber sample case

When you have an indefinite number of different possible subscribers, it is essential to decouple the component that broadcasts information from the components that consume it. The Publisher/Subscriber pattern does this for us.

Implementing this pattern is complex, since distributing environments is not a trivial task. Therefore, it is recommended that you consider already existing technologies for implementing the message broker that connects the input channel to the output channels, instead of building it from scratch. Azure Service Bus is a reliable implementation of this pattern, so all you need to do is connect to it.

RabbitMQ, which we have mentioned in Chapter 5, Applying a Microservice Architecture to Your Enterprise Application, is another service that can be used to implement a message broker, but it is a lower-level implementation of the pattern and requires several related tasks, such as retries, in case errors have to be coded manually.

Dependency Injection pattern

The Dependency Injection pattern is considered a good way to implement the Dependency Inversion principle. One useful side effect is that it forces any implementation to follow all the other SOLID principles.

The concept is quite simple. Instead of creating instances of the objects that the component depends on, you just need to define their dependencies, declare their interfaces, and enable the reception of the objects by injection.

There are three ways to perform dependency injection:

  • Use the constructor of the class to receive the objects
  • Tag some class properties to receive the objects
  • Define an interface with a method to inject all the necessary components

The following diagram shows the implementation of the Dependency Injection pattern:

Figure 11.11: Dependency Injection pattern

Apart from this, Dependency Injection can be used with an Inversion of Control (IoC) container. This container enables the automatic injection of dependencies whenever they are asked for. There are several IoC container frameworks available on the market, but with .NET Core, there is no need to use third-party software since it contains a set of libraries to solve this in the Microsoft.Extensions.DependencyInjection namespace.

This IoC container is responsible for creating and disposing of the objects that are requested. The implementation of Dependency Injection is based on constructor types. There are three options for the injected component's lifetime:

  • Transient: The objects are created each time they are requested.
  • Scoped: The objects are created for each scope defined in the application. In a web app, a scope is identified with a web request.
  • Singleton: Each object has the same application lifetime, so a single object is reused to serve all the requests for a given type. If your object contains state, you should not use this one, unless it is thread-safe.

The way you are going to use these options depends on the business rules of the project you are developing. It is also a matter of how you are going to register the services of the application. You need to be careful in deciding the correct one, since the behavior of the application will change according to the type of object you are injecting.

Understanding the available design patterns in .NET 5

As we discovered in the previous sections, C# allows us to implement any of the patterns. .NET 5 provides many implementations in its SDK that follow all the patterns we have discussed, such as Entity Framework Core proxy lazy loading. Another good example that has been available since .NET Core 2.1 is .NET Generic Host.

In Chapter 15, Presenting ASP.NET Core MVC, we will detail the hosting that's available for web apps in .NET 5. This web host helps us since the startup of the app and lifetime management is set up alongside it. The idea of .NET Generic Host is to enable this pattern for applications that do not need HTTP implementation. With this Generic Host, any .NET Core program can have a startup class where we can configure the dependency injection engine. This can be useful for creating multi-service apps.

You can find out more about .NET Generic Host at https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/generic-host, which contains some sample code and is the current recommendation from Microsoft. The code provided in the GitHub repository is simpler, but it focuses on the creation of a console app that can run a service for monitoring. The great thing about this is the way the console app is set up to run, where the builder configures the services that will be provided by the application, and the way logging will be managed.

This is shown in the following code:

public static void Main()
{
    var host = new HostBuilder()
        .ConfigureServices((hostContext, services) =>
        {
            services.AddHostedService<HostedService>();
            services.AddHostedService<MonitoringService>();
        })
        .ConfigureLogging((hostContext, configLogging) =>
        {
            configLogging.AddConsole();
        })
        .Build();
    host.Run();
    Console.WriteLine("Host has terminated. Press any key to finish the App.");
    Console.ReadKey();
}

The preceding code gives us an idea of how .NET Core uses design patterns. Using the Builder pattern, .NET Generic Host allows you to set the classes that will be injected as services. Apart from this, the Builder pattern helps you configure some other features, such as the way logs will be shown/stored. This configuration allows the services to inject ILogger<out TCategoryName> objects into any instance.

Summary

In this chapter, we understood why design patterns help with the maintainability and reusability of the parts of the system you are building. We also looked at some typical use cases and code snippets that you can use in your projects. Finally, we presented .NET Generic Host, which is a good example of how .NET uses design patterns to enable code reusability and enforce best practices.

All this content will help you while architecting new software or even maintaining an existing one, since design patterns are already-known solutions for some real-life problems in software development.

In the next chapter, we will cover the domain-driven design approach. We will also learn how to use the SOLID design principles so that we can map different domains to our software solutions.

Questions

  1. What are design patterns?
  2. What is the difference between design patterns and design principles?
  3. When is it a good idea to implement the Builder pattern?
  4. When is it a good idea to implement the Factory pattern?
  5. When is it a good idea to implement the Singleton pattern?
  6. When is it a good idea to implement the Proxy pattern?
  7. When is it a good idea to implement the Command pattern?
  8. When is it a good idea to implement the Publisher/Subscriber pattern?
  9. When is it a good idea to implement the Dependency Injection pattern?

Further reading

The following are some books and websites where you can find out more regarding what was covered in this chapter:

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.141.244.201