The Strategy Pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable at runtime. Instead of hardcoding behavior into a single class with conditionals, you extract each algorithm into its own class and let the client choose which one to use.
This is one of the most practical and frequently used design patterns in real-world software. If you have ever swapped out a payment method at checkout or changed a sorting algorithm based on data size, you have seen the Strategy Pattern in action.
Imagine you are building a payment processing system. Customers can pay with credit cards, PayPal, or cryptocurrency. The naive approach is to write a giant if-else or switch block inside a single method:
“If credit card, do X. If PayPal, do Y. If crypto, do Z.”
This works initially, but every time you add a new payment method (Apple Pay, bank transfer, etc.), you must modify the existing class. This violates the Open/Closed Principle, increases the risk of bugs, and makes the code harder to test because every payment path lives in the same method.
The Strategy Pattern solves this by extracting each payment algorithm into its own class that implements a common interface. A context class (the checkout processor) holds a reference to the current strategy and delegates the payment call to it. Adding a new payment method means creating a new class — no existing code changes required.
The context does not care how the payment is processed. It only knows that the strategy it holds can pay(). This decouples the “what” from the “how.”
Program to an interface, not an implementation. By depending on an abstraction (the strategy interface) rather than concrete payment classes, the system becomes flexible, testable, and open for extension without modification.
Scenario: A checkout system that supports multiple payment strategies.
// Strategy interface
public interface PaymentStrategy {
void pay(double amount);
String getPaymentMethod();
}
// Concrete Strategy: Credit Card
public class CreditCardPayment implements PaymentStrategy {
private final String cardNumber;
private final String cardHolder;
public CreditCardPayment(String cardNumber, String cardHolder) {
this.cardNumber = cardNumber;
this.cardHolder = cardHolder;
}
@Override
public void pay(double amount) {
String masked = "****-" + cardNumber.substring(cardNumber.length() - 4);
System.out.printf("Charged $%.2f to credit card %s (%s)%n",
amount, masked, cardHolder);
}
@Override
public String getPaymentMethod() {
return "Credit Card";
}
}
// Concrete Strategy: PayPal
public class PayPalPayment implements PaymentStrategy {
private final String email;
public PayPalPayment(String email) {
this.email = email;
}
@Override
public void pay(double amount) {
System.out.printf("Transferred $%.2f via PayPal to %s%n", amount, email);
}
@Override
public String getPaymentMethod() {
return "PayPal";
}
}
// Concrete Strategy: Cryptocurrency
public class CryptoPayment implements PaymentStrategy {
private final String walletAddress;
public CryptoPayment(String walletAddress) {
this.walletAddress = walletAddress;
}
@Override
public void pay(double amount) {
System.out.printf("Sent $%.2f in crypto to wallet %s%n",
amount, walletAddress);
}
@Override
public String getPaymentMethod() {
return "Cryptocurrency";
}
}
// Context
public class CheckoutProcessor {
private PaymentStrategy paymentStrategy;
public void setPaymentStrategy(PaymentStrategy strategy) {
this.paymentStrategy = strategy;
}
public void checkout(double amount) {
if (paymentStrategy == null) {
throw new IllegalStateException("Payment strategy not set");
}
System.out.println("Processing checkout...");
System.out.println("Payment method: " + paymentStrategy.getPaymentMethod());
paymentStrategy.pay(amount);
System.out.println("Checkout complete.\n");
}
}
// Usage
public class Main {
public static void main(String[] args) {
CheckoutProcessor processor = new CheckoutProcessor();
// Pay with credit card
processor.setPaymentStrategy(
new CreditCardPayment("4111111111111234", "John Doe"));
processor.checkout(99.99);
// Switch to PayPal at runtime
processor.setPaymentStrategy(
new PayPalPayment("john@example.com"));
processor.checkout(49.50);
// Switch to crypto
processor.setPaymentStrategy(
new CryptoPayment("0xABC123DEF456"));
processor.checkout(250.00);
}
}
Same scenario in Python, using abstract base classes and duck typing.
from abc import ABC, abstractmethod
# Strategy interface
class PaymentStrategy(ABC):
@abstractmethod
def pay(self, amount: float) -> None:
pass
@abstractmethod
def get_payment_method(self) -> str:
pass
# Concrete Strategy: Credit Card
class CreditCardPayment(PaymentStrategy):
def __init__(self, card_number: str, card_holder: str):
self.card_number = card_number
self.card_holder = card_holder
def pay(self, amount: float) -> None:
masked = f"****-{self.card_number[-4:]}"
print(f"Charged ${amount:.2f} to credit card {masked} ({self.card_holder})")
def get_payment_method(self) -> str:
return "Credit Card"
# Concrete Strategy: PayPal
class PayPalPayment(PaymentStrategy):
def __init__(self, email: str):
self.email = email
def pay(self, amount: float) -> None:
print(f"Transferred ${amount:.2f} via PayPal to {self.email}")
def get_payment_method(self) -> str:
return "PayPal"
# Concrete Strategy: Cryptocurrency
class CryptoPayment(PaymentStrategy):
def __init__(self, wallet_address: str):
self.wallet_address = wallet_address
def pay(self, amount: float) -> None:
print(f"Sent ${amount:.2f} in crypto to wallet {self.wallet_address}")
def get_payment_method(self) -> str:
return "Cryptocurrency"
# Context
class CheckoutProcessor:
def __init__(self):
self._payment_strategy: PaymentStrategy | None = None
def set_payment_strategy(self, strategy: PaymentStrategy) -> None:
self._payment_strategy = strategy
def checkout(self, amount: float) -> None:
if self._payment_strategy is None:
raise RuntimeError("Payment strategy not set")
print("Processing checkout...")
print(f"Payment method: {self._payment_strategy.get_payment_method()}")
self._payment_strategy.pay(amount)
print("Checkout complete.\n")
# Usage
if __name__ == "__main__":
processor = CheckoutProcessor()
# Pay with credit card
processor.set_payment_strategy(
CreditCardPayment("4111111111111234", "John Doe"))
processor.checkout(99.99)
# Switch to PayPal at runtime
processor.set_payment_strategy(
PayPalPayment("john@example.com"))
processor.checkout(49.50)
# Switch to crypto
processor.set_payment_strategy(
CryptoPayment("0xABC123DEF456"))
processor.checkout(250.00)
if-else or switch blocks that choose between behaviors based on type or configuration.Comparator — Collections.sort(list, comparator) lets you pass different sorting strategies.AuthenticationProvider — Different authentication strategies (LDAP, OAuth, database) implement the same interface.sorted() with key — The key parameter is a strategy function that determines sort order.The Decorator Pattern is a structural design pattern that lets you attach new behaviors to objects by wrapping them in special objects that contain the added behavior. Think of it like adding toppings to a coffee — you start with a base espresso, then wrap it with milk, then wrap that with whipped cream. Each layer adds something new without changing the original drink.
This pattern is incredibly powerful when you need to combine multiple optional features in various configurations without creating an explosion of subclasses.
Your application has a notification system that sends messages via email. New requirements come in: some notifications also need to go via SMS, some via Slack, some via email AND SMS, some via all three. Future channels like push notifications are already being discussed.
Creating a subclass for every combination (EmailAndSMS, EmailAndSlack, EmailSMSAndSlack) leads to a combinatorial explosion. With just 4 channels, you would need 15 different subclasses to cover all combinations.
Create a base notifier interface and a set of decorator classes — one per channel — that wrap any notifier and add their own sending logic. You can then stack decorators at runtime to compose exactly the combination you need. Need email + Slack? Wrap an email notifier with a Slack decorator. Need all three? Add another layer.
Each decorator delegates to the wrapped object first, then adds its own behavior, forming a chain of responsibility.
The Decorator Pattern follows the Open/Closed Principle — you can add new behaviors without modifying existing classes. It also demonstrates composition over inheritance, building complex behavior by combining simple, focused objects rather than creating deep class hierarchies.
Here we build a notification system where channels can be combined freely at runtime.
// Base interface
public interface Notifier {
void send(String message);
}
// Concrete base implementation
public class EmailNotifier implements Notifier {
private final String emailAddress;
public EmailNotifier(String emailAddress) {
this.emailAddress = emailAddress;
}
@Override
public void send(String message) {
System.out.println("[EMAIL] Sending to " + emailAddress + ": " + message);
}
}
// Abstract decorator
public abstract class NotifierDecorator implements Notifier {
protected final Notifier wrapped;
public NotifierDecorator(Notifier wrapped) {
this.wrapped = wrapped;
}
@Override
public void send(String message) {
wrapped.send(message); // Delegate to wrapped notifier
}
}
// SMS decorator
public class SMSDecorator extends NotifierDecorator {
private final String phoneNumber;
public SMSDecorator(Notifier wrapped, String phoneNumber) {
super(wrapped);
this.phoneNumber = phoneNumber;
}
@Override
public void send(String message) {
super.send(message); // Send via wrapped notifier first
System.out.println("[SMS] Sending to " + phoneNumber + ": " + message);
}
}
// Slack decorator
public class SlackDecorator extends NotifierDecorator {
private final String slackChannel;
public SlackDecorator(Notifier wrapped, String slackChannel) {
super(wrapped);
this.slackChannel = slackChannel;
}
@Override
public void send(String message) {
super.send(message); // Send via wrapped notifier first
System.out.println("[SLACK] Posting to #" + slackChannel + ": " + message);
}
}
// Usage
public class Main {
public static void main(String[] args) {
// Email only
Notifier emailOnly = new EmailNotifier("dev@company.com");
emailOnly.send("Server is healthy");
System.out.println("---");
// Email + SMS
Notifier emailAndSms = new SMSDecorator(
new EmailNotifier("admin@company.com"),
"+1-555-0123"
);
emailAndSms.send("High memory usage detected");
System.out.println("---");
// Email + SMS + Slack (stack all three)
Notifier allChannels = new SlackDecorator(
new SMSDecorator(
new EmailNotifier("oncall@company.com"),
"+1-555-0911"
),
"incidents"
);
allChannels.send("CRITICAL: Database is down!");
}
}
The same notification system with stackable channel decorators in Python.
from abc import ABC, abstractmethod
# Base interface
class Notifier(ABC):
@abstractmethod
def send(self, message: str) -> None:
pass
# Concrete base implementation
class EmailNotifier(Notifier):
def __init__(self, email_address: str):
self._email_address = email_address
def send(self, message: str) -> None:
print(f"[EMAIL] Sending to {self._email_address}: {message}")
# Abstract decorator
class NotifierDecorator(Notifier):
def __init__(self, wrapped: Notifier):
self._wrapped = wrapped
def send(self, message: str) -> None:
self._wrapped.send(message)
# SMS decorator
class SMSDecorator(NotifierDecorator):
def __init__(self, wrapped: Notifier, phone_number: str):
super().__init__(wrapped)
self._phone_number = phone_number
def send(self, message: str) -> None:
super().send(message) # Send via wrapped notifier first
print(f"[SMS] Sending to {self._phone_number}: {message}")
# Slack decorator
class SlackDecorator(NotifierDecorator):
def __init__(self, wrapped: Notifier, slack_channel: str):
super().__init__(wrapped)
self._slack_channel = slack_channel
def send(self, message: str) -> None:
super().send(message) # Send via wrapped notifier first
print(f"[SLACK] Posting to #{self._slack_channel}: {message}")
# Usage
if __name__ == "__main__":
# Email only
email_only = EmailNotifier("dev@company.com")
email_only.send("Server is healthy")
print("---")
# Email + SMS
email_and_sms = SMSDecorator(
EmailNotifier("admin@company.com"),
"+1-555-0123"
)
email_and_sms.send("High memory usage detected")
print("---")
# Email + SMS + Slack (stack all three)
all_channels = SlackDecorator(
SMSDecorator(
EmailNotifier("oncall@company.com"),
"+1-555-0911"
),
"incidents"
)
all_channels.send("CRITICAL: Database is down!")
BufferedInputStream(new FileInputStream("file.txt")) is the textbook decorator example, stacking buffering on top of file reading@decorator syntax — Function decorators like @login_required and @cache wrap functions with additional behaviorThe Proxy Pattern is a structural design pattern that provides a substitute or placeholder for another object to control access to it. Just like a security guard controls access to a building — they do not change what is inside, but they decide who gets in and can log every entry — a proxy wraps a real object and adds a layer of control.
Proxies are everywhere in software: caching layers that prevent redundant API calls, security wrappers that enforce permissions, and lazy-loading mechanisms that defer expensive operations until truly needed.
Your application makes frequent calls to an external weather API. Each call takes 2–3 seconds and costs money per request. Many of these calls request the same data within short time windows — checking weather for the same city multiple times within minutes.
You cannot modify the third-party API client, and scattering caching logic throughout your business code would create duplication and make the codebase harder to maintain.
Create a proxy class that implements the same interface as the real API client. The proxy intercepts every call, checks if a cached response exists and is still fresh, and either returns the cached data or delegates to the real client. Your business code continues calling the same interface with zero changes.
The proxy transparently adds caching behavior without the caller ever knowing it exists.
The Proxy Pattern embodies the Single Responsibility Principle by separating cross-cutting concerns (caching, logging, access control) from core business logic. The real service focuses on its job; the proxy handles the additional behavior.
Here we build a caching proxy for a weather API client that avoids redundant external calls.
// Shared interface
public interface WeatherService {
WeatherData getWeather(String city);
}
// Data class
public class WeatherData {
private String city;
private double temperature;
private String condition;
private long timestamp;
public WeatherData(String city, double temperature, String condition) {
this.city = city;
this.temperature = temperature;
this.condition = condition;
this.timestamp = System.currentTimeMillis();
}
public long getTimestamp() { return timestamp; }
@Override
public String toString() {
return String.format("%s: %.1f\u00b0F, %s", city, temperature, condition);
}
}
// Real service that makes expensive API calls
public class RealWeatherService implements WeatherService {
@Override
public WeatherData getWeather(String city) {
System.out.println("[API CALL] Fetching weather for " + city + "...");
// Simulate expensive API call
try { Thread.sleep(2000); } catch (InterruptedException e) { }
return new WeatherData(city, 72.5, "Sunny");
}
}
// Caching proxy
public class CachingWeatherProxy implements WeatherService {
private final WeatherService realService;
private final Map<String, WeatherData> cache = new HashMap<>();
private final long cacheDurationMs;
public CachingWeatherProxy(WeatherService realService, long cacheDurationMs) {
this.realService = realService;
this.cacheDurationMs = cacheDurationMs;
}
@Override
public WeatherData getWeather(String city) {
String key = city.toLowerCase();
if (cache.containsKey(key)) {
WeatherData cached = cache.get(key);
long age = System.currentTimeMillis() - cached.getTimestamp();
if (age < cacheDurationMs) {
System.out.println("[CACHE HIT] Returning cached data for " + city);
return cached;
}
System.out.println("[CACHE EXPIRED] Refreshing data for " + city);
}
WeatherData fresh = realService.getWeather(city);
cache.put(key, fresh);
return fresh;
}
public void clearCache() {
cache.clear();
System.out.println("[CACHE] Cleared all entries");
}
}
// Usage
public class Main {
public static void main(String[] args) {
WeatherService service = new CachingWeatherProxy(
new RealWeatherService(),
5 * 60 * 1000 // 5 minute cache
);
// First call - hits the real API
System.out.println(service.getWeather("New York"));
// Second call - served from cache instantly
System.out.println(service.getWeather("New York"));
// Different city - hits the real API
System.out.println(service.getWeather("London"));
}
}
The same caching weather proxy scenario in Python.
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
import time
@dataclass
class WeatherData:
city: str
temperature: float
condition: str
timestamp: float = field(default_factory=time.time)
def __str__(self):
return f"{self.city}: {self.temperature}\u00b0F, {self.condition}"
# Shared interface
class WeatherService(ABC):
@abstractmethod
def get_weather(self, city: str) -> WeatherData:
pass
# Real service that makes expensive API calls
class RealWeatherService(WeatherService):
def get_weather(self, city: str) -> WeatherData:
print(f"[API CALL] Fetching weather for {city}...")
time.sleep(2) # Simulate expensive API call
return WeatherData(city=city, temperature=72.5, condition="Sunny")
# Caching proxy
class CachingWeatherProxy(WeatherService):
def __init__(self, real_service: WeatherService, cache_duration_sec: int = 300):
self._real_service = real_service
self._cache: dict[str, WeatherData] = {}
self._cache_duration = cache_duration_sec
def get_weather(self, city: str) -> WeatherData:
key = city.lower()
if key in self._cache:
cached = self._cache[key]
age = time.time() - cached.timestamp
if age < self._cache_duration:
print(f"[CACHE HIT] Returning cached data for {city}")
return cached
print(f"[CACHE EXPIRED] Refreshing data for {city}")
fresh = self._real_service.get_weather(city)
self._cache[key] = fresh
return fresh
def clear_cache(self):
self._cache.clear()
print("[CACHE] Cleared all entries")
# Usage
if __name__ == "__main__":
service = CachingWeatherProxy(
real_service=RealWeatherService(),
cache_duration_sec=300 # 5 minute cache
)
# First call - hits the real API
print(service.get_weather("New York"))
# Second call - served from cache instantly
print(service.get_weather("New York"))
# Different city - hits the real API
print(service.get_weather("London"))
@Transactional, @Cacheable, and @Secured annotationsjava.lang.reflect.Proxy — Dynamic proxy generation for runtime interface implementationfunctools.lru_cache — A function-level caching proxy decoratorThe Adapter Pattern is a structural design pattern that allows objects with incompatible interfaces to work together. Think of it like a power adapter you use when traveling abroad — your laptop plug does not change, the foreign outlet does not change, but the adapter bridges the gap between them.
In software, you will frequently encounter situations where you need to integrate a third-party library, legacy system, or external service whose interface does not match what your application expects. Rewriting either side is risky and expensive. The Adapter Pattern solves this elegantly.
Imagine your e-commerce application processes payments through a clean, modern interface. A new business requirement demands integration with a legacy payment gateway that has a completely different API — different method names, different parameter formats, and different response structures.
You cannot modify the legacy system (it is maintained by another team or vendor), and you should not pollute your existing codebase with legacy-specific logic scattered across multiple files.
Create an adapter class that implements your application’s expected interface while internally translating calls to the legacy system’s API. Your application code continues to work with the interface it already knows, completely unaware that a different system is handling the actual payment processing behind the scenes.
The adapter acts as a translator, converting requests from one format to another without either side needing to change.
The Adapter Pattern follows the Open/Closed Principle — your existing code remains open for extension (new payment gateways) but closed for modification (no changes to existing business logic). It also promotes programming to interfaces, keeping your code decoupled from specific implementations.
Here we adapt a legacy payment gateway to work with our modern payment interface.
// Modern payment interface our application expects
public interface PaymentProcessor {
PaymentResult processPayment(String customerId, double amount, String currency);
PaymentResult refund(String transactionId, double amount);
}
// Result object
public class PaymentResult {
private boolean success;
private String transactionId;
private String message;
public PaymentResult(boolean success, String transactionId, String message) {
this.success = success;
this.transactionId = transactionId;
this.message = message;
}
// Getters
public boolean isSuccess() { return success; }
public String getTransactionId() { return transactionId; }
public String getMessage() { return message; }
}
// Legacy payment gateway with a completely different interface
public class LegacyPaymentGateway {
public Map<String, Object> submitTransaction(Map<String, String> params) {
System.out.println("Legacy gateway processing: " + params);
Map<String, Object> response = new HashMap<>();
response.put("status_code", 0); // 0 = success in legacy system
response.put("ref_number", "LEG-" + System.currentTimeMillis());
response.put("status_msg", "Transaction approved");
return response;
}
public Map<String, Object> reverseTransaction(String refNumber, String amountInCents) {
System.out.println("Legacy gateway reversing: " + refNumber);
Map<String, Object> response = new HashMap<>();
response.put("status_code", 0);
response.put("ref_number", "REV-" + System.currentTimeMillis());
response.put("status_msg", "Reversal approved");
return response;
}
}
// Adapter: bridges modern interface to legacy gateway
public class LegacyPaymentAdapter implements PaymentProcessor {
private final LegacyPaymentGateway legacyGateway;
public LegacyPaymentAdapter(LegacyPaymentGateway legacyGateway) {
this.legacyGateway = legacyGateway;
}
@Override
public PaymentResult processPayment(String customerId, double amount, String currency) {
// Translate modern parameters to legacy format
Map<String, String> params = new HashMap<>();
params.put("cust_id", customerId);
params.put("amt", String.valueOf((int)(amount * 100))); // Legacy uses cents
params.put("curr", currency.toUpperCase());
Map<String, Object> response = legacyGateway.submitTransaction(params);
// Translate legacy response to modern format
boolean success = (int) response.get("status_code") == 0;
return new PaymentResult(
success,
(String) response.get("ref_number"),
(String) response.get("status_msg")
);
}
@Override
public PaymentResult refund(String transactionId, double amount) {
String amountInCents = String.valueOf((int)(amount * 100));
Map<String, Object> response = legacyGateway.reverseTransaction(transactionId, amountInCents);
boolean success = (int) response.get("status_code") == 0;
return new PaymentResult(
success,
(String) response.get("ref_number"),
(String) response.get("status_msg")
);
}
}
// Usage
public class Main {
public static void main(String[] args) {
// Wrap legacy gateway in adapter
LegacyPaymentGateway legacyGateway = new LegacyPaymentGateway();
PaymentProcessor processor = new LegacyPaymentAdapter(legacyGateway);
// Application code works with the modern interface
PaymentResult result = processor.processPayment("CUST-123", 49.99, "USD");
System.out.println("Success: " + result.isSuccess());
System.out.println("Transaction ID: " + result.getTransactionId());
}
}
The same payment gateway adapter scenario in Python.
from abc import ABC, abstractmethod
from dataclasses import dataclass
import time
@dataclass
class PaymentResult:
success: bool
transaction_id: str
message: str
# Modern payment interface our application expects
class PaymentProcessor(ABC):
@abstractmethod
def process_payment(self, customer_id: str, amount: float, currency: str) -> PaymentResult:
pass
@abstractmethod
def refund(self, transaction_id: str, amount: float) -> PaymentResult:
pass
# Legacy payment gateway with a completely different interface
class LegacyPaymentGateway:
def submit_transaction(self, params: dict) -> dict:
print(f"Legacy gateway processing: {params}")
return {
"status_code": 0, # 0 = success in legacy system
"ref_number": f"LEG-{int(time.time() * 1000)}",
"status_msg": "Transaction approved"
}
def reverse_transaction(self, ref_number: str, amount_in_cents: str) -> dict:
print(f"Legacy gateway reversing: {ref_number}")
return {
"status_code": 0,
"ref_number": f"REV-{int(time.time() * 1000)}",
"status_msg": "Reversal approved"
}
# Adapter: bridges modern interface to legacy gateway
class LegacyPaymentAdapter(PaymentProcessor):
def __init__(self, legacy_gateway: LegacyPaymentGateway):
self._legacy_gateway = legacy_gateway
def process_payment(self, customer_id: str, amount: float, currency: str) -> PaymentResult:
# Translate modern parameters to legacy format
params = {
"cust_id": customer_id,
"amt": str(int(amount * 100)), # Legacy uses cents
"curr": currency.upper()
}
response = self._legacy_gateway.submit_transaction(params)
return PaymentResult(
success=response["status_code"] == 0,
transaction_id=response["ref_number"],
message=response["status_msg"]
)
def refund(self, transaction_id: str, amount: float) -> PaymentResult:
amount_in_cents = str(int(amount * 100))
response = self._legacy_gateway.reverse_transaction(transaction_id, amount_in_cents)
return PaymentResult(
success=response["status_code"] == 0,
transaction_id=response["ref_number"],
message=response["status_msg"]
)
# Usage
if __name__ == "__main__":
legacy_gateway = LegacyPaymentGateway()
processor = LegacyPaymentAdapter(legacy_gateway)
result = processor.process_payment("CUST-123", 49.99, "USD")
print(f"Success: {result.success}")
print(f"Transaction ID: {result.transaction_id}")
HandlerAdapter adapts different handler types to the DispatcherServletInputStreamReader adapts a byte stream (InputStream) to a character stream (Reader)csv.DictReader — Adapts CSV file reading into dictionary-based accessThe Abstract Factory pattern provides an interface for creating families of related objects without specifying their concrete classes. While the Factory Method creates one product, the Abstract Factory creates an entire suite of products that are designed to work together. This is the pattern you reach for when your system needs to support multiple variants of a product family.
Imagine building a cross-platform UI toolkit that must render components differently on Windows, macOS, and Linux. Each platform needs its own buttons, text inputs, checkboxes, and dialogs — but they must be internally consistent. A Windows button should never accidentally appear alongside a macOS checkbox. If you create each component independently, you risk mixing platform-specific implementations in ways that break the user experience.
The Abstract Factory defines an interface with creation methods for each component type (button, text input, checkbox). Each platform provides a concrete factory that produces the matching component family. The application code works exclusively through the abstract interfaces — it never knows which platform-specific classes it is using. Switching platforms means swapping one factory for another.
Ensure product compatibility by constraining creation to families. The abstract factory guarantees that all objects produced by a single factory are designed to work together, eliminating cross-family mixing at the structural level.
This example creates a UI component factory system that produces consistent component families for different themes (Material and iOS).
// Abstract products
public interface Button {
String render();
String onClick(String action);
}
public interface TextField {
String render(String placeholder);
String getValue();
}
public interface Checkbox {
String render(String label);
boolean isChecked();
}
// Material Design implementations
public class MaterialButton implements Button {
@Override
public String render() {
return "[Material Button with ripple effect and elevation]";
}
@Override
public String onClick(String action) {
return "Material ripple animation -> " + action;
}
}
public class MaterialTextField implements TextField {
private String value = "";
@Override
public String render(String placeholder) {
return "[Material TextField: floating label '" + placeholder + "']";
}
@Override
public String getValue() { return value; }
}
public class MaterialCheckbox implements Checkbox {
private boolean checked = false;
@Override
public String render(String label) {
return "[Material Checkbox: " + label + " with animated checkmark]";
}
@Override
public boolean isChecked() { return checked; }
}
// iOS implementations
public class IOSButton implements Button {
@Override
public String render() {
return "[iOS Button with haptic feedback and rounded corners]";
}
@Override
public String onClick(String action) {
return "iOS haptic tap -> " + action;
}
}
public class IOSTextField implements TextField {
private String value = "";
@Override
public String render(String placeholder) {
return "[iOS TextField: inline placeholder '" + placeholder + "']";
}
@Override
public String getValue() { return value; }
}
public class IOSCheckbox implements Checkbox {
private boolean checked = false;
@Override
public String render(String label) {
return "[iOS Toggle Switch: " + label + " with smooth slide]";
}
@Override
public boolean isChecked() { return checked; }
}
// Abstract factory interface
public interface UIComponentFactory {
Button createButton();
TextField createTextField();
Checkbox createCheckbox();
}
// Concrete factories
public class MaterialUIFactory implements UIComponentFactory {
@Override
public Button createButton() { return new MaterialButton(); }
@Override
public TextField createTextField() { return new MaterialTextField(); }
@Override
public Checkbox createCheckbox() { return new MaterialCheckbox(); }
}
public class IOSUIFactory implements UIComponentFactory {
@Override
public Button createButton() { return new IOSButton(); }
@Override
public TextField createTextField() { return new IOSTextField(); }
@Override
public Checkbox createCheckbox() { return new IOSCheckbox(); }
}
// Client code works with abstractions only
public class LoginForm {
private final Button submitButton;
private final TextField usernameField;
private final TextField passwordField;
private final Checkbox rememberMe;
public LoginForm(UIComponentFactory factory) {
this.submitButton = factory.createButton();
this.usernameField = factory.createTextField();
this.passwordField = factory.createTextField();
this.rememberMe = factory.createCheckbox();
}
public void render() {
System.out.println("=== Login Form ===");
System.out.println(usernameField.render("Username"));
System.out.println(passwordField.render("Password"));
System.out.println(rememberMe.render("Remember me"));
System.out.println(submitButton.render());
System.out.println(submitButton.onClick("submit login"));
}
public static void main(String[] args) {
// Switch theme by swapping the factory
String theme = "material";
UIComponentFactory factory = theme.equals("ios")
? new IOSUIFactory()
: new MaterialUIFactory();
LoginForm form = new LoginForm(factory);
form.render();
}
}
The same UI component factory system in Python, using abstract base classes.
from abc import ABC, abstractmethod
# Abstract products
class Button(ABC):
@abstractmethod
def render(self) -> str:
pass
@abstractmethod
def on_click(self, action: str) -> str:
pass
class TextField(ABC):
@abstractmethod
def render(self, placeholder: str) -> str:
pass
@abstractmethod
def get_value(self) -> str:
pass
class Checkbox(ABC):
@abstractmethod
def render(self, label: str) -> str:
pass
@abstractmethod
def is_checked(self) -> bool:
pass
# Material Design implementations
class MaterialButton(Button):
def render(self) -> str:
return "[Material Button with ripple effect and elevation]"
def on_click(self, action: str) -> str:
return f"Material ripple animation -> {action}"
class MaterialTextField(TextField):
def __init__(self):
self._value = ""
def render(self, placeholder: str) -> str:
return f"[Material TextField: floating label '{placeholder}']"
def get_value(self) -> str:
return self._value
class MaterialCheckbox(Checkbox):
def __init__(self):
self._checked = False
def render(self, label: str) -> str:
return f"[Material Checkbox: {label} with animated checkmark]"
def is_checked(self) -> bool:
return self._checked
# iOS implementations
class IOSButton(Button):
def render(self) -> str:
return "[iOS Button with haptic feedback and rounded corners]"
def on_click(self, action: str) -> str:
return f"iOS haptic tap -> {action}"
class IOSTextField(TextField):
def __init__(self):
self._value = ""
def render(self, placeholder: str) -> str:
return f"[iOS TextField: inline placeholder '{placeholder}']"
def get_value(self) -> str:
return self._value
class IOSCheckbox(Checkbox):
def __init__(self):
self._checked = False
def render(self, label: str) -> str:
return f"[iOS Toggle Switch: {label} with smooth slide]"
def is_checked(self) -> bool:
return self._checked
# Abstract factory
class UIComponentFactory(ABC):
@abstractmethod
def create_button(self) -> Button:
pass
@abstractmethod
def create_text_field(self) -> TextField:
pass
@abstractmethod
def create_checkbox(self) -> Checkbox:
pass
# Concrete factories
class MaterialUIFactory(UIComponentFactory):
def create_button(self) -> Button:
return MaterialButton()
def create_text_field(self) -> TextField:
return MaterialTextField()
def create_checkbox(self) -> Checkbox:
return MaterialCheckbox()
class IOSUIFactory(UIComponentFactory):
def create_button(self) -> Button:
return IOSButton()
def create_text_field(self) -> TextField:
return IOSTextField()
def create_checkbox(self) -> Checkbox:
return IOSCheckbox()
# Client code works with abstractions only
class LoginForm:
def __init__(self, factory: UIComponentFactory):
self.submit_button = factory.create_button()
self.username_field = factory.create_text_field()
self.password_field = factory.create_text_field()
self.remember_me = factory.create_checkbox()
def render(self):
print("=== Login Form ===")
print(self.username_field.render("Username"))
print(self.password_field.render("Password"))
print(self.remember_me.render("Remember me"))
print(self.submit_button.render())
print(self.submit_button.on_click("submit login"))
# Usage
if __name__ == "__main__":
theme = "material"
factory = IOSUIFactory() if theme == "ios" else MaterialUIFactory()
form = LoginForm(factory)
form.render()
LookAndFeel system uses abstract factories to produce platform-consistent UI components (buttons, scrollbars, menus).DocumentBuilderFactory and SAXParserFactory produce families of XML parsing components.