Skip to main content

9.3 - Structural Patterns

Structural patterns are concerned 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 use inheritance to compose interfaces or implementations.

9.3.1 - Adapter Pattern​

The Adapter pattern allows objects with incompatible interfaces to collaborate. It acts as a bridge between two incompatible interfaces by wrapping an instance of one class into an adapter class that presents the expected interface.

Object Adapter Implementation​

// PROBLEM: We have an existing class (Adaptee) with a useful implementation,
// but its interface is incompatible with our client code that expects ITarget

/// <summary>
/// Target interface that the client code expects to work with
/// This represents the interface that our client code is designed to use
/// </summary>
public interface ITarget
{
/// <summary>
/// Gets a standardized request string in the format expected by client code
/// </summary>
/// <returns>A formatted request string</returns>
string GetRequest();
}

/// <summary>
/// The existing class with an incompatible interface
/// This class has useful functionality but cannot be used directly by client code
/// </summary>
public class Adaptee
{
/// <summary>
/// Gets a specific request string in a format that's incompatible with ITarget
/// </summary>
/// <returns>A specific request string in Adaptee's format</returns>
public string GetSpecificRequest()
{
// This method returns data in a format that client code doesn't understand
return "Specific request from Adaptee";
}
}

/// <summary>
/// Adapter class that makes Adaptee's interface compatible with ITarget
/// This is the key class in the Adapter pattern - it bridges between incompatible interfaces
/// </summary>
public class Adapter : ITarget
{
// Reference to the adaptee object that we're adapting
// The 'readonly' keyword ensures this reference cannot be changed after initialization
private readonly Adaptee _adaptee;

/// <summary>
/// Initializes a new instance of the Adapter class
/// </summary>
/// <param name="adaptee">The Adaptee instance to adapt</param>
public Adapter(Adaptee adaptee)
{
// Store the adaptee instance for later use
_adaptee = adaptee ?? throw new ArgumentNullException(nameof(adaptee));
}

/// <summary>
/// Implements the ITarget interface by translating the call to Adaptee's format
/// </summary>
/// <returns>A formatted request string that meets the ITarget contract</returns>
public string GetRequest()
{
// This is where the adaptation happens:
// 1. Call the incompatible method on the adaptee
// 2. Transform its result into the format expected by ITarget
// 3. Return the transformed result
return $"Adapter: {_adaptee.GetSpecificRequest()}";
}
}

// USAGE EXAMPLE:
// // Client code that works with ITarget
// void ClientCode(ITarget target)
// {
// Console.WriteLine(target.GetRequest());
// }
//
// // Create an instance of the existing class
// Adaptee adaptee = new Adaptee();
//
// // Create an adapter to make it work with client code
// ITarget adapter = new Adapter(adaptee);
//
// // Now the client code can work with the adaptee through the adapter
// ClientCode(adapter); // Output: "Adapter: Specific request from Adaptee"

Real-World Example​

// REAL-WORLD PROBLEM: We have a legacy payment processing system that we can't modify,
// but our new application uses a modern payment interface. We need to make them work together.

/// <summary>
/// Legacy payment processor that cannot be modified
/// This represents an existing system with an incompatible interface
/// </summary>
public class LegacyPaymentProcessor
{
/// <summary>
/// Processes a payment using the legacy system
/// </summary>
/// <param name="amount">The payment amount as a string</param>
/// <param name="account">The account identifier as a string</param>
public void ProcessPayment(string amount, string account)
{
// This method expects string parameters and doesn't support modern features like currency
Console.WriteLine($"Processing payment of {amount} from account {account} using legacy system");

// In a real system, this would connect to the legacy payment gateway
// and process the transaction using the legacy protocol
}
}

/// <summary>
/// Modern payment interface used by the new application
/// This is the interface our new code is designed to work with
/// </summary>
public interface IPaymentProcessor
{
/// <summary>
/// Processes a payment using a structured payment details object
/// </summary>
/// <param name="paymentDetails">The payment details including amount, account, and currency</param>
void ProcessPayment(PaymentDetails paymentDetails);
}

/// <summary>
/// Data transfer object containing structured payment information
/// </summary>
public class PaymentDetails
{
/// <summary>
/// Gets or sets the payment amount as a decimal value
/// </summary>
public decimal Amount { get; set; }

/// <summary>
/// Gets or sets the account identifier
/// </summary>
public string AccountId { get; set; }

/// <summary>
/// Gets or sets the currency code (e.g., USD, EUR)
/// </summary>
public string Currency { get; set; }
}

