The Programmer's Guide
  • About
  • Algorithm
    • Big O Notation
      • Tree
      • Problems
    • Basic Notes
    • Data Structure Implementation
      • Custom LinkedList
      • Custom Stack
      • Custom Queue
      • Custom Tree
        • Binary Tree Implementation
        • Binary Search Tree Implementation
        • Min Heap Implementation
        • Max Heap Implementation
        • Trie Implementation
      • Custom Graph
        • Adjacency List
        • Adjacency Matrix
        • Edge List
        • Bidirectional Search
    • Mathematical Algorithms
      • Problems - Set 1
      • Problems - Set 2
    • Bit Manipulation
      • Representation
      • Truth Tables
      • Number System
        • Java Program
      • Problems - Set 1
    • Searching
    • Sorting
    • Array Algorithms
    • String Algorithms
    • Tree
      • Tree Traversal Techniques
      • Tree Implementation
      • Applications of Trees
      • Problems - Set 1
    • Graph
      • Graph Traversal Techniques
      • Shortest Path Algorithms
      • Minimum Spanning Tree (MST) Algorithms
    • Dynamic Programming
      • Problems - Set 1
    • Recursion
    • Parallel Programming
    • Miscellaneous
      • Problems - Set 1
  • API
    • API Basics
      • What is an API?
      • Types of API
        • Comparison - TBU
      • Synchronous vs Asynchronous API
    • API Architecture
      • Synchronous & Asynchronous Communication
    • API Specification
  • Cloud Computing
    • Cloud Fundamentals
      • Cloud Terminology
      • Core Terminology
      • Cloud Models
      • Cloud Service Models
      • Benefits, Challenges and Risk of Cloud Computing
      • Cloud Ecosystem
  • Database
    • DBMS
      • Types of DBMS
        • Relational DBMS (RDBMS)
        • NoSQL DBMS
        • Object-Oriented DBMS (OODBMS)
        • Columnar DBMS
        • In-Memory DBMS
        • Distributed DBMS
        • Cloud-Based DBMS
        • Hierarchical DBMS
      • DBMS Architecture
      • DBMS Structure
    • SQL Databases
      • Terminology
      • RDBMS Concepts
        • Entity Relationship Diagram (ERD)
          • ERD Examples
        • Normalization
        • Denormalization
        • ACID & BASE Properties
          • ACID Properties
          • BASE Properties
        • Locking and Unlocking
      • SQL Fundamentals
        • SQL Commands
          • DDL (Data Definition Language)
          • DML (Data Manipulation Language)
          • DCL (Data Control Language)
          • TCL (Transaction Control Language)
          • DQL (Data Query Language)
        • SQL Operators
          • INTERSECT
          • EXCEPT
          • MINUS
          • IN and NOT IN
          • EXISTS and NOT EXISTS
        • SQL Clauses
          • Joins
          • OVER
          • WITH
          • CONNECT BY
          • MODEL
          • FETCH FIRST
          • KEEP
          • OFFSET with FETCH
        • SQL Functions
          • Oracle Specific
        • SQL Data Types
          • Numeric Types
          • Character Types
          • Date & Time Types
          • Large Object Types
        • Others
          • Indexing
      • Vendor Specific Concepts
        • Oracle Specific
          • Data Types
          • Character Set
          • Rownum, Rowid, Urowid
          • Order of Execution of the query
          • Keys
          • Tablespace
          • Partition
      • Best Practice
      • Resources & References
        • O’Reilly SQL Cookbook (2nd Edition)
          • 1. Retrieving Records
          • 2. Sorting Query Results
          • 3. Working with Multiple Tables
          • 4. Inserting, Updating, and Deleting
          • 5. Metadata Queries
          • 6. Working with Strings
          • 7. Working with Numbers
          • 8. Date Arithmetic
          • 9. Date Manipulation
          • 10. Working with Ranges
          • 11. Advanced Searching
          • 12. Reporting and Reshaping
          • 13. Hierarchical Queries
          • 14. Odds 'n' Ends
    • SQL vs NoSQL
    • Best Practices
  • Git
    • Commands
      • Setup and Configuration Commands
      • Getting and Creating Projects
      • Tracking Changes
      • Branching and Merging
      • Sharing and Updating Projects
      • Inspection and Comparison
      • Debugging
      • Patching
      • Stashing and Cleaning
      • Advanced Manipulations
    • Workflows
      • Branching Strategies
        • Git Flow
        • Trunk-Based Development
        • GitHub Flow
        • Comparison
      • Merge Strategies
        • Merge
        • Rebase
        • Squash
        • Fast-forward vs No-fast-forward
        • MR vs PR
      • Conflict Resolution
        • Handling Merge Conflicts
        • Merge Conflicts
        • Rebase Conflicts
        • Divergent Branches After git pull
        • Force Push
      • Patch & Recovery
        • Cherry-pick strategies
        • Revert vs Reset
        • Recover from a bad rebase
      • Rebasing Practices
        • Merge vs Rebase
        • Rebase develop branch on main branch
      • Repository Management
        • Working Directory
        • Mirror a repository
        • Convert a local folder to a Git repo
        • Backup and restore a Git repository
  • Java
    • Java Installation
    • Java Distributions
    • Java Platform Editions
      • Java SE
      • Java EE
      • Jakarta EE
      • Java ME
      • JavaFX
    • Java Overview
      • OOP Principles
        • Encapsulation
        • Inheritance
        • Polymorphism
        • Abstraction
          • Abstract Class & Method
          • Interface
            • Functional Interfaces
            • Marker Interfaces
          • Abstract Class vs Interface
      • OOP Basics
        • What is a Class?
          • Types of Classes
        • What is an Object?
          • Equals and HashCode
            • FAQ
          • Shallow Copy and Deep Copy
          • Ways to Create Object
          • Serialization & Deserialization
        • Methods & Fields
          • Method Overriding & Overloading
          • Method Signature & Header
          • Variables
        • Constructors
        • Access Modifiers
      • Parallelism & Concurrency
        • Ways to Identify Thread Concurrency or Parallelism
        • Thread Basics
          • Thread vs Process
          • Creating Threads
          • Thread Context Switching
          • Thread Lifecycle & States
          • Runnable & Callable
          • Types of Threads
          • Thread Priority
        • Thread Management & Synchronisation
          • Thread Resource Sharing
          • Thread Synchronization
            • Why is Synchronization Needed?
            • Synchronized Blocks & Methods
          • Thread Lock
            • Types of Locks
            • Intrinsic Lock (Monitor Lock)
            • Reentrant Lock
          • Semaphore
          • Thread Starvation
          • Thread Contention
          • Thread Deadlock
          • Best Practices for Avoiding Thread Issues
      • Keywords
        • this
        • super
        • Access Modifiers
      • Data Types
        • Default Values
        • Primitive Types
          • byte
          • short
          • int
          • long
          • float
          • double
          • char
          • boolean
        • Non-Primitive (Reference) Types
          • String
            • StringBuilder
            • StringBuffer
              • Problems
            • Multiline String
            • Comparison - String, StringBuilder & StringBuffer
          • Array
          • Collections
            • List
              • Array vs List
              • ArrayList
              • Vector
                • Stack
                  • Problems
              • LinkedList
            • Queue
              • PriorityQueue
              • Deque (Double-Ended Queue)
                • ArrayDeque
                • ConcurrentLinkedDeque - TBU
                • LinkedBlockingDeque - TBU
            • Map
              • HashMap
              • Hashtable
              • LinkedHashMap
              • ConcurrentHashMap
              • TreeMap
              • EnumMap
              • WeakHashMap
            • Set
              • HashSet
              • LinkedHashSet
              • TreeSet
              • EnumSet
              • ConcurrentSkipListSet
              • CopyOnWriteArraySet
        • Specialized Classes
          • BigInteger
          • BigDecimal
            • Examples
          • BitSet
          • Date and Time
            • Examples
          • Optional
          • Math
          • UUID
          • Scanner
          • Formatter
            • Examples
          • Properties
          • Regex (Pattern and Matcher)
            • Examples
          • Atomic Classes
          • Random
          • Format
            • NumberFormat
            • DateFormat
            • DecimalFormat
        • Others
          • Object
          • Enum
            • Pre-Defined Enum
            • Custom Enum
            • EnumSet and EnumMap
          • Record
          • Optional
          • System
          • Runtime
          • ProcessBuilder
          • Class
          • Void
          • Throwable
            • Error
            • Exception
              • Custom Exception Handling
              • Best Practice
            • Error vs Exception
            • StackTraceElement
    • Java Features by Version
      • How New Java Features are Released ?
      • Java Versions
        • Java 8
        • Java 9
        • Scoped Values
        • Unnamed Variables & Patterns
      • FAQ
    • Concepts
      • Set 1
        • Streams
          • flatmap
          • Collectors Utility Class
          • Problems
        • Functional Interfaces
          • Standard Built-In Interfaces
          • Custom Interfaces
        • Annotation
          • Custom Annotation
          • Meta Annotation
        • Generics
          • Covariance and Invariance
        • Asynchronous Computation
          • Future
          • CompletableFuture
          • Future v/s CompletableFuture
          • ExecutorService
            • Thread Pool
            • Types of Work Queues
            • Rejection Policies
            • ExecutorService Implementations
            • ExecutorService Usage
          • Locks, Atomic Variables, CountDownLatch, CyclicBarrier - TBU
          • Parallel Streams, Fork/Join Framework,Stream API with Parallelism - TBU
      • Set 2
        • Standards
          • ISO Standards
          • JSR
            • JSR 303, 349, 380 (Bean Validation)
        • Operator Precedence
      • Set 3
        • Date Time Formatter
        • Validation
      • Set 4
        • Input from User
        • Comparison & Ordering
          • Object Equality Check
          • Comparable and Comparator
            • Comparator Interface
          • Sorting of Objects
          • Insertion Ordering
    • Packages
      • Core Packages
        • java.lang
          • java.lang.System
          • java.lang.Thread
      • Jakarta Packages
        • jakarta.validation
        • javax.validation
      • Third-party Packages
    • Code Troubleshoot
      • Thread Dump
      • Heap Dump
    • Code Quality & Analysis
      • ArchUnit
      • Terminologies
        • Cyclic dependencies
    • Code Style
      • Naming Convention
      • Package Structure
      • Formatting
      • Comments and Documentation
      • Imports
      • Exception Handling
      • Class Structure
      • Method Guidelines
      • Page 1
      • Code Smells to Avoid
      • Lambdas and Streams Style
      • Tools
    • Tools
      • IntelliJ IDEA
        • Shortcuts for MAC
      • Apache JMeter
        • Examples
      • Thread Dump Capture
        • jstack
        • VisualVM - TBU
        • jcmd - TBU
        • JConsole - TBU
        • YourKit Java Profiler - TBU
        • Eclipse MAT - TBU
        • IntelliJ IDEA Profiler - TBU
        • AppDynamics - TBU
        • Dynatrace - TBU
        • Thread Dump Analyzers - TBU
      • Heap Dump Capture
        • jmap
        • VisualVM - TBU
        • jcmd - TBU
        • Eclipse MAT (Memory Analyzer Tool) - TBU
        • IntelliJ IDEA Profiler - TBU
        • YourKit Java Profiler - TBU
        • AppDynamics - TBU
        • Dynatrace - TBU
        • Kill -3 Command - TBU
        • jhat (Java Heap Analysis Tool) - TBU
        • JVM Options - TBU
      • Wireshark
        • Search Filters
    • Best Practices
      • Artifact and BOM Versioning
  • Maven
    • Installation
    • Local Repository & Configuration
    • Command-line Options
    • Build & Lifecycle
    • Dependency Management
      • Dependency
        • Transitive Dependency
        • Optional Dependency
      • Dependency Scope
        • Maven Lifecycle and Dependency Scope
      • Dependency Exclusions & Overrides
      • Bill of Materials (BOM)
      • Dependency Conflict Resolution
      • Dependency Tree & Analysis
      • Dependency Versioning Strategies
    • Plugins
      • Build Lifecycle Management
      • Dependency Management
      • Code Quality and Analysis
      • Documentation Generation
      • Code Generation
      • Packaging and Deployment
      • Reporting
      • Integration and Testing
      • Customization and Enhancement
        • build-helper-maven-plugin
        • properties-maven-plugin
        • ant-run plugin
        • exec-maven-plugin
        • gmavenplus-plugin
      • Performance Optimization
    • FAQs
      • Fixing Maven SSL Issues: Unable to Find Valid Certification Path
  • Spring
    • Spring Basics
      • What is Spring?
      • Why Use Spring
      • Spring Ecosystem
      • Versioning
      • Setting Up a Spring Project
    • Core Concepts
      • Spring Core
        • Dependency Injection (DI)
        • Stereotype Annotation
      • Spring Beans
        • Bean Lifecycle
        • Bean Scope
          • Singleton Bean
        • Lazy & Eager Initialization
          • Use Case of Lazy Initialization
        • BeanFactory
        • ApplicationContext
      • Spring Annotations
        • Spring Boot Specific
        • Controller Layer (Web & REST Controllers)
    • Spring Features
      • Auto Configuration
        • Spring Boot 2: spring.factories
        • Spring Boot 3: spring.factories
      • Spring Caching
        • In-Memory Caching
      • Spring AOP
        • Before Advice
        • After Returning Advice
        • After Throwing Advice
        • After (finally) Advice
        • Around Advice
      • Spring File Handling
      • Reactive Programming
        • Reactive System
        • Reactive Stream Specification
        • Project Reactor
          • Mono & Flux
      • Asynchronous Computation
        • @Async annotation
      • Spring Security
        • Authentication
          • Core Components
            • Security Filter Chain
              • HttpSecurity
              • Example
            • AuthenticationManager
            • AuthenticationProvider
            • UserDetailsService
              • UserDetails
              • PasswordEncoder
            • SecurityContext
            • SecurityContextHolder
            • GrantedAuthority
            • Security Configuration (Spring Security DSL)
          • Authentication Models
            • One-Way Authentication
            • Mutual Authentication
          • Authentication Mechanism
            • Basic Authentication
            • Form-Based Authentication
            • Token-Based Authentication (JWT)
            • OAuth2 Authentication
            • Multi-Factor Authentication (MFA)
            • SAML Authentication
            • X.509 Certificate Authentication
            • API Key Authentication
            • Remember-Me Authentication
            • Custom Authentication
          • Logout Handling
        • Authorization
        • Security Filters and Interceptors
        • CSRF
          • Real-World CSRF Attacks & Prevention
        • CORS
        • Session Management and Security
        • Best Practices
      • Spring Persistence
        • JDBC
          • JDBC Components
          • JDBC Template
          • Transaction Management
          • Best Practices in JDBC Usage
          • Datasource
            • Connection Pooling
              • HikariCP
            • Caching
        • JPA (Java Persistence API)
          • JPA Fundamentals
          • ORM Mapping Annotations
            • 1. Entity and Table Mappings
            • 2. Field/Column Mappings
            • 3. Relationship Mappings
            • 4. Inheritance Mappings
            • 5. Additional Configuration Annotations
          • Querying Data
            • JPQL
            • Criteria API
            • JPA Specification
              • Example - Employee Portal
            • Native SQL Queries
            • Named Queries
            • Query Return Types
            • Pagination & Sorting
              • Example - Employee Portal
            • Projection
          • Fetch Strategies in JPA
        • JPA Implementation
          • Hibernate
            • Properties
            • Example
        • Spring Data JPA
          • Repository Abstractions
          • Entity-to-Table Mapping
          • Derived Query Methods
        • Cross-Cutting Concerns
          • Transactions
          • Caching
          • Concurrency
        • Examples
          • Employee Portal
            • API
    • Distributed Systems & Communication
      • Distributed Scheduling
      • Inter-Service Communication
        • 1. RestTemplate
        • 2. WebClient
        • 3. OpenFeign
        • Retry Mechanism
          • @Retryable annotation
            • Example
    • Security & Data Protection
      • Encoding | Decoding
        • Types
          • Base Encoding
            • Base16 - TBD
              • Encoding and Decoding in Java - TBD
            • Base32
              • Encoding and Decoding in Java
            • Base64 -TBD
              • Encoding and Decoding in Java - TBD
          • Text Encoding - TBD
            • Extended ASCII
              • Encoding and Decoding in Java - TBD
                • ISO-8859-1
                • Windows-1252 - TBD
                • IBM Code Pages - TBD
            • ASCII
              • Encoding and Decoding in Java
        • Java Guidelines
          • Text Encoding Decoding Examples
          • Base Encoding Decoding Examples
          • Best Practices and Concepts
          • Libraries
      • Cryptography
        • Terminology
        • Java Cryptography Architecture (JCA)
        • Key Management
          • Key Generation
            • Tools and Libraries
              • OpenSSL
              • Java Keytool
                • Concept
                • Use Cases
            • Key & Certificate File Formats
          • Key Distribution
          • Key Storage
          • Key Rotation
          • Key Revocation
        • Encryption & Decryption
          • Symmetric Encryption
            • Algorithm
            • Modes of Operation
            • Examples
          • Asymmetric Encryption
            • Algorithm
            • Mode of Operation
            • Examples
    • Utilities & Libraries
      • Apache Libraries
        • Apache Camel
          • Camel Architecture
            • Camel Context
            • Camel Endpoints
            • Camel Components
            • Camel Exchange & MEP
          • Spring Dependency
          • Different Components
            • Camel SFTP
        • Apache Commons Lang
      • MapStruct Mapper
      • Utilities by Spring framework
        • FileCopyUtils
    • General Concepts
      • Spring Boot Artifact Packaging
      • Classpath and Resource Loading
      • Configuration - Mapping Properties to Java Class
      • Validations in Spring Framework
        • Jakarta Validation
          • Jakarta Bean Validation Annotations
    • Practical Guidelines
      • Spring Configuration
      • Spring Code Design
  • Software Testing
    • Software Testing Methodologies
      • Functional Testing
      • Non Functional Testing
    • Software Testing Life Cycle (STLC)
    • Integration Test
      • Dynamic Property Registration
    • Java Test Framework
      • JUnit
        • JUnit 4
          • Examples
        • JUnit 5
          • Examples
        • JUnit 4 vs JUnit 5
  • System Design
    • Foundations
      • Programming Paradigms
      • Object-Oriented Design
        • SOLID Principles
        • GRASP Principles
        • Composition
        • Aggregation
        • Association
      • Design Pattern
        • Creational Pattern
        • Structural Pattern
        • Behavioral Pattern
        • Examples
          • Data Collector
          • Payment Processor
        • Design Enhancements
          • Fluent API Design
            • Examples
    • Architectural Building Blocks
      • CAP Theorem
      • Load Balancer
        • Load Balancer Architecture
        • Load Balancing in Java Microservices
          • Client-Side Load Balancing Example
          • Server-Side Load Balancing Example
        • Load Balancer Monitoring Tool
      • Scaling
        • Vertical Scaling (Scaling Up)
        • Horizontal Scaling (Scaling Out)
        • Auto-Scaling
        • Database Scaling via Sharding
      • Caching
        • Pod-Level vs Distributed Caching
      • Networking Metrics
        • Types of Delay
        • Scenario
      • System Characteristics
      • Workload Types
      • Resilience & Failure Handling
    • Performance
      • Why Is My API Sometimes Slow ?
    • Security
      • Security by Design
      • Zero Trust Security Model
      • Zero Trust Architecture
      • Principles
        • CIA
        • Least Privilege Principle
        • Defense in Depth
      • Security Threats & Mitigations
        • OWASP
          • Top 10 Security Threats
          • Application Security Verification Standard
          • Software Assurance Maturity Model
          • Dependency Check
          • CSRFGuard
          • Cheat Sheets
          • Security Testing Guide
          • Threat Dragon
        • Threat Modeling
      • Compliance & Regulation
        • PCI DSS
    • Deployment Patterns
    • Diagrams
      • UML Diagrams
        • PlantUML
          • Class Diagram
          • Object Diagram
          • Sequence Diagram
          • Use Case Diagram
          • Activity Diagram
          • State Diagram
          • Architecture Diagram
          • Component Diagram
          • Timing Diagram
          • ER Diagram (Entity-Relationship)
          • Network Diagram
    • Common Terminologies
    • Problems
      • Reference Materials
      • Cache Design
  • Interview Guide
    • Non-Technical
      • Behavioural or Introductory Guide
      • Project Specific
    • Technical
      • Java Interview Companion
        • Java Key Concepts
          • Set 1
          • Set 2
        • Java Code Snippets
        • Java Practice Programs
          • Set 3 - Strings
          • Set 4 - Search
          • Set 5 - Streams and Collection
      • SQL Interview Companion
        • SQL Practice Problems
          • Set 1
      • Spring Interview Companion
        • Spring Key Concepts
          • Set 1 - General
          • Set 2 - Core Spring
        • Spring Code Snippets
          • JPA
      • Application Server
      • Maven
      • Containerized Application
      • Microservices
    • General
      • Applicant Tracking System (ATS)
      • Flowchart - How to Solve Coding Problem?
