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.
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:
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.
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:
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:
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:
Maintainability:?Code becomes cleaner and easier to understand by separating legacy concerns.
Remember:
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:
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);
});
}
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:
Followers register as observers and receive instant updates about relevant events, keeping them engaged in the platform.
Interfaces:
public interface INotifiable
{
void RegisterObserver(IObserver observer);
void UnregisterObserver(IObserver observer);
void NotifyObservers();
}
public interface IObserver
{
void Update(INotifiable sender, object eventData);
}
Implementations:
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
Benefits
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
How It Works
// 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();
}
}