/// <summary>
/// Adapter that allows the legacy payment processor to be used with the modern interface
/// This is the key class that bridges between the two incompatible systems
/// </summary>
public class LegacyPaymentAdapter : IPaymentProcessor
{
// Reference to the legacy processor that we're adapting
private readonly LegacyPaymentProcessor _legacyProcessor;

/// <summary>
/// Initializes a new instance of the LegacyPaymentAdapter
/// </summary>
/// <param name="legacyProcessor">The legacy payment processor to adapt</param>
public LegacyPaymentAdapter(LegacyPaymentProcessor legacyProcessor)
{
_legacyProcessor = legacyProcessor ?? throw new ArgumentNullException(nameof(legacyProcessor));
}

/// <summary>
/// Implements the modern IPaymentProcessor interface by translating to legacy format
/// </summary>
/// <param name="paymentDetails">The modern payment details</param>
public void ProcessPayment(PaymentDetails paymentDetails)
{
// Validate input
if (paymentDetails == null)
{
throw new ArgumentNullException(nameof(paymentDetails));
}

// This is where the adaptation happens:
// 1. Extract data from the modern PaymentDetails object
// 2. Convert it to the format expected by the legacy processor
// 3. Call the legacy processor with the converted data

// Note: The legacy system doesn't support currency, so we're ignoring that field
// In a real system, we might need to perform currency conversion or handle it differently
_legacyProcessor.ProcessPayment(
paymentDetails.Amount.ToString("F2"), // Format as string with 2 decimal places
paymentDetails.AccountId);
}
}

// USAGE EXAMPLE:
// // Create an instance of the legacy payment processor
// var legacyProcessor = new LegacyPaymentProcessor();
//
// // Create an adapter to make it work with the modern interface
// IPaymentProcessor modernPaymentProcessor = new LegacyPaymentAdapter(legacyProcessor);
//
// // Create payment details using the modern format
// var paymentDetails = new PaymentDetails
// {
// Amount = 99.99m,
// AccountId = "ACC123456",
// Currency = "USD"
// };
//
// // Process the payment using the modern interface
// // The adapter will convert it to the legacy format behind the scenes
// modernPaymentProcessor.ProcessPayment(paymentDetails);

When to Use​

  • When you want to use an existing class, but its interface doesn't match the one you need
  • When you want to create a reusable class that cooperates with classes that don't necessarily have compatible interfaces
  • When you need to use several existing subclasses but it's impractical to adapt their interface by subclassing each one

9.3.2 - Bridge Pattern​

The Bridge pattern separates an abstraction from its implementation so that the two can vary independently. It involves an interface acting as a bridge between the abstract class and implementor classes.

Basic Implementation​

// PROBLEM: We need to control different types of devices (TV, Radio) with different types of remotes
// (basic, advanced) without creating a class explosion (BasicTVRemote, AdvancedTVRemote, BasicRadioRemote, etc.)

/// <summary>
/// Implementor interface that defines operations for all device types
/// This is the "implementation" side of the bridge
/// </summary>
public interface IDevice
{
/// <summary>
/// Checks if the device is currently powered on
/// </summary>
/// <returns>True if the device is enabled, false otherwise</returns>
bool IsEnabled();

/// <summary>
/// Powers on the device
/// </summary>
void Enable();

/// <summary>
/// Powers off the device
/// </summary>
void Disable();

/// <summary>
/// Gets the current volume level
/// </summary>
/// <returns>The current volume (0-100)</returns>
int GetVolume();

/// <summary>
/// Sets the volume to a specific level
/// </summary>
/// <param name="volume">The desired volume level (0-100)</param>
void SetVolume(int volume);

/// <summary>
/// Gets the current channel number
/// </summary>
/// <returns>The current channel number</returns>
int GetChannel();

/// <summary>
/// Sets the device to a specific channel
/// </summary>
/// <param name="channel">The desired channel number</param>
void SetChannel(int channel);
}