Powered by GitBook
On this page
  • Description
  • Types of Structural Pattern
  • Adapter Pattern
  • Bridge Pattern
  • Composite Pattern
  • Decorator Pattern
  • Facade Pattern
  • Flyweight Pattern
  • Proxy Pattern

Was this helpful?

  1. System Design
  2. Foundations
  3. Design Pattern

Structural Pattern

Description

Structural patterns in software engineering deal with the composition of classes or objects to form larger structures while keeping the system flexible and efficient. These patterns focus on how classes and objects are connected or structured to provide new functionality or improve system architecture. Structural patterns often involve creating interfaces or abstract classes that define the structure of the system and providing implementations that realize this structure.

Types of Structural Pattern

Adapter Pattern

Description

The Adapter Pattern is a structural design pattern that allows objects with incompatible interfaces to work together. It acts as a bridge between two incompatible interfaces, converting the interface of a class into another interface that a client expects. The Adapter Pattern allows classes to work together that couldn't otherwise because of incompatible interfaces, promoting code reuse and interoperability.

Imagine if we have two existing classes with incompatible interfaces (methods, properties) that need to collaborate. The Adapter Pattern bridges this gap by:

  1. Defining an Adapter Class: This class implements the target interface (the interface the client code expects) and also holds a reference to the incompatible object (the adaptee).

  2. Adapting the Interface: The adapter class implements the target interface methods by delegating the work to the adaptee's methods or by converting data as needed.

