Table of Contents
Introduction to Structural Design Patterns
Adapter (Wrapper) Pattern
2.1 Overview and Purpose
2.2 Real-World Example: Legacy Payment Gateway Integration
2.3 C# Implementation with ASP.NET and SQL Server
2.4 Best Practices and Exception Handling
2.5 Pros, Cons, and Alternatives
Bridge Pattern
3.1 Overview and Purpose
3.2 Real-World Example: Notification System Abstraction
3.3 C# Implementation with ASP.NET
3.4 Best Practices and Exception Handling
3.5 Pros, Cons, and Alternatives
Composite Pattern
4.1 Overview and Purpose
4.2 Real-World Example: Organizational Hierarchy
4.3 C# Implementation with ASP.NET
4.4 Best Practices and Exception Handling
4.5 Pros, Cons, and Alternatives
Decorator Pattern
5.1 Overview and Purpose
5.2 Real-World Example: Customizable Product Pricing
5.3 C# Implementation with ASP.NET and SQL Server
5.4 Best Practices and Exception Handling
5.5 Pros, Cons, and Alternatives
Facade Pattern
6.1 Overview and Purpose
6.2 Real-World Example: Order Processing System
6.3 C# Implementation with ASP.NET and SQL Server
6.4 Best Practices and Exception Handling
6.5 Pros, Cons, and Alternatives
Flyweight Pattern
7.1 Overview and Purpose
7.2 Real-World Example: Inventory Item Rendering
7.3 C# Implementation with ASP.NET
7.4 Best Practices and Exception Handling
7.5 Pros, Cons, and Alternatives
Proxy Pattern (Static, Dynamic, Virtual)
8.1 Overview and Purpose
8.2 Real-World Example: Secure Data Access
8.3 C# Implementation with ASP.NET and SQL Server
8.4 Best Practices and Exception Handling
8.5 Pros, Cons, and Alternatives
Conclusion and Next Steps
1. Introduction to Structural Design Patterns
Structural Design Patterns focus on organizing objects and classes into larger structures while keeping systems flexible and efficient. They simplify relationships between entities, making software more modular and easier to maintain. In this module, we explore seven key structural patterns—Adapter, Bridge, Composite, Decorator, Facade, Flyweight, and Proxy—using real-world examples in .NET with C#, ASP.NET, and SQL Server. Each section includes practical implementations, best practices, exception handling, pros, cons, and alternatives to ensure you can apply these patterns effectively in modern software development.
2. Adapter (Wrapper) Pattern
2.1 Overview and Purpose
The Adapter Pattern allows incompatible interfaces to work together by acting as a translator between two systems. It wraps an existing class with a new interface, enabling seamless integration without modifying the original code.
When to Use:
Integrating legacy systems with modern APIs.
Adapting third-party libraries to fit your application’s interface.
Ensuring compatibility between different modules.
2.2 Real-World Example: Legacy Payment Gateway Integration
Imagine an e-commerce platform built with ASP.NET Core that needs to integrate with a legacy payment gateway (e.g., OldPay) with an outdated interface. The modern system uses a standard IPaymentProcessor interface, but OldPay has a different method signature. The Adapter Pattern bridges this gap.
2.3 C# Implementation with ASP.NET and SQL Server
Below is an example of integrating a legacy payment system using the Adapter Pattern.
// Standard interface for payment processing
public interface IPaymentProcessor
{
Task<bool> ProcessPaymentAsync(decimal amount, string customerId);
}
// Legacy payment gateway with incompatible interface
public class OldPayGateway
{
public bool ExecuteTransaction(string customer, double amount)
{
// Simulate legacy payment processing
Console.WriteLine($"Processing ${amount} for {customer} via OldPay");
return true;
}
}
// Adapter to make OldPayGateway compatible with IPaymentProcessor
public class OldPayAdapter : IPaymentProcessor
{
private readonly OldPayGateway _oldPayGateway;
public OldPayAdapter(OldPayGateway oldPayGateway)
{
_oldPayGateway = oldPayGateway ?? throw new ArgumentNullException(nameof(oldPayGateway));
}
public async Task<bool> ProcessPaymentAsync(decimal amount, string customerId)
{
try
{
if (amount <= 0)
throw new ArgumentException("Amount must be positive.", nameof(amount));
if (string.IsNullOrEmpty(customerId))
throw new ArgumentException("Customer ID cannot be empty.", nameof(customerId));
// Convert decimal to double for legacy system
double amountDouble = Convert.ToDouble(amount);
return await Task.FromResult(_oldPayGateway.ExecuteTransaction(customerId, amountDouble));
}
catch (Exception ex)
{
// Log exception (e.g., to SQL Server)
await LogErrorAsync(ex);
throw new PaymentProcessingException("Failed to process payment.", ex);
}
}
private async Task LogErrorAsync(Exception ex)
{
// Simulate logging to SQL Server
using var connection = new SqlConnection("YourConnectionString");
await connection.OpenAsync();
var command = new SqlCommand(
"INSERT INTO ErrorLogs (Message, StackTrace, Date) VALUES (@Message, @StackTrace, @Date)",
connection);
command.Parameters.AddWithValue("@Message", ex.Message);
command.Parameters.AddWithValue("@StackTrace", ex.StackTrace);
command.Parameters.AddWithValue("@Date", DateTime.UtcNow);
await command.ExecuteNonQueryAsync();
}
}
// Custom exception for payment processing
public class PaymentProcessingException : Exception
{
public PaymentProcessingException(string message, Exception innerException)
: base(message, innerException) { }
}
// ASP.NET Controller using the adapter
[ApiController]
[Route("api/[controller]")]
public class PaymentController : ControllerBase
{
private readonly IPaymentProcessor _paymentProcessor;
public PaymentController(IPaymentProcessor paymentProcessor)
{
_paymentProcessor = paymentProcessor;
}
[HttpPost("process")]
public async Task<IActionResult> ProcessPayment([FromBody] PaymentRequest request)
{
try
{
bool result = await _paymentProcessor.ProcessPaymentAsync(request.Amount, request.CustomerId);
return Ok(new { Success = result });
}
catch (PaymentProcessingException ex)
{
return StatusCode(500, new { Error = ex.Message });
}
}
}
public class PaymentRequest
{
public decimal Amount { get; set; }
public string CustomerId { get; set; }
}
SQL Server Schema for Error Logging:
CREATE TABLE ErrorLogs (
Id INT IDENTITY(1,1) PRIMARY KEY,
Message NVARCHAR(MAX),
StackTrace NVARCHAR(MAX),
Date DATETIME
);
Dependency Injection Setup:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddSingleton<OldPayGateway>();
services.AddSingleton<IPaymentProcessor, OldPayAdapter>();
}
}
2.4 Best Practices and Exception Handling
Validate Inputs: Check for invalid amounts or customer IDs to prevent errors in the legacy system.
Centralized Logging: Log errors to SQL Server for debugging and auditing.
Asynchronous Operations: Use async/await for non-blocking I/O operations.
Null Checks: Ensure dependencies (e.g., OldPayGateway) are not null in the constructor.
Custom Exceptions: Use custom exceptions like PaymentProcessingException for meaningful error handling.
2.5 Pros, Cons, and Alternatives
Pros:
Enables integration of legacy systems without modification.
Promotes loose coupling between components.
Reusable for multiple incompatible interfaces.
Cons:
Adds an extra layer of complexity.
Performance overhead due to additional abstraction.
Alternatives:
Facade Pattern: Simplifies the interface but doesn’t adapt incompatible systems.
Mediator Pattern: Manages communication but is less focused on interface compatibility.
3. Bridge Pattern
3.1 Overview and Purpose
The Bridge Pattern decouples an abstraction from its implementation, allowing both to vary independently. It’s useful when you want to separate high-level logic from low-level details.
When to Use:
Supporting multiple platforms or implementations (e.g., email vs. SMS notifications).
Avoiding tight coupling between abstraction and implementation.
Enabling extensibility for new implementations.
3.2 Real-World Example: Notification System Abstraction
Consider an ASP.NET application that sends notifications via email or SMS. The Bridge Pattern separates the notification logic from the delivery mechanism.
3.3 C# Implementation with ASP.NET
// Abstraction
public abstract class Notification
{
protected INotificationSender _sender;
protected Notification(INotificationSender sender)
{
_sender = sender ?? throw new ArgumentNullException(nameof(sender));
}
public abstract Task SendAsync(string recipient, string message);
}
// Refined abstraction
public class UserNotification : Notification
{
public UserNotification(INotificationSender sender) : base(sender) { }
public override async Task SendAsync(string recipient, string message)
{
try
{
if (string.IsNullOrEmpty(recipient))
throw new ArgumentException("Recipient cannot be empty.", nameof(recipient));
await _sender.SendAsync(recipient, $"User Notification: {message}");
}
catch (Exception ex)
{
await LogErrorAsync(ex);
throw new NotificationException("Failed to send notification.", ex);
}
}
}
// Implementation interface
public interface INotificationSender
{
Task SendAsync(string recipient, string message);
}
// Concrete implementations
public class EmailSender : INotificationSender
{
public async Task SendAsync(string recipient, string message)
{
// Simulate email sending
Console.WriteLine($"Email sent to {recipient}: {message}");
await Task.CompletedTask;
}
}
public class SmsSender : INotificationSender
{
public async Task SendAsync(string recipient, string message)
{
// Simulate SMS sending
Console.WriteLine($"SMS sent to {recipient}: {message}");
await Task.CompletedTask;
}
}
// Custom exception
public class NotificationException : Exception
{
public NotificationException(string message, Exception innerException)
: base(message, innerException) { }
}
// Logging method (simplified)
private async Task LogErrorAsync(Exception ex)
{
// Log to SQL Server (similar to Adapter example)
}
// ASP.NET Controller
[ApiController]
[Route("api/[controller]")]
public class NotificationController : ControllerBase
{
private readonly Notification _notification;
public NotificationController(Notification notification)
{
_notification = notification;
}
[HttpPost("send")]
public async Task<IActionResult> SendNotification([FromBody] NotificationRequest request)
{
try
{
await _notification.SendAsync(request.Recipient, request.Message);
return Ok(new { Success = true });
}
catch (NotificationException ex)
{
return StatusCode(500, new { Error = ex.Message });
}
}
}
public class NotificationRequest
{
public string Recipient { get; set; }
public string Message { get; set; }
}
Dependency Injection Setup:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddSingleton<INotificationSender, EmailSender>(); // Or SmsSender
services.AddSingleton<Notification, UserNotification>();
}
}
3.4 Best Practices and Exception Handling
Dependency Injection: Use DI to swap implementations (e.g., Email vs. SMS).
Input Validation: Ensure recipients and messages are valid.
Extensibility: Design abstractions to support new senders without modifying existing code.
Exception Handling: Catch and log specific exceptions for reliable debugging.
3.5 Pros, Cons, and Alternatives
Pros:
Decouples abstraction from implementation.
Supports extensibility for new implementations.
Improves code maintainability.
Cons:
Increases complexity with additional abstractions.
May be overkill for simple scenarios.
Alternatives:
Strategy Pattern: Focuses on interchangeable algorithms rather than abstractions.
Adapter Pattern: Adapts interfaces but doesn’t decouple implementation.
4. Composite Pattern
4.1 Overview and Purpose
The Composite Pattern allows you to treat individual objects and compositions uniformly. It’s ideal for hierarchical structures where clients can work with both single items and groups seamlessly.
When to Use:
Representing tree-like structures (e.g., organizational charts, file systems).
Simplifying client code by treating objects and compositions uniformly.
4.2 Real-World Example: Organizational Hierarchy
An ASP.NET application managing a company’s employee hierarchy uses the Composite Pattern to handle individual employees and departments uniformly.
4.3 C# Implementation with ASP.NET
// Component interface
public interface IEmployee
{
string Name { get; }
decimal Salary { get; }
Task DisplayDetailsAsync(int depth = 0);
}
// Leaf
public class Employee : IEmployee
{
public string Name { get; }
public decimal Salary { get; }
public Employee(string name, decimal salary)
{
Name = name;
Salary = salary;
}
public async Task DisplayDetailsAsync(int depth)
{
Console.WriteLine(new string('-', depth) + $" {Name} (${Salary})");
await Task.CompletedTask;
}
}
// Composite
public class Department : IEmployee
{
public string Name { get; }
public decimal Salary => CalculateTotalSalary();
private readonly List<IEmployee> _employees = new List<IEmployee>();
public Department(string name)
{
Name = name;
}
public void AddEmployee(IEmployee employee)
{
if (employee == null)
throw new ArgumentNullException(nameof(employee));
_employees.Add(employee);
}
public async Task DisplayDetailsAsync(int depth = 0)
{
try
{
Console.WriteLine(new string('-', depth) + $" Department: {Name} (Total Salary: ${Salary})");
foreach (var employee in _employees)
{
await employee.DisplayDetailsAsync(depth + 2);
}
}
catch (Exception ex)
{
await LogErrorAsync(ex);
throw new CompositeException("Error displaying department details.", ex);
}
}
private decimal CalculateTotalSalary()
{
return _employees.Sum(e => e.Salary);
}
}
// Custom exception
public class CompositeException : Exception
{
public CompositeException(string message, Exception innerException)
: base(message, innerException) { }
}
// ASP.NET Controller
[ApiController]
[Route("api/[controller]")]
public class OrganizationController : ControllerBase
{
private readonly IEmployee _organization;
public OrganizationController(IEmployee organization)
{
_organization = organization;
}
[HttpGet("hierarchy")]
public async Task<IActionResult> GetHierarchy()
{
try
{
await _organization.DisplayDetailsAsync();
return Ok(new { Success = true });
}
catch (CompositeException ex)
{
return StatusCode(500, new { Error = ex.Message });
}
}
}
Dependency Injection Setup:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
var dept = new Department("Engineering");
dept.AddEmployee(new Employee("Alice", 80000));
dept.AddEmployee(new Employee("Bob", 75000));
services.AddSingleton<IEmployee>(dept);
}
}
4.4 Best Practices and Exception Handling
Uniform Interface: Ensure all components implement the same interface.
Recursive Traversal: Handle hierarchical operations safely with depth tracking.
Null Checks: Validate additions to the composite to avoid null references.
Exception Handling: Log errors during traversal or calculations.
4.5 Pros, Cons, and Alternatives
Pros:
Simplifies client code for hierarchical structures.
Supports uniform treatment of objects and compositions.
Easy to add new components.
Cons:
Can make the design overly general.
Complex hierarchies may impact performance.
Alternatives:
Decorator Pattern: Enhances objects but doesn’t handle hierarchies.
Visitor Pattern: Separates operations from the object structure.
5. Decorator Pattern
5.1 Overview and Purpose
The Decorator Pattern dynamically adds responsibilities to objects without modifying their code. It’s useful for extending functionality in a flexible and reusable way.
When to Use:
Adding features to objects dynamically (e.g., customizable product pricing).
Extending functionality without subclassing.
5.2 Real-World Example: Customizable Product Pricing
An e-commerce application calculates product prices with optional discounts or taxes. The Decorator Pattern allows stacking multiple pricing rules.
5.3 C# Implementation with ASP.NET and SQL Server
// Component interface
public interface IProduct
{
decimal GetPrice();
string GetDescription();
}
// Concrete component
public class BaseProduct : IProduct
{
private readonly string _name;
private readonly decimal _price;
public BaseProduct(string name, decimal price)
{
_name = name;
_price = price;
}
public decimal GetPrice() => _price;
public string GetDescription() => _name;
}
// Decorator abstract class
public abstract class ProductDecorator : IProduct
{
protected readonly IProduct _product;
protected ProductDecorator(IProduct product)
{
_product = product ?? throw new ArgumentNullException(nameof(product));
}
public virtual decimal GetPrice() => _product.GetPrice();
public virtual string GetDescription() => _product.GetDescription();
}
// Concrete decorators
public class DiscountDecorator : ProductDecorator
{
private readonly decimal _discount;
public DiscountDecorator(IProduct product, decimal discount) : base(product)
{
_discount = discount;
}
public override decimal GetPrice()
{
try
{
decimal basePrice = _product.GetPrice();
if (_discount < 0 || _discount > basePrice)
throw new ArgumentException("Invalid discount amount.");
return basePrice - _discount;
}
catch (Exception ex)
{
LogErrorAsync(ex).GetAwaiter().GetResult();
throw new PricingException("Error calculating discount.", ex);
}
}
public override string GetDescription()
{
return $"{_product.GetDescription()} (Discounted by ${_discount})";
}
}
public class TaxDecorator : ProductDecorator
{
private readonly decimal _taxRate;
public TaxDecorator(IProduct product, decimal taxRate) : base(product)
{
_taxRate = taxRate;
}
public override decimal GetPrice()
{
try
{
decimal basePrice = _product.GetPrice();
if (_taxRate < 0)
throw new ArgumentException("Invalid tax rate.");
return basePrice * (1 + _taxRate);
}
catch (Exception ex)
{
LogErrorAsync(ex).GetAwaiter().GetResult();
throw new PricingException("Error calculating tax.", ex);
}
}
public override string GetDescription()
{
return $"{_product.GetDescription()} (Tax: {_taxRate * 100}%)";
}
}
// Custom exception
public class PricingException : Exception
{
public PricingException(string message, Exception innerException)
: base(message, innerException) { }
}
// Logging method (similar to previous examples)
private async Task LogErrorAsync(Exception ex)
{
using var connection = new SqlConnection("YourConnectionString");
await connection.OpenAsync();
var command = new SqlCommand(
"INSERT INTO ErrorLogs (Message, StackTrace, Date) VALUES (@Message, @StackTrace, @Date)",
connection);
command.Parameters.AddWithValue("@Message", ex.Message);
command.Parameters.AddWithValue("@StackTrace", ex.StackTrace);
command.Parameters.AddWithValue("@Date", DateTime.UtcNow);
await command.ExecuteNonQueryAsync();
}
// ASP.NET Controller
[ApiController]
[Route("api/[controller]")]
public class ProductController : ControllerBase
{
[HttpGet("price")]
public IActionResult GetProductPrice()
{
try
{
IProduct product = new BaseProduct("Laptop", 1000);
product = new DiscountDecorator(product, 100);
product = new TaxDecorator(product, 0.1m);
return Ok(new
{
Description = product.GetDescription(),
Price = product.GetPrice()
});
}
catch (PricingException ex)
{
return StatusCode(500, new { Error = ex.Message });
}
}
}
5.4 Best Practices and Exception Handling
Flexible Decoration: Allow stacking multiple decorators for complex behavior.
Validation: Check discount and tax values to prevent invalid calculations.
Logging: Persist errors to SQL Server for traceability.
Immutability: Ensure decorators don’t modify the base object’s state.
5.5 Pros, Cons, and Alternatives
Pros:
Extends functionality without modifying existing code.
Supports multiple combinations of features.
Promotes single responsibility principle.
Cons:
Can lead to many small classes.
Complex decorator chains may be hard to debug.
Alternatives:
Strategy Pattern: Swaps algorithms but doesn’t stack behaviors.
Inheritance: Less flexible than decorators for dynamic behavior.
6. Facade Pattern
6.1 Overview and Purpose
The Facade Pattern provides a simplified interface to a complex subsystem, making it easier to use without exposing internal details.
When to Use:
Simplifying complex APIs (e.g., order processing with multiple services).
Reducing coupling between clients and subsystems.
6.2 Real-World Example: Order Processing System
An e-commerce platform integrates inventory, payment, and shipping services. The Facade Pattern simplifies the order placement process.
6.3 C# Implementation with ASP.NET and SQL Server
// Subsystem classes
public class InventoryService
{
public async Task<bool> CheckStockAsync(int productId, int quantity)
{
// Simulate stock check
return await Task.FromResult(quantity > 0);
}
}
public class PaymentService
{
public async Task<bool> ProcessPaymentAsync(decimal amount, string customerId)
{
// Simulate payment
return await Task.FromResult(true);
}
}
public class ShippingService
{
public async Task<string> ShipOrderAsync(int orderId)
{
// Simulate shipping
return await Task.FromResult($"Order {orderId} shipped");
}
}
// Facade
public class OrderFacade
{
private readonly InventoryService _inventory;
private readonly PaymentService _payment;
private readonly ShippingService _shipping;
public OrderFacade(InventoryService inventory, PaymentService payment, ShippingService shipping)
{
_inventory = inventory ?? throw new ArgumentNullException(nameof(inventory));
_payment = payment ?? throw new ArgumentNullException(nameof(payment));
_shipping = shipping ?? throw new ArgumentNullException(nameof(shipping));
}
public async Task<string> PlaceOrderAsync(int productId, int quantity, decimal amount, string customerId)
{
try
{
if (quantity <= 0)
throw new ArgumentException("Quantity must be positive.", nameof(quantity));
// Step 1: Check inventory
bool inStock = await _inventory.CheckStockAsync(productId, quantity);
if (!inStock)
throw new OrderException("Product out of stock.");
// Step 2: Process payment
bool paymentSuccess = await _payment.ProcessPaymentAsync(amount, customerId);
if (!paymentSuccess)
throw new OrderException("Payment failed.");
// Step 3: Ship order
int orderId = await SaveOrderToDatabaseAsync(productId, quantity, customerId);
string shippingResult = await _shipping.ShipOrderAsync(orderId);
return $"Order placed successfully: {shippingResult}";
}
catch (Exception ex)
{
await LogErrorAsync(ex);
throw new OrderException("Failed to place order.", ex);
}
}
private async Task<int> SaveOrderToDatabaseAsync(int productId, int quantity, string customerId)
{
using var connection = new SqlConnection("YourConnectionString");
await connection.OpenAsync();
var command = new SqlCommand(
"INSERT INTO Orders (ProductId, Quantity, CustomerId, OrderDate) OUTPUT INSERTED.Id VALUES (@ProductId, @Quantity, @CustomerId, @OrderDate)",
connection);
command.Parameters.AddWithValue("@ProductId", productId);
command.Parameters.AddWithValue("@Quantity", quantity);
command.Parameters.AddWithValue("@CustomerId", customerId);
command.Parameters.AddWithValue("@OrderDate", DateTime.UtcNow);
return (int)await command.ExecuteScalarAsync();
}
private async Task LogErrorAsync(Exception ex)
{
// Log to SQL Server (similar to previous examples)
}
}
// Custom exception
public class OrderException : Exception
{
public OrderException(string message, Exception innerException = null)
: base(message, innerException) { }
}
// SQL Server Schema
CREATE TABLE Orders (
Id INT IDENTITY(1,1) PRIMARY KEY,
ProductId INT,
Quantity INT,
CustomerId NVARCHAR(50),
OrderDate DATETIME
);
// ASP.NET Controller
[ApiController]
[Route("api/[controller]")]
public class OrderController : ControllerBase
{
private readonly OrderFacade _orderFacade;
public OrderController(OrderFacade orderFacade)
{
_orderFacade = orderFacade;
}
[HttpPost("place")]
public async Task<IActionResult> PlaceOrder([FromBody] OrderRequest request)
{
try
{
string result = await _orderFacade.PlaceOrderAsync(
request.ProductId, request.Quantity, request.Amount, request.CustomerId);
return Ok(new { Message = result });
}
catch (OrderException ex)
{
return StatusCode(500, new { Error = ex.Message });
}
}
}
public class OrderRequest
{
public int ProductId { get; set; }
public int Quantity { get; set; }
public decimal Amount { get; set; }
public string CustomerId { get; set; }
}
Dependency Injection Setup:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddSingleton<InventoryService>();
services.AddSingleton<PaymentService>();
services.AddSingleton<ShippingService>();
services.AddSingleton<OrderFacade>();
}
}
6.4 Best Practices and Exception Handling
Simplified Interface: Expose only necessary methods to clients.
Transaction Management: Ensure atomic operations across subsystems.
Validation: Check inputs before processing.
Error Logging: Persist errors to SQL Server for traceability.
6.5 Pros, Cons, and Alternatives
Pros:
Simplifies complex subsystem interactions.
Reduces client-side coupling.
Improves maintainability.
Cons:
Facade can become a god object if not designed carefully.
Doesn’t add new functionality.
Alternatives:
Mediator Pattern: Coordinates interactions but is more complex.
Adapter Pattern: Adapts interfaces but doesn’t simplify subsystems.
7. Flyweight Pattern
7.1 Overview and Purpose
The Flyweight Pattern minimizes memory usage by sharing common data across multiple objects. It’s ideal for applications with many similar objects.
When to Use:
Managing large numbers of objects with shared state (e.g., inventory items).
Reducing memory footprint in resource-constrained environments.
7.2 Real-World Example: Inventory Item Rendering
An ASP.NET application renders thousands of inventory items (e.g., products in a catalog). The Flyweight Pattern shares common product data to save memory.
7.3 C# Implementation with ASP.NET
// Flyweight interface
public interface IProductFlyweight
{
void Display(int productId, string uniqueDetails);
}
// Flyweight
public class ProductFlyweight : IProductFlyweight
{
private readonly string _name;
private readonly decimal _basePrice;
public ProductFlyweight(string name, decimal basePrice)
{
_name = name;
_basePrice = basePrice;
}
public void Display(int productId, string uniqueDetails)
{
try
{
Console.WriteLine($"Product ID: {productId}, Name: {_name}, Price: ${_basePrice}, Details: {uniqueDetails}");
}
catch (Exception ex)
{
LogErrorAsync(ex).GetAwaiter().GetResult();
throw new FlyweightException("Error displaying product.", ex);
}
}
}
// Flyweight factory
public class ProductFlyweightFactory
{
private readonly Dictionary<string, IProductFlyweight> _flyweights = new Dictionary<string, IProductFlyweight>();
public IProductFlyweight GetFlyweight(string key)
{
if (!_flyweights.ContainsKey(key))
{
// Simulate fetching shared data
_flyweights[key] = new ProductFlyweight(key, 100);
}
return _flyweights[key];
}
}
// Custom exception
public class FlyweightException : Exception
{
public FlyweightException(string message, Exception innerException)
: base(message, innerException) { }
}
// Logging method (similar to previous examples)
private async Task LogErrorAsync(Exception ex)
{
// Log to SQL Server
}
// ASP.NET Controller
[ApiController]
[Route("api/[controller]")]
public class InventoryController : ControllerBase
{
private readonly ProductFlyweightFactory _factory;
public InventoryController(ProductFlyweightFactory factory)
{
_factory = factory;
}
[HttpGet("products")]
public IActionResult GetProducts()
{
try
{
var flyweight = _factory.GetFlyweight("Laptop");
flyweight.Display(1, "Serial: ABC123");
flyweight.Display(2, "Serial: XYZ789");
return Ok(new { Success = true });
}
catch (FlyweightException ex)
{
return StatusCode(500, new { Error = ex.Message });
}
}
}
Dependency Injection Setup:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddSingleton<ProductFlyweightFactory>();
}
}
7.4 Best Practices and Exception Handling
Shared State: Store only immutable, shared data in flyweights.
Factory Pattern: Use a factory to manage flyweight instances.
Thread Safety: Ensure the factory is thread-safe for concurrent access.
Error Handling: Log errors during display or instantiation.
7.5 Pros, Cons, and Alternatives
Pros:
Reduces memory usage for large object sets.
Improves performance in resource-constrained systems.
Cons:
Increases complexity with shared state management.
Not suitable for objects with unique state.
Alternatives:
Singleton Pattern: Manages single instances but not shared state.
Object Pooling: Reuses objects but doesn’t share intrinsic state.
8. Proxy Pattern (Static, Dynamic, and Virtual Proxy)
8.1 Overview and Purpose
The Proxy Pattern controls access to an object by acting as an intermediary. It supports lazy loading, access control, and caching.
Types:
Static Proxy: Fixed at compile time.
Dynamic Proxy: Created at runtime using reflection or libraries.
Virtual Proxy: Defers object creation until needed.
When to Use:
Implementing lazy loading for expensive resources.
Adding security or logging to method calls.
Controlling access to sensitive data.
8.2 Real-World Example: Secure Data Access
An ASP.NET application restricts access to sensitive customer data stored in SQL Server. A Virtual Proxy ensures data is loaded only when authorized.
8.3 C# Implementation with ASP.NET and SQL Server
// Subject interface
public interface ICustomerData
{
Task<string> GetCustomerDetailsAsync(int customerId);
}
// Real subject
public class CustomerData : ICustomerData
{
public async Task<string> GetCustomerDetailsAsync(int customerId)
{
try
{
using var connection = new SqlConnection("YourConnectionString");
await connection.OpenAsync();
var command = new SqlCommand(
"SELECT Name FROM Customers WHERE Id = @Id",
connection);
command.Parameters.AddWithValue("@Id", customerId);
var result = await command.ExecuteScalarAsync();
return result?.ToString() ?? throw new KeyNotFoundException("Customer not found.");
}
catch (Exception ex)
{
await LogErrorAsync(ex);
throw new DataAccessException("Error accessing customer data.", ex);
}
}
}
// Virtual Proxy with authorization
public class CustomerDataProxy : ICustomerData
{
private readonly ICustomerData _realCustomerData;
private readonly IAuthorizationService _authService;
public CustomerDataProxy(ICustomerData realCustomerData, IAuthorizationService authService)
{
_realCustomerData = realCustomerData ?? throw new ArgumentNullException(nameof(realCustomerData));
_authService = authService ?? throw new ArgumentNullException(nameof(authService));
}
public async Task<string> GetCustomerDetailsAsync(int customerId)
{
try
{
if (!await _authService.IsAuthorizedAsync())
throw new UnauthorizedAccessException("Access denied.");
return await _realCustomerData.GetCustomerDetailsAsync(customerId);
}
catch (Exception ex)
{
await LogErrorAsync(ex);
throw new DataAccessException("Proxy error accessing customer data.", ex);
}
}
}
// Authorization service (simplified)
public interface IAuthorizationService
{
Task<bool> IsAuthorizedAsync();
}
public class AuthorizationService : IAuthorizationService
{
public async Task<bool> IsAuthorizedAsync()
{
// Simulate authorization check
return await Task.FromResult(true);
}
}
// Custom exception
public class DataAccessException : Exception
{
public DataAccessException(string message, Exception innerException)
: base(message, innerException) { }
}
// Logging method (similar to previous examples)
private async Task LogErrorAsync(Exception ex)
{
// Log to SQL Server
}
// SQL Server Schema
CREATE TABLE Customers (
Id INT PRIMARY KEY,
Name NVARCHAR(100)
);
// ASP.NET Controller
[ApiController]
[Route("api/[controller]")]
public class CustomerController : ControllerBase
{
private readonly ICustomerData _customerData;
public CustomerController(ICustomerData customerData)
{
_customerData = customerData;
}
[HttpGet("{id}")]
public async Task<IActionResult> GetCustomer(int id)
{
try
{
string details = await _customerData.GetCustomerDetailsAsync(id);
return Ok(new { Details = details });
}
catch (DataAccessException ex)
{
return StatusCode(500, new { Error = ex.Message });
}
catch (UnauthorizedAccessException ex)
{
return StatusCode(403, new { Error = ex.Message });
}
}
}
Dependency Injection Setup:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddSingleton<IAuthorizationService, AuthorizationService>();
services.AddSingleton<ICustomerData, CustomerData>();
services.AddSingleton<ICustomerData>(provider =>
new CustomerDataProxy(
provider.GetService<ICustomerData>(),
provider.GetService<IAuthorizationService>()));
}
}
8.4 Best Practices and Exception Handling
Lazy Loading: Defer expensive operations until necessary.
Security: Implement authorization checks in the proxy.
Error Handling: Catch and log specific exceptions (e.g., UnauthorizedAccessException).
DI Integration: Use dependency injection to manage proxy and real subject.
8.5 Pros, Cons, and Alternatives
Pros:
Controls access and adds functionality (e.g., logging, security).
Supports lazy initialization for performance.
Transparent to clients.
Cons:
Adds complexity with additional layers.
Dynamic proxies may have performance overhead.
Alternatives:
Decorator Pattern: Adds responsibilities but doesn’t control access.
Facade Pattern: Simplifies interfaces but doesn’t proxy individual objects.
🚀 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