/// <summary>
/// Concrete implementor for TV devices
/// </summary>
public class TV : IDevice
{
// Internal state of the TV
private bool _enabled = false;
private int _volume = 30;
private int _channel = 1;

/// <summary>
/// Checks if the TV is powered on
/// </summary>
/// <returns>True if the TV is on, false otherwise</returns>
public bool IsEnabled() => _enabled;

/// <summary>
/// Powers on the TV
/// </summary>
public void Enable()
{
_enabled = true;
Console.WriteLine("TV powered on");
}

/// <summary>
/// Powers off the TV
/// </summary>
public void Disable()
{
_enabled = false;
Console.WriteLine("TV powered off");
}

/// <summary>
/// Gets the current TV volume
/// </summary>
/// <returns>The current volume level</returns>
public int GetVolume() => _volume;

/// <summary>
/// Sets the TV volume with range validation
/// </summary>
/// <param name="volume">The desired volume level</param>
public void SetVolume(int volume)
{
// Ensure volume stays within valid range (0-100)
_volume = Math.Clamp(volume, 0, 100);
Console.WriteLine($"TV volume set to {_volume}");
}

/// <summary>
/// Gets the current TV channel
/// </summary>
/// <returns>The current channel number</returns>
public int GetChannel() => _channel;

/// <summary>
/// Sets the TV to a specific channel
/// </summary>
/// <param name="channel">The desired channel number</param>
public void SetChannel(int channel)
{
_channel = channel;
Console.WriteLine($"TV channel set to {_channel}");
}

/// <summary>
/// Returns a string representation of the device
/// </summary>
/// <returns>The string "TV"</returns>
public override string ToString() => "TV";
}

/// <summary>
/// Concrete implementor for Radio devices
/// </summary>
public class Radio : IDevice
{
// Internal state of the Radio
private bool _enabled = false;
private int _volume = 20;
private int _channel = 1;

/// <summary>
/// Checks if the radio is powered on
/// </summary>
/// <returns>True if the radio is on, false otherwise</returns>
public bool IsEnabled() => _enabled;

/// <summary>
/// Powers on the radio
/// </summary>
public void Enable()
{
_enabled = true;
Console.WriteLine("Radio powered on");
}

/// <summary>
/// Powers off the radio
/// </summary>
public void Disable()
{
_enabled = false;
Console.WriteLine("Radio powered off");
}

/// <summary>
/// Gets the current radio volume
/// </summary>
/// <returns>The current volume level</returns>
public int GetVolume() => _volume;

/// <summary>
/// Sets the radio volume with range validation
/// </summary>
/// <param name="volume">The desired volume level</param>
public void SetVolume(int volume)
{
// Ensure volume stays within valid range (0-100)
_volume = Math.Clamp(volume, 0, 100);
Console.WriteLine($"Radio volume set to {_volume}");
}

/// <summary>
/// Gets the current radio station/channel
/// </summary>
/// <returns>The current station number</returns>
public int GetChannel() => _channel;

/// <summary>
/// Sets the radio to a specific station/channel
/// </summary>
/// <param name="channel">The desired station number</param>
public void SetChannel(int channel)
{
_channel = channel;
Console.WriteLine($"Radio tuned to station {_channel}");
}

/// <summary>
/// Returns a string representation of the device
/// </summary>
/// <returns>The string "Radio"</returns>
public override string ToString() => "Radio";
}

/// <summary>
/// Abstraction that defines the interface for controlling devices
/// This is the "abstraction" side of the bridge
/// </summary>
public abstract class RemoteControl
{
// Reference to the device being controlled
// This is the bridge between the abstraction and implementation
protected IDevice _device;

/// <summary>
/// Initializes a new instance of the RemoteControl class
/// </summary>
/// <param name="device">The device to control</param>
public RemoteControl(IDevice device)
{
_device = device ?? throw new ArgumentNullException(nameof(device));
}

/// <summary>
/// Toggles the power state of the device
/// </summary>
public void TogglePower()
{
if (_device.IsEnabled())
{
_device.Disable();
}
else
{
_device.Enable();
}
}

/// <summary>
/// Decreases the volume of the device
/// </summary>
public void VolumeDown()
{
_device.SetVolume(_device.GetVolume() - 10);
}

/// <summary>
/// Increases the volume of the device
/// </summary>
public void VolumeUp()
{
_device.SetVolume(_device.GetVolume() + 10);
}

/// <summary>
/// Moves to the previous channel/station
/// </summary>
public void ChannelDown()
{
_device.SetChannel(_device.GetChannel() - 1);
}

/// <summary>
/// Moves to the next channel/station
/// </summary>
public void ChannelUp()
{
_device.SetChannel(_device.GetChannel() + 1);
}
}

/// <summary>
/// Refined abstraction that adds advanced remote control features
/// This demonstrates how the abstraction can be extended independently of the implementation
/// </summary>
public class AdvancedRemoteControl : RemoteControl
{
/// <summary>
/// Initializes a new instance of the AdvancedRemoteControl class
/// </summary>
/// <param name="device">The device to control</param>
public AdvancedRemoteControl(IDevice device) : base(device) { }

/// <summary>
/// Mutes the device by setting volume to 0
/// </summary>
public void Mute()
{
_device.SetVolume(0);
Console.WriteLine($"Muted the {_device}");
}

/// <summary>
/// Sets the device to a specific channel directly
/// </summary>
/// <param name="channel">The desired channel number</param>
public void SetChannel(int channel)
{
_device.SetChannel(channel);
}
}