Benefits of Adapter Pattern

  • Promotes reusability: Allows you to reuse existing incompatible classes without modifying their code.

  • Improves maintainability: Keeps the core functionality of the incompatible class separate from the adapter, making changes easier.

  • Increases flexibility: Enables working with different implementations as long as they can be adapted to the target interface.

Drawbacks of Adapter Pattern

  • Increased complexity: Introduces an extra layer of abstraction (the adapter class) which can add complexity.

  • Potential performance overhead: Adapting method calls might introduce some overhead compared to direct calls.

  • Tight coupling to the adaptee: Changes to the adaptee's interface might require modifications in the adapter.

When to Use Adapter Pattern

The Adapter Pattern is suitable when:

  • You need to integrate with existing, incompatible classes or libraries.

  • You want to isolate the core functionality of a class from the way it's used by clients.

  • You anticipate needing to support different implementations that can be adapted to a common interface.

Example 1: Legacy printer interface and modern computer (client)

Consider a example where we have a legacy printer that only supports printing in plain text format, and we want to connect it to a modern computer that expects to print in PDF format. We can use the Adapter Pattern to create an adapter class that converts the modern computer's PDF printing interface into the legacy printer's plain text printing interface

We have target interface Printer that defines the interface expected by the client code for printing, an adaptee class LegacyPrinter that represents the legacy printer with a method to print in plain text format, an adapter class PrinterAdapter that implements the Printer interface and wraps the LegacyPrinter object.

