Design Patterns for Dot Net #dotnet #designpatterns
https://designer.microsoft.com/

Design Patterns for Dot Net #dotnet #designpatterns


Design Patterns

Reusable solutions to common software design problems that promote good object-oriented principles, improve code quality and make it easier to understand and maintain.

Use design patterns.

Reduce code duplication, improve flexibility and maintainability, promote code readability, and leverage proven solutions.

Different types of design patterns

There are many, but some common types include?1. Creational patterns?(Singleton, Factory Method, Builder),?2. Structural patterns?(Adapter, Decorator, Facade), and?3. Behavioral patterns?(Observer, Strategy, Iterator).

1. Creational design patterns

Creational design patterns are reusable solutions to common object-creation problems. They provide various mechanisms for creating objects that improve flexibility, decoupling, and maintainability in software design.

2. Structural design patterns

Structural design patterns focus on how classes and objects can be effectively composed to form larger structures. They provide strategies for organizing and relating objects to achieve flexibility, efficiency, and maintainability in your software design. By applying these patterns, you can ensure your object relationships are well-defined and easily manageable, even as your software grows in complexity.

3. Behavioral design patterns

These patterns focus on how objects communicate and collaborate, defining the dynamics of interaction within your software system. They're like choreographers, dictating how objects will "dance" together to achieve specific goals.

When should you use a design pattern?

When you recognize a common design problem that a pattern addresses, using the pattern provides clear benefits in terms of code quality, flexibility, and maintainability.

  1. Explanation of the Singleton pattern with an example of its use.

Ensures only one instance of a class exists, often used for configurations or global access points. Example: Logging or Database connection manager.

public class Logger
{
    private static readonly Logger instance = new Logger();

    private Logger() {}

    public static Logger Instance
    {
        get { return instance; }
    }

    public void LogMessage(string message)
    {
        // Implement your logging logic here, e.g., write to a file or console
        Console.WriteLine(message);
    }
}

// Accessing the Singleton instance
Logger.Instance.LogMessage("This is a log message!");
        

This code uses the following key techniques:

  • Private constructor:?This prevents creating any new instances with?new?keywords.
  • Static instance:?A single static instance called?instance?is declared and initialized early.
  • Public static property:?The?Instance?property provides access to the existing instance.

This implementation ensures that only one Logger object exists, and you can access it anywhere in your application using Logger. Instance.

2. Factory Method pattern

Creates objects without specifying their concrete class, promoting flexibility, and decoupling from specific implementations.

  • E.g. Creating database connections:

o?? Imagine a system that supports multiple databases (MySQL, PostgreSQL, etc.). Instead of hardcoding the specific database connection logic within your application, you can define a base interface for "DatabaseConnection" and have concrete factories for each type of database. This allows you to choose the desired database connection at runtime based on configuration settings or user input.

public interface IDatabaseConnection
{
    DbConnection GetConnection();
}
        

This interface defines the GetConnection method that returns a DbConnection object. Any concrete database connection factory implementing this interface must provide its implementation for this method.

We'll create a base?DatabaseConnectionFactory?with common configuration options:

public abstract class DatabaseConnectionFactory : IDatabaseConnection
{
    protected readonly string _connectionString;
    protected readonly int _timeout;

    public DatabaseConnectionFactory(string connectionString, int timeout)
    {
        _connectionString = connectionString;
        _timeout = timeout;
    }

    public abstract DbConnection GetConnection();
}
 
        

Concrete factories subclass this base and add their specific settings:

public class SqlServerConnectionFactory : DatabaseConnectionFactory
{
    private readonly bool _useIntegratedSecurity;

    public SqlServerConnectionFactory(string connectionString, int timeout, bool useIntegratedSecurity)
        : base(connectionString, timeout)
    {
        _useIntegratedSecurity = useIntegratedSecurity;
    }

    public override DbConnection GetConnection()
    {
        var builder = new SqlConnectionStringBuilder(_connectionString);
        builder.ConnectTimeout = _timeout;
        builder.IntegratedSecurity = _useIntegratedSecurity;
        return new SqlConnection(builder.ConnectionString);
    }
}

public class MySqlConnectionFactory : DatabaseConnectionFactory
{
    private readonly string _database;

