• 4 hours
  • Easy

Free online content available in this course.

course.header.alt.is_video

course.header.alt.is_certifying

Got it!

Last updated on 3/28/24

Organize Objects With Structural Design Patterns

Structural Design Patterns

The next category of design patterns are called structural. Structural design patterns help design an application by identifying a simple way to realize relationships among entities.

In this chapter, we’ll look at three structural design patterns:

  • Adapter

  • Composite

  • Decorator 

Adapter Pattern

This one is pretty straightforward when you think about adapters in general. Consider the power adapters you have at your home. For example, your smartphone comes with a power adapter that has a USB input on one end and a power plug output that sticks into the wall. The adapter’s job is to convert one to the other.

A smartphone adapter
An example of an adapter

In our application, the adapter pattern allows incompatible classes to work together by converting the interface of one class into an interface expected by the client

To make the adapter pattern work, you need:

  • Target interface.

  • An Adaptee class.

  • An Adapter class.

  • Client application.

Let’s look at an example of a company that manages employee records for different third parties. Each one has its own system of maintaining records. We have to write an adapter for the company that can get employee records from these other systems.

First, the ITarget  interface:

public interface ITarget
{
    List<string> GetEmployees();
}

Next, the  Adaptee  class:

public class ThirdPartyEmployee
{
    public List<string> GetEmployeeList()
    {
        List<string> EmployeeList = new List<string>();
        EmployeeList.Add("Bob");
        EmployeeList.Add("Joe");
        
        return EmployeeList;
    }
}

Now the  Adapter  class:

public class EmployeeAdapter : ThirdPartyEmployee, ITarget
{
    public List<string> GetEmployees()
    {
        return GetEmployeeList();
    }
}

Lastly, the  Client  application to implement:

public class Client
{
    static void Main(string[] args)
    {
        ITarget adapter = new EmployeeAdapter();
 
        foreach(string employee in adapter.GetEmployees())
        {
            Console.WriteLine(employee);
        }
    }
}

The output will be:

Bob
Joe 

As you can see from the  Client, the  ITarget  interface was used to call the functionality of the   Adaptee  class.

What Are the Advantages?

  • You achieve reusability and flexibility.

  • The client class isn’t complicated by having to use a different interface. 

Composite Pattern

Composite is the next structural design pattern. It describes a group of objects that are treated the same way as a single instance of the same type of object. The intent is to compose objects into a tree structure to represent part-whole hierarchies (trees).

In other words, you can represent all or part of a hierarchy by reducing the pieces down to common components.

You will need the following for a composite pattern:

  • Component: It declares the interface for objects and behavior that is common to all objects.

  • Leaves: It represents leaf behavior.

  • Composite: It defines behavior for components that have children (contrasting the leaves). 

  • Client: The main application.

So, for an example, let’s look at a fancy coffee dispenser.

A coffee dispenser with options for both type and flavor.
Fancy coffee dispenser

What’s so unique about this dispenser is that it wants you to “drill-down” by first selecting a type (i.e., espresso, latte, cappuccino) and then by flavor.

Hierarchy of coffee options
Coffee options tree

This is the hierarchy (tree) mentioned earlier. So, let’s model this tree, and for all of these coffee flavors, let’s track how many calories each flavor has.

public class Coffee
{
    public int Calories { get; set; }
    public List<Coffee> Flavors { get; set; }
 
    public Coffee(int calories)
    {
        Calories = calories;
        Flavors = new List<Coffee>();
    }
 
    public void DisplayCalories()
    {
        Console.WriteLine(this.GetType().Name + ": " + this.Calories.ToString() + "calories.");
        
        foreach(var drink in this.Flavors)
        {
            drink.DisplayCalories();
        }
    }
}

Did you notice the  DisplayCalories()  method? This is a recursive method that will show the calories for all the flavors.

Now, implement the leaves for the concrete coffee flavors:

public class VanillaLatte : Coffee
{
    public VanillaLatte(int calories)
        : base(calories)
    { }
}
 
public class CinnamonLatte : Coffee
{
    public CinnamonLatte(int calories)
        : base(calories)
    { }
}
 
public class PeppermintCappuccino : Coffee
{
    public PeppermintCappuccino(int calories)
        : base(calories)
    { }
}
 
public class CaramelCappuccino : Coffee
{
    public CaramelCappuccino(int calories)
        : base(calories)
    { }
}

Next, implement the composite, which represents objects in the tree that have children: 

public class Expresso : Coffee
{
    public Espresso(int calories)
        : base(calories)
    { }
}
 
public class Cappuccino : Coffee
{
    public Cappuccino(int calories)
        : base(calories)
    { }
}
 
public class Latte : Coffee
{
    public Latte(int calories)
        : base(calories)
    { }
}

Finally, we need one more composite class for the root node of the tree:

public class HotCoffee : Coffee
{
    public HotCoffee(int calories)
        : base(calories)
    { }
}

The composite classes are exactly the same as the leaf classes, and this is on purpose.

Now, in the  Main  method of our application, we can initialize a  Coffee  tree with several flavors, and then display the calories for each.