// Target interface: Printer
interface Printer {
    void print(String text);
}

// Adaptee: LegacyPrinter
class LegacyPrinter {
    void printPlainText(String text) {
        System.out.println("Printing plain text: " + text);
    }
}

// Adapter: PrinterAdapter
class PrinterAdapter implements Printer {
    private LegacyPrinter legacyPrinter;

    public PrinterAdapter(LegacyPrinter legacyPrinter) {
        this.legacyPrinter = legacyPrinter;
    }

    @Override
    public void print(String text) {
        // Convert PDF text to plain text format
        String plainText = convertPDFtoPlainText(text);
        // Call the legacy printer's method to print plain text
        legacyPrinter.printPlainText(plainText);
    }

    private String convertPDFtoPlainText(String pdfText) {
        // Convert PDF text to plain text format (simplified for demonstration)
        return "Converted from PDF: " + pdfText;
    }
}

// Main Application class
public class Application {
    public static void main(String[] args) {
        // Create a legacy printer
        LegacyPrinter legacyPrinter = new LegacyPrinter();

        // Create a printer adapter for the legacy printer
        Printer printerAdapter = new PrinterAdapter(legacyPrinter);

        // Modern computer expects to print in PDF format
        String pdfText = "PDF Document Content";

        // Call the print method on the adapter to print PDF text using the legacy printer
        printerAdapter.print(pdfText);
    }
}

Example 2: Legacy payment processor and e-commerce application (client)

Consider a scenario where you want to use a legacy payment processor library (adaptee) with your new e-commerce application (client code). The legacy library might have methods like chargeCreditCard while your application expects a processPayment method.

The LegacyPaymentProcessor interface represents the incompatible library. The PaymentProcessor interface defines the expected interface for your application. The LegacyPaymentProcessorAdapter implements the PaymentProcessor and adapts the chargeCreditCard method to the processPayment method.

// Legacy Payment Processor Interface (incompatible with your application)
public interface LegacyPaymentProcessor {
  void chargeCreditCard(String cardNumber, double amount) throws PaymentException;
}

// Payment Processor you want to use in your application (target interface)
public interface PaymentProcessor {
  void processPayment(String paymentMethod, double amount) throws PaymentException;
}

// Adapter Class bridges the gap
public class LegacyPaymentProcessorAdapter implements PaymentProcessor {

  private LegacyPaymentProcessor legacyProcessor;

  public LegacyPaymentProcessorAdapter(LegacyPaymentProcessor legacyProcessor) {
    this.legacyProcessor = legacyProcessor;
  }

  @Override
  public void processPayment(String paymentMethod, double amount) throws PaymentException {
    if (paymentMethod.equals("credit_card")) {
      legacyProcessor.chargeCreditCard( /* extract card number from paymentMethod */, amount);
    } else {
      throw new UnsupportedOperationException("Only credit card payments supported");
    }
  }
}

// Client code (e-commerce application) uses the PaymentProcessor interface
public class ECommerce {
  private PaymentProcessor paymentProcessor;

