1
Mastering Java Design Patterns: A
Comprehensive Guide for
Developers
This guide serves as a comprehensive resource for Java developers looking to enhance the
quality, maintainability, and extensibility of their applications through the practical
application of design patterns. Design patterns are proven solutions to common software
design problems, offering a standardized approach to building robust and flexible systems.
By understanding and implementing these patterns, you can write cleaner, more modular,
and easier-to-understand code, moving away from complex conditional structures and
promoting better object-oriented design principles. This guide will explore six essential
design patterns in Java: Singleton, Factory, Strategy, Observer, Decorator, and Adapter,
providing detailed explanations, practical examples, and insights into their appropriate
use.
Prerequisites
Basic understanding of Java programming language syntax and features.
Familiarity with Object-Oriented Programming (OOP) concepts such as classes,
objects, inheritance, polymorphism, and interfaces.
2
Step 1: Singleton Pattern: Ensuring a Single
Instance
The Singleton pattern is a creational design pattern that ensures a class has only one
instance and provides a global point of access to that instance. This is particularly useful
when you need to manage a single, shared resource or service across your entire
application, such as a database connection pool, a configuration manager, a logging utility,
or a thread pool. Its primary goal is to prevent multiple instances from consuming
excessive resources or leading to inconsistent states. While powerful, it should be used
judiciously, as overuse can introduce global state, making testing and dependency
management more challenging.
public class SMSService {
private static SMSService instance;
// Private constructor to prevent direct instantiation
private SMSService() {
// Initialize SMS configuration, e.g., connect to SMS gateway
[Link]("SMSService initialized.");
}
// Public static method to get the single instance
public static synchronized SMSService getInstance() {
if (instance == null) {
instance = new SMSService();
}
return instance;
}
// Business method
public void sendOrderConfirmation(String phoneNumber, String
orderInfo) {
[Link]("Sending order confirmation to " +
phoneNumber + ": " + orderInfo);
}
3
// Example usage:
public static void main(String[] args) {
SMSService smsService1 = [Link]();
SMSService smsService2 = [Link]();
[Link]("Are both instances the same? " +
(smsService1 == smsService2));
[Link]("123-456-7890", "Order
#1234");
}
}
In this example, SMSService uses the Singleton pattern. The constructor is declared
private to prevent external classes from instantiating SMSService directly. The
getInstance() method is the sole entry point for obtaining an instance of SMSService.
The static keyword ensures that instance belongs to the class itself, not to any specific
object. The synchronized keyword in getInstance() ensures thread safety, meaning
that if multiple threads try to access getInstance() simultaneously, only one will be
allowed to proceed at a time, preventing race conditions that could lead to multiple
instances being created. This ensures lazy initialization, where the instance is created only
when it's first needed.
4
Step 2: Factory Pattern: Abstracting Object Creation
The Factory pattern is a creational design pattern that encapsulates the process of creating
objects, allowing a client to create objects without knowing the concrete class that will be
instantiated. This pattern is particularly useful when the exact type of object to be created
depends on external factors, configurations, or complex logic, or when you want to hide the
implementation details of the created objects. By decoupling the client code from the
concrete classes, the Factory pattern promotes loose coupling, enhances flexibility, and
makes the system easier to extend (e.g., adding new product types without modifying the
client code). It adheres to the Open/Closed Principle, making your code more robust to
change.
public interface PaymentMethod {
void processPayment(double amount);
}
public class WeChatPay implements PaymentMethod {
@Override
public void processPayment(double amount) {
[Link]("Processing WeChat Pay for " + amount +
".");
}
}
public class CreditCardPay implements PaymentMethod {
@Override
public void processPayment(double amount) {
[Link]("Processing Credit Card Pay for " + amount
+ ".");
}
}
public class PaymentFactory {
public static PaymentMethod getPaymentMethod(String type) {
if ("wechat".equalsIgnoreCase(type)) {
return new WeChatPay();
} else if ("creditcard".equalsIgnoreCase(type)) {
5
return new CreditCardPay();
} else {
throw new IllegalArgumentException("Unsupported payment
method: " + type);
}
}
// Example usage:
public static void main(String[] args) {
PaymentMethod wechat =
[Link]("wechat");
[Link](100.50);
PaymentMethod creditCard =
[Link]("creditcard");
[Link](250.75);
// PaymentMethod unsupported =
[Link]("paypal"); // Throws
IllegalArgumentException
}
}
Here, PaymentMethod is an interface defining the contract for all payment methods.
WeChatPay and CreditCardPay are concrete implementations. The PaymentFactory
class contains a static method getPaymentMethod() that takes a String type as input
and returns an instance of the appropriate PaymentMethod. The client code (e.g., main
method) interacts only with the PaymentFactory and the PaymentMethod interface,
without needing to know the specific concrete classes being instantiated. If a new payment
method (e.g., PayPal) needs to be added, you would only need to create a new concrete
class (e.g., PayPalPay) and modify the PaymentFactory to include its creation logic,
leaving existing client code untouched.
6
Step 3: Strategy Pattern: Interchangeable
Algorithms
The Strategy pattern is a behavioral design pattern that allows you to define a family of
algorithms, encapsulate each one as an object, and make them interchangeable. This
pattern lets the algorithm vary independently from clients that use it. It's ideal for
situations where a class has many behaviors, and these behaviors can be selected or
changed at runtime, often replacing large if-else or switch statements with a more
flexible and extensible structure. Common use cases include different sorting algorithms,
validation rules, tax calculation methods, or, as in the example, various discount strategies.
public interface DiscountStrategy {
double calculateDiscount(double totalAmount);
}
public class NoDiscountStrategy implements DiscountStrategy {
@Override
public double calculateDiscount(double totalAmount) {
return 0;
}
}
public class PercentageDiscountStrategy implements DiscountStrategy {
private double percentage;
public PercentageDiscountStrategy(double percentage) {
[Link] = percentage;
}
@Override
public double calculateDiscount(double totalAmount) {
return totalAmount * (percentage / 100);
}
}
public class FixedAmountDiscountStrategy implements DiscountStrategy {
7
private double fixedAmount;
public FixedAmountDiscountStrategy(double fixedAmount) {
[Link] = fixedAmount;
}
@Override
public double calculateDiscount(double totalAmount) {
return fixedAmount;
}
}
public class Order {
private double totalAmount;
private DiscountStrategy discountStrategy;
public Order(double totalAmount) {
[Link] = totalAmount;
// Default strategy if none is set
[Link] = new NoDiscountStrategy();
}
public void setDiscountStrategy(DiscountStrategy discountStrategy)
{
[Link] = discountStrategy;
}
public double calculateFinalTotal() {
double discount =
[Link](totalAmount);
return totalAmount - discount;
}
// Example usage:
public static void main(String[] args) {
Order order1 = new Order(100.0);
[Link]("Order 1 (No Discount): " +
[Link]());
8
Order order2 = new Order(100.0);
[Link](new
PercentageDiscountStrategy(10)); // 10% off
[Link]("Order 2 (10% Discount): " +
[Link]());
Order order3 = new Order(100.0);
[Link](new
FixedAmountDiscountStrategy(15.0)); // $15 off
[Link]("Order 3 ($15 Discount): " +
[Link]());
}
}
In this example, DiscountStrategy is an interface that defines a common method
calculateDiscount(). NoDiscountStrategy, PercentageDiscountStrategy,
and FixedAmountDiscountStrategy are concrete implementations, each providing a
different way to calculate a discount. The Order class, which is the 'Context' in this
pattern, holds a reference to a DiscountStrategy object. It can dynamically change its
strategy at runtime via the setDiscountStrategy() method. When
calculateFinalTotal() is called, the Order object delegates the discount calculation
to its currently configured strategy. This allows the Order class to be independent of how
the discount is calculated, making it highly flexible and easy to introduce new discount
types without modifying the Order class itself.
9
Step 4: Observer Pattern: Event Notification System
The Observer pattern is a behavioral design pattern that defines a one-to-many
dependency between objects. When one object (the 'subject' or 'publisher') changes its
state, all its dependents (the 'observers' or 'subscribers') are notified and updated
automatically. This pattern is fundamental for implementing event-driven systems,
'publish-subscribe' models, and GUI components where changes in one part of the
application need to be reflected in others. It promotes loose coupling between the subject
and its observers, as the subject only knows about the observer interface, not their concrete
implementations, making the system more extensible and maintainable.
import [Link];
import [Link];
public interface OrderObserver {
void update(Order order);
}
public class InventorySystem implements OrderObserver {
private String name;
public InventorySystem(String name) {
[Link] = name;
}
@Override
public void update(Order order) {
if ("PAID".equals([Link]())) {
[Link](name + " System Notified: Order #" +
[Link]() + " paid. Deducting stock.");
} else if ("SHIPPED".equals([Link]())) {
[Link](name + " System Notified: Order #" +
[Link]() + " shipped. Updating inventory records.");
}
}
}
10
public class EmailService implements OrderObserver {
@Override
public void update(Order order) {
if ("PAID".equals([Link]())) {
[Link]("Email Service Notified: Sending
payment confirmation for Order #" + [Link]() + ".");
}
}
}
public class Order {
private int id;
private String status;
private List observers = new ArrayList<>();
public Order(int id, String initialStatus) {
[Link] = id;
[Link] = initialStatus;
}
public int getId() {
return id;
}
public String getStatus() {
return status;
}
public void addObserver(OrderObserver observer) {
[Link](observer);
}
public void removeObserver(OrderObserver observer) {
[Link](observer);
}
private void notifyObservers() {
for (OrderObserver observer : observers) {
[Link](this);
11
}
}
public void setStatus(String status) {
[Link] = status;
[Link]("Order #" + id + " status changed to: " +
status);
notifyObservers(); // Notify all registered observers
}
// Example usage:
public static void main(String[] args) {
Order order = new Order(101, "PENDING");
InventorySystem inventory = new InventorySystem("Main
Inventory");
EmailService email = new EmailService();
[Link](inventory);
[Link](email);
[Link]("PAID");
[Link]("SHIPPED");
[Link](email);
[Link]("DELIVERED"); // Email service will no longer
be notified
}
}
In this example, the Order class is the 'Subject' (or Observable). It maintains a list of
OrderObserver objects and has methods to addObserver(), removeObserver(),
and notifyObservers(). OrderObserver is the 'Observer' interface, defining the
update() method that all concrete observers must implement. InventorySystem and
EmailService are concrete observers. When the Order's status changes via
setStatus(), it calls notifyObservers(), which in turn calls the update() method
on all registered observers. Each observer then reacts to the change as needed. This setup
ensures that the Order class doesn't need to know the specific details of how
12
InventorySystem or EmailService handle the status update, promoting loose
coupling and making the system highly extensible; new notification mechanisms can be
added simply by creating new observer classes.
13
Step 5: Decorator Pattern: Dynamically Extending
Functionality
The Decorator pattern is a structural design pattern that allows behavior to be added to an
individual object, dynamically, without affecting the behavior of other objects from the
same class. It provides a flexible alternative to subclassing for extending functionality. This
pattern is useful when you need to add responsibilities to objects dynamically, or when you
want to avoid a 'class explosion' that can occur with inheritance when many different
combinations of features are required. It allows you to wrap objects with new functionality,
much like stacking layers, where each layer adds a specific behavior or feature. This pattern
adheres to the Single Responsibility Principle and Open/Closed Principle.
public interface Coffee {
String getDescription();
double getCost();
}
public class SimpleCoffee implements Coffee {
@Override
public String getDescription() { return "Simple Coffee"; }
@Override
public double getCost() { return 10.0; }
}
// Abstract Decorator
public abstract class CoffeeDecorator implements Coffee {
protected Coffee decoratedCoffee; // Reference to the wrapped
component
public CoffeeDecorator(Coffee coffee) {
[Link] = coffee;
}
// By default, decorators simply delegate to the wrapped object
@Override
public String getDescription() { return
14
[Link](); }
@Override
public double getCost() { return [Link](); }
}
// Concrete Decorators
public class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) { super(coffee); }
@Override
public String getDescription() { return
[Link]() + ", with Milk"; }
@Override
public double getCost() { return [Link]() + 2.0;
}
}
public class SugarDecorator extends CoffeeDecorator {
public SugarDecorator(Coffee coffee) { super(coffee); }
@Override
public String getDescription() { return
[Link]() + ", with Sugar"; }
@Override
public double getCost() { return [Link]() + 0.5;
}
}
public class CaramelDecorator extends CoffeeDecorator {
public CaramelDecorator(Coffee coffee) { super(coffee); }
@Override
public String getDescription() { return
[Link]() + ", with Caramel"; }
@Override
public double getCost() { return [Link]() + 3.0;
15
}
}
// Example usage:
public static void main(String[] args) {
Coffee myCoffee = new SimpleCoffee();
[Link]("Description: " + [Link]() +
", Cost: " + [Link]());
// Add milk
myCoffee = new MilkDecorator(myCoffee);
[Link]("Description: " + [Link]() +
", Cost: " + [Link]());
// Add sugar to the milky coffee
myCoffee = new SugarDecorator(myCoffee);
[Link]("Description: " + [Link]() +
", Cost: " + [Link]());
// Create another coffee with different decorations
Coffee anotherCoffee = new CaramelDecorator(new MilkDecorator(new
SimpleCoffee()));
[Link]("Description: " +
[Link]() + ", Cost: " +
[Link]());
}
In this example, Coffee is the component interface, and SimpleCoffee is a concrete
component. CoffeeDecorator is an abstract decorator class that also implements
Coffee and holds a reference to a Coffee object (the decoratedCoffee). Concrete
decorators like MilkDecorator, SugarDecorator, and CaramelDecorator extend
CoffeeDecorator and add specific functionality (like adding 'milk' to the description
and cost). Notice how getDescription() and getCost() in the decorators call the
same methods on their decoratedCoffee object before adding their own part. This
allows you to 'wrap' a SimpleCoffee object with multiple decorators dynamically,
building up its features and cost without altering the original SimpleCoffee class or
16
creating a complex inheritance hierarchy of many different coffee types (e.g.,
MilkCoffee, SugarCoffee, MilkSugarCoffee, etc.).
17
Step 6: Adapter Pattern: Bridging Incompatible
Interfaces
The Adapter pattern is a structural design pattern that allows objects with incompatible
interfaces to collaborate. It acts as a wrapper between two classes, translating calls from
one interface into an interface that the client expects. This pattern is particularly useful
when you need to integrate existing classes or third-party libraries into a system that
expects a different interface, without modifying the source code of the existing classes. It
promotes code reuse and helps in unifying disparate systems. There are two main types:
Class Adapter (using inheritance) and Object Adapter (using composition), with Object
Adapter being more commonly used in Java due to its flexibility and the absence of
multiple inheritance for classes.
import [Link];
import [Link];
// Unified interface in our system (Target Interface)
public interface PaymentProcessor {
boolean processPayment(String orderId, double amount, String
currency);
PaymentStatus checkStatus(String paymentId);
}
// Enum for payment status
enum PaymentStatus { SUCCESS, FAILED, PROCESSING, REFUNDED }
// 3rd Party Platform A (Adaptee Interface) - e.g., Alipay SDK
public class AlipaySDK {
// Simulates an external SDK's payment method
public String pay(String orderNo, double money, String type) {
[Link]("AlipaySDK: Processing payment for order "
+ orderNo + ", amount " + money + ", type " + type);
// In a real scenario, this would interact with Alipay API
if ([Link]() > 0.1) { // Simulate success 90% of the time
return "ALI_TXN_" + [Link]();
} else {
18
throw new RuntimeException("Alipay transaction failed.");
}
}
// Simulates an external SDK's status query method
public int queryPayStatus(String tradeNo) {
[Link]("AlipaySDK: Querying status for transaction
" + tradeNo);
// 1 = SUCCESS, 0 = PROCESSING, -1 = FAILED
if ([Link]("SUCCESS")) return 1;
if ([Link]("PROCESS")) return 0;
return (int) ([Link]() * 3) - 1; // Simulate various
states
}
}
// Adapter for Alipay, implementing our unified interface
public class AlipayAdapter implements PaymentProcessor {
private AlipaySDK alipaySDK; // The adaptee
private Map orderToPaymentMap = new HashMap<>(); // To store
mapping between our orderId and Alipay's paymentId
public AlipayAdapter() {
[Link] = new AlipaySDK();
}
@Override
public boolean processPayment(String orderId, double amount,
String currency) {
[Link]("AlipayAdapter: Adapting processPayment for
order " + orderId);
try {
// Translate our interface call to AlipaySDK's interface
call
String paymentId = [Link](orderId, amount,
"direct");
[Link](orderId, paymentId); // Store for
status check
return true;
19
} catch (Exception e) {
[Link]("Alipay payment failed for order " +
orderId + ": " + [Link]());
return false;
}
}
@Override
public PaymentStatus checkStatus(String orderId) {
[Link]("AlipayAdapter: Adapting checkStatus for
order " + orderId);
String paymentId = [Link](orderId);
if (paymentId == null) {
return [Link]; // No record of this order's
payment
}
// Translate AlipaySDK's status codes to our PaymentStatus
enum
int status = [Link](paymentId);
switch (status) {
case 1: return [Link];
case 0: return [Link];
case -1: return [Link];
default: return [Link];
}
}
// Example usage:
public static void main(String[] args) {
PaymentProcessor processor = new AlipayAdapter();
String orderId1 = "ORDER_001";
boolean success1 = [Link](orderId1, 150.0,
"USD");
if (success1) {
[Link]("Payment for " + orderId1 + " initiated
successfully.");
[Link]("Status for " + orderId1 + ": " +
20
[Link](orderId1));
} else {
[Link]("Payment for " + orderId1 + "
failed.");
}
[Link]("\n---\n");
String orderId2 = "ORDER_002";
boolean success2 = [Link](orderId2, 200.0,
"EUR");
if (success2) {
[Link]("Payment for " + orderId2 + " initiated
successfully.");
// Simulate some time passing, then check status
[Link]("Status for " + orderId2 + ": " +
[Link](orderId2));
} else {
[Link]("Payment for " + orderId2 + "
failed.");
}
}
}
In this scenario, our system uses a unified PaymentProcessor interface. However, we
need to integrate with a third-party AlipaySDK that has its own distinct methods (pay
and queryPayStatus). The AlipayAdapter class acts as the 'Adapter'. It implements
our PaymentProcessor interface, making it compatible with our system's expectations.
Internally, AlipayAdapter holds an instance of the AlipaySDK (the 'Adaptee') and
translates calls from PaymentProcessor's methods (processPayment, checkStatus)
into the appropriate calls on the AlipaySDK instance. For example, processPayment
translates to [Link], and checkStatus translates to
[Link], mapping Alipay's integer status codes to our
PaymentStatus enum. The orderToPaymentMap helps manage the transaction IDs
generated by AlipaySDK, linking them back to our internal orderIds. This allows our
system to interact with AlipaySDK seamlessly through a familiar interface.
21
Conclusion
Mastering these core Java design patterns empowers you to write more resilient, scalable,
and maintainable applications. By consciously applying patterns like Singleton for resource
management, Factory for abstracting object creation, Strategy for flexible algorithms,
Observer for event handling, Decorator for dynamic functionality extension, and Adapter
for interface compatibility, you elevate your code quality significantly. Remember that
design patterns are not rigid rules but flexible guidelines—tools in your development
toolkit. The key is to understand the problem each pattern solves and apply it judiciously
where it brings the most value, often after identifying areas in your code (such as excessive
'if-else' statements or tightly coupled components) that could benefit from refactoring.
Continuous learning and practical application will solidify your understanding and
expertise in leveraging these powerful design principles.
22