    public MySqlConnectionFactory(string connectionString, int timeout, string database)
        : base(connectionString, timeout)
    {
        _database = database;
    }

    public override DbConnection GetConnection()
    {
        var builder = new MySqlConnectionStringBuilder(_connectionString);
        builder.ConnectTimeout = _timeout;
        builder.Database = _database;
        return new MySqlConnection(builder.ConnectionString);
    }
}
        

You can pass configuration options during factory initialization. Get the connection string from the app settings config file.z

var sqlServerFactory = new SqlServerConnectionFactory(
    "your_sql_server_connection_string",
    30, // timeout in seconds
    true // use integrated security
);

var mysqlFactory = new MySqlConnectionFactory(
    "your_mysql_connection_string",
    10, // timeout in seconds
    "your_database_name"
);
        

This demonstrates how each factory can handle its specific settings without modifying the core functionalities. You can further extend this by:

  • Adding more configuration options (e.g.,?pool size,?logging level).
  • Reading configuration options from external sources (e.g.,?configuration files,?environment variables).
  • Creating different factory configurations for different environments (e.g.,?development,?production).

By combining the Factory Method pattern with configuration options, you gain greater flexibility and control over your database connections while maintaining decoupling and code organization.

3. Adapter pattern work

Makes incompatible interfaces compatible by translating between them, allowing them to work together.

Legacy Integration:

  • Problem: You have a new system that needs to use data from an existing system with an incompatible interface (e.g., different data format, communication protocol).
  • Solution: Create an adapter that translates between the two interfaces, allowing the new system to seamlessly access and process data from the old system.
  • Example: Integrating a modern e-commerce platform with an outdated inventory management system using different data formats.Third-Party Billing System IntegrationProblem: Your application needs to use a third-party billing system, but their API has a different structure than your existing code expects.Solution: Create an adapter that maps your application's interface to the third-party API.

public interface IBillingSystem
{
    void ProcessPayment(PaymentDetails details);
}
        

Adaptee:

public class ThirdPartyBillingSystem
{
    public void ChargeCreditCard(string cardNumber, decimal amount)
    {
        // Third-party billing logic
    }
}

Adapter:

public class BillingSystemAdapter : IBillingSystem
{
    private readonly ThirdPartyBillingSystem _thirdPartySystem;

    public BillingSystemAdapter(ThirdPartyBillingSystem thirdPartySystem)
    {
        _thirdPartySystem = thirdPartySystem;
    }

    public void ProcessPayment(PaymentDetails details)
    {
        _thirdPartySystem.ChargeCreditCard(details.CardNumber, details.Amount);
    }
}
        

Benefits:

  • Isolation:?Legacy code remains untouched,?avoiding potential modification risks.
  • Compatibility:?Makes legacy functionality accessible to new code using a modern interface.

Maintainability:?Code becomes cleaner and easier to understand by separating legacy concerns.

Remember:

  • Choose the appropriate adapter type (Object Adapter,?Class Adapter) based on your specific needs.
  • Consider potential performance overhead due to translation between interfaces.
  • Document the adapter's purpose and usage clearly for future maintainers.

4. Benefits of using the Decorator pattern

Dynamically adds new functionalities to objects without modifying their original class, promoting flexible composition. Example: Adding loggingService and Decorator Interfaces:

public interface IService
{
    string ProcessData(string data);
}

public interface ILogService
{
    void LogInfo(string message);
}
        

Implementations:

  • DataService:?implements?IService?with some specific data processing logic.
  • LoggingDecorator:?wraps another service and logs information before and after its?ProcessData?call.

public class DataService : IService
{
    public string ProcessData(string data)
    {
        // Implement your specific data processing logic here
        return $"Processed data: {data}";
    }
}

public class LoggingDecorator : IService
{
    private readonly IService _decoratedService;
    private readonly ILogService _logger;

    public LoggingDecorator(IService decoratedService, ILogService logger)
    {
        _decoratedService = decoratedService;
        _logger = logger;
    }

    public string ProcessData(string data)
    {
        _logger.LogInfo($"Starting data processing: {data}");
        var processedData = _decoratedService.ProcessData(data);
        _logger.LogInfo($"Data processing completed: {processedData}");
        return processedData;
    }
}
        

Register Services in Startup.cs:

public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient<IService, DataService>(); // DataService is transient here
    services.AddSingleton<ILogService, ConsoleLogger>(); // Logger could be singleton
    services.AddTransient<IService, LoggingDecorator>(sp => // Decorate IService with LoggingDecorator
    {
        var decoratedService = sp.GetRequiredService<IService>();
        var logger = sp.GetRequiredService<ILogService>();
        return new LoggingDecorator(decoratedService, logger);
    });
}
        

  • Register?DataService?as transient (new instance per request).
  • Register?ConsoleLogger?as a singleton (one instance).
  • Add a delegate to register?LoggingDecorator?as a transient.?Within the delegate,?access the already registered?IService?and?ILogService?using dependency injection,?and use them to create a new?LoggingDecorator?instance.Inject Decorated Service:

public class MyController : Controller
{
    private readonly IService _service; // Now injected with LoggingDecorator

    public MyController(IService service)
    {
        _service = service;
    }

    public IActionResult ProcessData(string data)
    {
        var processedData = _service.ProcessData(data);
        // Use processed data...
        return Ok(processedData);
    }
}
        

Inject?IService?through constructor injection.?By setting up the service registration as shown,?inject the actual?LoggingDecorator?instance that wraps the?DataService.

5. Observer pattern a good choice

When one object needs to notify multiple other objects of changes, facilitates loosely coupled communication.

Social Media Notifications:

  • Users and communities act as subjects, notifying followers of new posts, replies, or activities.

Followers register as observers and receive instant updates about relevant events, keeping them engaged in the platform.

Interfaces:

  • INotifiable:?Defines the methods subjects (entities generating notifications) use to notify observers.
  • IObserver:?Defines the methods observers (entities receiving notifications) implement to handle them.

public interface INotifiable
{
    void RegisterObserver(IObserver observer);
    void UnregisterObserver(IObserver observer);
    void NotifyObservers();
}

public interface IObserver
{
    void Update(INotifiable sender, object eventData);
}
        

Implementations:

  • User:?Represents a social media user (subject).
  • Post:?Represents a user's post (subject).
  • Follower:?Represents a user following another user (observer).
  • Notification Manager:?Helper class to manage registrations and unregistrations (optional).

public class User : INotifiable
{
   private List<IObserver> _followers;
   public string Username { get; set; }

   public List<IObserver> Followers => _followers;

   public User(string username)
   {
     _followers = new List<IObserver>();
     Username = username;
   }

   public void RegisterObserver(IObserver observer)
   {
     _followers.Add(observer);
   }

   public void UnregisterObserver(IObserver observer)
   {
     _followers.Remove(observer);
   }

    public void NotifyObservers()
    {
       foreach (var follower in _followers)
       {
          follower.Update(this, null); // Pass relevant event data here
       }
    }
}
	
public class Post : INotifiable
{
   private List<IObserver> _likedBy;
   public string Content { get; set; }

   public List<IObserver> LikedBy => _likedBy;

    public Post(string content)
    {
      _likedBy = new List<IObserver>();
      Content = content;
    }

    public void RegisterObserver(IObserver observer)
    {
       _likedBy.Add(observer);
    }

    public void UnregisterObserver(IObserver observer)
    {
      _likedBy.Remove(observer);
    }

    public void NotifyObservers()
    {
       foreach (var follower in _likedBy)
       {
          follower.Update(this, null); // Pass relevant event data like  liking user etc.
       }
    }
}

public class Follower : IObserver
{
    public User Following { get; set; }

    public Follower(User following)
    {
       Following = following;
       following.RegisterObserver(this);
    }

    public void Update(INotifiable sender, object eventData)
    {
      if (sender is User user)
      {
        Console.WriteLine($"{Following.Username} posted: '{user.Username}'!");
      }
      else if (sender is Post post)
      {
       Console.WriteLine($"{Following.Username} liked '{post.Content}'!");
      }
    }
}

public class NotificationManager
{
   public static void RegisterUserFollower(User user, Follower follower)
   {
      user.RegisterObserver(follower);
   }

   public static void UnregisterUserFollower(User user, Follower follower)
   {
      user.UnregisterObserver(follower);
   }

   public static void RegisterPostLike(Post post, Follower follower)
   {
     post.RegisterObserver(follower);
   }