// USAGE EXAMPLE:
// // Create different device implementations
// IDevice tv = new TV();
// IDevice radio = new Radio();
//
// // Create different remote control abstractions
// RemoteControl basicRemote = new BasicRemoteControl(tv);
// AdvancedRemoteControl advancedRemote = new AdvancedRemoteControl(radio);
//
// // Use the remotes to control the devices
// basicRemote.TogglePower(); // Turns on the TV
// basicRemote.VolumeUp(); // Increases TV volume
//
// advancedRemote.TogglePower(); // Turns on the Radio
// advancedRemote.Mute(); // Mutes the Radio
//
// // We can easily swap implementations
// advancedRemote = new AdvancedRemoteControl(tv);
// advancedRemote.SetChannel(5); // Sets TV to channel 5

When to Use​

  • When you want to avoid a permanent binding between an abstraction and its implementation
  • When both the abstractions and their implementations should be extensible through subclasses
  • When changes in the implementation should not impact the client code
  • When you have a proliferation of classes resulting from a coupled interface and numerous implementations

9.3.3 - Composite Pattern​

The Composite pattern lets you compose objects into tree structures to represent part-whole hierarchies. It lets clients treat individual objects and compositions of objects uniformly.

Basic Implementation​

// Component interface
public interface IComponent
{
string Name { get; }
void Add(IComponent component);
void Remove(IComponent component);
void Display(int depth);
}

// Leaf class
public class Leaf : IComponent
{
public string Name { get; }

public Leaf(string name)
{
Name = name;
}

public void Add(IComponent component)
{
Console.WriteLine("Cannot add to a leaf");
}

public void Remove(IComponent component)
{
Console.WriteLine("Cannot remove from a leaf");
}

public void Display(int depth)
{
Console.WriteLine(new string('-', depth) + Name);
}
}

// Composite class
public class Composite : IComponent
{
private List<IComponent> _children = new List<IComponent>();
public string Name { get; }

public Composite(string name)
{
Name = name;
}

public void Add(IComponent component)
{
_children.Add(component);
}

public void Remove(IComponent component)
{
_children.Remove(component);
}

public void Display(int depth)
{
Console.WriteLine(new string('-', depth) + Name);

foreach (var component in _children)
{
component.Display(depth + 2);
}
}
}

File System Example​

// Component interface
public interface IFileSystemItem
{
string Name { get; }
long GetSize();
void Print(string indent = "");
}

// Leaf - File
public class File : IFileSystemItem
{
public string Name { get; }
private long _size;

public File(string name, long size)
{
Name = name;
_size = size;
}

public long GetSize() => _size;

public void Print(string indent = "")
{
Console.WriteLine($"{indent}File: {Name}, Size: {GetSize()} bytes");
}
}

// Composite - Directory
public class Directory : IFileSystemItem
{
private List<IFileSystemItem> _children = new List<IFileSystemItem>();
public string Name { get; }

public Directory(string name)
{
Name = name;
}

public void Add(IFileSystemItem item)
{
_children.Add(item);
}

public void Remove(IFileSystemItem item)
{
_children.Remove(item);
}

public long GetSize()
{
long totalSize = 0;
foreach (var item in _children)
{
totalSize += item.GetSize();
}
return totalSize;
}

public void Print(string indent = "")
{
Console.WriteLine($"{indent}Directory: {Name}, Size: {GetSize()} bytes");
foreach (var item in _children)
{
item.Print(indent + " ");
}
}
}

When to Use​

  • When you want to represent part-whole hierarchies of objects
  • When you want clients to be able to ignore the difference between compositions of objects and individual objects
  • When the structure can have any level of complexity and is dynamic

9.3.4 - Decorator Pattern​

The Decorator pattern lets you attach new behaviors to objects by placing these objects inside special wrapper objects that contain the behaviors.

Basic Implementation​

// Component interface
public interface IComponent
{
string Operation();
}

// Concrete component
public class ConcreteComponent : IComponent
{
public string Operation()
{
return "ConcreteComponent";
}
}

// Base decorator
public abstract class Decorator : IComponent
{
protected IComponent _component;

public Decorator(IComponent component)
{
_component = component;
}

public virtual string Operation()
{
return _component.Operation();
}
}

// Concrete decorators
public class ConcreteDecoratorA : Decorator
{
public ConcreteDecoratorA(IComponent component) : base(component) { }

public override string Operation()
{
return $"ConcreteDecoratorA({base.Operation()})";
}
}

