Structural Design Patterns

Structural design patterns deal with the composition of classes and objects to form larger structures, while keeping them flexible and easy to modify. In this chapter, we will explore several structural design patterns, including adapter, bridge, composite, decorator, facade, flyweight, and proxy patterns.

3.1: Adapter Pattern

Summary

The Adapter pattern allows classes with incompatible interfaces to work together by wrapping its own interface around that of an already existing class. This enables the ability to reuse existing code without having to modify it directly.

Example

Imagine you have a third-party library that provides a useful feature, but its interface is incompatible with the rest of your codebase. By implementing the adapter pattern, you can create an adapter class that translates the third-party library's interface into one that is compatible with your codebase, allowing you to reuse the library without modifying it.

Implementation

  1. Define the target interface that your codebase expects.
  2. Implement the adapter class, which should implement the target interface and contain an instance of the third-party library.
  3. In the adapter class, implement the methods of the target interface by delegating to the corresponding methods of the third-party library.

Benefits

  • Allows for the reuse of existing code without modifying it directly.
  • Provides a flexible and modular design.

Drawbacks

  • Adds an extra layer of indirection, which can impact performance.
  • Increases the complexity of the codebase.

When to use

  • When you need to integrate two incompatible interfaces.
  • When you want to reuse existing code without modifying it directly.

Example code

// Target interface
public interface ITarget
{
    void Request();
}

// Adaptee class
public class Adaptee
{
    public void SpecificRequest()
    {
        Console.WriteLine("Specific request from Adaptee.");
    }
}

// Adapter class
public class Adapter : ITarget
{
    private readonly Adaptee _adaptee;

    public Adapter(Adaptee adaptee)
    {
        _adaptee = adaptee;
    }

    public void Request()
    {
        _adaptee.SpecificRequest();
    }
}

Summary

The adapter pattern is useful when you need to integrate two incompatible interfaces or reuse existing code without modifying it directly. However, it adds an extra layer of indirection and increases the complexity of the codebase.

3.2: Bridge Pattern

Summary

The Bridge pattern separates an abstraction from its implementation, allowing the two to vary independently. This pattern enables the creation of a modular design, where the structure, behavior, and implementation can be changed without affecting each other.

Example

Imagine you have a class hierarchy where the implementation details are tightly coupled with the abstraction, making it difficult to modify or extend. By implementing the bridge pattern, you can separate the abstraction from the implementation, allowing for greater flexibility and modularity.

Implementation

  1. Define the abstraction interface, which should contain the methods that the client will use.
  2. Define the implementation interface, which should contain the methods that the abstraction will use.
  3. Implement the concrete implementation classes, which should implement the implementation interface.
  4. Implement the concrete abstraction classes, which should use the implementation interface and contain a reference to an instance of the concrete implementation class.

Benefits

  • Allows for the abstraction and implementation to vary independently.
  • Enables the creation of a modular design.

Drawbacks

  • Increases the complexity of the codebase.
  • Requires more code than a simple implementation.

When to use

  • When you need to separate the abstraction from the implementation.
  • When you want to enable the abstraction and implementation to vary independently.

Example code

// Abstraction interface
public interface IAbstraction
{
    void Operation();
}

// Implementation interface
public interface IImplementation
{
    void ImplementationMethod();
}

// Concrete implementation class
public class ConcreteImplementation : IImplementation
{
    public void ImplementationMethod()
    {
        Console.WriteLine("Implementation method from ConcreteImplementation.");
    }
}

// Concrete abstraction class
public class ConcreteAbstraction : IAbstraction
{
    private readonly IImplementation _implementation;

    public ConcreteAbstraction(IImplementation implementation)
    {
        _implementation = implementation;
    }

    public void Operation()
    {
        Console.WriteLine("Operation from ConcreteAbstraction.");
        _implementation.ImplementationMethod();
    }
}

Summary

The bridge pattern is useful when you need to separate the abstraction from the implementation or enable the abstraction and implementation to vary independently. However, it increases the complexity of the codebase and requires more code than a simple implementation.

In the next section, we will explore advanced structural design patterns, including composite, decorator, facade, flyweight, and proxy patterns. These patterns provide more advanced solutions to complex design challenges.