static void Main(string[] args)
{
    Latte latte = new Latte(350);
    latte.Flavors.Add(new CinnamonLatte(400));
    latte.Flavors.Add(new VanillaLatte(300));
 
    Espresso espresso = new Espresso(10);
 
    Cappuccino cappuccino = new Cappuccino(450);
    cappuccino.Flavors.Add(new CaramelCappuccino(500));
    cappuccino.Flavors.Add(new PeppermintCappuccino(425));
 
    Coffee coffee = new Coffee(100);
    coffee.Flavors.Add(latte);
    coffee.Flavors.Add(espresso);
    coffee.Flavors.Add(cappuccino);
 
    coffee.DisplayCalories();
}

The output would be:

Coffee: 100 calories.
Latte: 350 calories.
CinnamonLatte: 400 calories.
VanillaLatte: 300 calories.
Espresso: 10 calories.
Cappuccino: 450 calories.
CaramelCappuccino: 500 calories.
PeppermintCappuccino: 425 calories. 

What Are the Advantages?

  • It allows clients to treat different parts of the object hierarchy as the same in two ways:

    • If a call is made to a leaf, the request is handled directly.

    • If a call is to a composite, it forwards the request to its child components.

  • It makes it easier to add new kinds of components.

  • It provides a flexible structure with a manageable class or interface. 

Decorator Pattern

The final structural design pattern is the decorator pattern. This pattern allows you to add behavior to an object without affecting the behavior of the other objects in the same class. A decorator is similar to an alternative to inheritance. 

In simpler terms, think about ordering an ice cream sundae. The core of the dessert is a scoop of ice cream. You eat it with a spoon. You can also add toppings and sauces. In other words, decorate the dessert. But you still eat it with a spoon. So the interface stays the same, even though the sundae has become more complex.

You will need the following for the decorator pattern:

  1. Component: It declares the interface for objects that can have responsibilities added to them dynamically. 

  2. ConcreteComponent: An object to which additional responsibilities will be attached.

  3. Decorator: To keep a reference to the component object and define an interface that conforms to the component’s interface.

  4. ConcreteDecorator: To add responsibilities to the component. 

As an example, let’s decorate a pizza with toppings.

Start with the component class, which is  Pizza:

public abstract class Pizza
{
    protected string description = "Plain Pizza";
    
    public string GetDescription()
    {
        return description;
    }
    
    public abstract int GetCost();
}

Next, create concrete  Pizza  classes, which are the types of pizzas:

public class Pepperoni : Pizza
{
    public Pepperoni()
    {
        description = "Pepperoni";
    }
 
    public override int GetCost()
    {
        return 10;
    }
}
 
public class Sausage : Pizza
{
    public Sausage()
    {
        description = "Sausage";
    }
 
    public override int GetCost()
    {
        return 14;
    }
}

Next, add the  Decorator  class, which extends  Pizza:

public abstract class ToppingsDecorator : Pizza
{
    public abstract string GetDescription();
}

Then, add the concrete decorators, which are the pizza toppings:

public class Cheese : ToppingsDecorator
{
    Pizza pizza;
 
    public Cheese(Pizza pizza)
    {
        this.pizza = pizza;
    }
 
    public override string GetDescription()
    {
        return pizza.GetDescription() + ", Cheese";
    }
 
    public override int GetCost()
    {
        return 2 + pizza.GetCost();
    }
}
 
public class Onion : ToppingsDecorator
{
    Pizza pizza;
 
    public Onion(Pizza pizza)
    {
        this.pizza = pizza;
    }
 
    public string GetDescription()
    {
        return pizza.GetDescription() + ", Onion";
    }
 
    public override int GetCost()
    {
        return 3 + pizza.GetCost();
    }
}

Finally, put it all together in the client application:

public static void Main(string[] args)
{
    //create a new pepperoni pizza
    Pizza pepperoniPizza = new Pepperoni();
    Console.WriteLine(pizzaPepperoni.GetDescription() + " Cost: " + pizza.GetCost());
 
    //create a new sausage pizza
    Pizza sausagePizza = new Sausage();
    //decorate it with cheese topping
    sausagePizza = new Cheese(sausagePizza);
 
    //decorate it with onion topping
    sausagePizza = new Onion(sausagePizza);
 
    Console.WriteLine(sausagePizza.GetDescription() + " Cost:" + sausagePizza.GetCost());
}

And here’s the output:

Pepperoni Cost: $10.
Sausage, Cheese, Onion Cost: $19.

You can see how to add and remove new pizzas and toppings with no alteration.

What Are the Advantages?

  • It's an alternative to complicated use cases of inheritance (i.e., too many layers).

  • It's useful when you need to add additional behaviors to your objects at runtime. 

  • It's useful in cases where different instances of the same object might behave differently. 

Let’s Recap!

  • The adapter pattern allows incompatible classes to work together by converting the interface of one class into an interface expected by the client. 

  • The composite pattern describes a group of objects that are treated the same way as a single instance of the same type of object. 

  • The decorator pattern allows you to add behavior to an object without affecting the behavior of the other objects in the same class. 

Now that we’ve covered structural design patterns let’s look at some examples in the last group: behavioral design patterns.

Example of certificate of achievement
Example of certificate of achievement