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.