Software Design Patterns: Structural

This post continues my series on the Gang of Four design patterns, following on from the creational patterns. All of the code samples come from my accompanying GitHub repository, where each pattern is a standalone console application you can clone, run, and debug through:
Structural patterns are concerned with how classes and objects are composed to form larger structures. Where creational patterns focus on how things are built, structural patterns focus on how things fit together – simplifying relationships between entities, decoupling abstractions from implementations, and letting behaviour and composition vary independently.
The classic GoF catalogue contains seven structural patterns: Adapter, Bridge, Composite, Decorator, Fçcade, Flyweight, and Proxy. This post also covers the Marker and Repository patterns. Whilst these aren’t part of the GoF classification, they were included in the Pluralsight course this series is based on, and they’re both patterns worth knowing.
- Adapter pattern
- Bridge pattern
- Composite pattern
- Decorator pattern
- Façade pattern
- Flyweight pattern
- Marker pattern
- Proxy pattern
- Repository pattern
Adapter pattern
A concept familiar to most people is the use of a power adapter to connect a device to a power source. The adapter acts as a bridge between the device and the power source, allowing the device to be used with a power source other than the one it was originally designed for.
Software adapters do exactly the same thing: they translate one interface into another that a client expects, letting incompatible classes work together without modifying either side.
In this example we have:
- Two power plugs:
AmericanPowerPlug,UkPowerPlug - Two power plug adapters:
AmericanPowerPlugAdapter,UkPowerPlugAdapter - Two pin types:
FlatPin,RectangularPin
The power plugs are the devices that need to be connected to a power source, while the power adapters are the adapters that connect the power plugs to the power source. The pin types are the different types of pins that the power plugs and power adapters have.
In both cases the power plugs need to be connected to a power source with a different pin type than they were originally designed for - an AustralianPlugSocket - which has a single PlugIn() method that takes an IPowerPlugAdapter interface:
public class AustralianPlugSocket
{
public void PlugIn(IPowerPlugAdapter powerPlugAdapter)
{
Electricity electricity = new Electricity();
powerPlugAdapter.SupplyPower(electricity);
}
}Both the AmericanPowerPlugAdapter and UkPowerPlugAdapter classes implement the IPowerPlugAdapter interface, which has a single SupplyPower() method that takes an Electricity object:
// AmericanPowerPlugAdapter
public class AmericanPowerPlugAdapter : IPowerPlugAdapter
{
private readonly IAmericanPowerPlug _americanPowerPlug;
public AmericanPowerPlugAdapter(IAmericanPowerPlug americanPowerPlug)
{
_americanPowerPlug = americanPowerPlug;
}
public void SupplyPower(Electricity electricity)
{
FlatPin flatPin1 = new FlatPin();
FlatPin flatPin2 = new FlatPin();
_americanPowerPlug.SupplyPower(electricity, flatPin1, flatPin2);
}
}
// UkPowerPlugAdapter
public class UkPowerPlugAdapter : IPowerPlugAdapter
{
private readonly IUkPowerPlug _ukPowerPlug;
public UkPowerPlugAdapter(IUkPowerPlug ukPowerPlug)
{
_ukPowerPlug = ukPowerPlug;
}
public void SupplyPower(Electricity electricity)
{
RectangularPin rectangularPin1 = new RectangularPin();
RectangularPin rectangularPin2 = new RectangularPin();
RectangularPin rectangularPin3 = new RectangularPin();
_ukPowerPlug.SupplyPower(electricity, rectangularPin1, rectangularPin2, rectangularPin3);
}
}To plug in the power plugs, we need to create an instance of AmericanPowerPlugAdapter and pass an AmericanPowerPlug to the constructor:
AmericanPowerPlug americanPowerPlug = new AmericanPowerPlug();
AmericanPowerPlugAdapter americanPowerPlugAdapter = new AmericanPowerPlugAdapter(americanPowerPlug);
australianPlugSocket.PlugIn(americanPowerPlugAdapter);Conversely, for the UkPowerPlugAdapter we need to create an instance of UkPowerPlugAdapter and pass a UkPowerPlug to the constructor:
UkPowerPlug ukPowerPlug = new UkPowerPlug();
UkPowerPlugAdapter ukPowerPlugAdapter = new UkPowerPlugAdapter(ukPowerPlug);
australianPlugSocket.PlugIn(ukPowerPlugAdapter);Both adapter objects are then passed into the AustralianPlugSocket instance via the PlugIn() method, which internally creates an Electricity object and passes it to the SupplyPower() method of the IPowerPlugAdapter implementation. The adapters are then able to use the Electricity object, along with any other objects they require – such as RectangularPin or FlatPin instances - before passing everything on to the respective AmericanPowerPlug and UkPowerPlug instances.
Benefits of the Adapter pattern:
- Interface compatibility – allows existing classes to work together even when their interfaces are incompatible
- Open for extension, closed for modification - keeps the original class untouched, meaning no changes to working, tested code
- Single point of translation – localises conversion logic in one place rather than scattering it across client code
- Third-party insulation – makes external or legacy libraries easy to incorporate behind an interface you control
If you’ve ever dragged a travel adapter across continents, you already understand the Adapter pattern – it’s a thin translation layer that lets two things that were never designed for each other cooperate cleanly.
Bridge pattern
The Bridge pattern decouples an abstraction from its implementation so that the two can vary independently. It separates responsibility for the implementation from the abstraction and allows an abstraction to be connected to an implementation at runtime.
Rather than inheritance exploding into a class per combination (think FancyBook, BackwardsMagazine, StandardNewspaper…), the Bridge splits the axes of variation into separate hierarchies joined by composition.
In the example there exists an IFormatter interface that simply takes a key and a value string:
public interface IFormatter
{
string Format(string key, string value);
}To demonstrate the pattern in action, three different types of classes are implemented: BackwardsFormatter, FancyFormatter and StandardFormatter. Each class implements the IFormatter interface and provides its own implementation for formatting a key-value pair:
// BackwardsFormatter
public class BackwardsFormatter : IFormatter
{
public string Format(string key, string value)
{
return string.Format("{0}: {1}", key, new string(value.Reverse().ToArray()));
}
}
// FancyFormatter
public class FancyFormatter : IFormatter
{
public string Format(string key, string value)
{
return string.Format("-> {0} >>> {1}", key, value);
}
}
// StandardFormatter
public class StandardFormatter : IFormatter
{
public string Format(string key, string value)
{
return string.Format("{0}: {1}", key, value);
}
}An abstract class Publication defines an abstract Print() method and takes an IFormatter implementation as a constructor parameter:
public abstract class Publication
{
protected Publication(IFormatter formatter)
{
Formatter = formatter;
}
protected IFormatter Formatter { get; private set; }
public abstract void Print();
}Three classes inherit from the Publication base class - Book, Magazine and Newspaper - each using the IFormatter instance to output their contents:
// Book
public class Book : Publication
{
public Book(IFormatter formatter)
: base(formatter)
{
}
public string Title { get; set; }
public string Author { get; set; }
public string Text { get; set; }
public override void Print()
{
Console.WriteLine(Formatter.Format("Title", Title));
Console.WriteLine(Formatter.Format("Author", Author));
Console.WriteLine(Formatter.Format("Text", Text));
Console.WriteLine();
}
}
// Magazine
public class Magazine : Publication
{
public Magazine(IFormatter formatter)
: base(formatter)
{
}
public string Name { get; set; }
public string PrintDate { get; set; }
public string CoverHeadline { get; set; }
public override void Print()
{
Console.WriteLine(Formatter.Format("Name", Name));
Console.WriteLine(Formatter.Format("Print Date", PrintDate));
Console.WriteLine(Formatter.Format("Cover Headline", CoverHeadline));
Console.WriteLine();
}
}
// Newspaper
public class Newspaper : Publication
{
public Newspaper(IFormatter formatter)
: base(formatter)
{
Articles = new Dictionary<string, string>();
}
public string Title { get; set; }
public Dictionary<string, string> Articles { get; set; }
public override void Print()
{
Console.WriteLine(Formatter.Format("Title", Title));
foreach (var article in Articles)
{
Console.WriteLine(Formatter.Format(" Heading", article.Key));
Console.WriteLine(Formatter.Format(" Author", article.Value));
}
Console.WriteLine();
}
}These classes are then instantiated with the corresponding IFormatter instance as they are created:
private static void Print(IFormatter formatter)
{
List<Publication> documents = new List<Publication>();
var newspaper = new Newspaper(formatter);
newspaper.Title = "The Publicist";
newspaper.Articles.Add("Sugar linked to bad eyesight", "Rod Sugar");
newspaper.Articles.Add("Sweden bans chocolate", "Willy Wonka");
newspaper.Articles.Add("Opera house to be painted orange", "Orange Arup");
documents.Add(newspaper);
var book = new Book(formatter)
{
Title = "Price of Silence",
Author = "Jay and Silent Bob",
Text = "Blah-de-blah-de-blah..."
};
documents.Add(book);
var magazine = new Magazine(formatter)
{
Name = "MixMag",
PrintDate = "30/08/1993",
CoverHeadline = "Downloads outstrip CD sales"
};
documents.Add(magazine);
foreach (var doc in documents)
{
doc.Print();
}
}
private static void PrintWithStandardFormatter()
{
Console.WriteLine("-> Start Standard Formatter <-");
Print(new StandardFormatter());
Console.WriteLine("-> End Standard Formatter <-");
Console.WriteLine();
}
private static void PrintWithBackwardsFormatter()
{
Console.WriteLine("-> Start Backwards Formatter <-");
Print(new BackwardsFormatter());
Console.WriteLine("-> End Backwards Formatter <-");
Console.WriteLine();
}
private static void PrintWithFancyFormatter()
{
Console.WriteLine("-> Start Fancy Formatter <-");
Print(new FancyFormatter());
Console.WriteLine("-> End Fancy Formatter <-");
Console.WriteLine();
}Benefits of the Bridge pattern:
- Independent variation - decouples abstractions from implementations so each can evolve on its own timeline
- Combinatorial explosion avoided – collapses N subclasses per axis into a flat structure (3 publications × N formatters becomes 3 + N, not 3 × N)
- Runtime flexibility – allows implementations to be swapped at runtime rather than fixed at compile time
- Focused hierarchies – keeps each class hierarchy responsible for a single axis of change
The Bridge pattern is essentially composition doing the heavy lifting that inheritance was never well suited to – it buys you flexibility on both sides of the divide.
Composite pattern
This pattern is probably the most widely used and easiest to understand, as it is commonly used without really realising it. It lets you compose objects into tree structures and treat individual objects and compositions of objects uniformly – a client that works with a leaf works exactly the same way with a whole subtree.
In this example an IParty interface defines a Gold property and a Stats() method:
public interface IParty
{
int Gold { get; set; }
void Stats();
}A Person class then implements this interface as follows:
public class Person : IParty
{
public string Name { get; set; }
public int Gold { get; set; }
public void Stats()
{
Console.WriteLine("{0} has {1} gold coins.", Name, Gold);
}
}A Group class then also implements the IParty interface with its own implementation:
public class Group : IParty
{
public Group()
{
Members = new List<IParty>();
}
public string Name { get; set; }
public List<IParty> Members { get; set; }
public int Gold
{
get
{
return Members.Sum(member => member.Gold);
}
set
{
var eachSplit = value / Members.Count;
var leftOver = value % Members.Count;
foreach (var member in Members)
{
member.Gold += (eachSplit + leftOver);
leftOver = 0;
}
}
}
public void Stats()
{
foreach (var member in Members)
{
member.Stats();
}
}
}We can then instantiate a set of Person instances and add them to the Members collection of the Group class:
int goldForKill = 120000;
Console.WriteLine("The monster has been slain! You earned {0} gold!", goldForKill);
var virion = new Person { Name = "Virion Scourgeglare" };
var zehaden = new Person { Name = "Zehaden Leafreaver" };
var bommink = new Person { Name = "Bommink Spannergear" };
var miloraen = new Person { Name = "Miloraen Greenwater" };
var dong = new Person { Name = "Dong Thunderflower" };
var bramren = new Person { Name = "Bramren Frostmane" };
var towne = new Person { Name = "Towne Thackeray" };
var stanmore = new Person { Name = "Stanmore Brent" };
var humans = new Group { Members = { towne, stanmore } };
var alliance = new Group { Name = "Alliance", Members = { bommink, miloraen, dong, bramren, humans } };
var raidGroup = new Group { Members = { alliance, virion, zehaden } };
raidGroup.Gold += goldForKill;
raidGroup.Stats();Calling the Stats() method on the raidGroup instance then iterates through each IParty member and calls the Stats() method on that implementation - which, for nested Group instances, recurses into their own members, and so on.
Benefits of the Composite pattern:
- Uniform treatment – treats individual objects and groups of objects identically, simplifying client code
- Recursive structure - makes it trivial to build arbitrarily deep, nested hierarchies
- Open extension – adding new leaf or composite types requires no changes to existing consumers
- Encapsulated aggregation - pushes operations like totalling or distributing gold inside the structure itself
The Composite pattern’s real elegance is that the client never needs to know whether it’s holding a single item or an entire tree – the shape of the data becomes invisible to the code using it.
Decorator pattern
One of the easiest ways to describe the Decorator pattern is to think of a cake that has different toppings applied to it. In object-oriented programming, this allows behaviour to be added to an individual object dynamically – in this case, our cake – without affecting the behaviour of other instances of the same class.
In this example we have an abstract Cake base class that has a Description property and two abstract methods:
public abstract class Cake
{
public string Description { get; set; }
public abstract string GetDescription();
public abstract double CalculateCost();
}Three classes - SmallCake, MediumCake and LargeCake - all inherit from the Cake base class and have their own concrete implementations of these methods:
// SmallCake
public class SmallCake : Cake
{
public SmallCake()
{
Description = "Small Cake";
}
public override string GetDescription()
{
return Description;
}
public override double CalculateCost()
{
return 5.00;
}
}
// MediumCake
public class MediumCake : Cake
{
public MediumCake()
{
Description = "Medium Cake";
}
public override string GetDescription()
{
return Description;
}
public override double CalculateCost()
{
return 8.00;
}
}
// LargeCake
public class LargeCake : Cake
{
public LargeCake()
{
Description = "Large Cake";
}
public override string GetDescription()
{
return Description;
}
public override double CalculateCost()
{
return 10.00;
}
}A CakeDecorator class then provides the base functionality for toppings:
public class CakeDecorator : Cake
{
protected Cake _cake;
public CakeDecorator(Cake cake)
{
_cake = cake;
}
public override string GetDescription()
{
return _cake.Description;
}
public override double CalculateCost()
{
return _cake.CalculateCost();
}
}Each topping - Icing, Cherries and ChocolateChips - changes the behaviour of a Cake instance without affecting other instances:
// Icing
public class Icing : CakeDecorator
{
public Icing(Cake cake)
: base(cake)
{
Description = "Icing";
}
public override string GetDescription()
{
return string.Format("{0}, {1}", _cake.GetDescription(), Description);
}
public override double CalculateCost()
{
return _cake.CalculateCost() + 1.25;
}
}
// Cherries
public class Cherries : CakeDecorator
{
public Cherries(Cake cake)
: base(cake)
{
Description = "Cherries";
}
public override string GetDescription()
{
return string.Format("{0}, {1}", _cake.GetDescription(), Description);
}
public override double CalculateCost()
{
return _cake.CalculateCost() + 1.00;
}
}
// ChocolateChips
public class ChocolateChips : CakeDecorator
{
public ChocolateChips(Cake cake)
: base(cake)
{
Description = "Chocolate Chips";
}
public override string GetDescription()
{
return string.Format("{0}, {1}", _cake.GetDescription(), Description);
}
public override double CalculateCost()
{
return _cake.CalculateCost() + 2.00;
}
}After instantiation of a Cake object, the behaviour is changed by ‘wrapping’ the instance using decorators:
private static void BuildLargeCake()
{
Cake largeCake = new LargeCake();
largeCake = new Icing(largeCake);
largeCake = new Cherries(largeCake);
largeCake = new ChocolateChips(largeCake);
Console.WriteLine(largeCake.GetDescription()); // "Large Cake, Icing, Cherries, Chocolate Chips"
Console.WriteLine("{0:C2}", largeCake.CalculateCost()); // "$14.25"
}
private static void BuildMediumCake()
{
Cake mediumCake = new MediumCake();
mediumCake = new Icing(mediumCake);
mediumCake = new Cherries(mediumCake);
Console.WriteLine(mediumCake.GetDescription()); // "Medium Cake, Icing, Cherries"
Console.WriteLine("{0:C2}", mediumCake.CalculateCost()); // "$10.25"
}
private static void BuildSmallCake()
{
Cake smallCake = new SmallCake();
smallCake = new Icing(smallCake);
Console.WriteLine(smallCake.GetDescription()); // "Small Cake, Icing"
Console.WriteLine("{0:C2}", smallCake.CalculateCost()); // "$6.25"
}Benefits of the Decorator pattern:
- Runtime enhancement – adds behaviour to individual objects at runtime without any subclassing
- Reusability – keeps each decoration small, focused and independently reusable
- Composability – combines decorations in any order and quantity, one class per feature rather than one per combination
- Interface preservation – maintains the decorated object’s interface, so decorated and undecorated instances are interchangeable
If you find yourself considering an explosion of subclasses like IcedCake, IcedCakeWithCherries, and IcedCakeWithCherriesAndChocolateChips, the Decorator pattern is telling you it’s time to wrap instead of inherit.
Façade pattern
The word ‘façade’ is a loanword from the French façade, meaning “frontage” or “face”. The Façade pattern provides a single, simplified interface over a set of more complex subsystems – the client talks to the façade, and the façade orchestrates the subsystems behind it.
Where the Composite pattern makes a group of objects behave like one object, the Façade pattern makes a complicated workflow appear simple by hiding it behind one entry point.
In the example code this is represented by a ComputerFacade that internally instantiates the classes it depends on:
public class ComputerFacade
{
private readonly Processor _processor;
private readonly Memory _memory;
private readonly HardDrive _hardDrive;
public ComputerFacade()
{
_processor = new Processor();
_memory = new Memory();
_hardDrive = new HardDrive();
}
public void Start()
{
_processor.Start();
_memory.Allocate();
_hardDrive.Spin();
}
}Each dependency has its own instance that is created by the ComputerFacade class. When the Start() method is called, the respective methods on the dependencies are invoked:
// Processor
public class Processor
{
public void Start()
{
Console.WriteLine("75MHz of pure processing power ready");
}
}
// Memory
public class Memory
{
public void Allocate()
{
Console.WriteLine("A beastly 640k has been allocated - ought to be enough for anyone");
}
}
// HardDrive
public class HardDrive
{
public void Spin()
{
Console.WriteLine("Hard drives spinning at a rapid 10RPM");
}
}After instantiating the ComputerFacade class, all dependent classes are created and the Start() method is invoked:
var computerFacade = new ComputerFacade();
computerFacade.Start();Benefits of the Façade pattern:
- Simplified interface – presents a single entry point over complex subsystem interactions
- Reduced coupling – shields clients from the internal details of the subsystems they use
- Coordination point - gives you one sensible place to orchestrate multi-step operations
- Refactoring shelter - protects client code from subsystem churn, since internals can change without breaking callers
Every time you’ve started a computer with one button rather than individually powering the processor, memory, and hard drive, you’ve benefited from a façade.
Flyweight pattern
The Flyweight pattern is a structural design pattern that aims to minimise memory usage by sharing as much data as possible with other similar objects. It is particularly useful when dealing with a large number of objects that have a significant amount of shared state.
The trick is separating intrinsic state (shared, immutable, stored once) from extrinsic state (unique, supplied by the client at call time). In this example, the coffee flavour is intrinsic – the table number is extrinsic.
In this example there is a CoffeeShop class that has an internal list of Order instances:
public class CoffeeShop
{
private readonly List<Order> _orders = new List<Order>();
private readonly Menu _menu = new Menu();
public void TakeOrder(string flavourName, int table)
{
CoffeeFlavour coffeeFlavour = _menu.Lookup(flavourName);
Order order = new Order(table, coffeeFlavour);
_orders.Add(order);
}
public void Service()
{
foreach (Order order in _orders)
{
order.Serve();
}
}
public string Report()
{
return string.Format("Total CoffeeFlavour objects made: {0}", _menu.TotalCoffeeFlavoursMade);
}
}The Order class itself simply holds a table number and a CoffeeFlavour:
public class Order
{
private readonly int _tableNumber;
private readonly CoffeeFlavour _flavour;
public Order(int tableNumber, CoffeeFlavour flavour)
{
_tableNumber = tableNumber;
_flavour = flavour;
}
public void Serve()
{
Console.WriteLine("Serving {0} to table {1}", _flavour, _tableNumber);
}
}The CoffeeFlavour class has a name which is returned from the overridden ToString() method:
public class CoffeeFlavour
{
private readonly string _name;
public CoffeeFlavour(string name)
{
_name = name;
}
public override string ToString()
{
return _name;
}
}However, rather than instantiating a new CoffeeFlavour instance every time an order is created, the real magic happens inside the Menu class, which has an internal lookup for each CoffeeFlavour keyed by the name:
public class Menu
{
private readonly Dictionary<string, CoffeeFlavour> _flavours = new Dictionary<string, CoffeeFlavour>();
public CoffeeFlavour Lookup(string flavourName)
{
if (!_flavours.ContainsKey(flavourName))
{
_flavours.Add(flavourName, new CoffeeFlavour(flavourName));
}
return _flavours[flavourName];
}
public int TotalCoffeeFlavoursMade
{
get
{
return _flavours.Count;
}
}
}Having this Dictionary<TKey, TValue> lookup prevents multiple instances of the CoffeeFlavour class from being created, as they can simply be referenced when the CoffeeShop.TakeOrder() method is called:
CoffeeShop shop = new CoffeeShop();
shop.TakeOrder("Cappuccino", 2);
shop.TakeOrder("Frappe", 1);
shop.TakeOrder("Espresso", 1);
shop.TakeOrder("Frappe", 897);
shop.TakeOrder("Cappuccino", 97);
shop.TakeOrder("Frappe", 3);
shop.TakeOrder("Espresso", 3);
shop.TakeOrder("Cappuccino", 3);
shop.TakeOrder("Espresso", 96);
shop.TakeOrder("Frappe", 552);
shop.TakeOrder("Cappuccino", 121);
shop.TakeOrder("Espresso", 121);
shop.Service();
Console.WriteLine(shop.Report());Twelve orders are taken, yet the report shows only three CoffeeFlavour objects were ever created – one each for Cappuccino, Frappe, and Espresso.
Benefits of the Flyweight pattern:
- Memory efficiency – dramatically reduces memory footprint when many objects share common state
- Centralised caching – concentrates the creation and caching of shared objects in one place
- Scalability – scales gracefully, as a thousand orders for the same drink cost barely more than one
- Lightweight instances – keeps per-instance state small by externalising whatever isn’t truly unique
The Flyweight pattern is essentially caching applied to object identity – if you’re creating millions of objects that only differ in a few fields, share what you can and pass in the rest.
Marker pattern
The Marker pattern is probably one of the most straightforward patterns of all – so much so that it isn’t part of the GoF catalogue, but it earns its place alongside them. By using an empty interface we can ‘mark’ an object by implementing that interface:
public interface IHuman
{
}The implementation of a Dog class does not have this interface, whereas the implementation of the Person class does:
// Dog
public class Dog
{
public string Name { get; set; }
public string Gender { get; set; }
public string Breed { get; set; }
public override string ToString()
{
return string.Format("{0} - {1} ({2})", Name, Breed, Gender);
}
}
// Person
public class Person : IHuman // Has marker interface
{
public string Name { get; set; }
public string Gender { get; set; }
public override string ToString()
{
return string.Format("{0} ({1})", Name, Gender);
}
}We can then distinguish between these two object instances by checking for the existence of the marker interface on the object instance itself:
private static void PrintIsHuman(object entity)
{
if (entity is IHuman)
{
Console.WriteLine("Entity '{0}' is human", entity);
}
else
{
Console.WriteLine("Entity '{0}' is not human", entity);
}
}Benefits of the Marker pattern:
- Zero-cost metadata - conveys information about a type without adding any behaviour or state
- Filtering capability – enables type checking and selection with no API surface whatsoever
- Framework integration – works naturally with generic constraints and framework scanning
- Explicit intent – communicates meaning directly in the type system where tooling can see it
Markers are a classic example of the pattern family ISerializable belongs to - sometimes the most powerful interface is the one that contains nothing at all.
Proxy pattern
The Proxy pattern allows a proxy class to act as a stand-in for another object, exposing the same interface so that client code cannot tell the difference. This allows for controlling access and adding behaviour while forwarding the request to the real object – essentially, the proxy decides whether the call should reach its target.
In this example we have a Car class that defines a DriveCar() method:
public class Car : ICar
{
public void DriveCar()
{
Console.WriteLine("Car has been driven!");
}
}A ProxyCar class has the same DriveCar() method but with a Driver object instance passed in via the constructor. Before the DriveCar() method is called, the Age property of the Driver class is checked to ensure the driver is of age to drive the car:
public class ProxyCar : ICar
{
private readonly Driver _driver;
private readonly ICar _car;
public ProxyCar(Driver driver)
{
_driver = driver;
_car = new Car();
}
public void DriveCar()
{
if (_driver.Age <= 18)
{
Console.WriteLine("You are too young to drive!");
}
else
{
_car.DriveCar();
}
}
}Both of these classes implement an ICar interface so they expose the same functionality:
public interface ICar
{
void DriveCar();
}When the Car class is instantiated without the age check, the calling code can drive the car without any age restrictions:
var car = new Car();
car.DriveCar(); // Returns "Car has been driven!"However, when the ProxyCar class is instantiated with a Driver object instance, the age check is performed before the DriveCar() method is called:
Driver driver = new Driver(16);
ProxyCar proxyCar = new ProxyCar(driver);
proxyCar.DriveCar(); // Returns "You are too young to drive!"Benefits of the Proxy pattern:
- Access control – governs access to the real object without changing a line of its code
- Transparent augmentation – adds cross-cutting behaviour like validation, logging or caching invisibly to clients
- Subject isolation - keeps the real object and its clients untouched; the proxy intercepts between them
- Deferred construction – puts off expensive object creation until it’s genuinely needed (lazy initialisation proxies)
The Proxy pattern is the polite bouncer of the structural world: everyone queues at the same door, but the proxy decides who actually gets through to the real object behind it.
Repository pattern
The Repository pattern adds a data-access layer between an application’s business logic and the data storage, so that business logic does not depend on the database directly. This pattern is also useful in testing, as the database that sits under the data-access layer can be swapped out during test runs.
Strictly speaking this is more of an enterprise/data-access pattern than a GoF structural pattern, but it slots naturally alongside them because it mediates between your domain and its persistence – an abstraction over storage structure, if you like.
In this example we have an IRepository<T> interface that defines five methods for data-access - FindAll(), Find(), FindById(), Add() and Remove():
public interface IRepository<T> where T : class, IEntity
{
IQueryable<T> FindAll();
IQueryable<T> Find(Expression<Func<T, bool>> predicate);
T FindById(Guid id);
void Add(T newEntity);
void Remove(T entity);
}Supporting this is an IEntity interface that defines the Id property:
public interface IEntity
{
int Id { get; set; }
}An Employee class holds an Id and Name property:
public class Employee : IEntity
{
public Employee()
{
Id = Guid.NewGuid();
}
public virtual Guid Id { get; private set; }
public virtual string Name { get; set; }
public override string ToString()
{
return string.Format("Name: {0} [{1}]", Name, Id);
}
}An IUnitOfWork interface is used to define an Employees property for retrieving Employee instances and a Commit() method that is called to persist changes to the database:
public interface IUnitOfWork
{
IObjectSet<Employee> Employees { get; }
void Commit();
}A concrete implementation of the IUnitOfWork is defined in the InMemoryUnitOfWork class:
public class InMemoryUnitOfWork : IUnitOfWork
{
public InMemoryUnitOfWork()
{
Committed = false;
}
public IObjectSet<Employee> Employees
{
get;
set;
}
public bool Committed { get; set; }
public void Commit()
{
Committed = true;
}
}An InMemoryObjectSet<T> generic base class defines the functionality of the repository:
public class InMemoryObjectSet<T> : IObjectSet<T> where T : class
{
private readonly HashSet<T> _set;
private readonly IQueryable<T> _queryableSet;
public InMemoryObjectSet()
: this(Enumerable.Empty<T>())
{
}
public InMemoryObjectSet(IEnumerable<T> entities)
{
_set = new HashSet<T>();
foreach (var entity in entities)
{
_set.Add(entity);
}
_queryableSet = _set.AsQueryable();
}
public Type ElementType
{
get { return _queryableSet.ElementType; }
}
public Expression Expression
{
get { return _queryableSet.Expression; }
}
public IQueryProvider Provider
{
get { return _queryableSet.Provider; }
}
public void AddObject(T entity)
{
_set.Add(entity);
}
public void Attach(T entity)
{
_set.Add(entity);
}
public void DeleteObject(T entity)
{
_set.Remove(entity);
}
public void Detach(T entity)
{
_set.Remove(entity);
}
public IEnumerator<T> GetEnumerator()
{
return _set.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}Usage of this pattern is then achieved through instantiating an InMemoryObjectSet<T> of Employee classes and passing that to an InMemoryUnitOfWork, which is used to perform the respective repository functionality:
var employees = GenerateEmployeeList();
InMemoryObjectSet<Employee> employeeObjectSet = new InMemoryObjectSet<Employee>(employees);
InMemoryUnitOfWork inMemoryUnitOfWork = new InMemoryUnitOfWork();
inMemoryUnitOfWork.Employees = employeeObjectSet;
// Find
var employeeFind = inMemoryUnitOfWork.Employees.Single(e => e.Name == "Lenny");
Console.WriteLine(employeeFind.ToString());
inMemoryUnitOfWork.Commit();
// Add
var employeeAdd = new Employee { Name = "Bart Simpson" };
inMemoryUnitOfWork.Employees.AddObject(employeeAdd);
Console.WriteLine("Added {0}", employeeAdd);
inMemoryUnitOfWork.Commit();
// Remove
var employeeRemove = inMemoryUnitOfWork.Employees.Single(e => e.Name == "Lenny");
inMemoryUnitOfWork.Employees.DeleteObject(employeeRemove);
Console.WriteLine("Removed {0}", employeeRemove);
inMemoryUnitOfWork.Commit();
// Update
var employeeUpdate = inMemoryUnitOfWork.Employees.Single(e => e.Name == "Mr Burns");
employeeUpdate.Name = "Waylon Smithers";
Console.WriteLine("Updated {0}", employeeUpdate);
inMemoryUnitOfWork.Commit();Benefits of the Repository pattern:
- Storage abstraction – decouples business logic from data-storage details and query technology
- Testability – makes unit testing practical by swapping a database-backed repository for an in-memory one
- Centralised data access – gathers queries, caching and data-access policy into a single place
- Domain simplicity – lets the domain deal in collections and entities rather than connections and commands
The Repository pattern is the quiet workhorse of layered architectures: a promise that your business logic never has to know – or care – where its data actually lives.
Wrapping up
That covers the structural patterns – Adapter, Bridge, Composite, Decorator, Façade, Flyweight, and Proxy from the classic GoF catalogue, plus Marker and Repository from the Pluralsight course. Between them, they answer one question in various ways: how do you fit things together without everything becoming tangled?
If there’s a thread running through all nine, it’s indirection. Each pattern introduces a seam of some kind – an interface that translates (Adapter), a hierarchy that delegates (Bridge), a wrapper that layers behaviour (Decorator), a gatekeeper that intercepts (Proxy) – and in every case the payoff is the same: parts of the system can change without dragging the rest along with them. That’s also what separates patterns from incidental design: a pattern exists precisely so that the thing you’ll likely change later is the thing that’s easy to swap now.
A few parting thoughts on applying these in the real world:
- Most of the time you won’t consciously pick a pattern – you’ll notice a code smell (subclass explosion, duplicated translation logic, a client reaching into five subsystems) and the appropriate pattern will be waiting there with a name. Knowing the catalogue turns vague discomfort into a vocabulary.
- The Composite pattern quietly underpins half of what you interact with daily - file systems, DOM trees, UI control hierarchies. Once you see it, you can’t unsee it.
- The Flyweight pattern is the odd one out: it’s not about flexibility at all, but about memory efficiency at scale. Useful, but profile before you reach for it – three coffee flavours don’t justify a cache.
- Finally, don’t feel obliged to implement patterns by the letter. The demo code in this post keeps things deliberately explicit so the shape is visible; in production code you’d lean on DI containers, generics, and the language itself to do much of the scaffolding for you.
All the code in this post is available in the design-patterns repository – each pattern is a standalone console application, so you can clone, run, and step through any of them to see the flow for yourself.
Next up in the series: the behavioural patterns, where we stop worrying about how things are built and structured, and start worrying about how they communicate.
Related Posts
2024-02-12
2024-02-11
2024-01-28