Structural Design Patterns
Table of contents
Adapter Pattern
What is the Adapter Pattern?
The Adapter Pattern is a structural design pattern that allows incompatible interfaces to work together. It acts as a bridge between two different interfaces, enabling smooth integration.
When to Use the Adapter Pattern?
When you need to integrate a new class with an existing system without modifying its source code.
When you want to create reusable code that works with different implementations.
When adapting legacy code to a new system.
Real-World Example: Power Adapter
Imagine you're working on a project that initially used MongoDB, but you now need to support MySQL as well. Instead of modifying all database-related code, you can use an adapter to bridge the differences between MongoDB's document-based storage and MySQL's relational structure.
// Step 1: Define the Target Interface
interface Database {
void connect();
void executeQuery(String query);
}
// Step 2: Implement an Existing Class with a Different Interface
class MongoDB {
void establishConnection() {
System.out.println("Connected to MongoDB");
}
void runQuery(String jsonQuery) {
System.out.println("Executing MongoDB query: " + jsonQuery);
}
}
// Step 3: Create an Adapter Class
class MongoDBAdapter implements Database {
private MongoDB mongoDB;
public MongoDBAdapter() {
this.mongoDB = new MongoDB();
}
@Override
public void connect() {
mongoDB.establishConnection();
}
@Override
public void executeQuery(String query) {
// Convert SQL query to JSON format (mock implementation)
String jsonQuery = "{converted: '" + query + "'}";
mongoDB.runQuery(jsonQuery);
}
}
// Step 4: Implement the Client Class
class DatabaseClient {
public static void main(String[] args) {
Database mysqlDatabase = new MySQLDatabase();
mysqlDatabase.connect();
mysqlDatabase.executeQuery("SELECT * FROM users");
Database mongoAdapter = new MongoDBAdapter();
mongoAdapter.connect();
mongoAdapter.executeQuery("SELECT * FROM users");
}
}
// Step 5: A Concrete MySQL Implementation (for Comparison)
class MySQLDatabase implements Database {
@Override
public void connect() {
System.out.println("Connected to MySQL");
}
@Override
public void executeQuery(String query) {
System.out.println("Executing MySQL query: " + query);
}
}
Decorator Pattern
What is the Decorator Pattern?
The Decorator Pattern is a structural design pattern that allows behavior to be dynamically added to an object without modifying its original structure. It is used to extend the functionality of objects at runtime.
When to Use the Decorator Pattern?
When you want to add functionalities to objects dynamically.
When extending a class using inheritance is not feasible.
When modifying the original class is not allowed (e.g., third-party libraries).
Real-World Example: Text Formatting in an Editor
Consider a text editor where you can apply multiple formatting styles (bold, italic, underline) to text. Instead of modifying the base Text
class, we can use decorators to dynamically add styles at runtime.
// Step 1: Define the Component Interface
interface Text {
String render();
}
// Step 2: Create a Concrete Component
class PlainText implements Text {
private String content;
public PlainText(String content) {
this.content = content;
}
@Override
public String render() {
return content;
}
}
// Step 3: Create an Abstract Decorator
abstract class TextDecorator implements Text {
protected Text text;
public TextDecorator(Text text) {
this.text = text;
}
@Override
public String render() {
return text.render();
}
}
// Step 4: Create Concrete Decorators
class BoldText extends TextDecorator {
public BoldText(Text text) {
super(text);
}
@Override
public String render() {
return "<b>" + super.render() + "</b>";
}
}
class ItalicText extends TextDecorator {
public ItalicText(Text text) {
super(text);
}
@Override
public String render() {
return "<i>" + super.render() + "</i>";
}
}
// Step 5: Test the Decorator Pattern
public class DecoratorPatternExample {
public static void main(String[] args) {
Text text = new PlainText("Hello, World!");
Text boldText = new BoldText(text);
Text italicBoldText = new ItalicText(boldText);
System.out.println(italicBoldText.render());
}
}
Facade Pattern
What is the Facade Pattern?
The Facade Pattern is a structural design pattern that provides a simplified interface to a complex system of classes, making it easier to use. It hides the complexities of the system and provides a unified interface for the client.
When to Use the Facade Pattern?
When working with a complex system with multiple dependencies.
When you want to provide a simple, easy-to-use API for clients.
When you need to reduce dependencies between clients and subsystems.
Real-World Example: Video Streaming Service
Imagine a video streaming service where users can play videos. The system has multiple components like VideoLoader, AudioManager, SubtitlesManager, and CodecManager. Instead of making the client interact with all these components separately, we create a Facade to simplify access.
// Step 1: Define the Complex Subsystem Components
class VideoLoader {
void load(String video) {
System.out.println("Loading video: " + video);
}
}
class AudioManager {
void setAudioTrack(String track) {
System.out.println("Setting audio track: " + track);
}
}
class SubtitlesManager {
void enableSubtitles(String language) {
System.out.println("Enabling subtitles: " + language);
}
}
class CodecManager {
void decodeVideo(String format) {
System.out.println("Decoding video format: " + format);
}
}
// Step 2: Create the Facade Class
class VideoPlayerFacade {
private VideoLoader videoLoader;
private AudioManager audioManager;
private SubtitlesManager subtitlesManager;
private CodecManager codecManager;
public VideoPlayerFacade() {
this.videoLoader = new VideoLoader();
this.audioManager = new AudioManager();
this.subtitlesManager = new SubtitlesManager();
this.codecManager = new CodecManager();
}
public void playVideo(String video, String format, String audioTrack, String subtitleLang) {
videoLoader.load(video);
codecManager.decodeVideo(format);
audioManager.setAudioTrack(audioTrack);
subtitlesManager.enableSubtitles(subtitleLang);
System.out.println("Playing video: " + video);
}
}
// Step 3: Client Code
public class FacadePatternExample {
public static void main(String[] args) {
VideoPlayerFacade videoPlayer = new VideoPlayerFacade();
videoPlayer.playVideo("example.mp4", "MP4", "English", "Spanish");
}
}
Proxy Pattern
What is the Proxy Pattern?
The Proxy Pattern is a structural design pattern that acts as a substitute or intermediary for another object. It controls access to the actual object by adding an additional layer of control, such as authentication, logging, or caching.
When to Use the Proxy Pattern?
When restricting or controlling access to an object.
When implementing lazy initialization or caching.
When adding security features like authentication and authorization.
Real-World Example: Secure File Access
Imagine a system where users need to access files, but access should be restricted based on user roles.
// Step 1: Define the Subject Interface
interface FileAccess {
void accessFile(String filename);
}
// Step 2: Create the Real Subject
class RealFileAccess implements FileAccess {
@Override
public void accessFile(String filename) {
System.out.println("Accessing file: " + filename);
}
}
// Step 3: Create the Proxy Class
class FileAccessProxy implements FileAccess {
private RealFileAccess realFileAccess;
private String userRole;
public FileAccessProxy(String userRole) {
this.userRole = userRole;
}
@Override
public void accessFile(String filename) {
if ("ADMIN".equalsIgnoreCase(userRole)) {
if (realFileAccess == null) {
realFileAccess = new RealFileAccess();
}
realFileAccess.accessFile(filename);
} else {
System.out.println("Access denied. Only ADMIN can access files.");
}
}
}
// Step 4: Client Code
public class ProxyPatternExample {
public static void main(String[] args) {
FileAccess adminAccess = new FileAccessProxy("ADMIN");
adminAccess.accessFile("secure_document.pdf");
FileAccess userAccess = new FileAccessProxy("USER");
userAccess.accessFile("secure_document.pdf");
}
}