   public static void UnregisterPostLike(Post post, Follower follower)
   {
     post.UnregisterObserver(follower);
   }
}
        

6.Strangler Fig Pattern?

The?Strangler Fig Pattern?is a software design pattern used to incrementally replace a legacy system with a new one. This approach allows you to modernize a system piece by piece while the existing system continues to operate without interruption.

How It Works

  1. Facade Creation: Introduce a facade that intercepts requests to the legacy system.
  2. Incremental Replacement: Gradually replace specific pieces of functionality with new applications or services.
  3. Routing Requests: The facade routes requests to either the legacy system or the new system based on the functionality required.
  4. Complete Migration: Over time, as more features are migrated, the legacy system is “strangled” and can eventually be decommissioned.

Benefits

  • Reduced Risk: Minimizes the risk associated with a complete system overhaul.
  • Continuous Operation: Ensures the system remains operational during the migration.
  • Flexibility: Allows for gradual adoption of new technologies and architectures.


public class StranglerFacade
{
    private readonly LegacySystem _legacySystem;
    private readonly NewSystem _newSystem;

    public StranglerFacade(LegacySystem legacySystem, NewSystem newSystem)
    {
        _legacySystem = legacySystem;
        _newSystem = newSystem;
    }

    public void HandleRequest(string request)
    {
        if (IsNewFunctionality(request))
        {
            _newSystem.HandleRequest(request);
        }
        else
        {
            _legacySystem.HandleRequest(request);
        }
    }

    private bool IsNewFunctionality(string request)
    {
        // Logic to determine if the request should be handled by the new system
        return request.Contains("new");
    }
}
        

7.Facade Design Pattern

The Facade Design Pattern is a structural design pattern that provides a simplified interface to a complex system of classes, libraries, or frameworks. This pattern is particularly useful when you want to make a system easier to use by hiding its complexities.

Key Concepts

  1. Simplified Interface: The facade provides a single, simplified interface to the complex subsystem.
  2. Delegation: The facade delegates client requests to the appropriate objects within the subsystem.
  3. Decoupling: It helps in decoupling the client from the complex subsystem, making the code easier to maintain.

How It Works

  • Subsystem Classes: These are the complex classes that the facade will simplify.
  • Facade Class: This class provides a simplified interface to the subsystem classes.
  • Client Code: The client interacts with the subsystem through the facade, which simplifies the interaction.

// Subsystem classes
public class SubsystemA
{
    public void OperationA()
    {
        Console.WriteLine("SubsystemA: OperationA");
    }
}

public class SubsystemB
{
    public void OperationB()
    {
        Console.WriteLine("SubsystemB: OperationB");
    }
}

// Facade class
public class Facade
{
    private readonly SubsystemA _subsystemA;
    private readonly SubsystemB _subsystemB;

    public Facade(SubsystemA subsystemA, SubsystemB subsystemB)
    {
        _subsystemA = subsystemA;
        _subsystemB = subsystemB;
    }

    public void Operation()
    {
        _subsystemA.OperationA();
        _subsystemB.OperationB();
    }
}

// Client code
class Program
{
    static void Main(string[] args)
    {
        SubsystemA subsystemA = new SubsystemA();
        SubsystemB subsystemB = new SubsystemB();
        Facade facade = new Facade(subsystemA, subsystemB);

        facade.Operation();
    }
}
        


要查看或添加评论,请登录

Tarakeswhara Rao Bandla的更多文章

  • C#

    C#

    C# is a versatile and powerful programming language developed by Microsoft as part of the .NET platform.

  • .NET Core #dotnet

    .NET Core #dotnet

    The .NET Core platform is a new .

  • Message Queuing Microservices

    Message Queuing Microservices

    1. Message Queuing A message queue is a middleware technology that acts as a buffer for messages between applications…

  • OOPs with C#

    OOPs with C#

    Object Oriented Programming(OO)P is a programming paradigm that revolves around creating objects that model real-world…

    2 条评论
  • Solid Principles with Real-Time

    Solid Principles with Real-Time

    The SOLID principles! These are five fundamental design principles in object-oriented programming that promote creating…

  • Beyond Tomorrow

    Beyond Tomorrow

    Stages of Artificial Intelligence Artificial Intelligence(AI) is the intelligence exhibited by machines or software, as…

    1 条评论

社区洞察