BCS1430
Dr. Ashish Sai
đź“… Week 5 Lecture 1
đź’» BCS1430.ashish.nl
📍 EPD150 MSM Conference Hall
Structural patterns focus on assembling objects and classes into larger structures while maintaining efficiency and flexibility.
| Pattern | Description | Covered |
|---|---|---|
| Adapter | Allows the interface of an existing class to be used as another interface. | âś… |
| Bridge | Separates an object’s abstraction from its implementation so that they can vary independently. | ❌ |
| Composite | Composes objects into tree structures to represent part-whole hierarchies. | ❌ |
| Decorator | Attaches additional responsibilities to an object dynamically. | âś… |
| Facade | Provides a unified interface to a set of interfaces in a subsystem. | âś… |
| Flyweight | Uses sharing to support a large number of fine-grained objects efficiently. | ❌ |
| Proxy | Provides a surrogate or placeholder for another object to control access to it. | âś… |
The need to extend the functionality of objects dynamically.
Avoiding “class explosion” for similar yet distinct objects.
Example: Different types of coffee in a coffee house application.
Multiple classes for each combination of coffee and add-ons (e.g., Espresso with Caramel, Decaf with Soy, etc.).
Results in a large, unmanageable number of subclasses.
Purpose: Dynamically adds behaviors to objects without modifying their structure.
Key Concept: Wraps additional behaviors around objects to enhance or modify their functionality.
Wraps the original object inside objects containing new behaviors.
Each wrapper (decorator) adds its behavior either before or after delegating to the wrapped object.
public class CaramelDecorator extends AddOnDecorator {
public CaramelDecorator(Beverage beverage) {
this.beverage = beverage;
}
public int cost() {
return beverage.cost() + 2; // Adding cost of caramel
}
}
public class SoyDecorator extends AddOnDecorator {
public SoyDecorator(Beverage beverage) {
this.beverage = beverage;
}
public int cost() {
return beverage.cost() + 1; // Adding cost of soy
}
}Creating a coffee with add-ons.
Calculating the total cost dynamically.
Component: Common interface for both wrappers and wrapped objects.
Concrete Component: The object being wrapped.
Decorator: Base class for all decorators with a reference to a Component.
Concrete Decorators: Classes that add new behaviors.
Steps for Implementation
Define the component interface.
Create a concrete component class.
Develop a base decorator class.
Implement concrete decorators.
Ensure all components and decorators implement the component interface.
Decorators should delegate to the wrapped object and add their behavior.
You have a different types of data sources (such as Text Files or Database)
You want to Encrypt the data yous tore or Compress it.
// Component Interface
interface DataSource {
void writeData(String data);
String readData();
}
// Concrete Component
class FileDataSource implements DataSource {
// Implementation details...
}
// Base Decorator
class DataSourceDecorator implements DataSource {
protected DataSource wrappee;
DataSourceDecorator(DataSource source) {
this.wrappee = source;
}
public void writeData(String data) {
wrappee.writeData(data);
}
public String readData() {
return wrappee.readData();
}
}// Encryption Decorator
class EncryptionDecorator extends DataSourceDecorator {
EncryptionDecorator(DataSource source) {
super(source);
}
public void writeData(String data) {
// Encrypt and write data
}
public String readData() {
// Read and decrypt data
return "decrypted data";
}
}
// Compression Decorator
class CompressionDecorator extends DataSourceDecorator {
CompressionDecorator(DataSource source) {
super(source);
}
public void writeData(String data) {
// Compress and write data
}
public String readData() {
// Read and decompress data
return "decompressed data";
}
}The Decorator Pattern allows stacking multiple decorators to add several behaviors.
DataSource basicData = new FileDataSource("data.txt");
DataSource encrypted = new EncryptionDecorator(basicData);
DataSource encryptedCompressed = new CompressionDecorator(encrypted);
// Now 'encryptedCompressed' has both encryption and compression capabilities.Flexibility in adding new functionality.
Avoids class explosion by using composition over inheritance.
Easier to maintain and extend.
Can lead to complex code structures.
Difficulty in debugging, as it introduces layers of abstraction.
Potential performance issues due to increased object creation.
Decorator Pattern allows for more flexibility than subclassing.
Avoids rigid class hierarchy.
Promotes loose coupling and adherence to the Open-Closed Principle.
Purpose: To make two incompatible interfaces compatible.
Also known as a “wrapper.”
Use Case: Connecting new code to legacy code or third-party libraries.
public class Client {
Target target = new Adapter (new Adaptee());
target.request();
}
public interface Target {
void request();
}
class Adapter implements Target {
public Adapter (Adaptee a){
this.adaptee = a;
}
public void request(){
this.adaptee.SpecialRequest();
}
}
class Adaptee {
void specialRequest(){
}
}The Adapter pattern allows objects with incompatible interfaces to work together.
It acts as a bridge between two incompatible interfaces, effectively allowing them to communicate.
Scenario: Stock market monitoring app downloads data in XML.
Challenge: Integration with a 3rd-party analytics library requiring JSON.
Problem: Incompatibility between data formats (XML vs. JSON).
Enables collaboration by converting XML interface for Analytics Library compatibility.
Purpose: To use an existing class whose interface is incompatible with your code.
Ideal For:
Pros:
Single Responsibility Principle: Separates the interface conversion code from the primary business logic.
Open/Closed Principle: Allows introducing new types of adapters without breaking existing client code.
Cons:
Definition: Simplifies complex system interactions
Purpose: Provide a unified interface to a set of interfaces in a subsystem
Key Principle: High-level abstraction over complex subsystems
Example: Starting a car (Key Turn → Engine Start, Lights On, etc.)
Multiple classes with intricate interactions
Challenge: Managing complex dependencies and interactions
Client: User of a piece of code, not end-user
Problem: Need to interact with complex subsystems
Complexity: High due to multiple, interdependent classes
Solution: Simplify interaction using a facade
Consider an operator in a shop as a facade. They provide you with a simple interface to various services and departments of the shop, hiding the complexities of the subsystems behind the scenes.
// Facade Class
public class CarEngineFacade {
private Ignition ignition;
private FuelInjector fuelInjector;
private AirFlowController airFlowController;
public CarEngineFacade() {
ignition = new Ignition();
fuelInjector = new FuelInjector();
airFlowController = new AirFlowController();
}
public void startEngine() {
fuelInjector.on();
airFlowController.takeAir();
ignition.ignite();
// Other complex interactions
}
public void stopEngine() {
fuelInjector.off();
airFlow
Controller.cutAir();
ignition.off();
// Other shutdown interactions
}
}Simplicity: Provides simple interface to complex subsystems
Decoupling: Clients interact with facade rather than direct subsystem
Maintainability: Changes in subsystems less likely to affect clients
Provides a surrogate or placeholder for another object.
Controls access to the original object.
Use cases: Security, Remote Object Access etc.
Purpose: Acts as a substitute to control access to another object.
Use Cases: Ideal for scenarios needing object access management without changing the object’s behavior.
A real-world analogy for the Proxy pattern is a credit card acting as a proxy for a bank account, which in turn is a proxy for a bundle of cash. Both provide a means for payment but with additional layers of control and convenience.
public interface Image {
void display();
}
public class RealImage implements Image {
private String fileName;
public RealImage(String fileName) {
this.fileName = fileName;
loadFromDisk(fileName);
}
@Override
public void display() {
System.out.println("Displaying " + fileName);
}
private void loadFromDisk(String fileName) {
System.out.println("Loading " + fileName);
}
}
public class ProxyImage implements Image {
private RealImage realImage;
private String fileName;
public ProxyImage(String fileName) {
this.fileName = fileName;
}
@Override
public void display() {
if (realImage == null) {
realImage = new RealImage(fileName);
}
realImage.display();
}
}public interface SecureResource {
void accessResource();
}
public class RealResource implements SecureResource {
@Override
public void accessResource() {
System.out.println("Accessing Secure Resource");
}
}
public class SecurityProxy implements SecureResource {
private RealResource realResource;
private boolean hasAccess;
public SecurityProxy(boolean hasAccess) {
this.hasAccess = hasAccess;
this.realResource = new RealResource();
}
@Override
public void accessResource() {
if (hasAccess) {
realResource.accessResource();
} else {
System.out.println("Access Denied");
}
}
}The Proxy Pattern is highly versatile, applicable in situations requiring:
Pros:
Control the service object indirectly.
Manage the lifecycle and initialization of the service.
Introduce new proxies without changing the service or clients.
Cons:
Can complicate the code structure with additional classes.
May introduce latency in the response from the service.