Structural Design Patterns for LLD
Series: Low Level Design for .NET Developers | Previous: Chapter 3: Creational Design Patterns | Next: Chapter 5: Behavioral Design Patterns
📖 Introduction
Structural design patterns deal with how classes and objects are composed to form larger structures. They help ensure that when parts of a system change, the entire structure doesn't need to change. These patterns focus on relationships between entities and help you achieve loose coupling while maintaining flexibility.
In this chapter, we'll explore seven essential structural patterns:
| Pattern | Purpose | Real-World Analogy |
|---|---|---|
| Adapter | Convert interface of one class to another | Power plug adapter |
| Bridge | Separate abstraction from implementation | Remote control and TV |
| Composite | Treat individual objects and compositions uniformly | File system (files and folders) |
| Decorator | Add responsibilities dynamically | Pizza toppings |
| Facade | Provide simplified interface to complex subsystem | Restaurant menu (hides kitchen complexity) |
| Flyweight | Share objects to reduce memory usage | Character glyphs in word processor |
| Proxy | Control access to another object | Credit card (proxy for bank account) |
1. Adapter Pattern
1.1 Understanding Adapter
Definition: The Adapter pattern allows objects with incompatible interfaces to collaborate. It acts as a bridge between two incompatible interfaces.
Real-World Analogy: When you travel to a different country, you use a power plug adapter to connect your device to foreign outlets. The adapter converts one interface to another that your device expects.
1.2 ❌ Without Adapter - Incompatible Systems
// Existing third-party payment gateway (cannot be modified)
public class StripePaymentGateway
{
private string _apiKey;
public StripePaymentGateway(string apiKey)
{
_apiKey = apiKey;
}
public void Charge(decimal amount, string currency, string cardToken)
{
Console.WriteLine($"Stripe: Charged {amount} {currency} using card {cardToken}");
}
public void Refund(string transactionId, decimal amount)
{
Console.WriteLine($"Stripe: Refunded {amount} for transaction {transactionId}");
}
}
// Another third-party gateway
public class PayPalClient
{
private string _clientId;
private string _clientSecret;
public PayPalClient(string clientId, string clientSecret)
{
_clientId = clientId;
_clientSecret = clientSecret;
}
public void MakePayment(decimal amount, string currency, string email)
{
Console.WriteLine($"PayPal: Processed payment of {amount} {currency} for {email}");
}
public void IssueRefund(string paymentId, decimal amount)
{
Console.WriteLine($"PayPal: Refunded {amount} for payment {paymentId}");
}
}
// Our application's expected interface
public interface IPaymentProcessor
{
void ProcessPayment(decimal amount, string currency, string paymentDetails);
void ProcessRefund(string transactionId, decimal amount);
}
// Problem: Stripe and PayPal have different interfaces. We need to adapt them.
1.3 ✅ Adapter Pattern Implementation
// Target interface - what our application expects
public interface IPaymentProcessor
{
void ProcessPayment(decimal amount, string currency, string paymentDetails);
void ProcessRefund(string transactionId, decimal amount);
}
// Adapter for Stripe
public class StripeAdapter : IPaymentProcessor
{
private readonly StripePaymentGateway _stripeGateway;
public StripeAdapter(string apiKey)
{
_stripeGateway = new StripePaymentGateway(apiKey);
}
public void ProcessPayment(decimal amount, string currency, string paymentDetails)
{
// paymentDetails is the card token for Stripe
_stripeGateway.Charge(amount, currency, paymentDetails);
}
public void ProcessRefund(string transactionId, decimal amount)
{
_stripeGateway.Refund(transactionId, amount);
}
}
// Adapter for PayPal
public class PayPalAdapter : IPaymentProcessor
{
private readonly PayPalClient _payPalClient;
public PayPalAdapter(string clientId, string clientSecret)
{
_payPalClient = new PayPalClient(clientId, clientSecret);
}
public void ProcessPayment(decimal amount, string currency, string paymentDetails)
{
// paymentDetails is the PayPal email for PayPal
_payPalClient.MakePayment(amount, currency, paymentDetails);
}
public void ProcessRefund(string transactionId, decimal amount)
{
_payPalClient.IssueRefund(transactionId, amount);
}
}
// Usage - Our application works with any payment processor
public class CheckoutService
{
private readonly IPaymentProcessor _paymentProcessor;
public CheckoutService(IPaymentProcessor paymentProcessor)
{
_paymentProcessor = paymentProcessor;
}
public void CompletePurchase(decimal amount, string paymentDetails)
{
Console.WriteLine("\n=== Processing Checkout ===");
_paymentProcessor.ProcessPayment(amount, "USD", paymentDetails);
Console.WriteLine("Checkout completed!");
}
public void RefundOrder(string transactionId, decimal amount)
{
Console.WriteLine("\n=== Processing Refund ===");
_paymentProcessor.ProcessRefund(transactionId, amount);
Console.WriteLine("Refund completed!");
}
}
// Usage demonstration
public class AdapterDemo
{
public static void Run()
{
// Use Stripe
Console.WriteLine("=== Using Stripe ===");
var stripeAdapter = new StripeAdapter("sk_test_123");
var checkout = new CheckoutService(stripeAdapter);
checkout.CompletePurchase(99.99m, "tok_visa_123");
checkout.RefundOrder("txn_456", 99.99m);
// Use PayPal - same interface, different implementation
Console.WriteLine("\n=== Using PayPal ===");
var payPalAdapter = new PayPalAdapter("client_id_123", "client_secret_456");
checkout = new CheckoutService(payPalAdapter);
checkout.CompletePurchase(49.99m, "user@example.com");
checkout.RefundOrder("PAY-123", 49.99m);
}
}
1.4 Object Adapter vs Class Adapter (C# Approach)
// Object Adapter (Composition) - Preferred in C#
public class LogAdapter : ILogger
{
private readonly ThirdPartyLogger _thirdPartyLogger;
public LogAdapter(ThirdPartyLogger logger)
{
_thirdPartyLogger = logger;
}
public void LogInfo(string message)
{
_thirdPartyLogger.WriteLog(LogLevel.Info, message);
}
public void LogError(string message, Exception ex)
{
_thirdPartyLogger.WriteLog(LogLevel.Error, $"{message}: {ex.Message}");
}
}
// Two-Way Adapter - Adapts both sides
public interface IXmlData
{
string GetXmlData();
}
public interface IJsonData
{
string GetJsonData();
}
public class XmlToJsonAdapter : IXmlData, IJsonData
{
private readonly IJsonData _jsonData;
private readonly IXmlData _xmlData;
public XmlToJsonAdapter(IJsonData jsonData)
{
_jsonData = jsonData;
}
public XmlToJsonAdapter(IXmlData xmlData)
{
_xmlData = xmlData;
}
public string GetXmlData()
{
// Convert JSON to XML
var json = _jsonData.GetJsonData();
return ConvertJsonToXml(json);
}
public string GetJsonData()
{
// Convert XML to JSON
var xml = _xmlData.GetXmlData();
return ConvertXmlToJson(xml);
}
private string ConvertJsonToXml(string json) => $"{json} ";
private string ConvertXmlToJson(string xml) => xml.Replace("", "").Replace(" ", "");
}
2. Bridge Pattern
2.1 Understanding Bridge
Definition: The Bridge pattern decouples an abstraction from its implementation so that both can vary independently. It prevents a "cartesian product" complexity explosion.
Real-World Analogy: A remote control (abstraction) and TV device (implementation). Different remotes (basic, advanced) can work with different TV brands (Sony, Samsung) without creating a separate class for each combination.
2.2 ❌ Without Bridge - Class Explosion
// Without Bridge, we'd need a class for every combination
public abstract class RemoteControl { }
public class BasicSonyRemote : RemoteControl { }
public class AdvancedSonyRemote : RemoteControl { }
public class BasicSamsungRemote : RemoteControl { }
public class AdvancedSamsungRemote : RemoteControl { }
// For 2 remote types × 2 TV brands = 4 classes
// For 3 remote types × 5 TV brands = 15 classes! This doesn't scale.
2.3 ✅ Bridge Pattern Implementation
// Implementation hierarchy - Device interfaces
public interface IDevice
{
void TurnOn();
void TurnOff();
void SetVolume(int percent);
int GetVolume();
void SetChannel(int channel);
int GetChannel();
}
// Concrete implementations
public class SonyTV : IDevice
{
private int _volume = 50;
private int _channel = 1;
public void TurnOn() => Console.WriteLine("Sony TV: Turning ON");
public void TurnOff() => Console.WriteLine("Sony TV: Turning OFF");
public void SetVolume(int percent)
{
_volume = Math.Clamp(percent, 0, 100);
Console.WriteLine($"Sony TV: Volume set to {_volume}%");
}
public int GetVolume() => _volume;
public void SetChannel(int channel)
{
_channel = channel;
Console.WriteLine($"Sony TV: Channel set to {_channel}");
}
public int GetChannel() => _channel;
}
public class SamsungTV : IDevice
{
private int _volume = 30;
private int _channel = 5;
public void TurnOn() => Console.WriteLine("Samsung TV: Power ON");
public void TurnOff() => Console.WriteLine("Samsung TV: Power OFF");
public void SetVolume(int percent)
{
_volume = Math.Clamp(percent, 0, 100);
Console.WriteLine($"Samsung TV: Volume at {_volume}%");
}
public int GetVolume() => _volume;
public void SetChannel(int channel)
{
_channel = channel;
Console.WriteLine($"Samsung TV: Channel changed to {_channel}");
}
public int GetChannel() => _channel;
}
public class LGTV : IDevice
{
private int _volume = 40;
private int _channel = 10;
public void TurnOn() => Console.WriteLine("LG TV: Powering up");
public void TurnOff() => Console.WriteLine("LG TV: Shutting down");
public void SetVolume(int percent)
{
_volume = Math.Clamp(percent, 0, 100);
Console.WriteLine($"LG TV: Volume = {_volume}%");
}
public int GetVolume() => _volume;
public void SetChannel(int channel)
{
_channel = channel;
Console.WriteLine($"LG TV: Channel {_channel}");
}
public int GetChannel() => _channel;
}
// Abstraction hierarchy - Remote controls
public abstract class RemoteControl
{
protected IDevice _device;
protected RemoteControl(IDevice device)
{
_device = device;
}
public abstract void TurnOn();
public abstract void TurnOff();
public abstract void SetVolume(int percent);
public abstract void SetChannel(int channel);
}
// Concrete abstractions
public class BasicRemote : RemoteControl
{
public BasicRemote(IDevice device) : base(device) { }
public override void TurnOn()
{
Console.Write("[BasicRemote] ");
_device.TurnOn();
}
public override void TurnOff()
{
Console.Write("[BasicRemote] ");
_device.TurnOff();
}
public override void SetVolume(int percent)
{
Console.Write("[BasicRemote] ");
_device.SetVolume(percent);
}
public override void SetChannel(int channel)
{
Console.Write("[BasicRemote] ");
_device.SetChannel(channel);
}
}
public class AdvancedRemote : RemoteControl
{
public AdvancedRemote(IDevice device) : base(device) { }
public override void TurnOn()
{
Console.Write("[AdvancedRemote] ");
_device.TurnOn();
}
public override void TurnOff()
{
Console.Write("[AdvancedRemote] ");
_device.TurnOff();
}
public override void SetVolume(int percent)
{
Console.Write("[AdvancedRemote] ");
_device.SetVolume(percent);
}
public override void SetChannel(int channel)
{
Console.Write("[AdvancedRemote] ");
_device.SetChannel(channel);
}
// Additional advanced features
public void Mute()
{
Console.Write("[AdvancedRemote] Muting - ");
_device.SetVolume(0);
}
public void ChannelUp()
{
var newChannel = _device.GetChannel() + 1;
SetChannel(newChannel);
}
public void ChannelDown()
{
var newChannel = _device.GetChannel() - 1;
SetChannel(newChannel);
}
public void VolumeUp()
{
var newVolume = _device.GetVolume() + 10;
SetVolume(Math.Min(newVolume, 100));
}
public void VolumeDown()
{
var newVolume = _device.GetVolume() - 10;
SetVolume(Math.Max(newVolume, 0));
}
}
public class VoiceControlledRemote : RemoteControl
{
public VoiceControlledRemote(IDevice device) : base(device) { }
public override void TurnOn()
{
Console.Write("[Voice] Alexa, turn on TV - ");
_device.TurnOn();
}
public override void TurnOff()
{
Console.Write("[Voice] Alexa, turn off TV - ");
_device.TurnOff();
}
public override void SetVolume(int percent)
{
Console.Write($"[Voice] Set volume to {percent}% - ");
_device.SetVolume(percent);
}
public override void SetChannel(int channel)
{
Console.Write($"[Voice] Change to channel {channel} - ");
_device.SetChannel(channel);
}
public void VoiceCommand(string command)
{
if (command.Contains("turn on") || command.Contains("power on"))
TurnOn();
else if (command.Contains("turn off") || command.Contains("power off"))
TurnOff();
else if (command.Contains("volume up"))
_device.SetVolume(_device.GetVolume() + 10);
else if (command.Contains("volume down"))
_device.SetVolume(_device.GetVolume() - 10);
else if (command.Contains("channel up"))
_device.SetChannel(_device.GetChannel() + 1);
else if (command.Contains("channel down"))
_device.SetChannel(_device.GetChannel() - 1);
else if (command.Contains("mute"))
_device.SetVolume(0);
else
Console.WriteLine($"[Voice] Unknown command: {command}");
}
}
// Usage demonstration
public class BridgeDemo
{
public static void Run()
{
Console.WriteLine("=== Bridge Pattern Demo ===\n");
// Basic remote with Sony TV
var sonyTV = new SonyTV();
var basicRemote = new BasicRemote(sonyTV);
Console.WriteLine("Basic Remote + Sony TV:");
basicRemote.TurnOn();
basicRemote.SetChannel(7);
basicRemote.SetVolume(75);
Console.WriteLine();
// Advanced remote with Samsung TV
var samsungTV = new SamsungTV();
var advancedRemote = new AdvancedRemote(samsungTV);
Console.WriteLine("Advanced Remote + Samsung TV:");
advancedRemote.TurnOn();
advancedRemote.ChannelUp();
advancedRemote.ChannelUp();
advancedRemote.VolumeUp();
advancedRemote.Mute();
Console.WriteLine();
// Voice remote with LG TV
var lgTV = new LGTV();
var voiceRemote = new VoiceControlledRemote(lgTV);
Console.WriteLine("Voice Remote + LG TV:");
voiceRemote.VoiceCommand("turn on");
voiceRemote.VoiceCommand("channel up");
voiceRemote.VoiceCommand("volume up");
voiceRemote.VoiceCommand("mute");
voiceRemote.VoiceCommand("turn off");
Console.WriteLine("\n--- Flexibility: Switch TV without changing remote ---");
// Same advanced remote works with different TV
var anotherRemote = new AdvancedRemote(new LGTV());
anotherRemote.TurnOn();
anotherRemote.ChannelUp();
Console.WriteLine("\n--- Flexibility: Switch remote without changing TV ---");
// Same TV works with different remotes
var sameTV = new SamsungTV();
var basic = new BasicRemote(sameTV);
var advanced = new AdvancedRemote(sameTV);
basic.TurnOn();
advanced.Mute();
}
}
3. Composite Pattern
3.1 Understanding Composite
Definition: The Composite pattern composes objects into tree structures to represent part-whole hierarchies. It lets clients treat individual objects and compositions uniformly.
Real-World Analogy: A file system. Files (leaf nodes) and folders (composite nodes) both support operations like getSize(), delete(), etc. You can treat both uniformly.
3.2 Composite Pattern Implementation - File System
// Component interface
public interface IFileSystemItem
{
string Name { get; }
long GetSize();
void Display(int indent = 0);
void Delete();
}
// Leaf node - File
public class File : IFileSystemItem
{
public string Name { get; }
public long Size { get; }
public File(string name, long size)
{
Name = name;
Size = size;
}
public long GetSize() => Size;
public void Display(int indent = 0)
{
Console.WriteLine($"{new string(' ', indent)}📄 {Name} ({Size} bytes)");
}
public void Delete()
{
Console.WriteLine($"Deleting file: {Name}");
}
}
// Composite node - Directory
public class Directory : IFileSystemItem
{
public string Name { get; }
private readonly List<IFileSystemItem> _children = new List<IFileSystemItem>();
public Directory(string name)
{
Name = name;
}
public void Add(IFileSystemItem item)
{
_children.Add(item);
Console.WriteLine($"Added {item.Name} to {Name}");
}
public void Remove(IFileSystemItem item)
{
_children.Remove(item);
Console.WriteLine($"Removed {item.Name} from {Name}");
}
public IFileSystemItem GetChild(int index)
{
return _children[index];
}
public long GetSize()
{
return _children.Sum(child => child.GetSize());
}
public void Display(int indent = 0)
{
Console.WriteLine($"{new string(' ', indent)}📁 {Name}/ (Total: {GetSize()} bytes)");
foreach (var child in _children)
{
child.Display(indent + 2);
}
}
public void Delete()
{
Console.WriteLine($"Deleting directory: {Name}");
foreach (var child in _children)
{
child.Delete();
}
_children.Clear();
}
public List<IFileSystemItem> Search(string searchTerm)
{
var results = new List<IFileSystemItem>();
if (Name.Contains(searchTerm))
results.Add(this);
foreach (var child in _children)
{
if (child is Directory dir)
{
results.AddRange(dir.Search(searchTerm));
}
else if (child is File file && file.Name.Contains(searchTerm))
{
results.Add(file);
}
}
return results;
}
}
// Usage demonstration
public class CompositeDemo
{
public static void Run()
{
Console.WriteLine("=== Composite Pattern - File System Demo ===\n");
// Create files
var file1 = new File("document.txt", 1024);
var file2 = new File("image.jpg", 2048);
var file3 = new File("video.mp4", 10240);
var file4 = new File("notes.md", 512);
var file5 = new File("config.json", 256);
// Create directories
var root = new Directory("root");
var documents = new Directory("Documents");
var pictures = new Directory("Pictures");
var videos = new Directory("Videos");
// Build hierarchy
root.Add(documents);
root.Add(pictures);
root.Add(videos);
documents.Add(file1);
documents.Add(file4);
documents.Add(file5);
pictures.Add(file2);
videos.Add(file3);
// Add subdirectory within documents
var projects = new Directory("Projects");
documents.Add(projects);
projects.Add(new File("project1.cs", 4096));
projects.Add(new File("project2.cs", 8192));
// Display the entire structure
Console.WriteLine("\n=== File System Structure ===");
root.Display();
// Calculate total size
Console.WriteLine($"\nTotal size of root: {root.GetSize()} bytes");
// Search for files
Console.WriteLine("\n=== Searching for files containing 'project' ===");
var searchResults = root.Search("project");
foreach (var item in searchResults)
{
Console.WriteLine($"Found: {item.Name}");
}
// Delete a directory and its contents
Console.WriteLine("\n=== Deleting Videos directory ===");
videos.Delete();
Console.WriteLine("\n=== Updated Structure ===");
root.Display();
}
}
3.3 Composite Pattern - UI Components
// Component interface
public interface IUIComponent
{
void Render();
void Add(IUIComponent component);
void Remove(IUIComponent component);
IUIComponent GetChild(int index);
}
// Leaf - Button
public class Button : IUIComponent
{
public string Text { get; set; }
public Button(string text)
{
Text = text;
}
public void Render()
{
Console.WriteLine($" [Button: {Text}]");
}
public void Add(IUIComponent component)
{
throw new NotSupportedException("Button cannot have children");
}
public void Remove(IUIComponent component)
{
throw new NotSupportedException("Button cannot have children");
}
public IUIComponent GetChild(int index)
{
throw new NotSupportedException("Button cannot have children");
}
}
// Leaf - TextBox
public class TextBox : IUIComponent
{
public string Text { get; set; }
public string Placeholder { get; set; }
public TextBox(string placeholder)
{
Placeholder = placeholder;
}
public void Render()
{
Console.WriteLine($" [TextBox: {Placeholder}] = \"{Text}\"");
}
public void Add(IUIComponent component)
{
throw new NotSupportedException("TextBox cannot have children");
}
public void Remove(IUIComponent component)
{
throw new NotSupportedException("TextBox cannot have children");
}
public IUIComponent GetChild(int index)
{
throw new NotSupportedException("TextBox cannot have children");
}
}
// Leaf - Label
public class Label : IUIComponent
{
public string Text { get; set; }
public Label(string text)
{
Text = text;
}
public void Render()
{
Console.WriteLine($" [Label: {Text}]");
}
public void Add(IUIComponent component)
{
throw new NotSupportedException("Label cannot have children");
}
public void Remove(IUIComponent component)
{
throw new NotSupportedException("Label cannot have children");
}
public IUIComponent GetChild(int index)
{
throw new NotSupportedException("Label cannot have children");
}
}
// Composite - Panel
public class Panel : IUIComponent
{
public string Name { get; set; }
private readonly List<IUIComponent> _children = new List<IUIComponent>();
public Panel(string name)
{
Name = name;
}
public void Render()
{
Console.WriteLine($"+ Panel: {Name}");
foreach (var child in _children)
{
child.Render();
}
Console.WriteLine($"- End Panel: {Name}");
}
public void Add(IUIComponent component)
{
_children.Add(component);
}
public void Remove(IUIComponent component)
{
_children.Remove(component);
}
public IUIComponent GetChild(int index)
{
return _children[index];
}
}
// Composite - Form
public class Form : IUIComponent
{
public string Title { get; set; }
private readonly List<IUIComponent> _children = new List<IUIComponent>();
public Form(string title)
{
Title = title;
}
public void Render()
{
Console.WriteLine($"\n╔════════════════════════════╗");
Console.WriteLine($"║ {Title.PadRight(26)}║");
Console.WriteLine($"╠════════════════════════════╣");
foreach (var child in _children)
{
child.Render();
}
Console.WriteLine($"╚════════════════════════════╝");
}
public void Add(IUIComponent component)
{
_children.Add(component);
}
public void Remove(IUIComponent component)
{
_children.Remove(component);
}
public IUIComponent GetChild(int index)
{
return _children[index];
}
}
// Usage
public class UIDemo
{
public static void Run()
{
Console.WriteLine("=== Composite Pattern - UI Components ===\n");
// Create form
var loginForm = new Form("Login");
// Create panels
var headerPanel = new Panel("Header");
var mainPanel = new Panel("Main Content");
var footerPanel = new Panel("Footer");
// Build header
headerPanel.Add(new Label("Welcome to MyApp"));
// Build main content
mainPanel.Add(new Label("Username:"));
mainPanel.Add(new TextBox("Enter username"));
mainPanel.Add(new Label("Password:"));
mainPanel.Add(new TextBox("Enter password"));
mainPanel.Add(new Button("Login"));
// Build footer
footerPanel.Add(new Label("Forgot password? Contact support"));
// Assemble form
loginForm.Add(headerPanel);
loginForm.Add(mainPanel);
loginForm.Add(footerPanel);
// Render entire UI
loginForm.Render();
// Add a new panel dynamically
Console.WriteLine("\n--- Adding Registration Link Panel ---");
var registerPanel = new Panel("Registration Link");
registerPanel.Add(new Button("Create New Account"));
loginForm.Add(registerPanel);
loginForm.Render();
}
}
4. Decorator Pattern
4.1 Understanding Decorator
Definition: The Decorator pattern attaches additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.
Real-World Analogy: Pizza toppings. You start with a base pizza and can add toppings (decorators) like cheese, mushrooms, pepperoni. Each topping adds cost and description without modifying the base pizza class.
4.2 Decorator Pattern Implementation - Pizza Ordering
// Component interface
public interface IPizza
{
string GetDescription();
decimal GetCost();
}
// Concrete component - Base pizza
public class MargheritaPizza : IPizza
{
public string GetDescription() => "Margherita Pizza";
public decimal GetCost() => 8.99m;
}
public class PepperoniPizza : IPizza
{
public string GetDescription() => "Pepperoni Pizza";
public decimal GetCost() => 10.99m;
}
public class VegetarianPizza : IPizza
{
public string GetDescription() => "Vegetarian Pizza";
public decimal GetCost() => 9.99m;
}
// Base decorator
public abstract class PizzaDecorator : IPizza
{
protected IPizza _pizza;
protected PizzaDecorator(IPizza pizza)
{
_pizza = pizza;
}
public virtual string GetDescription() => _pizza.GetDescription();
public virtual decimal GetCost() => _pizza.GetCost();
}
// Concrete decorators - Toppings
public class CheeseDecorator : PizzaDecorator
{
public CheeseDecorator(IPizza pizza) : base(pizza) { }
public override string GetDescription() => $"{_pizza.GetDescription()}, Extra Cheese";
public override decimal GetCost() => _pizza.GetCost() + 1.50m;
}
public class MushroomDecorator : PizzaDecorator
{
public MushroomDecorator(IPizza pizza) : base(pizza) { }
public override string GetDescription() => $"{_pizza.GetDescription()}, Mushrooms";
public override decimal GetCost() => _pizza.GetCost() + 2.00m;
}
public class PepperoniDecorator : PizzaDecorator
{
public PepperoniDecorator(IPizza pizza) : base(pizza) { }
public override string GetDescription() => $"{_pizza.GetDescription()}, Pepperoni";
public override decimal GetCost() => _pizza.GetCost() + 2.50m;
}
public class OlivesDecorator : PizzaDecorator
{
public OlivesDecorator(IPizza pizza) : base(pizza) { }
public override string GetDescription() => $"{_pizza.GetDescription()}, Olives";
public override decimal GetCost() => _pizza.GetCost() + 1.75m;
}
public class ExtraSauceDecorator : PizzaDecorator
{
public ExtraSauceDecorator(IPizza pizza) : base(pizza) { }
public override string GetDescription() => $"{_pizza.GetDescription()}, Extra Sauce";
public override decimal GetCost() => _pizza.GetCost() + 1.00m;
}
// Usage
public class PizzaDemo
{
public static void Run()
{
Console.WriteLine("=== Decorator Pattern - Pizza Ordering ===\n");
// Order 1: Margherita with cheese and mushrooms
IPizza pizza1 = new MargheritaPizza();
pizza1 = new CheeseDecorator(pizza1);
pizza1 = new MushroomDecorator(pizza1);
Console.WriteLine($"Order 1: {pizza1.GetDescription()}");
Console.WriteLine($"Cost: ${pizza1.GetCost():F2}\n");
// Order 2: Pepperoni with extra cheese and pepperoni
IPizza pizza2 = new PepperoniPizza();
pizza2 = new CheeseDecorator(pizza2);
pizza2 = new PepperoniDecorator(pizza2);
pizza2 = new ExtraSauceDecorator(pizza2);
Console.WriteLine($"Order 2: {pizza2.GetDescription()}");
Console.WriteLine($"Cost: ${pizza2.GetCost():F2}\n");
// Order 3: Vegetarian with multiple toppings
IPizza pizza3 = new VegetarianPizza();
pizza3 = new MushroomDecorator(pizza3);
pizza3 = new OlivesDecorator(pizza3);
pizza3 = new CheeseDecorator(pizza3);
pizza3 = new ExtraSauceDecorator(pizza3);
Console.WriteLine($"Order 3: {pizza3.GetDescription()}");
Console.WriteLine($"Cost: ${pizza3.GetCost():F2}");
}
}
4.3 Decorator Pattern - .NET Middleware (ASP.NET Core Style)
// Request delegate
public delegate Task RequestDelegate(HttpContext context);
// Middleware interface
public interface IMiddleware
{
Task InvokeAsync(HttpContext context, RequestDelegate next);
}
// Base decorator for middleware
public abstract class MiddlewareDecorator : IMiddleware
{
protected readonly IMiddleware _next;
protected MiddlewareDecorator(IMiddleware next)
{
_next = next;
}
public abstract Task InvokeAsync(HttpContext context, RequestDelegate next);
}
// Concrete middleware - Logging
public class LoggingMiddleware : IMiddleware
{
private readonly ILogger _logger;
public LoggingMiddleware(ILogger logger)
{
_logger = logger;
}
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
_logger.Log($"Request: {context.Request.Method} {context.Request.Path}");
await next(context);
_logger.Log($"Response: {context.Response.StatusCode}");
}
}
// Concrete middleware - Authentication
public class AuthenticationMiddleware : IMiddleware
{
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
if (!context.Request.Headers.ContainsKey("Authorization"))
{
context.Response.StatusCode = 401;
await context.Response.WriteAsync("Unauthorized");
return;
}
await next(context);
}
}
// Concrete middleware - Caching
public class CachingMiddleware : IMiddleware
{
private readonly ICache _cache;
public CachingMiddleware(ICache cache)
{
_cache = cache;
}
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
var cacheKey = context.Request.Path;
var cachedResponse = _cache.Get(cacheKey);
if (cachedResponse != null)
{
await context.Response.WriteAsync(cachedResponse);
return;
}
// Capture response
var originalBody = context.Response.Body;
using var memoryStream = new MemoryStream();
context.Response.Body = memoryStream;
await next(context);
// Cache response
memoryStream.Position = 0;
var response = await new StreamReader(memoryStream).ReadToEndAsync();
_cache.Set(cacheKey, response, TimeSpan.FromMinutes(5));
memoryStream.Position = 0;
await memoryStream.CopyToAsync(originalBody);
}
}
// Application builder (simplified)
public class ApplicationBuilder
{
private IMiddleware _firstMiddleware;
private readonly List<Type> _middlewareTypes = new List<Type>();
public ApplicationBuilder Use<T>() where T : IMiddleware
{
_middlewareTypes.Add(typeof(T));
return this;
}
public RequestDelegate Build(IServiceProvider services)
{
// Build chain of middleware
RequestDelegate finalDelegate = context => Task.CompletedTask;
for (int i = _middlewareTypes.Count - 1; i >= 0; i--)
{
var middlewareType = _middlewareTypes[i];
var middleware = (IMiddleware)ActivatorUtilities.CreateInstance(services, middlewareType);
var next = finalDelegate;
finalDelegate = context => middleware.InvokeAsync(context, next);
}
return finalDelegate;
}
}
// Usage simulation
public class HttpContext
{
public HttpRequest Request { get; } = new HttpRequest();
public HttpResponse Response { get; } = new HttpResponse();
}
public class HttpRequest
{
public string Method { get; set; } = "GET";
public string Path { get; set; } = "/";
public Dictionary<string, string> Headers { get; } = new Dictionary<string, string>();
}
public class HttpResponse
{
public int StatusCode { get; set; } = 200;
public MemoryStream Body { get; set; } = new MemoryStream();
public async Task WriteAsync(string content)
{
var bytes = Encoding.UTF8.GetBytes(content);
await Body.WriteAsync(bytes, 0, bytes.Length);
}
}
public interface ILogger
{
void Log(string message);
}
public interface ICache
{
string Get(string key);
void Set(string key, string value, TimeSpan expiry);
}
5. Facade Pattern
5.1 Understanding Facade
Definition: The Facade pattern provides a simplified interface to a complex subsystem. It hides the complexity of the subsystem and makes it easier to use.
Real-World Analogy: A restaurant's waiter. You don't interact directly with the kitchen, the chef, the inventory, or the billing system. The waiter provides a simple interface for placing orders and receiving food.
5.2 Facade Pattern Implementation - E-Commerce System
// Complex subsystems
public class InventorySystem
{
private Dictionary<int, int> _stock = new Dictionary<int, int>
{
[101] = 10, // Product 101: 10 units
[102] = 5, // Product 102: 5 units
[103] = 0 // Product 103: Out of stock
};
public bool CheckAvailability(int productId, int quantity)
{
if (_stock.ContainsKey(productId))
{
var available = _stock[productId] >= quantity;
Console.WriteLine($"Inventory: Product {productId} - {(available ? "Available" : "Out of stock")}");
return available;
}
return false;
}
public void ReserveStock(int productId, int quantity)
{
if (_stock.ContainsKey(productId))
{
_stock[productId] -= quantity;
Console.WriteLine($"Inventory: Reserved {quantity} units of product {productId}");
}
}
}
public class PaymentSystem
{
public bool ProcessPayment(string cardNumber, decimal amount)
{
Console.WriteLine($"Payment: Processing {amount:C} with card ending in {cardNumber.Substring(cardNumber.Length - 4)}");
// Simulate payment processing
return true;
}
public void RefundPayment(string transactionId, decimal amount)
{
Console.WriteLine($"Payment: Refunded {amount:C} for transaction {transactionId}");
}
}
public class ShippingSystem
{
public string CreateShippingLabel(int orderId, string address)
{
var trackingNumber = $"TRK-{orderId}-{DateTime.Now.Ticks}";
Console.WriteLine($"Shipping: Created label for order {orderId} to {address}");
Console.WriteLine($"Shipping: Tracking number: {trackingNumber}");
return trackingNumber;
}
public void SchedulePickup(string trackingNumber, DateTime pickupDate)
{
Console.WriteLine($"Shipping: Scheduled pickup for {trackingNumber} on {pickupDate:d}");
}
}
public class NotificationSystem
{
public void SendEmail(string email, string subject, string message)
{
Console.WriteLine($"Email: To: {email}, Subject: {subject}");
Console.WriteLine($"Email: {message}\n");
}
public void SendSms(string phoneNumber, string message)
{
Console.WriteLine($"SMS: To: {phoneNumber}, Message: {message}");
}
}
public class CustomerSystem
{
private Dictionary<int, Customer> _customers = new Dictionary<int, Customer>();
public CustomerSystem()
{
_customers[1] = new Customer { Id = 1, Name = "John Doe", Email = "john@example.com", Phone = "555-1234", Address = "123 Main St" };
_customers[2] = new Customer { Id = 2, Name = "Jane Smith", Email = "jane@example.com", Phone = "555-5678", Address = "456 Oak Ave" };
}
public Customer GetCustomer(int customerId)
{
return _customers.ContainsKey(customerId) ? _customers[customerId] : null;
}
}
public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public string Phone { get; set; }
public string Address { get; set; }
}
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}
public class Order
{
public int Id { get; set; }
public int CustomerId { get; set; }
public List<OrderItem> Items { get; set; } = new List<OrderItem>();
public decimal TotalAmount { get; set; }
public string Status { get; set; } = "Pending";
public string TrackingNumber { get; set; }
public string TransactionId { get; set; }
}
public class OrderItem
{
public int ProductId { get; set; }
public int Quantity { get; set; }
public decimal UnitPrice { get; set; }
}
// Facade - Simplifies the complex ordering process
public class OrderFacade
{
private readonly InventorySystem _inventory;
private readonly PaymentSystem _payment;
private readonly ShippingSystem _shipping;
private readonly NotificationSystem _notification;
private readonly CustomerSystem _customerSystem;
private static int _nextOrderId = 1000;
public OrderFacade()
{
_inventory = new InventorySystem();
_payment = new PaymentSystem();
_shipping = new ShippingSystem();
_notification = new NotificationSystem();
_customerSystem = new CustomerSystem();
}
public OrderResult PlaceOrder(int customerId, List<OrderItemRequest> items, string cardNumber)
{
Console.WriteLine("\n=== Starting Order Processing ===");
var result = new OrderResult();
// Step 1: Get customer
var customer = _customerSystem.GetCustomer(customerId);
if (customer == null)
{
result.Success = false;
result.Message = "Customer not found";
return result;
}
// Step 2: Check inventory for all items
foreach (var item in items)
{
if (!_inventory.CheckAvailability(item.ProductId, item.Quantity))
{
result.Success = false;
result.Message = $"Product {item.ProductId} is out of stock";
return result;
}
}
// Step 3: Calculate total
decimal total = 0;
var orderItems = new List<OrderItem>();
foreach (var item in items)
{
var product = GetProduct(item.ProductId);
var orderItem = new OrderItem
{
ProductId = item.ProductId,
Quantity = item.Quantity,
UnitPrice = product.Price
};
orderItems.Add(orderItem);
total += product.Price * item.Quantity;
}
// Step 4: Process payment
if (!_payment.ProcessPayment(cardNumber, total))
{
result.Success = false;
result.Message = "Payment failed";
return result;
}
// Step 5: Create order
var order = new Order
{
Id = _nextOrderId++,
CustomerId = customerId,
Items = orderItems,
TotalAmount = total,
TransactionId = $"TXN-{DateTime.Now.Ticks}"
};
// Step 6: Reserve inventory
foreach (var item in items)
{
_inventory.ReserveStock(item.ProductId, item.Quantity);
}
// Step 7: Create shipping label
var trackingNumber = _shipping.CreateShippingLabel(order.Id, customer.Address);
order.TrackingNumber = trackingNumber;
// Step 8: Send notifications
_notification.SendEmail(
customer.Email,
$"Order Confirmation #{order.Id}",
$"Thank you for your order! Total: {total:C}\nTracking: {trackingNumber}"
);
_notification.SendSms(
customer.Phone,
$"Order #{order.Id} confirmed! Total: {total:C}"
);
result.Success = true;
result.OrderId = order.Id;
result.TrackingNumber = trackingNumber;
result.TotalAmount = total;
result.Message = "Order placed successfully";
Console.WriteLine("=== Order Processing Complete ===\n");
return result;
}
private Product GetProduct(int productId)
{
// Simulate product lookup
return productId switch
{
101 => new Product { Id = 101, Name = "Laptop", Price = 999.99m },
102 => new Product { Id = 102, Name = "Mouse", Price = 29.99m },
103 => new Product { Id = 103, Name = "Keyboard", Price = 79.99m },
_ => new Product { Id = productId, Name = "Unknown", Price = 0 }
};
}
public OrderStatusResult CheckOrderStatus(int orderId)
{
Console.WriteLine($"Checking status for order #{orderId}");
// Simulate status check
return new OrderStatusResult
{
OrderId = orderId,
Status = "Shipped",
TrackingNumber = $"TRK-{orderId}-12345",
EstimatedDelivery = DateTime.Now.AddDays(3)
};
}
public bool CancelOrder(int orderId)
{
Console.WriteLine($"Cancelling order #{orderId}");
// Simulate cancellation
return true;
}
}
// DTOs
public class OrderItemRequest
{
public int ProductId { get; set; }
public int Quantity { get; set; }
}
public class OrderResult
{
public bool Success { get; set; }
public int OrderId { get; set; }
public string TrackingNumber { get; set; }
public decimal TotalAmount { get; set; }
public string Message { get; set; }
}
public class OrderStatusResult
{
public int OrderId { get; set; }
public string Status { get; set; }
public string TrackingNumber { get; set; }
public DateTime EstimatedDelivery { get; set; }
}
// Usage - Client code sees only the simple facade
public class FacadeDemo
{
public static void Run()
{
Console.WriteLine("=== Facade Pattern - E-Commerce Order System ===\n");
var orderFacade = new OrderFacade();
// Simple interface - client doesn't need to know about inventory, payment, shipping, etc.
var items = new List<OrderItemRequest>
{
new OrderItemRequest { ProductId = 101, Quantity = 1 }, // Laptop
new OrderItemRequest { ProductId = 102, Quantity = 2 } // Mouse x2
};
var result = orderFacade.PlaceOrder(1, items, "4111111111111111");
if (result.Success)
{
Console.WriteLine($"✅ Order #{result.OrderId} placed successfully!");
Console.WriteLine($"📦 Tracking: {result.TrackingNumber}");
Console.WriteLine($"💰 Total: {result.TotalAmount:C}");
}
else
{
Console.WriteLine($"❌ Order failed: {result.Message}");
}
// Check order status
Console.WriteLine("\n--- Checking Order Status ---");
var status = orderFacade.CheckOrderStatus(1001);
Console.WriteLine($"Order #{status.OrderId}: {status.Status}");
Console.WriteLine($"Tracking: {status.TrackingNumber}");
Console.WriteLine($"Estimated Delivery: {status.EstimatedDelivery:d}");
}
}
6. Flyweight Pattern
6.1 Understanding Flyweight
Definition: The Flyweight pattern reduces memory usage by sharing as much data as possible with similar objects. It's useful when you need to create a large number of similar objects.
Real-World Analogy: In a word processor, each character is a flyweight object. The character's appearance (font, size, style) is shared, while position is extrinsic and stored separately.
6.2 Flyweight Pattern Implementation - Text Editor
// Flyweight - Shared intrinsic state
public class CharacterGlyph
{
public char Character { get; }
public string FontFamily { get; }
public int FontSize { get; }
public bool IsBold { get; }
public bool IsItalic { get; }
public ConsoleColor Color { get; }
public CharacterGlyph(char character, string fontFamily, int fontSize, bool isBold, bool isItalic, ConsoleColor color)
{
Character = character;
FontFamily = fontFamily;
FontSize = fontSize;
IsBold = isBold;
IsItalic = isItalic;
Color = color;
}
public void Display(int x, int y)
{
Console.ForegroundColor = Color;
if (IsBold) Console.Write($"\u001b[1m");
if (IsItalic) Console.Write($"\u001b[3m");
Console.SetCursorPosition(x, y);
Console.Write(Character);
// Reset formatting
Console.ResetColor();
if (IsBold || IsItalic) Console.Write($"\u001b[0m");
}
public override string ToString()
{
return $"{Character} ({FontFamily}, {FontSize}pt{(IsBold ? ", Bold" : "")}{(IsItalic ? ", Italic" : "")})";
}
}
// Flyweight Factory - Manages and reuses flyweights
public class CharacterGlyphFactory
{
private Dictionary<string, CharacterGlyph> _glyphs = new Dictionary<string, CharacterGlyph>();
public CharacterGlyph GetGlyph(char character, string fontFamily, int fontSize, bool isBold, bool isItalic, ConsoleColor color)
{
// Create a unique key based on intrinsic properties
string key = $"{character}|{fontFamily}|{fontSize}|{isBold}|{isItalic}|{color}";
if (!_glyphs.ContainsKey(key))
{
_glyphs[key] = new CharacterGlyph(character, fontFamily, fontSize, isBold, isItalic, color);
Console.WriteLine($"Created new glyph: {_glyphs[key]}");
}
return _glyphs[key];
}
public int GetGlyphCount() => _glyphs.Count;
}
// Context - Extrinsic state for each character instance
public class CharacterPosition
{
public CharacterGlyph Glyph { get; }
public int X { get; set; }
public int Y { get; set; }
public CharacterPosition(CharacterGlyph glyph, int x, int y)
{
Glyph = glyph;
X = x;
Y = y;
}
public void Display()
{
Glyph.Display(X, Y);
}
}
// Document class that uses flyweights
public class Document
{
private List<CharacterPosition> _characters = new List<CharacterPosition>();
private CharacterGlyphFactory _factory = new CharacterGlyphFactory();
public void AddCharacter(char character, int x, int y, string fontFamily = "Arial", int fontSize = 12,
bool isBold = false, bool isItalic = false, ConsoleColor color = ConsoleColor.White)
{
var glyph = _factory.GetGlyph(character, fontFamily, fontSize, isBold, isItalic, color);
_characters.Add(new CharacterPosition(glyph, x, y));
}
public void Display()
{
Console.Clear();
foreach (var character in _characters)
{
character.Display();
}
Console.SetCursorPosition(0, Console.WindowHeight - 1);
Console.WriteLine($"\nTotal characters: {_characters.Count}");
Console.WriteLine($"Unique glyphs: {_factory.GetGlyphCount()}");
Console.WriteLine($"Memory saved: {(_characters.Count - _factory.GetGlyphCount()) * 100 / _characters.Count}%");
}
}
// Tree example - Another common Flyweight use case
public class TreeType
{
public string Name { get; }
public ConsoleColor Color { get; }
public string Texture { get; }
public TreeType(string name, ConsoleColor color, string texture)
{
Name = name;
Color = color;
Texture = texture;
}
public void Draw(int x, int y)
{
Console.ForegroundColor = Color;
Console.SetCursorPosition(x, y);
Console.Write($"🌳");
Console.ResetColor();
}
}
public class TreeFactory
{
private Dictionary<string, TreeType> _treeTypes = new Dictionary<string, TreeType>();
public TreeType GetTreeType(string name, ConsoleColor color, string texture)
{
string key = $"{name}|{color}|{texture}";
if (!_treeTypes.ContainsKey(key))
{
_treeTypes[key] = new TreeType(name, color, texture);
Console.WriteLine($"Created new tree type: {name}");
}
return _treeTypes[key];
}
public int GetTreeTypeCount() => _treeTypes.Count;
}
public class Tree
{
public TreeType Type { get; }
public int X { get; }
public int Y { get; }
public Tree(TreeType type, int x, int y)
{
Type = type;
X = x;
Y = y;
}
public void Draw()
{
Type.Draw(X, Y);
}
}
public class Forest
{
private List<Tree> _trees = new List<Tree>();
private TreeFactory _factory = new TreeFactory();
public void PlantTree(int x, int y, string name, ConsoleColor color, string texture)
{
var type = _factory.GetTreeType(name, color, texture);
_trees.Add(new Tree(type, x, y));
}
public void Draw()
{
Console.Clear();
foreach (var tree in _trees)
{
tree.Draw();
}
Console.SetCursorPosition(0, Console.WindowHeight - 2);
Console.WriteLine($"Total trees: {_trees.Count}");
Console.WriteLine($"Unique tree types: {_factory.GetTreeTypeCount()}");
}
}
// Usage
public class FlyweightDemo
{
public static void Run()
{
Console.WriteLine("=== Flyweight Pattern - Document Editor ===\n");
var doc = new Document();
// Add text with shared glyphs
string text = "Hello World! This is a flyweight pattern demonstration.";
for (int i = 0; i < text.Length; i++)
{
// Different styles for demonstration
bool isBold = i % 10 == 0;
bool isItalic = i % 15 == 0;
var color = i % 3 == 0 ? ConsoleColor.Red : (i % 3 == 1 ? ConsoleColor.Green : ConsoleColor.White);
doc.AddCharacter(text[i], i * 2, 5, "Arial", 12, isBold, isItalic, color);
}
doc.Display();
Console.WriteLine("\n\n=== Flyweight Pattern - Forest Simulation ===\n");
var forest = new Forest();
// Plant many trees with only a few shared types
Random rand = new Random();
string[] treeNames = { "Oak", "Pine", "Maple", "Birch" };
ConsoleColor[] colors = { ConsoleColor.Green, ConsoleColor.DarkGreen, ConsoleColor.Yellow, ConsoleColor.DarkYellow };
for (int i = 0; i < 1000; i++)
{
int x = rand.Next(0, 80);
int y = rand.Next(0, 20);
int typeIndex = rand.Next(0, treeNames.Length);
forest.PlantTree(x, y, treeNames[typeIndex], colors[typeIndex], "leaf_texture");
}
forest.Draw();
Console.WriteLine("\nMemory saved by sharing tree types!");
}
}
7. Proxy Pattern
7.1 Understanding Proxy
Definition: The Proxy pattern provides a surrogate or placeholder for another object to control access to it. Proxies can be used for lazy loading, access control, logging, caching, etc.
Real-World Analogy: A credit card is a proxy for a bank account. You use the card to make purchases, and the card handles the communication with the bank, adding security and convenience.
7.2 Proxy Pattern Implementation
// Subject interface
public interface IImage
{
void Display();
string GetFilename();
long GetSize();
}
// Real subject - Expensive to create
public class HighResolutionImage : IImage
{
private string _filename;
private byte[] _imageData;
private long _size;
public HighResolutionImage(string filename)
{
_filename = filename;
LoadImageFromDisk();
}
private void LoadImageFromDisk()
{
Console.WriteLine($"Loading high-resolution image: {_filename} (This is expensive!)");
// Simulate loading large image
Thread.Sleep(2000);
_imageData = new byte[10_000_000]; // Simulate 10MB image
_size = _imageData.Length;
Console.WriteLine($"Image loaded: {_filename} ({_size / 1_000_000}MB)");
}
public void Display()
{
Console.WriteLine($"Displaying image: {_filename}");
}
public string GetFilename() => _filename;
public long GetSize() => _size;
}
// Virtual Proxy - Lazy loading
public class ImageProxy : IImage
{
private string _filename;
private HighResolutionImage _realImage;
private bool _isLoading;
public ImageProxy(string filename)
{
_filename = filename;
Console.WriteLine($"Proxy created for: {_filename}");
}
public void Display()
{
if (_realImage == null && !_isLoading)
{
Console.WriteLine($"Proxy: Loading image on demand...");
_isLoading = true;
// Simulate async loading
Task.Run(() =>
{
_realImage = new HighResolutionImage(_filename);
_realImage.Display();
_isLoading = false;
});
Console.WriteLine($"Proxy: Displaying placeholder while loading...");
DisplayPlaceholder();
}
else if (_realImage != null)
{
_realImage.Display();
}
else
{
Console.WriteLine($"Proxy: Image still loading, please wait...");
DisplayPlaceholder();
}
}
private void DisplayPlaceholder()
{
Console.WriteLine($"📷 [Placeholder] {_filename} - Loading...");
}
public string GetFilename() => _filename;
public long GetSize()
{
if (_realImage != null)
return _realImage.GetSize();
else
return 0; // Not loaded yet
}
}
// Protection Proxy - Access control
public interface IDocument
{
void Display();
void Edit(string content);
string GetContent();
}
public class Document : IDocument
{
private string _filename;
private string _content;
private string _owner;
public Document(string filename, string owner)
{
_filename = filename;
_owner = owner;
LoadDocument();
}
private void LoadDocument()
{
Console.WriteLine($"Loading document: {_filename}");
Thread.Sleep(1000);
_content = $"Content of {_filename}";
}
public void Display()
{
Console.WriteLine($"Document: {_filename}");
Console.WriteLine($"Content: {_content}");
}
public void Edit(string content)
{
_content = content;
Console.WriteLine($"Document edited: {_filename}");
}
public string GetContent() => _content;
public string GetOwner() => _owner;
}
public class DocumentProxy : IDocument
{
private Document _realDocument;
private string _currentUser;
public DocumentProxy(string filename, string owner, string currentUser)
{
_realDocument = new Document(filename, owner);
_currentUser = currentUser;
}
public void Display()
{
// Anyone can view documents
Console.WriteLine($"\n[Proxy] User '{_currentUser}' is viewing document");
_realDocument.Display();
}
public void Edit(string content)
{
// Only owner can edit
if (_currentUser == _realDocument.GetOwner())
{
Console.WriteLine($"\n[Proxy] User '{_currentUser}' is editing document");
_realDocument.Edit(content);
}
else
{
Console.WriteLine($"\n[Proxy] Access Denied: User '{_currentUser}' cannot edit this document");
Console.WriteLine($"Only owner '{_realDocument.GetOwner()}' can edit");
}
}
public string GetContent()
{
// Anyone can read content
return _realDocument.GetContent();
}
}
// Cache Proxy - Caching results
public interface IDataService
{
Task<string> GetDataAsync(string query);
}
public class RealDataService : IDataService
{
public async Task<string> GetDataAsync(string query)
{
Console.WriteLine($"RealDataService: Executing expensive query: {query}");
await Task.Delay(2000); // Simulate database query
return $"Result for '{query}' at {DateTime.Now:T}";
}
}
public class CachingDataServiceProxy : IDataService
{
private readonly IDataService _realService;
private readonly Dictionary<string, (string Result, DateTime Expiry)> _cache = new();
private readonly TimeSpan _cacheDuration = TimeSpan.FromSeconds(10);
public CachingDataServiceProxy(IDataService realService)
{
_realService = realService;
}
public async Task<string> GetDataAsync(string query)
{
// Check cache
if (_cache.ContainsKey(query) && _cache[query].Expiry > DateTime.Now)
{
Console.WriteLine($"CacheProxy: Returning cached result for '{query}'");
return _cache[query].Result;
}
// Get from real service
Console.WriteLine($"CacheProxy: Cache miss for '{query}', calling real service");
var result = await _realService.GetDataAsync(query);
// Store in cache
_cache[query] = (result, DateTime.Now + _cacheDuration);
return result;
}
public void ClearCache()
{
_cache.Clear();
Console.WriteLine("CacheProxy: Cache cleared");
}
}
// Logging Proxy - Adds logging
public class LoggingProxy<T> : DispatchProxy where T : class
{
private T _decorated;
private ILogger _logger;
public static T Create(T decorated, ILogger logger)
{
object proxy = Create<T, LoggingProxy<T>>();
((LoggingProxy<T>)proxy).SetParameters(decorated, logger);
return (T)proxy;
}
private void SetParameters(T decorated, ILogger logger)
{
_decorated = decorated;
_logger = logger;
}
protected override object Invoke(MethodInfo targetMethod, object[] args)
{
_logger.Log($"Calling {targetMethod.Name} with args: {string.Join(", ", args)}");
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
try
{
var result = targetMethod.Invoke(_decorated, args);
stopwatch.Stop();
_logger.Log($"{targetMethod.Name} completed in {stopwatch.ElapsedMilliseconds}ms");
return result;
}
catch (Exception ex)
{
_logger.Log($"{targetMethod.Name} failed: {ex.Message}");
throw;
}
}
}
// Usage
public class ProxyDemo
{
public static async Task RunAsync()
{
Console.WriteLine("=== Proxy Pattern Demo ===\n");
// 1. Virtual Proxy - Lazy Loading
Console.WriteLine("--- Virtual Proxy (Lazy Image Loading) ---");
IImage image = new ImageProxy("vacation_photo.jpg");
Console.WriteLine("\nImage created, not loaded yet");
Console.WriteLine($"Filename: {image.GetFilename()}");
Console.WriteLine($"Size: {image.GetSize()} bytes (not loaded)");
Console.WriteLine("\nFirst display call - will load image:");
image.Display();
Console.WriteLine("\nWaiting for image to load...");
Thread.Sleep(2500);
Console.WriteLine("\nSecond display call - image already loaded:");
image.Display();
// 2. Protection Proxy - Access Control
Console.WriteLine("\n\n--- Protection Proxy (Access Control) ---");
var docProxy = new DocumentProxy("confidential.txt", "admin", "john");
docProxy.Display();
docProxy.Edit("New content by john");
Console.WriteLine();
var docProxy2 = new DocumentProxy("confidential.txt", "admin", "admin");
docProxy2.Display();
docProxy2.Edit("New content by admin");
// 3. Cache Proxy
Console.WriteLine("\n\n--- Cache Proxy ---");
var realService = new RealDataService();
var cachedService = new CachingDataServiceProxy(realService);
Console.WriteLine("First call:");
var result1 = await cachedService.GetDataAsync("SELECT * FROM Users");
Console.WriteLine($"Result: {result1}");
Console.WriteLine("\nSecond call (should be cached):");
var result2 = await cachedService.GetDataAsync("SELECT * FROM Users");
Console.WriteLine($"Result: {result2}");
Console.WriteLine("\nDifferent query:");
var result3 = await cachedService.GetDataAsync("SELECT * FROM Orders");
Console.WriteLine($"Result: {result3}");
}
}
8. Choosing the Right Structural Pattern
| Pattern | When to Use | Key Benefit | .NET Use Cases |
|---|---|---|---|
| Adapter | Need to use existing class with incompatible interface | Makes incompatible interfaces work together | Database providers, API clients, third-party integrations |
| Bridge | Want to avoid permanent binding between abstraction and implementation | Decouples abstraction from implementation | UI frameworks, device drivers, database drivers |
| Composite | Need to treat individual objects and compositions uniformly | Simplifies tree structure operations | UI controls, file systems, organizational hierarchies |
| Decorator | Need to add responsibilities dynamically without subclassing | Flexible alternative to inheritance | ASP.NET Core Middleware, streams, logging, caching |
| Facade | Need to simplify a complex subsystem | Provides simple interface to complex system | Service layer, API gateways, libraries |
| Flyweight | Need to create large number of similar objects | Reduces memory usage | Character rendering, game objects, connection pools |
| Proxy | Need to control access to an object | Adds level of indirection | Lazy loading, caching, logging, access control, WCF remoting |
9. Summary and Key Takeaways
- Adapter: Converts one interface to another. Use when integrating with third-party code.
- Bridge: Separates abstraction from implementation. Use when both can vary independently.
- Composite: Treats individual objects and compositions uniformly. Use for tree structures.
- Decorator: Adds responsibilities dynamically. Use for open/closed principle compliance.
- Facade: Simplifies complex subsystems. Use to provide a clean API.
- Flyweight: Shares objects to reduce memory. Use for large numbers of similar objects.
- Proxy: Controls access to another object. Use for lazy loading, caching, logging, protection.
🎯 Design Tips:
- Start with Facade when you need to simplify complex legacy code
- Use Adapter when working with third-party libraries that you can't modify
- Apply Decorator when you have many combinations of features
- Consider Composite for any hierarchical data structure
- Use Proxy when you need lazy initialization or access control
- Apply Bridge when you anticipate changes in both abstraction and implementation
- Use Flyweight only when you have proven memory issues with many objects
10. Practice Exercises
Exercise 1: Implement a Logging Adapter
Create an adapter that makes a third-party logging library work with your application's logging interface.
Click to see solution
// Your application's logging interface
public interface IAppLogger
{
void Info(string message);
void Warning(string message);
void Error(string message, Exception ex = null);
}
// Third-party logger (cannot modify)
public class ThirdPartyLogger
{
public void Write(LogLevel level, string message, string source = null)
{
Console.WriteLine($"[{level}] [{source}] {message}");
}
}
public enum LogLevel
{
Info,
Warning,
Error,
Debug
}
// Adapter implementation
public class ThirdPartyLoggerAdapter : IAppLogger
{
private readonly ThirdPartyLogger _logger;
private readonly string _source;
public ThirdPartyLoggerAdapter(ThirdPartyLogger logger, string source)
{
_logger = logger;
_source = source;
}
public void Info(string message)
{
_logger.Write(LogLevel.Info, message, _source);
}
public void Warning(string message)
{
_logger.Write(LogLevel.Warning, message, _source);
}
public void Error(string message, Exception ex = null)
{
var fullMessage = ex != null ? $"{message}: {ex.Message}" : message;
_logger.Write(LogLevel.Error, fullMessage, _source);
}
}
Exercise 2: Create a Caching Proxy for a Weather Service
Implement a proxy that caches weather data for 5 minutes.
Click to see solution
public interface IWeatherService
{
Task<WeatherData> GetWeatherAsync(string city);
}
public class WeatherData
{
public string City { get; set; }
public double Temperature { get; set; }
public string Condition { get; set; }
public DateTime RetrievedAt { get; set; }
}
public class RealWeatherService : IWeatherService
{
private readonly Random _random = new Random();
public async Task<WeatherData> GetWeatherAsync(string city)
{
Console.WriteLine($"Fetching weather for {city} from external API...");
await Task.Delay(1500); // Simulate API call
return new WeatherData
{
City = city,
Temperature = _random.Next(-10, 40),
Condition = _random.Next(0, 3) switch
{
0 => "Sunny",
1 => "Cloudy",
2 => "Rainy",
_ => "Windy"
},
RetrievedAt = DateTime.Now
};
}
}
public class CachingWeatherProxy : IWeatherService
{
private readonly IWeatherService _realService;
private readonly Dictionary<string, (WeatherData Data, DateTime Expiry)> _cache;
private readonly TimeSpan _cacheDuration;
public CachingWeatherProxy(IWeatherService realService, TimeSpan cacheDuration)
{
_realService = realService;
_cache = new Dictionary<string, (WeatherData, DateTime)>();
_cacheDuration = cacheDuration;
}
public async Task<WeatherData> GetWeatherAsync(string city)
{
if (_cache.ContainsKey(city) && _cache[city].Expiry > DateTime.Now)
{
Console.WriteLine($"Returning cached weather for {city}");
return _cache[city].Data;
}
var data = await _realService.GetWeatherAsync(city);
_cache[city] = (data, DateTime.Now + _cacheDuration);
return data;
}
}
11. What's Next?
In Chapter 5, we'll explore Behavioral Design Patterns - Strategy, Observer, Chain of Responsibility, Command, State, Template Method, and Mediator. These patterns focus on communication between objects and the assignment of responsibilities.
Chapter 5 Preview:
- Strategy - Encapsulate interchangeable algorithms
- Observer - Define one-to-many dependencies
- Chain of Responsibility - Pass requests along a chain of handlers
- Command - Encapsulate requests as objects
- State - Alter behavior when internal state changes
- Template Method - Define algorithm skeleton, defer steps to subclasses
- Mediator - Reduce coupling between objects
📝 Practice Assignments:
- Create an adapter for a payment gateway (Stripe/PayPal) to work with your existing payment interface
- Implement a remote control system using Bridge pattern with multiple device types and remote types
- Build a file system browser using Composite pattern
- Create a coffee ordering system with decorators for different add-ons (milk, sugar, syrup)
- Implement a simple ORM facade that hides complex database operations
- Create a flyweight for a game that needs to render thousands of trees or enemies
- Implement a caching proxy for a slow API service
- Combine multiple structural patterns to build a complete document processing system
Happy Coding! 🚀