Browse Design Patterns in Java: Building Robust Applications

Design Patterns in Java: Comprehensive Answers to Exercises

Explore detailed solutions and explanations for exercises in 'Design Patterns in Java: Building Robust Applications', reinforcing key concepts and practical applications.

D. Answers to Exercises

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.

Exercise 1: Singleton Pattern Implementation

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:

  • Private Constructor: Ensures that the class cannot be instantiated from outside.
  • Static Inner Class: The SingletonHelper class is not loaded into memory until the getInstance() method is called, ensuring lazy initialization.
  • Thread Safety: The class loader mechanism ensures that the instance is created only once.

Advantages:

  • Lazy initialization without synchronization overhead.
  • Simple and easy to understand.

Disadvantages:

  • Does not handle exceptions during instance creation.

Alternative Approaches:

  • Double-Checked Locking: More complex and error-prone.
  • Enum Singleton: Provides serialization safety and protection against reflection attacks.

Real-World Application: Singletons are often used for logging, configuration settings, and managing shared resources.

Exercise 2: Factory Method Pattern

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:

  • Shape Interface: Defines the contract for all shape types.
  • Concrete Classes: Circle and Square implement the Shape interface.
  • Factory Method: ShapeFactory defines a method createShape() for creating objects, allowing subclasses to alter the type of objects that will be created.

Promoting Loose Coupling:

  • The client code interacts with the Shape interface rather than concrete classes, reducing dependencies.
  • New shapes can be added without modifying existing code, adhering to the Open/Closed Principle.

Common Errors:

  • Forgetting to implement the factory method in subclasses.
  • Tight coupling by directly instantiating objects in client code.

Exercise 3: Observer Pattern

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:

  • Observer Interface: Defines the update() method that observers must implement.
  • Subject Interface: Provides methods for registering, removing, and notifying observers.
  • Concrete Subject (WeatherData): Maintains a list of observers and notifies them of state changes.
  • Concrete Observer (CurrentConditionsDisplay): Implements the Observer interface and updates its state based on the subject’s changes.

Publisher-Subscriber Model:

  • The subject (publisher) maintains a list of subscribers (observers) and notifies them of changes.
  • Decouples the subject from the observers, allowing for dynamic subscription and notification.

Common Pitfalls:

  • Forgetting to remove observers when they are no longer needed, leading to memory leaks.
  • Not handling exceptions that occur during observer updates.

Exercise 4: Command Pattern

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:

  • Command Interface: Declares the execute() method for all command objects.
  • Concrete Command (LightOnCommand): Implements the Command interface and defines the action to be performed.
  • Receiver (Light): Contains the actual logic to perform the action.
  • Invoker (RemoteControl): Holds a command and triggers its execution.

Encapsulation of Requests:

  • Commands are encapsulated as objects, allowing for parameterization and queuing of requests.
  • Supports undo/redo operations by maintaining a history of executed commands.

Alternative Approaches:

  • Use lambda expressions in Java 8+ for simpler command implementations.
  • Implement a macro command to execute multiple commands in sequence.

Exercise 5: Strategy Pattern

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:

  • Strategy Interface (PaymentStrategy): Defines the algorithm interface.
  • Concrete Strategies: Implement the algorithm interface for different payment methods.
  • Context (ShoppingCart): Maintains a reference to a strategy object and delegates the payment processing to it.

Dynamic Algorithm Selection:

  • The client can change the payment strategy at runtime, allowing for flexible and dynamic behavior.
  • Promotes the Open/Closed Principle by enabling new payment methods to be added without modifying existing code.

Common Mistakes:

  • Tight coupling between the context and concrete strategies.
  • Not providing a default strategy, leading to runtime errors.

Exercise 6: Decorator Pattern

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:

  • Component Interface (Coffee): Defines the interface for objects that can have responsibilities added to them.
  • Concrete Component (SimpleCoffee): Implements the component interface.
  • Decorator (CoffeeDecorator): Maintains a reference to a component object and defines an interface that conforms to the component’s interface.
  • Concrete Decorators: Extend the decorator class and add responsibilities to the component.

Difference from Inheritance:

  • Decorators provide a flexible alternative to subclassing for extending functionality.
  • They allow for dynamic composition of behaviors at runtime, unlike inheritance which is static.

Best Practices:

  • Use decorators when you need to add responsibilities to individual objects, not to entire classes.
  • Avoid creating too many layers of decorators, which can complicate the code.

Exercise 7: Composite Pattern

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:

  • Component Interface (FileSystemComponent): Declares the interface for objects in the composition.
  • Leaf (File): Represents leaf objects in the composition. Implements the component interface.
  • Composite (Directory): Defines behavior for components having children and stores child components.

Simplifying Client Code:

  • Clients can treat individual objects and compositions uniformly, simplifying the code that uses these objects.
  • The pattern allows for complex tree structures to be built and managed easily.

Common Challenges:

  • Ensuring that operations on the composite structure are efficient.
  • Managing the complexity of operations that involve traversing the composite structure.

