Explore detailed solutions and explanations for exercises in 'Design Patterns in Java: Building Robust Applications', reinforcing key concepts and practical applications.
Welcome to the “Answers to Exercises” section of “Design Patterns in Java: Building Robust Applications.” This section is designed to provide you with detailed solutions and explanations for the exercises presented throughout the book. These exercises are crafted to reinforce your understanding of design patterns and their practical applications in Java. We encourage you to attempt these exercises on your own before reviewing the solutions provided here. Let’s dive into the answers and explore the insights they offer.
Problem Statement: Implement a thread-safe Singleton pattern in Java. Discuss the advantages and disadvantages of your chosen implementation.
Solution:
To implement a thread-safe Singleton pattern, we can use the Bill Pugh Singleton Design. This approach leverages the Java memory model’s guarantees about class initialization, ensuring that the Singleton instance is created in a thread-safe manner without requiring synchronized blocks.
public class Singleton {
private Singleton() {
// private constructor to prevent instantiation
}
private static class SingletonHelper {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonHelper.INSTANCE;
}
}
Explanation:
SingletonHelper
class is not loaded into memory until the getInstance()
method is called, ensuring lazy initialization.Advantages:
Disadvantages:
Alternative Approaches:
Real-World Application: Singletons are often used for logging, configuration settings, and managing shared resources.
Problem Statement:
Design a Factory Method pattern to create different types of Shape
objects (e.g., Circle, Square). Explain how this pattern promotes loose coupling.
Solution:
// Shape interface
public interface Shape {
void draw();
}
// Concrete implementations
public class Circle implements Shape {
@Override
public void draw() {
System.out.println("Drawing a Circle");
}
}
public class Square implements Shape {
@Override
public void draw() {
System.out.println("Drawing a Square");
}
}
// Factory Method
public abstract class ShapeFactory {
public abstract Shape createShape();
public void render() {
Shape shape = createShape();
shape.draw();
}
}
public class CircleFactory extends ShapeFactory {
@Override
public Shape createShape() {
return new Circle();
}
}
public class SquareFactory extends ShapeFactory {
@Override
public Shape createShape() {
return new Square();
}
}
Explanation:
Circle
and Square
implement the Shape
interface.ShapeFactory
defines a method createShape()
for creating objects, allowing subclasses to alter the type of objects that will be created.Promoting Loose Coupling:
Shape
interface rather than concrete classes, reducing dependencies.Common Errors:
Problem Statement:
Implement an Observer pattern to notify multiple observers about changes in a WeatherData
object. Illustrate how this pattern supports the Publisher-Subscriber model.
Solution:
import java.util.ArrayList;
import java.util.List;
// Observer interface
interface Observer {
void update(float temperature, float humidity, float pressure);
}
// Subject interface
interface Subject {
void registerObserver(Observer o);
void removeObserver(Observer o);
void notifyObservers();
}
// Concrete Subject
class WeatherData implements Subject {
private List<Observer> observers;
private float temperature;
private float humidity;
private float pressure;
public WeatherData() {
observers = new ArrayList<>();
}
@Override
public void registerObserver(Observer o) {
observers.add(o);
}
@Override
public void removeObserver(Observer o) {
observers.remove(o);
}
@Override
public void notifyObservers() {
for (Observer observer : observers) {
observer.update(temperature, humidity, pressure);
}
}
public void setMeasurements(float temperature, float humidity, float pressure) {
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
notifyObservers();
}
}
// Concrete Observer
class CurrentConditionsDisplay implements Observer {
private float temperature;
private float humidity;
@Override
public void update(float temperature, float humidity, float pressure) {
this.temperature = temperature;
this.humidity = humidity;
display();
}
public void display() {
System.out.println("Current conditions: " + temperature + "F degrees and " + humidity + "% humidity");
}
}
Explanation:
update()
method that observers must implement.WeatherData
): Maintains a list of observers and notifies them of state changes.CurrentConditionsDisplay
): Implements the Observer
interface and updates its state based on the subject’s changes.Publisher-Subscriber Model:
Common Pitfalls:
Problem Statement: Create a Command pattern to implement a remote control system for home appliances. Discuss how this pattern encapsulates requests as objects.
Solution:
// Command Interface
interface Command {
void execute();
}
// Concrete Command
class LightOnCommand implements Command {
private Light light;
public LightOnCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
light.on();
}
}
// Receiver
class Light {
public void on() {
System.out.println("The light is on");
}
public void off() {
System.out.println("The light is off");
}
}
// Invoker
class RemoteControl {
private Command command;
public void setCommand(Command command) {
this.command = command;
}
public void pressButton() {
command.execute();
}
}
Explanation:
execute()
method for all command objects.LightOnCommand
): Implements the Command
interface and defines the action to be performed.Light
): Contains the actual logic to perform the action.RemoteControl
): Holds a command and triggers its execution.Encapsulation of Requests:
Alternative Approaches:
Problem Statement: Implement a Strategy pattern for a payment processing system that supports different payment methods (e.g., Credit Card, PayPal). Explain how this pattern allows for dynamic algorithm selection.
Solution:
// Strategy Interface
interface PaymentStrategy {
void pay(int amount);
}
// Concrete Strategies
class CreditCardPayment implements PaymentStrategy {
private String cardNumber;
public CreditCardPayment(String cardNumber) {
this.cardNumber = cardNumber;
}
@Override
public void pay(int amount) {
System.out.println("Paid " + amount + " using Credit Card.");
}
}
class PayPalPayment implements PaymentStrategy {
private String email;
public PayPalPayment(String email) {
this.email = email;
}
@Override
public void pay(int amount) {
System.out.println("Paid " + amount + " using PayPal.");
}
}
// Context
class ShoppingCart {
private PaymentStrategy paymentStrategy;
public void setPaymentStrategy(PaymentStrategy paymentStrategy) {
this.paymentStrategy = paymentStrategy;
}
public void checkout(int amount) {
paymentStrategy.pay(amount);
}
}
Explanation:
PaymentStrategy
): Defines the algorithm interface.ShoppingCart
): Maintains a reference to a strategy object and delegates the payment processing to it.Dynamic Algorithm Selection:
Common Mistakes:
Problem Statement:
Design a Decorator pattern to add additional features to a Coffee
object, such as milk or sugar. Describe how this pattern differs from inheritance.
Solution:
// Component Interface
interface Coffee {
double cost();
String description();
}
// Concrete Component
class SimpleCoffee implements Coffee {
@Override
public double cost() {
return 2.0;
}
@Override
public String description() {
return "Simple Coffee";
}
}
// Decorator
abstract class CoffeeDecorator implements Coffee {
protected Coffee coffee;
public CoffeeDecorator(Coffee coffee) {
this.coffee = coffee;
}
@Override
public double cost() {
return coffee.cost();
}
@Override
public String description() {
return coffee.description();
}
}
// Concrete Decorators
class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) {
super(coffee);
}
@Override
public double cost() {
return super.cost() + 0.5;
}
@Override
public String description() {
return super.description() + ", Milk";
}
}
class SugarDecorator extends CoffeeDecorator {
public SugarDecorator(Coffee coffee) {
super(coffee);
}
@Override
public double cost() {
return super.cost() + 0.2;
}
@Override
public String description() {
return super.description() + ", Sugar";
}
}
Explanation:
Coffee
): Defines the interface for objects that can have responsibilities added to them.SimpleCoffee
): Implements the component interface.CoffeeDecorator
): Maintains a reference to a component object and defines an interface that conforms to the component’s interface.Difference from Inheritance:
Best Practices:
Problem Statement: Implement a Composite pattern for a file system where files and directories are treated uniformly. Explain how this pattern simplifies client code.
Solution:
import java.util.ArrayList;
import java.util.List;
// Component
interface FileSystemComponent {
void showDetails();
}
// Leaf
class File implements FileSystemComponent {
private String name;
public File(String name) {
this.name = name;
}
@Override
public void showDetails() {
System.out.println("File: " + name);
}
}
// Composite
class Directory implements FileSystemComponent {
private String name;
private List<FileSystemComponent> components = new ArrayList<>();
public Directory(String name) {
this.name = name;
}
public void addComponent(FileSystemComponent component) {
components.add(component);
}
public void removeComponent(FileSystemComponent component) {
components.remove(component);
}
@Override
public void showDetails() {
System.out.println("Directory: " + name);
for (FileSystemComponent component : components) {
component.showDetails();
}
}
}
Explanation:
FileSystemComponent
): Declares the interface for objects in the composition.File
): Represents leaf objects in the composition. Implements the component interface.Directory
): Defines behavior for components having children and stores child components.Simplifying Client Code:
Common Challenges:
Problem Statement: Implement a State pattern for a vending machine that transitions between different states (e.g., NoCoin, HasCoin, Dispensing). Discuss how this pattern manages state-specific behavior.
Solution:
// State Interface
interface VendingMachineState {
void insertCoin();
void pressButton();
void dispense();
}
// Context
class VendingMachine {
private VendingMachineState noCoinState;
private VendingMachineState hasCoinState;
private VendingMachineState dispensingState;
private VendingMachineState currentState;
public VendingMachine() {
noCoinState = new NoCoinState(this);
hasCoinState = new HasCoinState(this);
dispensingState = new DispensingState(this);
currentState = noCoinState;
}
public void setState(VendingMachineState state) {
currentState = state;
}
public void insertCoin() {
currentState.insertCoin();
}
public void pressButton() {
currentState.pressButton();
}
public void dispense() {
currentState.dispense();
}
// State Implementations
private class NoCoinState implements VendingMachineState {
private VendingMachine machine;
public NoCoinState(VendingMachine machine) {
this.machine = machine;
}
@Override
public void insertCoin() {
System.out.println("Coin inserted.");
machine.setState(machine.hasCoinState);
}
@Override
public void pressButton() {
System.out.println("Insert coin first.");
}
@Override
public void dispense() {
System.out.println("Insert coin first.");
}
}
private class HasCoinState implements VendingMachineState {
private VendingMachine machine;
public HasCoinState(VendingMachine machine) {
this.machine = machine;
}
@Override
public void insertCoin() {
System.out.println("Coin already inserted.");
}
@Override
public void pressButton() {
System.out.println("Button pressed.");
machine.setState(machine.dispensingState);
}
@Override
public void dispense() {
System.out.println("Press button to dispense.");
}
}
private class DispensingState implements VendingMachineState {
private VendingMachine machine;
public DispensingState(VendingMachine machine) {
this.machine = machine;
}
@Override
public void insertCoin() {
System.out.println("Wait for current dispensing to finish.");
}
@Override
public void pressButton() {
System.out.println("Already dispensing.");
}
@Override
public void dispense() {
System.out.println("Dispensing item.");
machine.setState(machine.noCoinState);
}
}
}
Explanation:
VendingMachineState
): Declares methods for handling requests corresponding to different states.VendingMachine
): Maintains an instance of a state subclass that defines the current state.Managing State-Specific Behavior:
Common Errors:
Problem Statement: Create a Template Method pattern for a data processing framework that reads, processes, and writes data. Explain the role of hook methods in this pattern.
Solution:
// Abstract Class
abstract class DataProcessor {
// Template Method
public final void process() {
readData();
processData();
writeData();
}
protected abstract void readData();
protected abstract void processData();
protected abstract void writeData();
// Hook Method
protected void preProcessHook() {
// Optional hook for subclasses
}
}
// Concrete Class
class CSVDataProcessor extends DataProcessor {
@Override
protected void readData() {
System.out.println("Reading CSV data.");
}
@Override
protected void processData() {
System.out.println("Processing CSV data.");
}
@Override
protected void writeData() {
System.out.println("Writing CSV data.");
}
}
Explanation:
process()
): Defines the skeleton of the algorithm, calling abstract methods that subclasses implement.preProcessHook()
): Provides optional behavior that subclasses can override.Role of Hook Methods:
Best Practices:
Problem Statement: Implement a Chain of Responsibility pattern for a logging system that handles different log levels (e.g., INFO, DEBUG, ERROR). Discuss how this pattern decouples senders and receivers.
Solution:
// Handler Interface
abstract class Logger {
public static int INFO = 1;
public static int DEBUG = 2;
public static int ERROR = 3;
protected int level;
protected Logger nextLogger;
public void setNextLogger(Logger nextLogger) {
this.nextLogger = nextLogger;
}
public void logMessage(int level, String message) {
if (this.level <= level) {
write(message);
}
if (nextLogger != null) {
nextLogger.logMessage(level, message);
}
}
protected abstract void write(String message);
}
// Concrete Handlers
class InfoLogger extends Logger {
public InfoLogger(int level) {
this.level = level;
}
@Override
protected void write(String message) {
System.out.println("INFO: " + message);
}
}
class DebugLogger extends Logger {
public DebugLogger(int level) {
this.level = level;
}
@Override
protected void write(String message) {
System.out.println("DEBUG: " + message);
}
}
class ErrorLogger extends Logger {
public ErrorLogger(int level) {
this.level = level;
}
@Override
protected void write(String message) {
System.out.println("ERROR: " + message);
}
}
Explanation:
Logger
): Defines the interface for handling requests and maintains a reference to the next handler.Decoupling Senders and Receivers:
Common Mistakes:
These exercises and their solutions illustrate the practical application of design patterns in Java. By working through these problems, you gain a deeper understanding of how to implement and utilize design patterns to build robust, maintainable software systems. Remember to reflect on how these solutions can be adapted or extended to fit your specific needs.
By engaging with these exercises and quizzes, you solidify your understanding of design patterns in Java and how they can be effectively applied in real-world scenarios. Continue exploring and experimenting with these patterns to enhance your software development skills.