public class ConcreteDecoratorB : Decorator
{
public ConcreteDecoratorB(IComponent component) : base(component) { }

public override string Operation()
{
return $"ConcreteDecoratorB({base.Operation()})";
}
}

Text Formatting Example​

// Component interface
public interface IText
{
string GetText();
}

// Concrete component
public class PlainText : IText
{
private string _text;

public PlainText(string text)
{
_text = text;
}

public string GetText()
{
return _text;
}
}

// Base decorator
public abstract class TextDecorator : IText
{
protected IText _text;

public TextDecorator(IText text)
{
_text = text;
}

public virtual string GetText()
{
return _text.GetText();
}
}

// Concrete decorators
public class BoldText : TextDecorator
{
public BoldText(IText text) : base(text) { }

public override string GetText()
{
return $"<b>{base.GetText()}</b>";
}
}

public class ItalicText : TextDecorator
{
public ItalicText(IText text) : base(text) { }

public override string GetText()
{
return $"<i>{base.GetText()}</i>";
}
}

public class UnderlineText : TextDecorator
{
public UnderlineText(IText text) : base(text) { }

public override string GetText()
{
return $"<u>{base.GetText()}</u>";
}
}

When to Use​

  • When you need to add responsibilities to objects dynamically and transparently without affecting other objects
  • When extension by subclassing is impractical
  • When you want to add responsibilities to individual objects dynamically without affecting others
  • When you can't use inheritance to extend functionality

9.3.5 - Facade Pattern​

The Facade pattern provides a simplified interface to a complex subsystem of classes, making it easier to use.

Basic Implementation​

// Complex subsystem classes
public class SubsystemA
{
public string OperationA()
{
return "Subsystem A: Ready!\n";
}

public string OperationAMore()
{
return "Subsystem A: Go!\n";
}
}

public class SubsystemB
{
public string OperationB()
{
return "Subsystem B: Get ready!\n";
}

public string OperationBMore()
{
return "Subsystem B: Fire!\n";
}
}

public class SubsystemC
{
public string OperationC()
{
return "Subsystem C: Standby!\n";
}

public string OperationCMore()
{
return "Subsystem C: Launch!\n";
}
}

// Facade
public class Facade
{
protected SubsystemA _subsystemA;
protected SubsystemB _subsystemB;
protected SubsystemC _subsystemC;

public Facade(SubsystemA subsystemA, SubsystemB subsystemB, SubsystemC subsystemC)
{
_subsystemA = subsystemA;
_subsystemB = subsystemB;
_subsystemC = subsystemC;
}

// The Facade provides simple methods for complex operations
public string Operation()
{
string result = "Facade initializes subsystems:\n";
result += _subsystemA.OperationA();
result += _subsystemB.OperationB();
result += _subsystemC.OperationC();
result += "Facade orders subsystems to perform the action:\n";
result += _subsystemA.OperationAMore();
result += _subsystemB.OperationBMore();
result += _subsystemC.OperationCMore();
return result;
}
}

Home Theater Example​

// Subsystem components
public class Amplifier
{
public void On() => Console.WriteLine("Amplifier on");
public void Off() => Console.WriteLine("Amplifier off");
public void SetVolume(int level) => Console.WriteLine($"Setting volume to {level}");
}

public class DVDPlayer
{
public void On() => Console.WriteLine("DVD Player on");
public void Off() => Console.WriteLine("DVD Player off");
public void Play(string movie) => Console.WriteLine($"Playing \"{movie}\"");
public void Stop() => Console.WriteLine("Stopping DVD");
}

public class Projector
{
public void On() => Console.WriteLine("Projector on");
public void Off() => Console.WriteLine("Projector off");
public void SetInput(string input) => Console.WriteLine($"Setting input to {input}");
}

public class Lights
{
public void Dim(int level) => Console.WriteLine($"Dimming lights to {level}%");
public void On() => Console.WriteLine("Lights on");
}

public class Screen
{
public void Down() => Console.WriteLine("Screen going down");
public void Up() => Console.WriteLine("Screen going up");
}

