Structural Design Patterns
Deals with how classes are designed.
Flyweight Pattern
Aims to minimize memory usage by sharing as much data as possible with similar objects. A way of optimization. Useful when dealing with a large number of objects that have some shared state.
- Flyweight Factory: This factory ensures that flyweight objects are shared and reused. It maintains a pool of flyweight objects and returns existing objects from the pool instead of creating new ones.
- Flyweight Object: This object contains the intrinsic state and methods to operate on it.
// Flyweight interfaceinterface Flyweight { void operation(String extrinsicState);}
// Concrete Flyweight classclass ConcreteFlyweight implements Flyweight { private final String intrinsicState;
public ConcreteFlyweight(String intrinsicState) { this.intrinsicState = intrinsicState; }
@Override public void operation(String extrinsicState) { System.out.println("Intrinsic State: " + intrinsicState + ", Extrinsic State: " + extrinsicState); }}
// Flyweight Factoryclass FlyweightFactory { private final Map<String, Flyweight> flyweights = new HashMap<>();
public Flyweight getFlyweight(String key) { if (!flyweights.containsKey(key)) { flyweights.put(key, new ConcreteFlyweight(key)); } return flyweights.get(key); }
public int getFlyweightCount() { return flyweights.size(); }}
// Client codepublic class FlyweightPatternExample { public static void main(String[] args) { FlyweightFactory factory = new FlyweightFactory();
Flyweight flyweight1 = factory.getFlyweight("A"); flyweight1.operation("First Call");
Flyweight flyweight2 = factory.getFlyweight("A"); flyweight2.operation("Second Call");
Flyweight flyweight3 = factory.getFlyweight("B"); flyweight3.operation("Third Call");
System.out.println("Total flyweights created: " + factory.getFlyweightCount()); }}
Bridge Pattern
Used to split large classes into separate hierarchies which can be developed independently. There are 2 layer of classes.
- Abstraction layer - high-level control logic
- Implementation layer - underlying code
Mostly used when the objects vary in more than 2 independent dimensions.
interface Device { void turnOn(); void turnOff(); void setChannel(int channel);}
// Concrete Implementationsclass TV implements Device { private boolean on = false; private int channel = 1;
@Override public void turnOn() { on = true; System.out.println("TV is ON"); }
@Override public void turnOff() { on = false; System.out.println("TV is OFF"); }
@Override public void setChannel(int channel) { this.channel = channel; System.out.println("TV channel set to " + channel); }}
class Radio implements Device { private boolean on = false; private int channel = 1;
@Override public void turnOn() { on = true; System.out.println("Radio is ON"); }
@Override public void turnOff() { on = false; System.out.println("Radio is OFF"); }
@Override public void setChannel(int channel) { this.channel = channel; System.out.println("Radio frequency set to " + channel); }}
// Abstractionabstract class RemoteControl { protected Device device;
public RemoteControl(Device device) { this.device = device; }
public abstract void turnOn(); public abstract void turnOff(); public abstract void setChannel(int channel);}
// Refined Abstractionclass BasicRemoteControl extends RemoteControl { public BasicRemoteControl(Device device) { super(device); }
@Override public void turnOn() { device.turnOn(); }
@Override public void turnOff() { device.turnOff(); }
@Override public void setChannel(int channel) { device.setChannel(channel); }}
// Client Codepublic class BridgePatternDemo { public static void main(String[] args) { Device tv = new TV(); RemoteControl remote = new BasicRemoteControl(tv);
remote.turnOn(); // TV is ON remote.setChannel(5); // TV channel set to 5 remote.turnOff(); // TV is OFF
Device radio = new Radio(); remote = new BasicRemoteControl(radio);
remote.turnOn(); // Radio is ON remote.setChannel(99); // Radio frequency set to 99 remote.turnOff(); // Radio is OFF }}
In the above example, the type of device and the type of remote are 2 independent dimensions and can be paired together without too much complexity.
Decorator Pattern
Lets you attach new behaviors to objects by placing them inside wrapper objects that contain these behaviors. Provides a flexible alternative to subclassing for extending functionality.
- Component: Defines the interface for objects that can have responsibilities added to them.
- Concrete Component: The base object that responsibilities can be added to.
- Decorator: Maintains a reference to a Component object and implements the Component interface.
- Concrete Decorator: Adds responsibilities to the component.
// Component interfaceinterface Coffee { double getCost(); String getDescription();}
// Concrete Componentclass SimpleCoffee implements Coffee { @Override public double getCost() { return 1.0; }
@Override public String getDescription() { return "Simple Coffee"; }}
// Decorator base classabstract class CoffeeDecorator implements Coffee { protected Coffee decoratedCoffee;
public CoffeeDecorator(Coffee coffee) { this.decoratedCoffee = coffee; }
public double getCost() { return decoratedCoffee.getCost(); }
public String getDescription() { return decoratedCoffee.getDescription(); }}
// Concrete Decoratorsclass MilkDecorator extends CoffeeDecorator { public MilkDecorator(Coffee coffee) { super(coffee); }
@Override public double getCost() { return super.getCost() + 0.5; }
@Override public String getDescription() { return super.getDescription() + ", with Milk"; }}
class SugarDecorator extends CoffeeDecorator { public SugarDecorator(Coffee coffee) { super(coffee); }
@Override public double getCost() { return super.getCost() + 0.2; }
@Override public String getDescription() { return super.getDescription() + ", with Sugar"; }}
// Client codepublic class DecoratorPatternExample { public static void main(String[] args) { Coffee coffee = new SimpleCoffee(); System.out.println(coffee.getDescription() + " costs $" + coffee.getCost());
Coffee coffeeWithMilk = new MilkDecorator(coffee); System.out.println(coffeeWithMilk.getDescription() + " costs $" + coffeeWithMilk.getCost());
Coffee coffeeWithMilkAndSugar = new SugarDecorator(coffeeWithMilk); System.out.println(coffeeWithMilkAndSugar.getDescription() + " costs $" + coffeeWithMilkAndSugar.getCost()); }}
In this example, we can add milk and sugar to a simple coffee by wrapping it with decorator objects. Each decorator adds its own behavior while maintaining the same interface as the base coffee object.