  public ECommerce(PaymentProcessor paymentProcessor) {
    this.paymentProcessor = paymentProcessor;
  }

  public void makePurchase(double amount) throws PaymentException {
    paymentProcessor.processPayment("credit_card", amount);
  }
}

// Main Application class
public class Application {
  public static void main(String[] args) {
    LegacyPaymentProcessor legacyProcessor = new LegacyPaymentProcessorImpl(); // Legacy library
    PaymentProcessor adapter = new LegacyPaymentProcessorAdapter(legacyProcessor);
    ECommerce ecommerce = new ECommerce(adapter);
    ecommerce.makePurchase(100);
  }
}

Bridge Pattern

Description

The Bridge Pattern is a structural design pattern that separates the abstraction from its implementation so that they can vary independently. It allows to create two separate hierarchies one for abstraction (interface or abstract class) and one for implementation (concrete class) and then connect them together using composition. This pattern promotes loose coupling between abstraction and implementation, enabling changes in one part of the system without affecting the other.

Imagine if we have a system with a complex hierarchy of classes representing different functionalities (e.g., shapes with different colors). The Bridge Pattern promotes flexibility and maintainability by separating these concerns:

  1. Defining an Abstraction Interface: This interface defines the operations that can be performed on the object (e.g., draw a shape).

  2. Creating Concrete Implementations (Implementors): These classes implement the functionalities behind the abstraction (e.g., specific shapes like circle, square).

  3. Creating a Bridge Class: This class holds a reference to an implementor object and implements the abstraction interface. It delegates the actual work to the implementor object based on the chosen functionality.

Benefits of Bridge Pattern

  • Decoupling abstraction and implementation: Allows independent changes to both aspects without affecting the other.

  • Improved maintainability: Easier to modify or extend shapes and colors independently.

  • Increased flexibility: Enables creating new combinations of shapes and colors easily.

Drawbacks of Bridge Pattern

  • Increased complexity: Introduces additional classes (interfaces and bridge class) which can add complexity.

  • Potential performance overhead: Delegation through the bridge class might introduce some overhead compared to direct calls.

  • Overkill for simple scenarios: If the relationship between abstraction and implementation is straightforward, the pattern might be unnecessary.

When to Use Bridge Pattern

The Bridge Pattern is suitable when:

  • We need to decouple an abstraction from its implementation for independent variation.

  • We have a large hierarchy of classes with multiple variations (e.g., shapes with different behaviors and appearances).

  • We anticipate the need to extend the system with new functionalities (shapes, colors) in the future.

Example

Let's consider example of a remote control for electronic devices, such as TVs and DVD players. Each device (TV or DVD player) can have different functionalities (turn on/off, adjust volume, change channels, etc.). We can use the Bridge Pattern to separate the abstraction (remote control) from its implementation (devices) and allow them to vary independently.

// Abstraction: RemoteControl
abstract class RemoteControl {
    protected Device device;

    public RemoteControl(Device device) {
        this.device = device;
    }

    abstract void powerOn();
    abstract void powerOff();
    abstract void volumeUp();
    abstract void volumeDown();
    // Other abstract methods for controlling the device
}

// Implementor: Device
interface Device {
    void powerOn();
    void powerOff();
    void adjustVolume(int delta);
    // Other methods for device functionality
}

/ Concrete Implementor A: TV
class TV implements Device {
    @Override
    public void powerOn() {
        System.out.println("TV is powered on");
    }

    @Override
    public void powerOff() {
        System.out.println("TV is powered off");
    }

    @Override
    public void adjustVolume(int delta) {
        System.out.println("Adjusting TV volume by " + delta);
    }
    // Other methods specific to TV functionality
}

// Concrete Implementor B: DVDPlayer
class DVDPlayer implements Device {
    @Override
    public void powerOn() {
        System.out.println("DVD player is powered on");
    }

    @Override
    public void powerOff() {
        System.out.println("DVD player is powered off");
    }

    @Override
    public void adjustVolume(int delta) {
        // DVD player does not have volume control
    }
    // Other methods specific to DVD player functionality
}

// Refined Abstraction: BasicRemoteControl
class BasicRemoteControl extends RemoteControl {
    public BasicRemoteControl(Device device) {
        super(device);
    }

    @Override
    void powerOn() {
        device.powerOn();
    }

    @Override
    void powerOff() {
        device.powerOff();
    }

    @Override
    void volumeUp() {
        device.adjustVolume(1);
    }

    @Override
    void volumeDown() {
        device.adjustVolume(-1);
    }
    // Other methods for basic remote control functionality
}

// Using the bridge pattern in Main Application class
public class Application {
    public static void main(String[] args) {
        // Create a TV
        TV tv = new TV();

        // Create a basic remote control for the TV
        RemoteControl basicRemote = new BasicRemoteControl(tv);

        // Use the basic remote control to power on the TV and adjust its volume
        basicRemote.powerOn();
        basicRemote.volumeUp();
        basicRemote.volumeDown();
        basicRemote.powerOff();
    }
}

Composite Pattern

Description

The Composite Pattern is a structural design pattern that allows to compose objects into tree-like structures to represent part-whole hierarchies. It enables clients to treat individual objects and compositions of objects uniformly. In other words, clients can treat a single object and a group of objects in a uniform manner without distinguishing between them. This pattern is useful when you want to represent hierarchical structures of objects and apply operations uniformly across the entire hierarchy.

Imagine we have a complex system with objects that can be treated individually or as part of a larger group. The Composite Pattern allows you to handle them uniformly by:

  1. Defining a Component Interface: This interface declares the operations (methods) that both individual objects (leaves) and composite objects (containers) can perform. These operations might include adding or removing child components and performing actions on the component itself.

  2. Creating Concrete Classes: These classes implement the Component interface and represent individual objects (leaves) or composite objects (containers). Leaves typically implement the operations directly, while containers can delegate them to their child components.

Benefits of Composite Pattern

  • Uniform treatment of objects: Allows treating individual objects and composite objects in the same way.

  • Hierarchical representation: Models part-whole hierarchies effectively.

  • Flexible structure: Enables building complex structures by composing objects.

Drawbacks of Composite Pattern

  • Increased complexity: Introduces an extra layer of abstraction (the interface) which can add complexity.

  • Overkill for flat structures: If you only have flat collections of objects, the pattern might be unnecessary.

When to Use Composite Pattern

The Composite Pattern is suitable when:

  • We need to represent hierarchical structures where objects can be treated individually or as a whole.

  • We want to perform operations on entire branches of the hierarchy.

  • We anticipate needing to extend the structure with new types of objects

Example 1

Consider a file system where we have files and folders. Both files and folders can be treated as components in the hierarchy. The Composite Pattern allows to manage them uniformly.

