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
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:
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.
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.
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.
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:
In modern systems, the Observer pattern appears in GUIs (button click listeners), event buses, and even reactive programming frameworks.
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.
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.
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.
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.