allthingsare🅿️.com Books ⬅️ Back allthingsare

2.2 Advanced Patterns – Structural & Behavioral


Learning Goal:
• Understand structural and behavioral design patterns to manage complexity and flexibility.


Detailed Content:
• Adapter pattern: interface conversion
• Proxy pattern: controlling access, lazy loading
• Observer pattern: publish/subscribe mechanisms
• Strategy pattern: encapsulating interchangeable behaviors
• Case studies: when to apply which pattern
• Hands-on mini refactoring practice


2.2.1 Adapter Pattern: Interface Conversion

As software grows, you often find yourself needing to connect parts that were never designed to work together. This is where the Adapter pattern comes in—a classic structural pattern used to bridge incompatible interfaces.

Imagine you have an old payment system that exposes a method called processPayment(), but your new system expects a method named makePayment(). Rather than rewriting the legacy code or tightly coupling your new code to the old naming, you write an Adapter that sits in between, translating one interface to another.

For example, in Java:

java

// Old interface
class OldPaymentGateway { public void processPayment(double amount) { System.out.println("Processing payment: $" + amount); } } // New expected interface interface NewPaymentGateway { void makePayment(double amount); } // Adapter class PaymentAdapter implements NewPaymentGateway { private OldPaymentGateway oldGateway; public PaymentAdapter(OldPaymentGateway oldGateway) { this.oldGateway = oldGateway; } @Override public void makePayment(double amount) { oldGateway.processPayment(amount); } }

This pattern is common when integrating legacy systems, third-party APIs, or external libraries. The Adapter keeps your system clean and consistent, even when underlying components do not match up perfectly.


2.2.2 Proxy Pattern: Controlling Access and Lazy Loading

The Proxy pattern is another structural pattern that acts as a stand-in for another object, controlling access to it or adding extra behavior. A proxy is like a gatekeeper that decides whether and when to forward a request to the real object.

One classic use case is lazy loading—delaying the creation of a resource-heavy object until it’s truly needed. For example, suppose you have a large image that shouldn’t be loaded until a user actually opens it.

java

interface Image {
void display(); } class RealImage implements Image { private String filename; public RealImage(String filename) { this.filename = filename; loadFromDisk(); } private void loadFromDisk() { System.out.println("Loading " + filename); } public void display() { System.out.println("Displaying " + filename); } } class ProxyImage implements Image { private RealImage realImage; private String filename; public ProxyImage(String filename) { this.filename = filename; } public void display() { if (realImage == null) { realImage = new RealImage(filename); } realImage.display(); } }

In this example, the ProxyImage delays loading the actual RealImage until display() is called. Proxies are also used for access control (security checks), logging, or remote objects in distributed systems.


2.2.3 Observer Pattern: Publish/Subscribe Mechanisms

The Observer pattern is one of the most widely used behavioral patterns, especially when you need parts of a system to react automatically to changes in other parts. It defines a one-to-many relationship: when the state of an object changes, all its dependents (observers) are notified automatically.

Consider a simple example of a weather station:

java

interface Observer {
void update(float temperature); } interface Subject { void addObserver(Observer o); void removeObserver(Observer o); void notifyObservers(); } class WeatherStation implements Subject { private List<Observer> observers = new ArrayList<>(); private float temperature; public void addObserver(Observer o) { observers.add(o); } public void removeObserver(Observer o) { observers.remove(o); } public void setTemperature(float temp) { this.temperature = temp; notifyObservers(); } public void notifyObservers() { for (Observer o : observers) { o.update(temperature); } } } class PhoneDisplay implements Observer { public void update(float temp) { System.out.println("Phone display: Temp is " + temp); } }

In modern systems, the Observer pattern appears in GUIs (button click listeners), event buses, and even reactive programming frameworks.


2.2.4 Strategy Pattern: Encapsulating Interchangeable Behaviors

The Strategy pattern is a behavioral pattern that defines a family of algorithms, encapsulates each one, and makes them interchangeable at runtime. Instead of hardcoding behavior, you define it as a strategy that can be swapped in and out.

Suppose you have a payment system that supports multiple discount policies. Instead of stuffing all conditions into if-else branches, you can use the Strategy pattern.

java

interface DiscountStrategy {
double applyDiscount(double price); } class NoDiscount implements DiscountStrategy { public double applyDiscount(double price) { return price; } } class SeasonalDiscount implements DiscountStrategy { public double applyDiscount(double price) { return price * 0.9; // 10% off } } class ShoppingCart { private DiscountStrategy discountStrategy; public ShoppingCart(DiscountStrategy discountStrategy) { this.discountStrategy = discountStrategy; } public double checkout(double price) { return discountStrategy.applyDiscount(price); } }

This makes it trivial to switch or add new discount strategies without rewriting the ShoppingCart logic. It keeps the code open to extension but closed to modification—a core principle of clean architecture.


2.2.5 Case Studies: Choosing the Right Pattern

In practice, the challenge is not just knowing patterns but knowing when to use them. For example, when integrating legacy and modern code, the Adapter pattern is usually your friend. When working with resource-heavy objects or security-sensitive resources, a Proxy is perfect. If you are building event-driven or real-time systems, Observer is natural. When you need flexible, pluggable algorithms—like multiple payment options, compression formats, or sorting methods—the Strategy pattern keeps the code organized and testable.

Good architects and developers see these recurring scenarios and reach for the right pattern like a craftsman reaches for the right tool.


2.2.6 Hands-On Mini Refactoring Practice

To truly grasp these patterns, small refactoring exercises help solidify the concepts. Take a naive implementation that mixes multiple concerns—like a class that checks a user’s permissions and loads data directly. Refactor it using a Proxy to handle access control.

Or take a block of duplicated conditional logic that switches behavior: extract it into a Strategy to make the behavior pluggable. Sketch out an event notification system for a chat app using the Observer pattern. Draw a quick Adapter for converting incompatible API responses.

Patterns become second nature only when you see how they turn messy, tangled logic into clear, testable modules—each with one reason to change.