As a software engineer, it’s important to be well-versed in design patterns.
Design patterns are standardized best practices that the software engineering community has identified for solving common problems when designing software or applications. In this article, we’ll focus on structural design patterns.
Structural design patterns deal with how you composes classes to form larger structures. They don’t deal with the functionality of those classes. This makes them an excellent tool for creating extensible and reusable code.
The structural patterns are the following:
- Adapter pattern
- Bridge pattern
- Composite pattern
- Decorator pattern
- Facade pattern
- Flyweight pattern
- Proxy pattern
Since applications grow in complexity all the time, it is important to maintain code readibility and reduce code duplication. Let’s explore 7 structural design patterns in C#. Read on to know more…
What are design patterns?
A design pattern is a repeatable solution to a commonly occurring problem in software development. A design pattern isn’t a finished design that you can copy and paste directly into code. Instead, it is a description or template for solving a problem that you can use in many different situations.
Design patterns provide detailed solutions that you can apply in real-world scenarios. In addition, they are written in specific language terminology so developers can easily understand them with different experience levels. That’s why you can also use them as a communication tool when discussing design decisions and architectural issues with others.
As a result, you don’t have to reinvent the wheel when encountering a common software design problem. Instead, you can use an existing design pattern to save time and effort.
In 1994, Erich Gamma, Richard Help, Ralph Johnson, and John Vlissides, also known as the Gang of Four, compiled a list of 23 design patterns and published them in the Design Patterns: Elements of Reusable Object-Oriented Software book. The book became a bestseller, read by millions of developers worldwide, and an all-time classic.
Another book related to design patterns that is very popular is Head First Design Patterns. It explains the design patterns in a very casual and interesting way. So I would definitely recommend reading that book as well, in addition to the original Design Patterns book.
The Gang of Four divided the design patterns into three categories:
- Creational design patterns – These are patterns that deal with object creation.
- Structural design patterns – These are patterns for structuring your code to make it more efficient, scalable, and maintainable.
- Behavioral design patterns – These are patterns that deal with the communication between objects. In other words, they define how objects interact with each other.
Let’s dive deeper into structural design patterns.
What are structural design patterns?
Structural design patterns are concerned with how classes and objects are organized to perform the desired functionality.
The structural patterns are Adapter, Bridge, Composite, Decorator, Facade, Flyweight, and Proxy.
These patterns provide different ways of partitioning responsibility between objects, making the overall structure more flexible and easier to maintain.
In most cases, the pattern choice will depend on the project’s specific requirements. For example, the Facade pattern is often used in applications that need to present a simplified interface to a complex system.
By contrast, the Flyweight pattern is typically used when there is a need to minimize memory usage by sharing data between objects.
As with all design patterns, choosing the correct pattern for the job is important. Using the wrong pattern can lead to a design that is hard to understand and maintain.
Adapter pattern
The Adapter design pattern is a software design pattern that allows two otherwise incompatible types of objects to work together. The adapter connects the two objects, providing an interface that converts one type of object into another.
This pattern is often used in computer programming when an application needs to interface with an external system that uses a different API. By creating an adapter that wraps the external system’s API, the application can use the adapter to call the external system’s methods without having to worry about incompatibilities.
You can use the Adapter design pattern in other situations as well, such as when you need to convert data from one format to another or when you need to connect two systems that use different protocols.
Ultimately, the Adapter pattern can be helpful any time you need two things to work together that wouldn’t be able to do so without some translation layer.
The following sample represents the structural code in C#.
//Declaring the Target that the Client will use
public interface ITarget
{
public void MethodA();
}
//Defining the Adaptee to be accessed by the Client
public class Adaptee
{
public void MethodB()
{
Console.WriteLine("Method from Adaptee class.");
}
}
//Defining the Adapter that implements the Target interface
public class Adapter: ITarget
{
private Adaptee _adaptee;
public Adapter(Adaptee adaptee)
{
_adaptee = adaptee;
}
public void MethodA()
{
_adaptee.MethodB(); //Calling Adaptee method via Adapter
}
}
//Defining Client who will request the access to Adaptee
public class Client
{
private ITarget target;
public Client(ITarget target)
{
this.target = target;
}
public void Request()
{
target.MethodA();
}
}
If you want to learn more about the Adapter pattern, see a real-world code example, then check out this article.
Bridge pattern
The Bridge pattern is a pattern used in software engineering to reduce code complexity by separating functionality into separate “layers”. The Bridge pattern is often used to create abstractions, where one layer contains the implementation details and the other layer contains the interface.
You can use the Bridge pattern to decouple an abstraction from its implementation, allowing different implementations to be used without changing the abstraction. For example, you would use it to connect a Graphical User Interface (GUI) with various back-end implementations (e.g., MySQL, Oracle, Microsoft SQL Server).
The following sample represents the structural code in C#.
class Abstraction
{
Bridge bridge;
public Abstraction(Bridge implementation)
{
bridge = implementation;
}
public String Operation()
{
return "Abstraction << BRIDGE >> " + bridge.OperationImp();
}
}
interface Bridge
{
string OperationImp();
}
class ImplementationA : Bridge
{
public string OperationImp()
{
return "ImplementationA";
}
}
class ImplementationB : Bridge
{
public string OperationImp()
{
return "ImplementationB";
}
}
If you want to learn more about the Bridge pattern, see a real-world code example, then check out this article.
Composite pattern
The Composite design pattern allows you to build complex objects from simpler ones. The composite object comprises many individual objects, known as components.
The Composite pattern lets you treat the composite object and its components in the same way. This makes your code more flexible and easier to maintain.
For example, consider a document with text and images. The text and images are both components of the document. With the Composite pattern, you can manipulate the document as a whole or work with each component individually. This flexibility is one of the main advantages of the Composite pattern.
Another advantage is that it makes your code more reusable. For example, suppose you have a composite object with two text components and one image component. In that case, you can easily reuse that composite object in another document.
The following sample represents the structural code in C#.
public abstract class Component
{
protected string _name;
public Component(string name)
{
_name = name;
}
public abstract void Add(Component component);
public abstract void Remove(Component component);
public abstract void Display(int depth);
}
public class Composite : Component
{
List<Component> _childs = new List<Component>();
public Composite(string name)
: base(name)
{
}
public override void Add(Component component)
{
_childs.Add(component);
}
public override void Remove(Component component)
{
_childs.Remove(component);
}
public override void Display(int depth)
{
Console.WriteLine(new String('-', depth) + _name);
// Recursively display child nodes
foreach (Component component in _childs)
{
component.Display(depth + 2);
}
}
}
//Concrete class to add and remove leaves
public class Leaf : Component
{
public Leaf(string name)
: base(name)
{
}
public override void Add(Component component)
{
Console.WriteLine("Cannot add to a leaf");
}
public override void Remove(Component component)
{
Console.WriteLine("Cannot remove from a leaf");
}
public override void Display(int depth)
{
Console.WriteLine(new String('-', depth) + _name);
}
}
If you want to learn more about the Decorator pattern, see a real-world code example, then check out this article.
Decorator pattern
The Decorator design pattern is a popular way to dynamically add new functionality to an object without having to subclass or change it. The Decorator pattern uses a decorator object that wraps the original object, adding new behavior before or after method calls to the original object. This kind of structure allows you to keep the original object’s interface intact while still being able to modify its behavior.
The Decorator pattern is also helpful for adding logging or performance monitoring capabilities to an object without changing the original implementation.
The following sample represents the structural code in C#.
interface IComponent
{
void Operation();
}
class ConcreteComponent : IComponent
{
public void Operation()
{
Console.WriteLine("ConcreteComponent operation");
}
}
abstract class Decorator : IComponent
{
protected IComponent component;
public Decorator(IComponent component)
{
this.component = component;
}
public virtual void Operation()
{
component.Operation();
}
}
class ConcreteDecoratorA : Decorator
{
public ConcreteDecoratorA(IComponent component) : base(component)
{
}
public override void Operation()
{
AddedBehavior();
base.Operation();
}
private void AddedBehavior()
{
Console.WriteLine("ConcreteDecoratorA AddedBehavior");
}
}
class ConcreteDecoratorB : Decorator
{
public ConcreteDecoratorB(IComponent component) : base(component)
{
}
public override void Operation()
{
base.Operation();
AddedBehavior();
}
private void AddedBehavior()
{
Console.WriteLine("ConcreteDecoratorB AddedBehavior");
}
}
If you want to learn more about the Decorator pattern, see a real-world code example, then check out this article.
Facade pattern
The Facade design pattern provides a simplified interface to a complex part of a system. The Facade pattern hides the complexity of the subsystem and exposes a simplified interface to the client.
You might use the Facade pattern in C# applications when a system needs to be easy to use, but its internals are complex and messy. The Facade pattern makes the system easier to use by providing the client with a clean and simple interface. The Facade design pattern can also decouple a client from a complex legacy code.
The Facade design pattern is often implemented as a singleton, meaning there is only one instance of the facade class.
The Facade pattern can make a system easier to use, but it can also make it harder to change or extend. If the facade class is not designed properly, it can become tightly coupled to the subsystem and challenging to change. That’s why you need to be careful when designing the facade class so that it doesn’t become too tightly coupled to the subsystem.
The following sample represents the structural code in C#.
public class Facade
{
protected Subsystem1 _subsystem1;
protected Subsystem2 _subsystem2;
public Facade(Subsystem1 subsystem1, Subsystem2 subsystem2)
{
this._subsystem1 = subsystem1;
this._subsystem2 = subsystem2;
}
public string Operation()
{
string result = "Facade initializes subsystems:\n";
result += this._subsystem1.operation1();
result += this._subsystem2.operation1();
result += "Facade calls subsystems to execute the action:\n";
result += this._subsystem1.operationN();
result += this._subsystem2.operationZ();
return result;
}
}
public class Subsystem1
{
public string operation1()
{
return "Subsystem1: Ready!\n";
}
public string operationN()
{
return "Subsystem1: Go!\n";
}
}
public class Subsystem2
{
public string operation1()
{
return "Subsystem2: Get ready!\n";
}
public string operationZ()
{
return "Subsystem2: Fire!\n";
}
}
class Client
{
public static void ClientCode(Facade facade)
{
Console.Write(facade.Operation());
}
}
If you want to learn more about the Facade pattern, see a real-world code example, then check out this article.
Flyweight pattern
The Flyweight design pattern is a software engineering design pattern that can improve software performance by reducing the amount of data that needs to be stored in memory. This pattern does it by sharing common data between objects rather than storing duplicate copies of the same data. This reduces memory usage, especially in applications where there is a large number of objects.
For example, you can use it in graphical applications where many objects share the same data. For example, a flyweight object might store a rectangle’s position, color, and size, while the actual rectangle is stored in another object.
In addition, this pattern can also improve an application’s performance by reducing the number of objects that need to be processed.
The following sample represents the structural code in C#.
public interface Flyweight
{
void Operation(string ExState);
}
public class ConcreteFlyweight : Flyweight
{
public string ExtrinsicState { get; set; }
public void Operation(string ExState)
{
Console.WriteLine(" ConcreteFlyweight: " + ExState);
}
}
public class FlyweightFactory
{
private static Dictionary<string, Flyweight> _flyweights = new();
public FlyweightFactory()
{
_flyweights.Add("X", new ConcreteFlyweight());
_flyweights.Add("Y", new ConcreteFlyweight());
_flyweights.Add("Z", new ConcreteFlyweight());
}
public Flyweight GetFlyweight(string key)
{
return _flyweights[key];
}
}
If you want to learn more about the Flyweight pattern, see a real-world code example, then check out this article.
Proxy pattern
The proxy pattern is a pattern that involves the creation of objects that act as substitutes or stand-ins for other objects. Proxies can be used to provide additional functionality and security or to simply improve performance by caching or pre-loading data. The Proxy pattern is also sometimes known as the Surrogate pattern.
In its most general form, a proxy is a class that acts as an interface to another class. The proxy could protect access to:
- a network connection,
- a large object in memory,
- any other resource that is expensive or difficult to duplicate.
For example, imagine that an image must be downloaded from a remote server to be displayed on a web page. To download the image, you have to open a network connection must and retrieve image data. If several web pages on the same site all display the same image, it makes sense to cache the image after it is downloaded the first time and reuse the cached copy after that. This avoids duplication of effort (i.e., downloading the same image multiple times) and makes better use of resources (i.e., critical network bandwidth).
In the above scenario, the proxy would manage the caching of images and provide each requester with the existing cached copy rather than retrieving fresh copies from the remote server each time.
Another common use of proxies is to provide security by filtering requests and blocking dangerous ones before they reach their intended target. For instance, a proxy might filter web traffic so only allowable website addresses can be accessed from within an organization’s network. By using proxies, organizations can take measures to protect their internal networks from attack.
When used accordingly, proxies can improve performance and reduce duplication of effort while providing additional flexibility and control over the system resources.
The following sample represents the structural code in C#.
//Creating the interface to be implemented by RealSubject and Proxy
interface ISubject
{
void DoOperation();
}
//RealSubject implementing the interface
class RealSubject : ISubject
{
public void DoOperation()
{
Console.WriteLine("Operation done by Real Subject!");
}
}
//Proxy implementing the interface
class Proxy : ISubject
{
private RealSubject _realSubject;
public void DoOperation()
{
if (_realSubject == null)
{
_realSubject = new RealSubject();
}
Console.WriteLine("Proxy operation before Real Subject's operation.");
_realSubject.DoOperation();
}
}
//Client class that requests the service
class Client
{
public void ClientCode(ISubject subject)
{
subject.DoOperation();
}
}
If you want to learn more about the Proxy pattern, see a real-world code example, then check out this article.
Conclusion
In conclusion, structural design patterns are critical in Object Oriented programming.
They provide different ways to partition responsibility between objects, making the overall structure more flexible and easier to maintain. Therefore, developers must choose the right pattern for the job, as using the wrong one can lead to a design that is hard to understand or maintain.