Table of Contents
Introduction to Creational Design Patterns
Singleton Pattern
2.1 Classic Singleton Implementation
2.2 Modern Thread-Safe Singleton
2.3 Real-World Example: Logging Service
2.4 Pros, Cons, and Alternatives
Factory Method Pattern
3.1 Implementation in C#
3.2 Real-World Example: Payment Gateway
3.3 Pros, Cons, and Alternatives
Abstract Factory Pattern
4.1 Implementation in C#
4.2 Real-World Example: Theme Factory
4.3 Pros, Cons, and Alternatives
Builder Pattern
5.1 Implementation in C#
5.2 Real-World Example: Report Generator
5.3 Pros, Cons, and Alternatives
Prototype Pattern
6.1 Implementation in C#
6.2 Real-World Example: Product Catalog
6.3 Pros, Cons, and Alternatives
Lazy Initialization Pattern
7.1 Implementation in C#
7.2 Real-World Example: Database Connection
7.3 Pros, Cons, and Alternatives
Dependency Injection Pattern
8.1 Implementation in ASP.NET Core
8.2 Real-World Example: Service Layer
8.3 Pros, Cons, and Alternatives
Best Practices for Creational Patterns
Conclusion
1. Introduction to Creational Design Patterns
Creational design patterns focus on object creation mechanisms, providing flexibility and control over how objects are instantiated. These patterns are crucial in .NET applications to ensure scalable, maintainable, and testable code. In this module, we explore Singleton, Factory Method, Abstract Factory, Builder, Prototype, Lazy Initialization, and Dependency Injection patterns, using C#, ASP.NET, and SQL Server to demonstrate real-world applications.
Each pattern is explained with:
Definition and Purpose: What the pattern does and why it’s useful.
C# Implementation: Code examples with exception handling.
Real-World Example: Practical scenarios using ASP.NET and SQL Server.
Pros, Cons, and Alternatives: Benefits, limitations, and alternative approaches.
Best Practices: Tips for effective implementation.
2. Singleton Pattern
The Singleton pattern ensures a class has only one instance and provides a global point of access to it. It’s ideal for scenarios requiring a single shared resource, such as logging or configuration management.
2.1 Classic Singleton Implementation
The classic Singleton uses a private constructor and a static instance.
public class Singleton
{
private static Singleton _instance;
private static readonly object _lock = new object();
// Private constructor prevents external instantiation
private Singleton()
{
// Initialization code
}
public static Singleton Instance
{
get
{
if (_instance == null)
{
lock (_lock)
{
if (_instance == null)
{
_instance = new Singleton();
}
}
}
return _instance;
}
}
public void LogMessage(string message)
{
Console.WriteLine($"Log: {message}");
}
}
Usage:
var singleton = Singleton.Instance;
singleton.LogMessage("Classic Singleton Example");
2.2 Modern Thread-Safe Singleton
Modern .NET applications use Lazy<T> for thread-safe initialization.
public class Singleton
{
private static readonly Lazy<Singleton> _instance = new Lazy<Singleton>(() => new Singleton());
private Singleton()
{
// Initialization code
}
public static Singleton Instance => _instance.Value;
public void LogMessage(string message)
{
Console.WriteLine($"Log: {message}");
}
}
Usage:
var singleton = Singleton.Instance;
singleton.LogMessage("Thread-Safe Singleton Example");
2.3 Real-World Example: Logging Service
In an ASP.NET application, a Singleton logging service can centralize logging to a SQL Server database.
public class LoggingService
{
private static readonly Lazy<LoggingService> _instance = new Lazy<LoggingService>(() => new LoggingService());
private readonly string _connectionString;
private LoggingService()
{
_connectionString = "Server=localhost;Database=LogsDB;Trusted_Connection=True;";
}
public static LoggingService Instance => _instance.Value;
public void Log(string message)
{
try
{
using (var connection = new SqlConnection(_connectionString))
{
connection.Open();
var command = new SqlCommand("INSERT INTO Logs (Message, LogDate) VALUES (@Message, @LogDate)", connection);
command.Parameters.AddWithValue("@Message", message);
command.Parameters.AddWithValue("@LogDate", DateTime.UtcNow);
command.ExecuteNonQuery();
}
}
catch (SqlException ex)
{
Console.WriteLine($"Logging failed: {ex.Message}");
}
}
}
Usage in ASP.NET Controller:
[ApiController]
[Route("api/[controller]")]
public class LogController : ControllerBase
{
[HttpPost]
public IActionResult LogMessage([FromBody] string message)
{
LoggingService.Instance.Log(message);
return Ok("Message logged");
}
}
2.4 Pros, Cons, and Alternatives
Pros:
Ensures a single instance, reducing resource usage.
Provides global access to shared resources.
Cons:
Can introduce tight coupling.
Difficult to unit test due to global state.
Alternatives:
Use Dependency Injection to manage a single instance.
Static classes for simple scenarios without state.
3. Factory Method Pattern
The Factory Method pattern defines an interface for creating objects but allows subclasses to decide which class to instantiate. It’s useful for creating objects without specifying their concrete types.
3.1 Implementation in C#
public abstract class PaymentProcessor
{
public abstract void ProcessPayment(decimal amount);
}
public class CreditCardProcessor : PaymentProcessor
{
public override void ProcessPayment(decimal amount)
{
Console.WriteLine($"Processing credit card payment of {amount:C}");
}
}
public class PayPalProcessor : PaymentProcessor
{
public override void ProcessPayment(decimal amount)
{
Console.WriteLine($"Processing PayPal payment of {amount:C}");
}
}
public abstract class PaymentFactory
{
public abstract PaymentProcessor CreateProcessor();
}
public class CreditCardFactory : PaymentFactory
{
public override PaymentProcessor CreateProcessor() => new CreditCardProcessor();
}
public class PayPalFactory : PaymentFactory
{
public override PaymentProcessor CreateProcessor() => new PayPalProcessor();
}
Usage:
PaymentFactory factory = new CreditCardFactory();
PaymentProcessor processor = factory.CreateProcessor();
processor.ProcessPayment(100.00m);
3.2 Real-World Example: Payment Gateway
In an e-commerce ASP.NET application, a Factory Method can create payment processors.
public class Order
{
public decimal Amount { get; set; }
public string PaymentType { get; set; }
}
public class PaymentService
{
private readonly PaymentFactory _factory;
public PaymentService(PaymentFactory factory)
{
_factory = factory ?? throw new ArgumentNullException(nameof(factory));
}
public void ProcessOrder(Order order)
{
try
{
var processor = _factory.CreateProcessor();
processor.ProcessPayment(order.Amount);
}
catch (Exception ex)
{
Console.WriteLine($"Payment processing failed: {ex.Message}");
throw;
}
}
}
ASP.NET Controller:
[ApiController]
[Route("api/[controller]")]
public class PaymentController : ControllerBase
{
[HttpPost]
public IActionResult ProcessPayment([FromBody] Order order)
{
PaymentFactory factory = order.PaymentType == "CreditCard" ? new CreditCardFactory() : new PayPalFactory();
var service = new PaymentService(factory);
service.ProcessOrder(order);
return Ok("Payment processed");
}
}
3.3 Pros, Cons, and Alternatives
Pros:
Promotes loose coupling by abstracting object creation.
Allows subclasses to define object types.
Cons:
Can increase complexity with additional classes.
Requires careful factory design.
Alternatives:
Abstract Factory for families of related objects.
Simple factory (not a pattern) for basic scenarios.
4. Abstract Factory Pattern
The Abstract Factory pattern provides an interface for creating families of related objects without specifying their concrete classes. It’s ideal for scenarios requiring consistent object sets, like UI themes.
4.1 Implementation in C#
public interface IButton
{
void Render();
}
public interface ITextBox
{
void Render();
}
public class DarkButton : IButton
{
public void Render() => Console.WriteLine("Rendering Dark Button");
}
public class DarkTextBox : ITextBox
{
public void Render() => Console.WriteLine("Rendering Dark TextBox");
}
public class LightButton : IButton
{
public void Render() => Console.WriteLine("Rendering Light Button");
}
public class LightTextBox : ITextBox
{
public void Render() => Console.WriteLine("Rendering Light TextBox");
}
public interface IUIFactory
{
IButton CreateButton();
ITextBox CreateTextBox();
}
public class DarkUIFactory : IUIFactory
{
public IButton CreateButton() => new DarkButton();
public ITextBox CreateTextBox() => new DarkTextBox();
}
public class LightUIFactory : IUIFactory
{
public IButton CreateButton() => new LightButton();
public ITextBox CreateTextBox() => new LightTextBox();
}
Usage:
IUIFactory factory = new DarkUIFactory();
IButton button = factory.CreateButton();
ITextBox textBox = factory.CreateTextBox();
button.Render();
textBox.Render();
4.2 Real-World Example: Theme Factory
In an ASP.NET application, an Abstract Factory can manage UI themes.
public class ThemeService
{
private readonly IUIFactory _factory;
public ThemeService(IUIFactory factory)
{
_factory = factory ?? throw new ArgumentNullException(nameof(factory));
}
public string RenderUI()
{
try
{
var button = _factory.CreateButton();
var textBox = _factory.CreateTextBox();
button.Render();
textBox.Render();
return "UI rendered successfully";
}
catch (Exception ex)
{
return $"UI rendering failed: {ex.Message}";
}
}
}
ASP.NET Controller:
[ApiController]
[Route("api/[controller]")]
public class ThemeController : ControllerBase
{
[HttpGet]
public IActionResult ApplyTheme([FromQuery] string theme)
{
IUIFactory factory = theme == "dark" ? new DarkUIFactory() : new LightUIFactory();
var service = new ThemeService(factory);
var result = service.RenderUI();
return Ok(result);
}
}
4.3 Pros, Cons, and Alternatives
Pros:
Ensures consistency across related objects.
Simplifies swapping entire object families.
Cons:
Increases complexity with multiple factories.
Can be overkill for simple applications.
Alternatives:
Factory Method for single object types.
Configuration-based object creation.
5. Builder Pattern
The Builder pattern separates the construction of complex objects from their representation, allowing step-by-step construction.
5.1 Implementation in C#
public class Report
{
public string Header { get; set; }
public string Body { get; set; }
public string Footer { get; set; }
}
public interface IReportBuilder
{
void BuildHeader();
void BuildBody();
void BuildFooter();
Report GetReport();
}
public class PdfReportBuilder : IReportBuilder
{
private Report _report = new Report();
public void BuildHeader() => _report.Header = "PDF Report Header";
public void BuildBody() => _report.Body = "PDF Report Content";
public void BuildFooter() => _report.Footer = "PDF Report Footer";
public Report GetReport() => _report;
}
public class Director
{
private readonly IReportBuilder _builder;
public Director(IReportBuilder builder)
{
_builder = builder ?? throw new ArgumentNullException(nameof(builder));
}
public Report Construct()
{
_builder.BuildHeader();
_builder.BuildBody();
_builder.BuildFooter();
return _builder.GetReport();
}
}
Usage:
IReportBuilder builder = new PdfReportBuilder();
var director = new Director(builder);
var report = director.Construct();
Console.WriteLine($"{report.Header}, {report.Body}, {report.Footer}");
5.2 Real-World Example: Report Generator
In an ASP.NET application, the Builder pattern can generate reports stored in SQL Server.
public class ReportService
{
private readonly Director _director;
private readonly string _connectionString;
public ReportService(IReportBuilder builder)
{
_director = new Director(builder);
_connectionString = "Server=localhost;Database=ReportsDB;Trusted_Connection=True;";
}
public async Task<Report> GenerateReportAsync()
{
try
{
var report = _director.Construct();
using (var connection = new SqlConnection(_connectionString))
{
await connection.OpenAsync();
var command = new SqlCommand("INSERT INTO Reports (Header, Body, Footer) VALUES (@Header, @Body, @Footer)", connection);
command.Parameters.AddWithValue("@Header", report.Header);
command.Parameters.AddWithValue("@Body", report.Body);
command.Parameters.AddWithValue("@Footer", report.Footer);
await command.ExecuteNonQueryAsync();
}
return report;
}
catch (SqlException ex)
{
throw new Exception($"Report generation failed: {ex.Message}");
}
}
}
ASP.NET Controller:
[ApiController]
[Route("api/[controller]")]
public class ReportController : ControllerBase
{
[HttpGet]
public async Task<IActionResult> GenerateReport()
{
var builder = new PdfReportBuilder();
var service = new ReportService(builder);
var report = await service.GenerateReportAsync();
return Ok(report);
}
}
5.3 Pros, Cons, and Alternatives
Pros:
Simplifies complex object creation.
Allows flexible construction steps.
Cons:
Requires additional classes (builder, director).
Can be complex for simple objects.
Alternatives:
Factory patterns for simpler object creation.
Fluent interfaces for configuration.
6. Prototype Pattern
The Prototype pattern creates new objects by copying an existing object, known as the prototype. It’s useful for expensive object creation.
6.1 Implementation in C#
public interface IPrototype
{
IPrototype Clone();
}
public class Product : IPrototype
{
public string Name { get; set; }
public decimal Price { get; set; }
public IPrototype Clone()
{
return (IPrototype)MemberwiseClone();
}
}
Usage:
Product product = new Product { Name = "Laptop", Price = 999.99m };
Product clonedProduct = (Product)product.Clone();
Console.WriteLine($"Cloned: {clonedProduct.Name}, {clonedProduct.Price}");
6.2 Real-World Example: Product Catalog
In an ASP.NET e-commerce application, the Prototype pattern can clone product templates.
public class ProductService
{
private readonly IPrototype _prototype;
private readonly string _connectionString;
public ProductService(IPrototype prototype)
{
_prototype = prototype ?? throw new ArgumentNullException(nameof(prototype));
_connectionString = "Server=localhost;Database=ProductsDB;Trusted_Connection=True;";
}
public async Task<Product> AddProductAsync(string name, decimal price)
{
try
{
var product = (Product)_prototype.Clone();
product.Name = name;
product.Price = price;
using (var connection = new SqlConnection(_connectionString))
{
await connection.OpenAsync();
var command = new SqlCommand("INSERT INTO Products (Name, Price) VALUES (@Name, @Price)", connection);
command.Parameters.AddWithValue("@Name", product.Name);
command.Parameters.AddWithValue("@Price", product.Price);
await command.ExecuteNonQueryAsync();
}
return product;
}
catch (SqlException ex)
{
throw new Exception($"Product creation failed: {ex.Message}");
}
}
}
ASP.NET Controller:
[ApiController]
[Route("api/[controller]")]
public class ProductController : ControllerBase
{
[HttpPost]
public async Task<IActionResult> AddProduct([FromBody] Product product)
{
var prototype = new Product { Name = "Default", Price = 0.00m };
var service = new ProductService(prototype);
var newProduct = await service.AddProductAsync(product.Name, product.Price);
return Ok(newProduct);
}
}
6.3 Pros, Cons, and Alternatives
Pros:
Reduces object creation overhead.
Simplifies creating complex objects.
Cons:
Deep cloning can be complex for nested objects.
Requires careful implementation of cloning.
Alternatives:
Factory patterns for simpler creation.
Serialization for object copying.
7. Lazy Initialization Pattern
The Lazy Initialization pattern delays object creation until it’s needed, improving performance.
7.1 Implementation in C#
public class DatabaseService
{
private readonly Lazy<SqlConnection> _connection;
public DatabaseService()
{
_connection = new Lazy<SqlConnection>(() =>
{
var conn = new SqlConnection("Server=localhost;Database=AppDB;Trusted_Connection=True;");
conn.Open();
return conn;
});
}
public SqlConnection Connection => _connection.Value;
}
Usage:
var dbService = new DatabaseService();
var connection = dbService.Connection; // Connection created here
7.2 Real-World Example: Database Connection
In an ASP.NET application, Lazy Initialization can optimize database connections.
public class DataAccessService
{
private readonly Lazy<SqlConnection> _connection;
public DataAccessService()
{
_connection = new Lazy<SqlConnection>(() =>
{
var conn = new SqlConnection("Server=localhost;Database=AppDB;Trusted_Connection=True;");
conn.Open();
return conn;
});
}
public async Task<string> FetchDataAsync(string query)
{
try
{
using (var command = new SqlCommand(query, _connection.Value))
{
var result = await command.ExecuteScalarAsync();
return result?.ToString() ?? "No data";
}
}
catch (SqlException ex)
{
throw new Exception($"Data fetch failed: {ex.Message}");
}
}
}
ASP.NET Controller:
[ApiController]
[Route("api/[controller]")]
public class DataController : ControllerBase
{
private readonly DataAccessService _service;
public DataController()
{
_service = new DataAccessService();
}
[HttpGet]
public async Task<IActionResult> GetData()
{
var result = await _service.FetchDataAsync("SELECT TOP 1 Name FROM Products");
return Ok(result);
}
}
7.3 Pros, Cons, and Alternatives
Pros:
Improves performance by delaying initialization.
Reduces memory usage for unused objects.
Cons:
Can hide initialization costs.
Thread-safety requires careful handling.
Alternatives:
Eager initialization for predictable performance.
Dependency Injection for managed resources.
8. Dependency Injection Pattern
Dependency Injection (DI) provides dependencies to a class, promoting loose coupling and testability. ASP.NET Core has built-in DI support.
8.1 Implementation in ASP.NET Core
public interface IProductRepository
{
Task<Product> GetProductAsync(int id);
}
public class ProductRepository : IProductRepository
{
private readonly string _connectionString;
public ProductRepository()
{
_connectionString = "Server=localhost;Database=ProductsDB;Trusted_Connection=True;";
}
public async Task<Product> GetProductAsync(int id)
{
try
{
using (var connection = new SqlConnection(_connectionString))
{
await connection.OpenAsync();
var command = new SqlCommand("SELECT Name, Price FROM Products WHERE Id = @Id", connection);
command.Parameters.AddWithValue("@Id", id);
using (var reader = await command.ExecuteReaderAsync())
{
if (await reader.ReadAsync())
{
return new Product
{
Name = reader.GetString(0),
Price = reader.GetDecimal(1)
};
}
return null;
}
}
}
catch (SqlException ex)
{
throw new Exception($"Product fetch failed: {ex.Message}");
}
}
}
Startup Configuration:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<IProductRepository, ProductRepository>();
services.AddControllers();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseRouting();
app.UseEndpoints(endpoints => endpoints.MapControllers());
}
}
8.2 Real-World Example: Service Layer
In an ASP.NET Core application, DI manages service dependencies.
public class ProductService
{
private readonly IProductRepository _repository;
public ProductService(IProductRepository repository)
{
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
}
public async Task<Product> GetProductAsync(int id)
{
return await _repository.GetProductAsync(id);
}
}
ASP.NET Controller:
[ApiController]
[Route("api/[controller]")]
public class ProductController : ControllerBase
{
private readonly ProductService _service;
public ProductController(ProductService service)
{
_service = service;
}
[HttpGet("{id}")]
public async Task<IActionResult> GetProduct(int id)
{
var product = await _service.GetProductAsync(id);
if (product == null) return NotFound();
return Ok(product);
}
}
8.3 Pros, Cons, and Alternatives
Pros:
Promotes loose coupling and testability.
Simplifies dependency management.
Cons:
Increases complexity for small applications.
Requires DI container setup.
Alternatives:
Service Locator pattern (less preferred).
Manual dependency passing.
9. Best Practices for Creational Patterns
Singleton:
Use Lazy<T> for thread-safe initialization.
Avoid overuse to prevent tight coupling.
Factory Method:
Use when subclasses need to define object creation.
Combine with DI for flexibility.
Abstract Factory:
Ensure consistent object families.
Keep factories simple to avoid complexity.
Builder:
Use for complex object construction.
Implement fluent interfaces for readability.
Prototype:
Use MemberwiseClone for shallow copies, implement deep cloning for complex objects.
Validate cloned objects for consistency.
Lazy Initialization:
Use with expensive resources like database connections.
Ensure thread-safety in multi-threaded environments.
Dependency Injection:
Leverage ASP.NET Core’s DI container.
Use constructor injection for clarity.
10. Conclusion
Creational design patterns like Singleton, Factory Method, Abstract Factory, Builder, Prototype, Lazy Initialization, and Dependency Injection are essential for building scalable .NET applications. By understanding their implementations, real-world applications, pros, cons, and best practices, developers can create robust, maintainable systems. Use these patterns judiciously, aligning them with your application’s needs, and combine them with ASP.NET and SQL Server for powerful solutions.
🚀 Expand Your Learning Journey
📘 Master Software Design Patterns: Complete Course Outline (.NET & Java) | 🎯 Free Learning Zone
📘 Master Software Design Patterns: Complete Course Outline (.NET & Java) 🎯 Visit Free Learning Zone
No comments:
Post a Comment
Thanks for your valuable comment...........
Md. Mominul Islam