Design Patterns

Decorator Pattern

The Decorator Pattern is a structural design pattern that allows you to dynamically add new behavior to an object without altering its structure or affecting other objects. This pattern works by wrapping the original object inside a “decorator” object, enabling behavior extension while keeping the original object’s type unchanged.

  • Object Composition: The Decorator Pattern relies on composition rather than inheritance. You wrap one object inside another to add additional functionality.
  • Same Interface: Both the decorator and the decorated object usually implement the same interface or inherit from the same parent class, making them interchangeable.
  • Dynamic Extension: New functionality can be added at runtime, unlike inheritance, where behavior is fixed at compile time.

Below is a simple example of the Decorator Pattern:

1
2
3
4
5
// Base interface for coffee
interface Coffee {
String getDescription();
double getCost();
}
1
2
3
4
5
6
7
8
9
10
11
12
// Concrete base coffee implementation (decorated object)
class SimpleCoffee implements Coffee {
@Override
public String getDescription() {
return "Simple Coffee";
}

@Override
public double getCost() {
return 5.0; // Base price
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Abstract decorator class
abstract class CoffeeDecorator implements Coffee {
protected Coffee decoratedCoffee;

public CoffeeDecorator(Coffee coffee) {
this.decoratedCoffee = coffee;
}

@Override
public String getDescription() {
return decoratedCoffee.getDescription();
}

@Override
public double getCost() {
return decoratedCoffee.getCost();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Milk decorator (concrete decorator)
class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) {
super(coffee);
}

@Override
public String getDescription() {
return decoratedCoffee.getDescription() + ", Milk";
}

@Override
public double getCost() {
return decoratedCoffee.getCost() + 1.5; // Additional cost for milk
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Sugar decorator (concrete decorator)
class SugarDecorator extends CoffeeDecorator {
public SugarDecorator(Coffee coffee) {
super(coffee);
}

@Override
public String getDescription() {
return decoratedCoffee.getDescription() + ", Sugar";
}

@Override
public double getCost() {
return decoratedCoffee.getCost() + 0.5; // Additional cost for sugar
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Main program
public class CoffeeShop {
public static void main(String[] args) {
// Create base coffee
Coffee coffee = new SimpleCoffee();

// Add milk
coffee = new MilkDecorator(coffee);

// Add sugar
coffee = new SugarDecorator(coffee);

// Output description and price
System.out.println("Description: " + coffee.getDescription());
// Description: Simple Coffee, Milk, Sugar
System.out.println("Cost: $" + coffee.getCost());
// Cost: $7.0
}
}

UML

DecoratorUML

Summary

The decorated object is the implementation class SimpleCoffee of the Coffee interface.
The abstract decorator base class is CoffeeDecorator, and the two concrete decorators are MilkDecorator and SugarDecorator.

In this scenario, MilkDecorator and SugarDecorator decorate SimpleCoffee, extending its functionality without modifying its structure or inheritance hierarchy.


Adapter Pattern

The Adapter Pattern is a structural design pattern that converts the interface of a class into another interface that clients expect. It allows classes with incompatible interfaces to work together.

This pattern is particularly useful when you want to reuse existing classes, but their interfaces do not match your current requirements.

  • Interface Conversion: The core of the Adapter Pattern is converting one interface into another expected by the client.
  • Class Adapter vs Object Adapter:
    • Class Adapter: Uses inheritance to adapt functionality.
    • Object Adapter: Uses composition to adapt functionality. It is more flexible because it depends on interfaces rather than concrete implementations.

Example:

1
2
3
4
// Target interface - Power socket
interface PowerSocket {
void plugIn();
}
1
2
3
4
5
6
// Existing incompatible class
class OldPlug {
void connect() {
System.out.println("Old plug connected.");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
// Adapter class
class PlugAdapter implements PowerSocket {
private OldPlug oldPlug;

public PlugAdapter(OldPlug oldPlug) {
this.oldPlug = oldPlug;
}

@Override
public void plugIn() {
oldPlug.connect();
}
}
1
2
3
4
5
6
7
8
// Client code
public class AdapterPatternExample {
public static void main(String[] args) {
OldPlug oldPlug = new OldPlug();
PowerSocket adapter = new PlugAdapter(oldPlug);
adapter.plugIn();
}
}

UML

AdaptorUML

Proxy Pattern

The Proxy Pattern can be summarized as: extending the functionality of an original interface through an intermediary object, without modifying the original interface.

Example:

1
2
3
4
// Original interface
public interface Browser {
void request();
}
1
2
3
4
5
6
7
// Concrete implementation
public class BrowserImpl implements Browser {
@Override
public void request() {
System.out.println("Sending Request....");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Proxy class
public class Proxy implements Browser {
private final Browser browser;

public Proxy(BrowserImpl browser) {
this.browser = browser;
}

@Override
public void request() {
System.out.println("Proxy handling request....");
browser.request();
}
}
1
2
3
4
5
6
7
8
// Client code
public class Client {
public static void main(String[] args) {
BrowserImpl browser = new BrowserImpl();
Proxy proxy = new Proxy(browser);
proxy.request();
}
}
1
2
3
Output:
Proxy handling request....
Sending Request....

UML

ProxyUML