// Facade
public class HomeTheaterFacade
{
private Amplifier _amplifier;
private DVDPlayer _dvdPlayer;
private Projector _projector;
private Lights _lights;
private Screen _screen;

public HomeTheaterFacade(
Amplifier amplifier,
DVDPlayer dvdPlayer,
Projector projector,
Lights lights,
Screen screen)
{
_amplifier = amplifier;
_dvdPlayer = dvdPlayer;
_projector = projector;
_lights = lights;
_screen = screen;
}

public void WatchMovie(string movie)
{
Console.WriteLine("Get ready to watch a movie...");
_lights.Dim(10);
_screen.Down();
_projector.On();
_projector.SetInput("DVD");
_amplifier.On();
_amplifier.SetVolume(5);
_dvdPlayer.On();
_dvdPlayer.Play(movie);
}

public void EndMovie()
{
Console.WriteLine("Shutting down the home theater...");
_dvdPlayer.Stop();
_dvdPlayer.Off();
_amplifier.Off();
_projector.Off();
_screen.Up();
_lights.On();
}
}

When to Use​

  • When you want to provide a simple interface to a complex subsystem
  • When there are many dependencies between clients and the implementation classes of an abstraction
  • When you want to layer your subsystems
  • When you want to decouple your client from a complex subsystem

9.3.6 - Flyweight Pattern​

The Flyweight pattern minimizes memory usage by sharing as much data as possible with similar objects. It's used when you need a large number of similar objects that would otherwise take up a lot of memory.

Basic Implementation​

// Flyweight interface
public interface IFlyweight
{
void Operation(string extrinsicState);
}

// Concrete flyweight
public class ConcreteFlyweight : IFlyweight
{
private readonly string _intrinsicState;

public ConcreteFlyweight(string intrinsicState)
{
_intrinsicState = intrinsicState;
}

public void Operation(string extrinsicState)
{
Console.WriteLine($"ConcreteFlyweight: Intrinsic={_intrinsicState}, Extrinsic={extrinsicState}");
}
}

// Unshared concrete flyweight
public class UnsharedConcreteFlyweight : IFlyweight
{
private readonly string _allState;

public UnsharedConcreteFlyweight(string allState)
{
_allState = allState;
}

public void Operation(string extrinsicState)
{
Console.WriteLine($"UnsharedConcreteFlyweight: {_allState}, Extrinsic={extrinsicState}");
}
}

// Flyweight factory
public class FlyweightFactory
{
private Dictionary<string, IFlyweight> _flyweights = new Dictionary<string, IFlyweight>();

public FlyweightFactory()
{
_flyweights.Add("X", new ConcreteFlyweight("X"));
_flyweights.Add("Y", new ConcreteFlyweight("Y"));
_flyweights.Add("Z", new ConcreteFlyweight("Z"));
}

public IFlyweight GetFlyweight(string key)
{
if (!_flyweights.ContainsKey(key))
{
Console.WriteLine($"FlyweightFactory: Can't find a flyweight with key {key}, creating new one");
_flyweights.Add(key, new ConcreteFlyweight(key));
}
else
{
Console.WriteLine($"FlyweightFactory: Reusing existing flyweight with key {key}");
}

return _flyweights[key];
}

public int GetFlyweightCount()
{
return _flyweights.Count;
}
}

Text Formatting Example​

// Character attributes (intrinsic state)
public class CharacterStyle
{
public string FontFamily { get; }
public int FontSize { get; }
public bool IsBold { get; }
public bool IsItalic { get; }

public CharacterStyle(string fontFamily, int fontSize, bool isBold, bool isItalic)
{
FontFamily = fontFamily;
FontSize = fontSize;
IsBold = isBold;
IsItalic = isItalic;
}

public override bool Equals(object obj)
{
if (!(obj is CharacterStyle other))
return false;

return FontFamily == other.FontFamily &&
FontSize == other.FontSize &&
IsBold == other.IsBold &&
IsItalic == other.IsItalic;
}

public override int GetHashCode()
{
return HashCode.Combine(FontFamily, FontSize, IsBold, IsItalic);
}
}

// Flyweight factory
public class StyleManager
{
private Dictionary<CharacterStyle, CharacterStyle> _styles = new Dictionary<CharacterStyle, CharacterStyle>();

public CharacterStyle GetStyle(string fontFamily, int fontSize, bool isBold, bool isItalic)
{
var style = new CharacterStyle(fontFamily, fontSize, isBold, isItalic);

if (!_styles.TryGetValue(style, out var existingStyle))
{
_styles[style] = style;
return style;
}

return existingStyle;
}

public int StyleCount => _styles.Count;
}

// Character (extrinsic state + reference to flyweight)
public class Character
{
public char Value { get; }
public int X { get; }
public int Y { get; }
public CharacterStyle Style { get; }

public Character(char value, int x, int y, CharacterStyle style)
{
Value = value;
X = x;
Y = y;
Style = style;
}

public void Draw()
{
Console.WriteLine($"Character '{Value}' at ({X},{Y}) with {Style.FontFamily}, {Style.FontSize}pt, " +
$"Bold: {Style.IsBold}, Italic: {Style.IsItalic}");
}
}