Exercise 8: State Pattern

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:

  • State Interface (VendingMachineState): Declares methods for handling requests corresponding to different states.
  • Concrete States: Implement state-specific behavior and transition logic.
  • Context (VendingMachine): Maintains an instance of a state subclass that defines the current state.

Managing State-Specific Behavior:

  • The pattern encapsulates state-specific behavior within state classes, allowing the context to delegate behavior to the current state.
  • Simplifies the context by removing conditional logic for state transitions.

Common Errors:

  • Not maintaining a reference to the context in state classes, preventing state transitions.
  • Forgetting to update the current state after a transition.

Exercise 9: Template Method Pattern

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:

  • Template Method (process()): Defines the skeleton of the algorithm, calling abstract methods that subclasses implement.
  • Abstract Methods: Must be implemented by subclasses to provide specific behavior.
  • Hook Method (preProcessHook()): Provides optional behavior that subclasses can override.

Role of Hook Methods:

  • Allow subclasses to extend or modify parts of the algorithm without changing the template method.
  • Provide flexibility for subclasses to add custom behavior at specific points in the algorithm.

Best Practices:

  • Use hook methods sparingly to avoid overcomplicating the template method.
  • Ensure that the template method remains final to prevent subclasses from altering the algorithm’s structure.

Exercise 10: Chain of Responsibility Pattern

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:

  • Handler Interface (Logger): Defines the interface for handling requests and maintains a reference to the next handler.
  • Concrete Handlers: Implement the handler interface and process requests they are responsible for.
  • Chain of Responsibility: Each handler decides whether to process the request or pass it to the next handler in the chain.

Decoupling Senders and Receivers:

  • The pattern decouples the sender of a request from its receivers by allowing multiple objects to handle the request.
  • Handlers can be added or removed without affecting the client code.

Common Mistakes:

  • Not setting the next handler, breaking the chain.
  • Creating circular chains, leading to infinite loops.

Conclusion

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.

Quiz Time!

### Which design pattern is used to ensure that a class has only one instance and provides a global point of access to it? - [x] Singleton Pattern - [ ] Factory Method Pattern - [ ] Observer Pattern - [ ] Decorator Pattern > **Explanation:** The Singleton Pattern is used to ensure a class has only one instance and provides a global point of access to it. ### What is the primary advantage of using the Factory Method pattern? - [x] It promotes loose coupling by allowing subclasses to decide which class to instantiate. - [ ] It ensures a class has only one instance. - [ ] It allows objects to be notified of changes in other objects. - [ ] It adds responsibilities to objects dynamically. > **Explanation:** The Factory Method pattern promotes loose coupling by allowing subclasses to decide which class to instantiate. ### In the Observer pattern, what is the role of the Subject? - [x] To maintain a list of observers and notify them of changes. - [ ] To encapsulate requests as objects. - [ ] To define a family of algorithms. - [ ] To ensure a class has only one instance. > **Explanation:** The Subject maintains a list of observers and notifies them of changes. ### How does the Command pattern encapsulate requests? - [x] By turning requests into objects that can be parameterized and queued. - [ ] By maintaining a list of observers. - [ ] By ensuring a class has only one instance. - [ ] By adding responsibilities to objects dynamically. > **Explanation:** The Command pattern encapsulates requests by turning them into objects that can be parameterized and queued. ### Which pattern allows for dynamic selection of algorithms at runtime? - [x] Strategy Pattern - [ ] Singleton Pattern - [ ] Observer Pattern - [ ] Composite Pattern > **Explanation:** The Strategy Pattern allows for dynamic selection of algorithms at runtime. ### What is the main difference between the Decorator pattern and inheritance? - [x] Decorators provide a flexible alternative to subclassing for extending functionality. - [ ] Decorators ensure a class has only one instance. - [ ] Decorators encapsulate requests as objects. - [ ] Decorators maintain a list of observers. > **Explanation:** Decorators provide a flexible alternative to subclassing for extending functionality. ### How does the Composite pattern simplify client code? - [x] By allowing clients to treat individual objects and compositions uniformly. - [ ] By encapsulating requests as objects. - [ ] By ensuring a class has only one instance. - [ ] By maintaining a list of observers. > **Explanation:** The Composite pattern simplifies client code by allowing clients to treat individual objects and compositions uniformly. ### What is the role of hook methods in the Template Method pattern? - [x] To provide optional behavior that subclasses can override. - [ ] To maintain a list of observers. - [ ] To encapsulate requests as objects. - [ ] To ensure a class has only one instance. > **Explanation:** Hook methods provide optional behavior that subclasses can override in the Template Method pattern. ### How does the Chain of Responsibility pattern decouple senders and receivers? - [x] By allowing multiple objects to handle a request without the sender knowing which object will handle it. - [ ] By maintaining a list of observers. - [ ] By encapsulating requests as objects. - [ ] By ensuring a class has only one instance. > **Explanation:** The Chain of Responsibility pattern decouples senders and receivers by allowing multiple objects to handle a request without the sender knowing which object will handle it. ### True or False: The State pattern encapsulates state-specific behavior within state classes. - [x] True - [ ] False > **Explanation:** True. The State pattern encapsulates state-specific behavior within state classes, allowing the context to delegate behavior to the current state.

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.