The FileSystemComponent interface defines methods for managing and displaying files and folders. The File class implements the interface for individual files. The Folder class implements the interface for folders and can contain other components. Both files and folders can be treated uniformly using the displayInfo() method, which recursively traverses the tree structure for folders.

// Component Interface (what can be done with a file or folder)
public interface FileSystemComponent {
  void displayInfo(); // Display name/size for files, structure for folders
  void addComponent(FileSystemComponent component); // Applicable to folders
  void removeComponent(FileSystemComponent component); // Applicable to folders
}

// Concrete Class (Leaf - File)
public class File implements FileSystemComponent {

  private String name;
  private int size;

  public File(String name, int size) {
    this.name = name;
    this.size = size;
  }

  @Override
  public void displayInfo() {
    System.out.println("File: " + name + " (" + size + " bytes)");
  }

  // Not applicable for files (empty implementations)
  @Override
  public void addComponent(FileSystemComponent component) {}
  @Override
  public void removeComponent(FileSystemComponent component) {}
}

// Concrete Class (Container - Folder)
public class Folder implements FileSystemComponent {

  private String name;
  private List<FileSystemComponent> components;

  public Folder(String name) {
    this.name = name;
    this.components = new ArrayList<>();
  }

  @Override
  public void displayInfo() {
    System.out.println("Folder: " + name);
    for (FileSystemComponent component : components) {
      component.displayInfo(); // Delegate to child components
    }
  }

  @Override
  public void addComponent(FileSystemComponent component) {
    components.add(component);
  }

  @Override
  public void removeComponent(FileSystemComponent component) {
    components.remove(component);
  }
}

public class Main {
  public static void main(String[] args) {
    Folder documents = new Folder("Documents");
    documents.addComponent(new File("report.txt", 1024));
    Folder work = new Folder("Work");
    work.addComponent(new File("presentation.pdf", 5120));
    documents.addComponent(work);
    documents.displayInfo(); // Output shows structure and file details
  }
}

Example 2

Let's consider another example of an organization structure, where employees are organized into departments, and departments can contain both individual employees and sub-departments. We can use the Composite Pattern to represent the organization structure as a tree-like hierarchy, with departments and individual employees as nodes.

In this example, we have an interface Employee representing individual employees and departments in the organization structure, a leaf class IndividualEmployee representing individual employees, a composite class Department representing departments which can contain both individual employees and sub-departments and the Department class contains a list of employees (individual employees and sub-departments) and implements the displayDetails() method to display details of the department and its employees.

// Component: Employee
interface Employee {
    void displayDetails();
}

// Leaf: IndividualEmployee
class IndividualEmployee implements Employee {
    private String name;

    public IndividualEmployee(String name) {
        this.name = name;
    }

    @Override
    public void displayDetails() {
        System.out.println("Employee: " + name);
    }
}

// Composite: Department
class Department implements Employee {
    private String name;
    private List<Employee> employees = new ArrayList<>();

    public Department(String name) {
        this.name = name;
    }

    public void addEmployee(Employee employee) {
        employees.add(employee);
    }

    @Override
    public void displayDetails() {
        System.out.println("Department: " + name);
        for (Employee employee : employees) {
            employee.displayDetails();
        }
    }
}

// Use the composite in the Main Application class
public class Application {
    public static void main(String[] args) {
        // Create individual employees
        Employee employee1 = new IndividualEmployee("John");
        Employee employee2 = new IndividualEmployee("Alice");

        // Create sub-departments
        Department marketingDepartment = new Department("Marketing");
        Department salesDepartment = new Department("Sales");

        // Add employees to departments
        marketingDepartment.addEmployee(employee1);
        salesDepartment.addEmployee(employee2);

        // Create the organization structure
        Department headOffice = new Department("Head Office");
        headOffice.addEmployee(marketingDepartment);
        headOffice.addEmployee(salesDepartment);

        // Display details of the organization structure
        headOffice.displayDetails();
    }
}

Decorator Pattern

Description

The Decorator Pattern is a structural design pattern that allows behavior to be added to individual objects dynamically without affecting the behavior of other objects from the same class. It is useful when you want to add new functionalities to objects without altering their structure. The Decorator Pattern involves creating a set of decorator classes that are used to wrap concrete components. Each decorator class adds its own functionality to the component, which can be stacked on top of each other to create a combination of behaviors.

Imagine we have objects with functionalities that we want to modify or extend at runtime without changing their core implementation. The Decorator Pattern achieves this by:

  1. Defining a Component Interface: This interface declares the core functionality of the objects you want to decorate.

  2. Creating Concrete Component Classes: These classes implement the Component interface and represent the base objects with their core functionalities.

  3. Creating Decorator Classes: These classes implement the Component interface and "wrap" a concrete component object. They add new functionalities or modify the behavior of the wrapped object dynamically. Decorators typically hold a reference to the wrapped component and delegate calls to it while potentially adding their own behavior before or after.

Benefits of Decorator Pattern

  • Dynamic extension of functionality: Allows adding new functionalities to objects at runtime without modifying their original code.

  • Flexible composition: You can combine different decorators to achieve complex behavior.

  • Loose coupling: Decorators and components are loosely coupled, promoting maintainability.

Drawbacks of Decorator Pattern

  • Increased complexity: Introduces additional classes (decorators) which can add complexity.

  • Potential performance overhead: Decorator method calls can add some overhead compared to direct calls.

  • Can lead to long chains of decorators: Managing a large number of decorators might become cumbersome.

When to Use Decorator Pattern

The Decorator Pattern is suitable when:

  • We need to add functionalities to objects dynamically without subclassing.

  • We want to support multiple layers of optional functionality.

  • We anticipate the need to extend functionality in the future without modifying existing objects.

Example 1

Consider a example of a coffee ordering system, where customers can order various types of coffee with optional toppings such as milk, sugar, and whipped cream. We can use the Decorator Pattern to create decorators for each optional topping and then dynamically add them to the base coffee order.

In this example, we have an interface Coffee representing the base component for coffee orders, with methods to get the description and cost of the coffee. We have a concrete component BasicCoffee representing the basic coffee order without any toppings. We have an abstract decorator class CoffeeDecorator that implements the Coffee interface and wraps concrete components. We have concrete decorator classes Milk and Sugar that add milk and sugar toppings to the coffee order. We create a basic coffee order and then dynamically add milk and sugar toppings to it using decorators.

// Component: Coffee
interface Coffee {
    String getDescription();
    double cost();
}

// Concrete Component: BasicCoffee
class BasicCoffee implements Coffee {
    @Override
    public String getDescription() {
        return "Basic Coffee";
    }

    @Override
    public double cost() {
        return 2.0;
    }
}

// Decorator: CoffeeDecorator
abstract class CoffeeDecorator implements Coffee {
    protected Coffee coffee;

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

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

    @Override
    public double cost() {
        return coffee.cost();
    }
}

