Table of Contents
Introduction to Software Design Patterns
1.1 What Are Design Patterns?
1.2 Importance in .NET and Java
1.3 Course Objectives and Structure
Foundational Concepts
2.1 Object-Oriented Programming (OOP) Principles
2.2 SOLID Principles in Depth
2.3 UML and Design Pattern Visualization
Creational Design Patterns
3.1 Singleton Pattern
3.2 Factory Method Pattern
3.3 Abstract Factory Pattern
3.4 Builder Pattern
3.5 Prototype Pattern
Structural Design Patterns
4.1 Adapter Pattern
4.2 Decorator Pattern
4.3 Facade Pattern
4.4 Composite Pattern
4.5 Proxy Pattern
Behavioral Design Patterns
5.1 Observer Pattern
5.2 Strategy Pattern
5.3 Command Pattern
5.4 Chain of Responsibility Pattern
5.5 State Pattern
Intermediate Patterns in Action
6.1 Repository Pattern
6.2 Unit of Work Pattern
6.3 Dependency Injection
Modern .NET and Java Features in Design Patterns
7.1 Records in .NET
7.2 Sealed Classes in Java
7.3 Pattern Matching in .NET and Java
Asynchronous and Reactive Design Patterns
8.1 Async/Await in .NET
8.2 Reactive Streams in Java
8.3 Task Parallel Library (TPL) in .NET
Cloud-Native Design Patterns
9.1 Circuit Breaker Pattern
9.2 Bulkhead Pattern
9.3 Retry Pattern
Event-Driven and Streaming Design Patterns
10.1 Event Sourcing
10.2 CQRS (Command Query Responsibility Segregation)
10.3 Publish-Subscribe Pattern
CI/CD and DevOps Integration
11.1 Design Patterns in CI/CD Pipelines
11.2 DevOps Best Practices for Pattern Implementation
Real-World Case Study: E-Commerce Platform
12.1 Applying Patterns in an ASP.NET Core Application
12.2 Database Integration with SQL Server
12.3 Exception Handling and Logging
Best Practices, Pros, Cons, and Alternatives
13.1 Best Practices for Design Pattern Implementation
13.2 Pros and Cons of Each Pattern
13.3 Alternatives to Traditional Patterns
Conclusion and Next Steps
14.1 Recap of Key Learnings
14.2 Resources for Further Learning
1. Introduction to Software Design Patterns
1.1 What Are Design Patterns?
Design patterns are reusable solutions to common software design problems, acting as blueprints for structuring code effectively. Originating from the "Gang of Four" (GoF) book, Design Patterns: Elements of Reusable Object-Oriented Software, they provide proven templates for solving issues in object creation, structure, and behavior.
1.2 Importance in .NET and Java
In .NET and Java, design patterns enhance code maintainability, scalability, and readability. They address challenges like object instantiation, system decoupling, and efficient communication, making them essential for building robust enterprise applications.
1.3 Course Objectives and Structure
This course guides beginners to advanced developers through design patterns in .NET and Java, focusing on modern features like async/await, cloud-native, and event-driven architectures. It includes practical C# and ASP.NET examples with SQL Server integration, emphasizing best practices and real-world applications.
2. Foundational Concepts
2.1 Object-Oriented Programming (OOP) Principles
OOP principles—encapsulation, inheritance, polymorphism, and abstraction—are the foundation of design patterns. Understanding these ensures effective pattern implementation.
2.2 SOLID Principles in Depth
The SOLID principles (Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, Dependency Inversion) guide clean code design. For example:
Single Responsibility Principle (SRP): A class should have one reason to change.
Example: Separating user authentication from user profile management.
2.3 UML and Design Pattern Visualization
Unified Modeling Language (UML) diagrams, like class and sequence diagrams, visualize pattern structures. Tools like StarUML help developers design and communicate patterns effectively.
3. Creational Design Patterns
3.1 Singleton Pattern
Purpose: Ensures a class has only one instance and provides global access. Real-Life Example: A logging service in an ASP.NET application. C# Example:
public sealed class Logger
{
private static readonly Lazy<Logger> instance = new Lazy<Logger>(() => new Logger());
private Logger() { }
public static Logger Instance => instance.Value;
public void Log(string message)
{
Console.WriteLine($"Log: {message}");
}
}
Pros: Controlled access, thread-safe with Lazy. Cons: Can lead to tight coupling; difficult to unit test. Alternatives: Dependency Injection for loose coupling.
3.2 Factory Method Pattern
Purpose: Defines an interface for creating objects, letting subclasses decide the class to instantiate. Real-Life Example: Creating payment processors (CreditCard, PayPal) in an e-commerce system. C# Example:
public interface IPaymentProcessor
{
void ProcessPayment(decimal amount);
}
public class CreditCardProcessor : IPaymentProcessor
{
public void ProcessPayment(decimal amount) => Console.WriteLine($"Processed {amount} via Credit Card");
}
public class PayPalProcessor : IPaymentProcessor
{
public void ProcessPayment(decimal amount) => Console.WriteLine($"Processed {amount} via PayPal");
}
public abstract class PaymentFactory
{
public abstract IPaymentProcessor CreateProcessor();
}
public class CreditCardFactory : PaymentFactory
{
public override IPaymentProcessor CreateProcessor() => new CreditCardProcessor();
}
Pros: Promotes loose coupling; supports Open/Closed principle. Cons: Increased complexity with additional classes. Alternatives: Abstract Factory for families of objects.
3.3 Abstract Factory Pattern
Purpose: Creates families of related objects without specifying their concrete classes. Real-Life Example: UI theme factories (Dark, Light) for a web application. C# Example:
public interface IButton { void Render(); }
public interface ITextBox { void Render(); }
public class DarkButton : IButton { public void Render() => Console.WriteLine("Dark Button"); }
public class LightButton : IButton { public void Render() => Console.WriteLine("Light Button"); }
public class DarkTextBox : ITextBox { public void Render() => Console.WriteLine("Dark TextBox"); }
public class LightTextBox : ITextBox { public void Render() => Console.WriteLine("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();
}
Pros: Ensures consistency among related objects. Cons: Complex to implement and extend. Alternatives: Factory Method for simpler scenarios.
3.4 Builder Pattern
Purpose: Constructs complex objects step-by-step. Real-Life Example: Building a complex order object in an e-commerce system. C# Example:
public class Order
{
public string CustomerName { get; set; }
public List<string> Items { get; set; }
public decimal TotalAmount { get; set; }
}
public class OrderBuilder
{
private Order _order = new Order();
public OrderBuilder SetCustomer(string name)
{
_order.CustomerName = name;
return this;
}
public OrderBuilder AddItem(string item)
{
_order.Items ??= new List<string>();
_order.Items.Add(item);
return this;
}
public OrderBuilder SetTotal(decimal amount)
{
_order.TotalAmount = amount;
return this;
}
public Order Build() => _order;
}
// Usage
var order = new OrderBuilder()
.SetCustomer("John Doe")
.AddItem("Laptop")
.AddItem("Mouse")
.SetTotal(1200.50m)
.Build();
Pros: Improves readability; supports immutability. Cons: Requires additional classes; can be verbose. Alternatives: Factory pattern for simpler object creation.
3.5 Prototype Pattern
Purpose: Creates new objects by copying an existing object. Real-Life Example: Cloning a product template in a catalog. C# Example:
public class Product : ICloneable
{
public string Name { get; set; }
public decimal Price { get; set; }
public object Clone() => MemberwiseClone();
}
// Usage
var product = new Product { Name = "Laptop", Price = 1000 };
var clonedProduct = (Product)product.Clone();
Pros: Reduces object creation overhead. Cons: Shallow vs. deep copy issues. Alternatives: Factory pattern for controlled instantiation.
4. Structural Design Patterns
4.1 Adapter Pattern
Purpose: Allows incompatible interfaces to work together. Real-Life Example: Integrating a third-party payment API with an internal system. C# Example:
public interface IPaymentGateway
{
void ExecutePayment(decimal amount);
}
public class ThirdPartyPayment
{
public void MakePayment(decimal amount) => Console.WriteLine($"Third-party payment: {amount}");
}
public class PaymentAdapter : IPaymentGateway
{
private readonly ThirdPartyPayment _thirdPartyPayment;
public PaymentAdapter(ThirdPartyPayment thirdPartyPayment)
{
_thirdPartyPayment = thirdPartyPayment;
}
public void ExecutePayment(decimal amount) => _thirdPartyPayment.MakePayment(amount);
}
Pros: Enables integration of legacy systems. Cons: Adds complexity with wrapper classes. Alternatives: Facade for simplified interfaces.
4.2 Decorator Pattern
Purpose: Dynamically adds responsibilities to objects. Real-Life Example: Adding logging to a service in ASP.NET. C# Example:
public interface INotificationService
{
void Send(string message);
}
public class EmailNotification : INotificationService
{
public void Send(string message) => Console.WriteLine($"Email: {message}");
}
public class LoggingDecorator : INotificationService
{
private readonly INotificationService _service;
public LoggingDecorator(INotificationService service)
{
_service = service;
}
public void Send(string message)
{
Console.WriteLine($"Logging: {message}");
_service.Send(message);
}
}
Pros: Flexible extension of behavior. Cons: Can lead to many small classes. Alternatives: Strategy pattern for behavior changes.
4.3 Facade Pattern
Purpose: Provides a simplified interface to a complex subsystem. Real-Life Example: Simplifying database operations in an ASP.NET app. C# Example:
public class OrderSubsystem
{
public void CreateOrder() => Console.WriteLine("Order created");
public void UpdateInventory() => Console.WriteLine("Inventory updated");
}
public class PaymentSubsystem
{
public void ProcessPayment() => Console.WriteLine("Payment processed");
}
public class OrderFacade
{
private readonly OrderSubsystem _orderSubsystem = new();
private readonly PaymentSubsystem _paymentSubsystem = new();
public void PlaceOrder()
{
_orderSubsystem.CreateOrder();
_paymentSubsystem.ProcessPayment();
_orderSubsystem.UpdateInventory();
}
}
Pros: Simplifies client code. Cons: Can become a god object. Alternatives: Mediator for complex interactions.
4.4 Composite Pattern
Purpose: Treats individual objects and compositions uniformly. Real-Life Example: Building a product category tree in an e-commerce system. C# Example:
public interface IComponent
{
void Display();
}
public class Product : IComponent
{
private readonly string _name;
public Product(string name) => _name = name;
public void Display() => Console.WriteLine($"Product: {_name}");
}
public class Category : IComponent
{
private readonly string _name;
private readonly List<IComponent> _components = new();
public Category(string name) => _name = name;
public void Add(IComponent component) => _components.Add(component);
public void Display()
{
Console.WriteLine($"Category: {_name}");
foreach (var component in _components) component.Display();
}
}
Pros: Simplifies hierarchical structures. Cons: Can overly generalize components. Alternatives: Decorator for single-object enhancements.
4.5 Proxy Pattern
Purpose: Controls access to an object, adding functionality like lazy loading. Real-Life Example: Lazy loading of images in a web application. C# Example:
public interface IImage
{
void Display();
}
public class RealImage : IImage
{
private readonly string _filename;
public RealImage(string filename)
{
_filename = filename;
Console.WriteLine($"Loading {_filename}");
}
public void Display() => Console.WriteLine($"Displaying {_filename}");
}
public class ImageProxy : IImage
{
private readonly string _filename;
private RealImage _realImage;
public ImageProxy(string filename) => _filename = filename;
public void Display()
{
_realImage ??= new RealImage(_filename);
_realImage.Display();
}
}
Pros: Reduces resource usage. Cons: Adds latency for proxy overhead. Alternatives: Flyweight for shared objects.
5. Behavioral Design Patterns
5.1 Observer Pattern
Purpose: Defines a one-to-many dependency where objects are notified of state changes. Real-Life Example: Notifying users of order status updates. C# Example:
public interface IObserver
{
void Update(string message);
}
public class Customer : IObserver
{
private readonly string _name;
public Customer(string name) => _name = name;
public void Update(string message) => Console.WriteLine($"{_name} received: {message}");
}
public class OrderSubject
{
private readonly List<IObserver> _observers = new();
public void Attach(IObserver observer) => _observers.Add(observer);
public void Notify(string message)
{
foreach (var observer in _observers) observer.Update(message);
}
}
Pros: Promotes loose coupling. Cons: Can lead to memory leaks if observers aren’t detached. Alternatives: Event-driven frameworks like SignalR.
5.2 Strategy Pattern
Purpose: Defines interchangeable algorithms at runtime. Real-Life Example: Applying different discount strategies in an e-commerce system. C# Example:
public interface IDiscountStrategy
{
decimal ApplyDiscount(decimal price);
}
public class RegularDiscount : IDiscountStrategy
{
public decimal ApplyDiscount(decimal price) => price * 0.9m;
}
public class PremiumDiscount : IDiscountStrategy
{
public decimal ApplyDiscount(decimal price) => price * 0.8m;
}
public class OrderContext
{
private IDiscountStrategy _strategy;
public void SetStrategy(IDiscountStrategy strategy) => _strategy = strategy;
public decimal CalculatePrice(decimal price) => _strategy.ApplyDiscount(price);
}
Pros: Flexible behavior swapping. Cons: Increases number of classes. Alternatives: Template Method for fixed algorithms.
5.3 Command Pattern
Purpose: Encapsulates a request as an object, allowing parameterization and queuing. Real-Life Example: Implementing undoable actions in a shopping cart. C# Example:
public interface ICommand
{
void Execute();
}
public class AddToCartCommand : ICommand
{
private readonly ShoppingCart _cart;
private readonly string _item;
public AddToCartCommand(ShoppingCart cart, string item)
{
_cart = cart;
_item = item;
}
public void Execute() => _cart.AddItem(_item);
}
public class ShoppingCart
{
private readonly List<string> _items = new();
public void AddItem(string item) => _items.Add(item);
}
Pros: Supports undo/redo; decouples sender and receiver. Cons: Can lead to class proliferation. Alternatives: Strategy for simpler behavior changes.
5.4 Chain of Responsibility Pattern
Purpose: Passes a request along a chain of handlers. Real-Life Example: Processing payment methods sequentially. C# Example:
public abstract class PaymentHandler
{
protected PaymentHandler _next;
public void SetNext(PaymentHandler next) => _next = next;
public abstract void Handle(decimal amount);
}
public class CreditCardHandler : PaymentHandler
{
public override void Handle(decimal amount)
{
if (amount <= 1000) Console.WriteLine("Paid with Credit Card");
else _next?.Handle(amount);
}
}
public class PayPalHandler : PaymentHandler
{
public override void Handle(decimal amount)
{
if (amount <= 5000) Console.WriteLine("Paid with PayPal");
else _next?.Handle(amount);
}
}
Pros: Decouples sender from receivers. Cons: Can result in unhandled requests. Alternatives: Observer for notification-based systems.
5.5 State Pattern
Purpose: Allows an object to alter its behavior when its state changes. Real-Life Example: Managing order states (Pending, Shipped, Delivered). C# Example:
public interface IOrderState
{
void ProcessOrder(OrderContext context);
}
public class PendingState : IOrderState
{
public void ProcessOrder(OrderContext context)
{
Console.WriteLine("Order is pending");
context.SetState(new ShippedState());
}
}
public class ShippedState : IOrderState
{
public void ProcessOrder(OrderContext context)
{
Console.WriteLine("Order is shipped");
context.SetState(new DeliveredState());
}
}
public class DeliveredState : IOrderState
{
public void ProcessOrder(OrderContext context) => Console.WriteLine("Order is delivered");
}
public class OrderContext
{
private IOrderState _state;
public OrderContext() => _state = new PendingState();
public void SetState(IOrderState state) => _state = state;
public void Process() => _state.ProcessOrder(this);
}
Pros: Encapsulates state-specific behavior. Cons: Increases class count. Alternatives: Strategy for simpler behavior changes.
6. Intermediate Patterns in Action
6.1 Repository Pattern
Purpose: Abstracts data access logic, providing a collection-like interface. Real-Life Example: Managing product data in an ASP.NET application. C# Example:
public interface IProductRepository
{
Task<Product> GetByIdAsync(int id);
Task AddAsync(Product product);
}
public class ProductRepository : IProductRepository
{
private readonly ApplicationDbContext _context;
public ProductRepository(ApplicationDbContext context)
{
_context = context;
}
public async Task<Product> GetByIdAsync(int id)
{
return await _context.Products.FindAsync(id) ?? throw new KeyNotFoundException("Product not found");
}
public async Task AddAsync(Product product)
{
await _context.Products.AddAsync(product);
await _context.SaveChangesAsync();
}
}
Pros: Centralizes data access; improves testability. Cons: Adds abstraction layer. Alternatives: Direct Entity Framework queries.
6.2 Unit of Work Pattern
Purpose: Maintains a list of objects affected by a business transaction and coordinates changes. Real-Life Example: Managing multiple repository operations in a transaction. C# Example:
public interface IUnitOfWork : IDisposable
{
IProductRepository Products { get; }
Task<int> CommitAsync();
}
public class UnitOfWork : IUnitOfWork
{
private readonly ApplicationDbContext _context;
public IProductRepository Products { get; }
public UnitOfWork(ApplicationDbContext context)
{
_context = context;
Products = new ProductRepository(context);
}
public async Task<int> CommitAsync() => await _context.SaveChangesAsync();
public void Dispose() => _context.Dispose();
}
Pros: Ensures transactional consistency. Cons: Can be overkill for simple applications. Alternatives: Direct database transactions.
6.3 Dependency Injection
Purpose: Implements Inversion of Control for dependency management. Real-Life Example: Injecting services in an ASP.NET Core controller. C# Example:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<IProductRepository, ProductRepository>();
services.AddScoped<IUnitOfWork, UnitOfWork>();
services.AddDbContext<ApplicationDbContext>();
}
}
public class ProductController : ControllerBase
{
private readonly IProductRepository _repository;
public ProductController(IProductRepository repository)
{
_repository = repository;
}
[HttpGet("{id}")]
public async Task<IActionResult> Get(int id)
{
try
{
var product = await _repository.GetByIdAsync(id);
return Ok(product);
}
catch (KeyNotFoundException ex)
{
return NotFound(ex.Message);
}
}
}
Pros: Enhances testability and modularity. Cons: Increases setup complexity. Alternatives: Service Locator pattern (less preferred).
7. Modern .NET and Java Features in Design Patterns
7.1 Records in .NET
Purpose: Immutable data structures for simplified data handling. Real-Life Example: Representing a product in an e-commerce system. C# Example:
public record Product(int Id, string Name, decimal Price);
// Usage
var product = new Product(1, "Laptop", 1000);
var productCopy = product with { Price = 1100 }; // Creates a new instance with modified price
Pros: Concise syntax; built-in immutability. Cons: Limited to data-centric scenarios. Alternatives: Classes with manual immutability.
7.2 Sealed Classes in Java
Purpose: Restricts class inheritance for better control. Java Example:
public sealed interface PaymentService permits CreditCardService, PayPalService {
void processPayment(double amount);
}
public final class CreditCardService implements PaymentService {
public void processPayment(double amount) {
System.out.println("Processed " + amount + " via Credit Card");
}
}
public final class PayPalService implements PaymentService {
public void processPayment(double amount) {
System.out.println("Processed " + amount + " via PayPal");
}
}
Pros: Enhances security and maintainability. Cons: Limits extensibility. Alternatives: Final classes or composition.
7.3 Pattern Matching in .NET and Java
Purpose: Simplifies type checking and data extraction. C# Example:
public static string GetPaymentDescription(IPaymentProcessor processor) => processor switch
{
CreditCardProcessor => "Credit Card Payment",
PayPalProcessor => "PayPal Payment",
_ => throw new NotSupportedException("Unknown payment processor")
};
Java Example:
public static String getPaymentDescription(PaymentService service) {
return switch (service) {
case CreditCardService c -> "Credit Card Payment";
case PayPalService p -> "PayPal Payment";
default -> throw new IllegalArgumentException("Unknown payment service");
};
}
Pros: Reduces boilerplate code; improves readability. Cons: Limited to supported types. Alternatives: Traditional if-else checks.
8. Asynchronous and Reactive Design Patterns
8.1 Async/Await in .NET
Purpose: Simplifies asynchronous programming. Real-Life Example: Fetching product data asynchronously in ASP.NET. C# Example:
public class ProductService
{
private readonly IProductRepository _repository;
public ProductService(IProductRepository repository)
{
_repository = repository;
}
public async Task<Product> GetProductAsync(int id)
{
try
{
return await _repository.GetByIdAsync(id);
}
catch (Exception ex)
{
throw new ApplicationException("Failed to fetch product", ex);
}
}
}
Pros: Improves responsiveness; simplifies async code. Cons: Can lead to deadlocks if misused. Alternatives: Callback-based async programming.
8.2 Reactive Streams in Java
Purpose: Handles asynchronous data streams with backpressure. Real-Life Example: Processing real-time order updates. Java Example:
import java.util.concurrent.Flow.*;
public class OrderPublisher implements Publisher<String> {
private List<Subscriber<? super String>> subscribers = new ArrayList<>();
@Override
public void subscribe(Subscriber<? super String> subscriber) {
subscribers.add(subscriber);
subscriber.onSubscribe(new Subscription() {
@Override
public void request(long n) {
// Simulate sending order updates
subscriber.onNext("Order updated");
subscriber.onComplete();
}
@Override
public void cancel() {}
});
}
}
Pros: Handles large data streams efficiently. Cons: Steep learning curve. Alternatives: Observer pattern for simpler scenarios.
8.3 Task Parallel Library (TPL) in .NET
Purpose: Simplifies parallel and concurrent programming. Real-Life Example: Processing multiple orders concurrently. C# Example:
public async Task ProcessOrdersAsync(List<int> orderIds)
{
var tasks = orderIds.Select(id => Task.Run(async () =>
{
try
{
var order = await _repository.GetByIdAsync(id);
Console.WriteLine($"Processed order {id}");
}
catch (Exception ex)
{
Console.WriteLine($"Error processing order {id}: {ex.Message}");
}
}));
await Task.WhenAll(tasks);
}
Pros: Enhances performance for CPU-bound tasks. Cons: Requires careful resource management. Alternatives: Parallel.For for simpler loops.
9. Cloud-Native Design Patterns
9.1 Circuit Breaker Pattern
Purpose: Prevents cascading failures in distributed systems. Real-Life Example: Handling API failures in an ASP.NET microservice. C# Example (Using Polly):
public class PaymentService
{
private readonly HttpClient _client;
private readonly AsyncCircuitBreakerPolicy _circuitBreaker = Policy
.Handle<HttpRequestException>()
.CircuitBreakerAsync(2, TimeSpan.FromSeconds(30));
public PaymentService(HttpClient client)
{
_client = client;
}
public async Task ProcessPaymentAsync(decimal amount)
{
await _circuitBreaker.ExecuteAsync(async () =>
{
var response = await _client.PostAsync("api/payment", null);
response.EnsureSuccessStatusCode();
});
}
}
Pros: Enhances system resilience. Cons: Requires configuration tuning. Alternatives: Retry pattern for simpler scenarios.
9.2 Bulkhead Pattern
Purpose: Isolates resources to prevent system-wide failures. Real-Life Example: Limiting concurrent API calls in a microservice. C# Example:
public class OrderService
{
private readonly SemaphoreSlim _semaphore = new(5); // Limit to 5 concurrent calls
public async Task ProcessOrderAsync(int orderId)
{
await _semaphore.WaitAsync();
try
{
// Process order
await Task.Delay(1000); // Simulate work
Console.WriteLine($"Processed order {orderId}");
}
finally
{
_semaphore.Release();
}
}
}
Pros: Prevents resource exhaustion. Cons: Adds complexity to resource management. Alternatives: Circuit Breaker for failure handling.
9.3 Retry Pattern
Purpose: Automatically retries failed operations. Real-Life Example: Retrying database connections in ASP.NET. C# Example (Using Polly):
public class DatabaseService
{
private readonly AsyncRetryPolicy _retryPolicy = Policy
.Handle<SqlException>()
.WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
public async Task ExecuteQueryAsync(string query)
{
await _retryPolicy.ExecuteAsync(async () =>
{
using var connection = new SqlConnection("connection_string");
await connection.OpenAsync();
// Execute query
});
}
}
Pros: Improves reliability for transient failures. Cons: Can mask underlying issues. Alternatives: Circuit Breaker for persistent failures.
10. Event-Driven and Streaming Design Patterns
10.1 Event Sourcing
Purpose: Stores state as a sequence of events. Real-Life Example: Tracking order history in an e-commerce system. C# Example:
public class OrderEvent
{
public string EventType { get; set; }
public string Data { get; set; }
}
public class OrderAggregate
{
private readonly List<OrderEvent> _events = new();
public void ApplyEvent(OrderEvent evt)
{
_events.Add(evt);
// Apply event to update state
}
public IEnumerable<OrderEvent> GetEvents() => _events;
}
Pros: Provides audit trail; supports state reconstruction. Cons: Complex to implement and query. Alternatives: Traditional CRUD for simpler scenarios.
10.2 CQRS (Command Query Responsibility Segregation)
Purpose: Separates read and write operations for scalability. Real-Life Example: Separating order creation from order queries. C# Example:
public class OrderCommand
{
public int OrderId { get; set; }
public string CustomerName { get; set; }
}
public class OrderQuery
{
public int OrderId { get; set; }
public string Status { get; set; }
}
public class OrderCommandHandler
{
private readonly IUnitOfWork _unitOfWork;
public OrderCommandHandler(IUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
}
public async Task HandleAsync(OrderCommand command)
{
var order = new Order { CustomerName = command.CustomerName };
await _unitOfWork.Products.AddAsync(order);
await _unitOfWork.CommitAsync();
}
}
public class OrderQueryHandler
{
private readonly IProductRepository _repository;
public OrderQueryHandler(IProductRepository repository)
{
_repository = repository;
}
public async Task<OrderQuery> HandleAsync(int orderId)
{
var order = await _repository.GetByIdAsync(orderId);
return new OrderQuery { OrderId = order.Id, Status = "Pending" };
}
}
Pros: Optimizes read/write performance. Cons: Increases system complexity. Alternatives: Simple CRUD for smaller systems.
10.3 Publish-Subscribe Pattern
Purpose: Decouples event producers from consumers. Real-Life Example: Notifying services of order updates using Azure Service Bus. C# Example:
public class OrderPublisher
{
private readonly ITopicClient _topicClient;
public OrderPublisher(ITopicClient topicClient)
{
_topicClient = topicClient;
}
public async Task PublishAsync(string message)
{
var msg = new Message(Encoding.UTF8.GetBytes(message));
await _topicClient.SendAsync(msg);
}
}
public class OrderSubscriber
{
private readonly ISubscriptionClient _subscriptionClient;
public OrderSubscriber(ISubscriptionClient subscriptionClient)
{
_subscriptionClient = subscriptionClient;
_subscriptionClient.RegisterMessageHandler(
async (message, token) =>
{
var content = Encoding.UTF8.GetString(message.Body);
Console.WriteLine($"Received: {content}");
await _subscriptionClient.CompleteAsync(message.SystemProperties.LockToken);
},
new MessageHandlerOptions(ExceptionReceivedHandler) { MaxConcurrentCalls = 1 });
}
private Task ExceptionReceivedHandler(ExceptionReceivedEventArgs args)
{
Console.WriteLine($"Error: {args.Exception.Message}");
return Task.CompletedTask;
}
}
Pros: Scalable event distribution. Cons: Requires message broker setup. Alternatives: Observer pattern for in-memory events.
11. CI/CD and DevOps Integration
11.1 Design Patterns in CI/CD Pipelines
Purpose: Ensures patterns align with automated build and deployment processes. Real-Life Example: Automating deployment of an ASP.NET microservice. C# Example (Azure DevOps YAML):
stages:
- stage: Build
jobs:
- job: BuildJob
steps:
- task: DotNetCoreCLI@2
inputs:
command: 'build'
projects: '**/*.csproj'
- stage: Deploy
jobs:
- job: DeployJob
steps:
- task: AzureWebApp@1
inputs:
azureSubscription: 'MySubscription'
appName: 'MyApp'
package: '$(Build.ArtifactStagingDirectory)/**/*.zip'
Pros: Streamlines deployment; ensures consistency. Cons: Requires CI/CD tool expertise. Alternatives: Manual deployments (less reliable).
11.2 DevOps Best Practices for Pattern Implementation
Containerization: Use Docker to encapsulate pattern-based services.
Monitoring: Implement logging with Serilog for pattern diagnostics.
Testing: Integrate unit tests for patterns in CI/CD pipelines.
12. Real-World Case Study: E-Commerce Platform
12.1 Applying Patterns in an ASP.NET Core Application
An e-commerce platform uses multiple patterns:
Repository and Unit of Work: Manage product and order data.
Factory: Create payment processors.
Observer: Notify users of order updates.
Circuit Breaker: Handle payment API failures.
12.2 Database Integration with SQL Server
C# Example (Entity Framework Core):
public class ApplicationDbContext : DbContext
{
public DbSet<Product> Products { get; set; }
public DbSet<Order> Orders { get; set; }
public ApplicationDbContext(DbContextOptions options) : base(options) { }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Product>().HasKey(p => p.Id);
modelBuilder.Entity<Order>().HasKey(o => o.Id);
}
}
SQL Server Schema:
CREATE TABLE Products (
Id INT PRIMARY KEY IDENTITY,
Name NVARCHAR(100),
Price DECIMAL(18,2)
);
CREATE TABLE Orders (
Id INT PRIMARY KEY IDENTITY,
CustomerName NVARCHAR(100),
TotalAmount DECIMAL(18,2)
);
12.3 Exception Handling and Logging
C# Example (Using Serilog):
public class ProductController : ControllerBase
{
private readonly ILogger<ProductController> _logger;
private readonly IProductRepository _repository;
public ProductController(ILogger<ProductController> logger, IProductRepository repository)
{
_logger = logger;
_repository = repository;
}
[HttpGet("{id}")]
public async Task<IActionResult> Get(int id)
{
try
{
var product = await _repository.GetByIdAsync(id);
_logger.LogInformation("Fetched product {Id}", id);
return Ok(product);
}
catch (KeyNotFoundException ex)
{
_logger.LogError(ex, "Product {Id} not found", id);
return NotFound(ex.Message);
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error fetching product {Id}", id);
return StatusCode(500, "Internal server error");
}
}
Pros: Centralized error handling; detailed logging. Cons: Requires careful logging configuration. Alternatives: Built-in ASP.NET logging.
13. Best Practices, Pros, Cons, and Alternatives
13.1 Best Practices for Design Pattern Implementation
Follow SOLID Principles: Ensure maintainable and scalable code.
Use Dependency Injection: Enhance testability and modularity.
Implement Exception Handling: Use try-catch blocks and logging.
Document Patterns: Use UML diagrams and comments for clarity.
13.2 Pros and Cons of Each Pattern
Singleton: Easy global access but risks tight coupling.
Factory: Promotes extensibility but adds complexity.
Observer: Decouples components but requires careful observer management.
13.3 Alternatives to Traditional Patterns
Functional Programming: Use immutable data and pure functions.
Microservices: Replace monolithic patterns with distributed systems.
Serverless: Leverage cloud functions for event-driven tasks.
14. Conclusion and Next Steps
14.1 Recap of Key Learnings
This course covered design patterns from foundational to advanced, focusing on .NET and Java implementations. Key takeaways include:
Understanding GoF patterns and their applications.
Leveraging modern features like records and async/await.
Building resilient cloud-native and event-driven systems.
14.2 Resources for Further Learning
Books: Design Patterns by GoF, Head First Design Patterns.
Online Courses: Udemy, Coursera, ScholarHat.
Tools: Visual Studio, StarUML, Azure DevOps.
This comprehensive guide equips you to apply design patterns effectively in .NET and Java, building robust, scalable applications with modern features and best practices.
🚀 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