// Document that uses characters
public class TextDocument
{
private List<Character> _characters = new List<Character>();
private StyleManager _styleManager = new StyleManager();

public void AddCharacter(char value, int x, int y, string fontFamily, int fontSize, bool isBold, bool isItalic)
{
var style = _styleManager.GetStyle(fontFamily, fontSize, isBold, isItalic);
_characters.Add(new Character(value, x, y, style));
}

public void Draw()
{
foreach (var character in _characters)
{
character.Draw();
}
}

public int StyleCount => _styleManager.StyleCount;
public int CharacterCount => _characters.Count;
}

When to Use​

  • When an application uses a large number of objects that have some shared state among them
  • When the majority of each object's state can be made extrinsic (outside the object)
  • When removing the extrinsic state would allow you to replace many groups of similar objects with relatively few shared objects
  • When the application doesn't depend on object identity

9.3.7 - Proxy Pattern​

The Proxy pattern provides a surrogate or placeholder for another object to control access to it. It creates a representative object that controls access to another object, which may be remote, expensive to create, or in need of securing.

Basic Implementation​

// Subject interface
public interface ISubject
{
void Request();
}

// Real subject
public class RealSubject : ISubject
{
public void Request()
{
Console.WriteLine("RealSubject: Handling Request");
}
}

// Proxy
public class Proxy : ISubject
{
private RealSubject _realSubject;

public Proxy(RealSubject realSubject)
{
_realSubject = realSubject;
}

public void Request()
{
if (CheckAccess())
{
_realSubject.Request();
LogAccess();
}
}

private bool CheckAccess()
{
Console.WriteLine("Proxy: Checking access prior to firing a real request.");
return true;
}

private void LogAccess()
{
Console.WriteLine("Proxy: Logging the time of request.");
}
}

Types of Proxies​

Virtual Proxy (Lazy Loading)​

public interface IImage
{
void Display();
}

public class RealImage : IImage
{
private string _filename;

public RealImage(string filename)
{
_filename = filename;
LoadFromDisk();
}

private void LoadFromDisk()
{
Console.WriteLine($"Loading image: {_filename}");
// Expensive operation simulated
Thread.Sleep(1000);
}

public void Display()
{
Console.WriteLine($"Displaying image: {_filename}");
}
}

public class ProxyImage : IImage
{
private RealImage _realImage;
private string _filename;

public ProxyImage(string filename)
{
_filename = filename;
}

public void Display()
{
if (_realImage == null)
{
_realImage = new RealImage(_filename);
}

_realImage.Display();
}
}

Protection Proxy​

public interface IDocument
{
void Open(string user);
void Edit(string user);
void Save(string user);
}

public class Document : IDocument
{
private string _content;
private string _name;

public Document(string name)
{
_name = name;
_content = "Default content";
}

public void Open(string user)
{
Console.WriteLine($"{user} opened document {_name}");
}

public void Edit(string user)
{
Console.WriteLine($"{user} edited document {_name}");
}

public void Save(string user)
{
Console.WriteLine($"{user} saved document {_name}");
}
}

public class DocumentProxy : IDocument
{
private Document _document;
private string _name;
private List<string> _allowedUsers;

public DocumentProxy(string name, List<string> allowedUsers)
{
_name = name;
_allowedUsers = allowedUsers;
}

private bool CheckAccess(string user)
{
return _allowedUsers.Contains(user);
}

public void Open(string user)
{
if (_document == null)
{
_document = new Document(_name);
}

_document.Open(user);
}

public void Edit(string user)
{
if (CheckAccess(user))
{
if (_document == null)
{
_document = new Document(_name);
}

_document.Edit(user);
}
else
{
Console.WriteLine($"Access denied: {user} cannot edit document {_name}");
}
}

public void Save(string user)
{
if (CheckAccess(user))
{
if (_document == null)
{
_document = new Document(_name);
}

_document.Save(user);
}
else
{
Console.WriteLine($"Access denied: {user} cannot save document {_name}");
}
}
}

When to Use​

  • When you need a more versatile or sophisticated reference to an object than a simple pointer
  • Remote Proxy: When the real object is in a different address space
  • Virtual Proxy: When creating an object is expensive and it's not needed right away
  • Protection Proxy: When you want to control access to the original object
  • Smart Reference: When you need to perform additional actions when an object is accessed

9.3.8 - Understanding Structural Patterns: A Beginner's Guide​

Structural patterns help us organize classes and objects to form larger structures while keeping them flexible and efficient. Let's break down these patterns in a way that's easy to understand for beginners.