// Concrete Decorator: Milk
class Milk extends CoffeeDecorator {
    public Milk(Coffee coffee) {
        super(coffee);
    }

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

    @Override
    public double cost() {
        return coffee.cost() + 0.5;
    }
}

// Concrete Decorator: Sugar
class Sugar extends CoffeeDecorator {
    public Sugar(Coffee coffee) {
        super(coffee);
    }

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

    @Override
    public double cost() {
        return coffee.cost() + 0.3;
    }
}

// Main Application class
public class Application {
    public static void main(String[] args) {
        // Create a basic coffee order
        Coffee basicCoffee = new BasicCoffee();

        // Add milk and sugar toppings to the basic coffee order
        Coffee milkCoffee = new Milk(basicCoffee);
        Coffee milkSugarCoffee = new Sugar(milkCoffee);

        // Display description and cost of the decorated coffee order
        System.out.println("Description: " + milkSugarCoffee.getDescription());
        System.out.println("Cost: $" + milkSugarCoffee.cost());
    }
}

Example 2

Consider another example of a text editor where we can format text with functionalities like bold, italic, and underline. The Decorator Pattern allows to add these features dynamically:

In this example, the Text interface defines a method to get the text content. The PlainText class implements the interface for plain text. The TextDecorator is an abstract class that wraps a Text object and provides a base for concrete decorators. Concrete decorators like BoldDecorator and ItalicDecorator modify the text by adding HTML formatting tags

// Component Interface (what can be done with text)
public interface Text {
  String getText();
}

// Concrete Component Class (Plain Text)
public class PlainText implements Text {

  private String text;

  public PlainText(String text) {
    this.text = text;
  }

  @Override
  public String getText() {
    return text;
  }
}

// Decorator Class (adds functionality)
public abstract class TextDecorator implements Text {

  private Text text;

  public TextDecorator(Text text) {
    this.text = text;
  }

  @Override
  public abstract String getText();

  // Delegate to the wrapped text and potentially add decoration logic
  protected String decorate(String text) {
    return text;
  }
}

// Concrete Decorators (specific formatting)
public class BoldDecorator extends TextDecorator {

  public BoldDecorator(Text text) {
    super(text);
  }

  @Override
  public String getText() {
    return "<b>" + decorate(super.getText()) + "</b>";
  }
}

public class ItalicDecorator extends TextDecorator {

  public ItalicDecorator(Text text) {
    super(text);
  }

  @Override
  public String getText() {
    return "<i>" + decorate(super.getText()) + "</i>";
  }
}

// Main Application class
public class Main {
  public static void main(String[] args) {
    Text text = new PlainText("Hello World");
    Text boldText = new BoldDecorator(text);
    Text italicBoldText = new ItalicDecorator(boldText);
    System.out.println(italicBoldText.getText()); // Output: <i><b>Hello World</b></i>
  }
}

Facade Pattern

Description

The Facade Pattern is a structural design pattern that provides a simplified interface to a set of interfaces in a subsystem. It hides the complexities of the subsystem and provides a single interface that the client can use to interact with the subsystem. The Facade Pattern promotes loose coupling between the client and the subsystem by providing a high-level interface that shields the client from the details of the subsystem's implementation.

Imagine we have a complex system with many interacting objects and functionalities. The Facade Pattern provides a simplified interface (facade) to this complexity, hiding the underlying implementation details. Here's the approach:

  1. Defining a Facade Class: This class acts as a single point of entry for interacting with the subsystem. It provides a simplified set of methods that expose essential functionalities of the complex system.

  2. Facade Implementation: The facade class encapsulates the logic for interacting with the various objects within the subsystem. It might delegate calls to specific objects or orchestrate a sequence of operations to fulfill the requested functionality.

Benefits of Facade Pattern

  • Simplified interface: Provides a simpler and more user-friendly way to interact with a complex system.

  • Decoupling client code: Client code only interacts with the facade, hiding the internal implementation details.

  • Improved maintainability: Changes within the subsystem can be contained within the facade without affecting client code.

Drawbacks of Facade Pattern

  • Reduced flexibility: The facade might limit access to certain functionalities of the underlying objects.

  • Tight coupling between facade and subsystem: Changes in the subsystem might require modifications to the facade.

  • Potential complexity for large systems: For very large systems, managing a single facade class might become complex.

When to Use Facade Pattern

The Facade Pattern is suitable when:

  • We have a complex system with many interacting objects.

  • We want to provide a simplified interface for client code to interact with the system.

  • We need to decouple client code from the implementation details of the subsystem.

Example

Consider a example of a home theater system, which consists of various subsystems such as the DVD player, amplifier, speakers, and screen. Each subsystem may have its own complex interface. We can use the Facade Pattern to create a home theater facade that provides a simple interface for common operations such as watching a movie.

In this example, we have a facade class HomeTheaterFacade that provides a simple interface for common operations such as watching a movie and ending the movie. We have subsystem classes DVDPlayer, Amplifier, Speakers, and Screen that represent the individual components of the home theater system. The HomeTheaterFacade class encapsulates the interactions with the subsystems and hides the complexities of the subsystems' interfaces.

// Facade: HomeTheaterFacade
class HomeTheaterFacade {
    private DVDPlayer dvdPlayer;
    private Amplifier amplifier;
    private Speakers speakers;
    private Screen screen;

    public HomeTheaterFacade() {
        this.dvdPlayer = new DVDPlayer();
        this.amplifier = new Amplifier();
        this.speakers = new Speakers();
        this.screen = new Screen();
    }

    public void watchMovie(String movie) {
        System.out.println("Get ready to watch a movie...");
        dvdPlayer.turnOn();
        amplifier.turnOn();
        speakers.turnOn();
        screen.unroll();
        dvdPlayer.play(movie);
    }

    public void endMovie() {
        System.out.println("Shutting down the home theater system...");
        dvdPlayer.turnOff();
        amplifier.turnOff();
        speakers.turnOff();
        screen.rollUp();
    }
}

// Subsystem: DVDPlayer
class DVDPlayer {
    public void turnOn() {
        System.out.println("DVD player is powered on");
    }

    public void turnOff() {
        System.out.println("DVD player is powered off");
    }

    public void play(String movie) {
        System.out.println("Playing movie: " + movie);
    }
}

// Subsystem: Amplifier
class Amplifier {
    public void turnOn() {
        System.out.println("Amplifier is powered on");
    }

    public void turnOff() {
        System.out.println("Amplifier is powered off");
    }
    // Other amplifier methods
}

// Subsystem: Speakers
class Speakers {
    public void turnOn() {
        System.out.println("Speakers are powered on");
    }

    public void turnOff() {
        System.out.println("Speakers are powered off");
    }
    // Other speakers methods
}

// Subsystem: Screen
class Screen {
    public void unroll() {
        System.out.println("Screen is unrolled");
    }

