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.
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:
A
Target
interface.An
Adaptee
class.An
Adapter
class.A
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.
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.
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:
Component: It declares the interface for objects that can have responsibilities added to them dynamically.
ConcreteComponent: An object to which additional responsibilities will be attached.
Decorator: To keep a reference to the component object and define an interface that conforms to the component’s interface.
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.