The Purpose of Structural Patterns​

Imagine you're building with LEGO blocks. Structural patterns are like different techniques for connecting those blocks to create larger, more complex structures. They help you:

  1. Connect incompatible pieces: Like using an adapter piece to connect different types of blocks
  2. Create flexible structures: So you can change parts without rebuilding everything
  3. Organize complex systems: By grouping related pieces together
  4. Optimize resource usage: By sharing common components

When to Use Each Structural Pattern​

  1. Adapter Pattern

    • Use when: You need to make existing classes work with others without modifying their source code
    • Real-world analogy: A power adapter that lets you plug a US device into a European outlet
    • Key benefit: Allows incompatible interfaces to work together
    • Example scenario: Integrating a legacy system with a new application
  2. Bridge Pattern

    • Use when: You want to separate an abstraction from its implementation so both can vary independently
    • Real-world analogy: A remote control (abstraction) that works with different devices (implementations)
    • Key benefit: Prevents an explosion of classes when you have multiple variations of multiple concepts
    • Example scenario: Creating UI components that work across different operating systems
  3. Composite Pattern

    • Use when: You want to treat individual objects and compositions of objects uniformly
    • Real-world analogy: A file system with files and folders, where both can be "items"
    • Key benefit: Clients can treat individual objects and compositions the same way
    • Example scenario: Building a menu system with items and submenus
  4. Decorator Pattern

    • Use when: You need to add responsibilities to objects dynamically without affecting other objects
    • Real-world analogy: Adding toppings to a pizza without changing the base pizza
    • Key benefit: More flexible than inheritance for extending functionality
    • Example scenario: Adding formatting options to a text component (bold, italic, etc.)
  5. Facade Pattern

    • Use when: You want to provide a simplified interface to a complex subsystem
    • Real-world analogy: A car dashboard that provides a simple interface to the complex engine systems
    • Key benefit: Reduces complexity for clients and decouples them from subsystem components
    • Example scenario: Creating a simplified API for a complex library
  6. Flyweight Pattern

    • Use when: You need to support a large number of fine-grained objects efficiently
    • Real-world analogy: A character library in a word processor that reuses the same letter objects
    • Key benefit: Reduces memory usage by sharing common state between multiple objects
    • Example scenario: Rendering thousands of similar objects in a game or simulation
  7. Proxy Pattern

    • Use when: You need to control access to an object, or defer its full creation
    • Real-world analogy: A security guard that controls access to a building
    • Key benefit: Provides a surrogate or placeholder for another object to control access to it
    • Example scenario: Implementing access control, lazy loading, or logging

Common Misconceptions​

  1. "Structural patterns are only about class hierarchies"

    • While some structural patterns do involve inheritance, many focus on object composition.
  2. "I should always use the most complex pattern"

    • Start with the simplest solution. Only use a pattern when it clearly solves a problem you have.
  3. "These patterns are only for large systems"

    • Even small applications can benefit from well-applied structural patterns.

Practical Implementation Tips​

  1. For Adapter:

    • Consider using composition over inheritance when possible.
    • Document the differences between the interfaces being adapted.
  2. For Bridge:

    • Identify the dimensions that can vary independently before implementing.
    • Start with a clear abstraction and implementation hierarchy.
  3. For Composite:

    • Decide whether to make the component interface simple (focus on leaf operations) or powerful (include child management).
    • Consider safety vs. transparency trade-offs.
  4. For Decorator:

    • Keep decorators lightweight; focus on adding behavior, not state.
    • Consider using the builder pattern to construct complex decorated objects.
  5. For Facade:

    • Don't make facades too smart; they should delegate to subsystem components.
    • Consider providing access to subsystem components for advanced clients.
  6. For Flyweight:

    • Clearly separate intrinsic (shared) and extrinsic (context-specific) state.
    • Use a factory to manage flyweight objects.
  7. For Proxy:

    • Ensure the proxy and real subject have identical interfaces.
    • Consider using dependency injection to provide the real subject to the proxy.

Choosing Between Structural Patterns​

Sometimes it can be difficult to choose which structural pattern to use. Here's a simple decision guide:

  • If you need to make incompatible interfaces work together: Adapter
  • If you need to separate an abstraction from its implementation: Bridge
  • If you need to build tree-like object structures: Composite
  • If you need to add responsibilities to objects dynamically: Decorator
  • If you need to simplify a complex subsystem: Facade
  • If you need to efficiently share fine-grained objects: Flyweight
  • If you need to control access to an object: Proxy

Remember, design patterns are tools, not rules. Use them when they solve a problem, not just because they exist.