    public void rollUp() {
        System.out.println("Screen is rolled up");
    }
    // Other screen methods
}

// Main Application class
public class Application {
    public static void main(String[] args) {
        // Create a home theater facade
        HomeTheaterFacade homeTheater = new HomeTheaterFacade();

        // Watch a movie using the home theater facade
        homeTheater.watchMovie("The Matrix");

        // End the movie using the home theater facade
        homeTheater.endMovie();
    }
}

Flyweight Pattern

Description

The Flyweight Pattern is a structural design pattern that aims to minimize memory usage and improve performance by sharing as much data as possible with similar objects. It is particularly useful when dealing with a large number of objects that have similar or identical intrinsic state, and when the extrinsic state can be managed externally.

Imagine we have an application that deals with a large number of similar objects. Each object might have some unique state, but also share a lot of common data or functionality. The Flyweight Pattern promotes memory efficiency by:

  1. Defining a Flyweight Interface: This interface declares the methods that all flyweight objects can perform.

  2. Creating Concrete Flyweight Classes: These classes implement the Flyweight interface and represent the intrinsic state (unchanging data) of the objects. They typically avoid storing any extrinsic state (data specific to each instance) within themselves.

  3. Creating a Flyweight Factory: This class (optional) is responsible for managing the pool of flyweight objects and ensuring efficient reuse. It might return existing flyweight objects with the appropriate intrinsic state or create new ones if necessary.

Benefits of Flyweight Pattern

  • Reduced memory usage: By sharing common data among objects, the Flyweight Pattern can significantly reduce memory consumption, especially for large numbers of similar objects.

  • Improved performance: Object creation can become faster as flyweight objects are reused instead of being created every time.

Drawbacks of Flyweight Pattern

  • Increased complexity: Introduces additional classes (flyweight factory) which can add complexity.

  • Limited applicability: Not suitable for objects with a lot of unique data or complex state management.

Example

Consider a game with many forest trees. Each tree might have a unique location (extrinsic state) but share the same image data (intrinsic state). The Flyweight Pattern can optimize memory usage.

In this example, the Tree interface defines a method to draw the tree. Concrete flyweight classes like OakTree and PineTree hold the image data (intrinsic state) and implement the draw method. The TreeFactory (optional) manages a pool of flyweight objects and provides a way to retrieve them based on the type. The Forest class uses the TreeFactory to plant trees, potentially reusing existing flyweight objects for the same tree type.

// Flyweight Interface (common operations)
public interface Tree {
  void draw(int x, int y); // Draw the tree at a specific location (extrinsic state)
}

// Concrete Flyweight Class (stores intrinsic state - image data)
public class OakTree implements Tree {

  private static final String imageData = "..."; // Load image data once

  @Override
  public void draw(int x, int y) {
    System.out.println("Drawing Oak Tree at (" + x + ", " + y + ")");
    // Use the pre-loaded image data to draw the tree
  }
}

// Concrete Flyweight Class (another example with different intrinsic state)
public class PineTree implements Tree {

  private static final String imageData = "..."; // Load image data once

  @Override
  public void draw(int x, int y) {
    System.out.println("Drawing Pine Tree at (" + x + ", " + y + ")");
    // Use the pre-loaded image data to draw the tree
  }
}

// Flyweight Factory (optional - manages object pool)
public class TreeFactory {

  private static Map<String, Tree> treePool = new HashMap<>();

  public static Tree getTree(String treeType) {
    Tree tree = treePool.get(treeType);
    if (tree == null) {
      switch (treeType) {
        case "oak":
          tree = new OakTree();
          break;
        case "pine":
          tree = new PineTree();
          break;
        // ... add other tree types
      }
      treePool.put(treeType, tree);
    }
    return tree;
  }
}

public class Forest {

  private List<Tree> trees;

  public Forest() {
    trees = new ArrayList<>();
  }

  public void plantTree(int x, int y, String treeType) {
    Tree tree = TreeFactory.getTree(treeType);
    tree.draw(x, y);
    trees.add(tree);
  }
}

public class Main {
  public static void main(String[] args) {
    Forest forest = new Forest();
    forest.plantTree(100, 50, "oak");
    forest.plantTree(200, 100, "pine");
    forest.plantTree(50, 150, "oak"); // Reuses existing OakTree object
  }
}

Proxy Pattern

Description

The Proxy Pattern is a structural design pattern that provides a surrogate or placeholder for another object to control access to it. It allows you to create a representative object (proxy) that controls the access to the original object (subject). The proxy object acts as an intermediary between the client and the real object, providing additional functionality such as lazy initialization, access control, logging, or caching.

Benefits of Proxy Pattern

  • Improved control: Provides an extra layer of control over access to the real subject.

  • Increased flexibility: Enables adding functionalities like caching, security checks, or lazy loading.

  • Decoupling client code: Client code only interacts with the proxy, hiding the details of the real subject.

Drawbacks of Proxy Pattern

  • Increased complexity: Introduces an extra layer of abstraction (the proxy class) which can add complexity.

  • Potential performance overhead: Method calls might have some overhead due to the extra layer of indirection through the proxy.

Example

Let's consider a example of internet access control in an organization, where employees need to access certain websites through a proxy server. The proxy server acts as an intermediary between the employees' computers and the external websites, controlling and monitoring the internet access.

In this example, we have a Internet interface that defines the common method connectTo() for connecting to websites. We have a real subject class RealInternet that implements the Internet interface and represents the real internet connection. We have a proxy class InternetProxy that implements the Internet interface and acts as a proxy for controlling access to the real internet connection. The InternetProxy class intercepts the requests to connect to websites and checks if the requested website is in the list of blocked websites.

// Subject: Internet
interface Internet {
    void connectTo(String website);
}

// Real Subject: RealInternet
class RealInternet implements Internet {
    @Override
    public void connectTo(String website) {
        System.out.println("Connecting to " + website);
    }
}

// Proxy: InternetProxy
class InternetProxy implements Internet {
    private Internet realInternet;
    private static final List<String> BLOCKED_WEBSITES = Arrays.asList("facebook.com", "twitter.com");

    public InternetProxy() {
        this.realInternet = new RealInternet();
    }

    @Override
    public void connectTo(String website) {
        if (BLOCKED_WEBSITES.contains(website.toLowerCase())) {
            System.out.println("Access to " + website + " is blocked");
        } else {
            realInternet.connectTo(website);
        }
    }
}

// Main Application class
public class Application {
    public static void main(String[] args) {
        Internet internet = new InternetProxy();

        // Allowed access
        internet.connectTo("google.com");

        // Blocked access
        internet.connectTo("facebook.com");
        internet.connectTo("twitter.com");
    }
}
PreviousCreational PatternNextBehavioral Pattern

Last updated 8 months ago

Was this helpful?