Java Fundamentals

Object-Oriented Design Patterns

Lecture 9

Reusable solutions to common software design problems

Summary of Previous Lectures

We have learned:
  • Object-Oriented Programming principles (encapsulation, inheritance, polymorphism)
  • Abstract classes and interfaces
  • Collections framework (ArrayList, HashMap, etc.)
  • File I/O and exception handling
Now we'll learn: How to combine these concepts into proven, reusable solutions called Design Patterns

What are Design Patterns?

  • Definition: Reusable solutions to commonly occurring problems in software design
  • Origin: Documented by the "Gang of Four" (GoF) in 1994
  • Purpose:
    • Provide proven development paradigms
    • Speed up development process
    • Improve code readability and maintainability
    • Establish common vocabulary among developers
Design patterns are templates, not finished code. They describe the general approach, not the specific implementation.

Categories of Design Patterns

Category Purpose Examples
Creational Object creation mechanisms Singleton, Factory, Builder
Structural Class and object composition Adapter, Decorator, Facade
Behavioral Communication between objects Observer, Strategy, Command
In this lecture, we'll cover the most commonly used patterns from each category.

Singleton Pattern

  • Category: Creational
  • Problem: Ensure a class has only one instance and provide global access to it
  • Use cases:
    • Configuration managers
    • Database connection pools
    • Logging services
    • Application settings
Key idea: Private constructor + static method to get the instance

Singleton: Basic Implementation

public class AppSettings {
    // The single instance (static field)
    private static AppSettings instance;

    // Private constructor prevents external instantiation
    private AppSettings() {
        // Load settings from file or defaults
    }

    // Public static method to get the instance
    public static AppSettings getInstance() {
        if (instance == null) {
            instance = new AppSettings();
        }
        return instance;
    }

    // Instance methods
    private String theme = "light";

    public String getTheme() {
        return theme;
    }

    public void setTheme(String theme) {
        this.theme = theme;
    }
}

Singleton: Usage

public class Application {
    public static void main(String[] args) {
        // Get the singleton instance
        AppSettings settings = AppSettings.getInstance();

        // Use it
        System.out.println("Current theme: " + settings.getTheme());
        settings.setTheme("dark");

        // Anywhere else in the application...
        AppSettings sameSettings = AppSettings.getInstance();
        System.out.println("Theme is still: " + sameSettings.getTheme());
        // Output: "dark" - same instance!

        // This proves it's the same object
        System.out.println(settings == sameSettings);  // true
    }
}
Important: new AppSettings() is not allowed - the constructor is private!

Singleton: Thread-Safe Version

public class AppSettings {
    // volatile ensures visibility across threads
    private static volatile AppSettings instance;

    private AppSettings() { }

    // Double-checked locking for thread safety
    public static AppSettings getInstance() {
        if (instance == null) {
            synchronized (AppSettings.class) {
                if (instance == null) {
                    instance = new AppSettings();
                }
            }
        }
        return instance;
    }
}
  • The basic version has a race condition in multi-threaded apps
  • Use synchronized or the enum approach for thread safety

Factory Pattern

  • Category: Creational
  • Problem: Create objects without specifying the exact class to instantiate
  • Use cases:
    • Creating different types of objects based on input
    • Hiding complex creation logic
    • Returning objects that implement a common interface
Key idea: Delegate object creation to a factory method or class

Factory: The Problem

// Without Factory - code is tightly coupled
public class ShapeDrawer {
    public void drawShape(String type) {
        Shape shape;

        // This code must know about ALL shape types
        if (type.equals("circle")) {
            shape = new Circle();
        } else if (type.equals("rectangle")) {
            shape = new Rectangle();
        } else if (type.equals("triangle")) {
            shape = new Triangle();
        } else {
            throw new IllegalArgumentException("Unknown shape");
        }

        shape.draw();
    }
}
Problems:
  • Adding new shapes requires modifying this class
  • Creation logic scattered throughout the codebase
  • Violates Open/Closed Principle

Factory: Implementation

public class ShapeFactory {

    public static Shape createShape(String type) {
        switch (type.toLowerCase()) {
            case "circle":
                return new Circle();
            case "rectangle":
                return new Rectangle();
            case "triangle":
                return new Triangle();
            default:
                throw new IllegalArgumentException(
                    "Unknown shape type: " + type);
        }
    }

    // Overloaded method with parameters
    public static Shape createCircle(double radius) {
        return new Circle(radius);
    }

    public static Shape createRectangle(double width, double height) {
        return new Rectangle(width, height);
    }
}

Factory: Usage

public class ShapeDrawer {
    public void drawShape(String type) {
        // Delegate creation to factory
        Shape shape = ShapeFactory.createShape(type);
        shape.draw();
    }
}

public class Application {
    public static void main(String[] args) {
        // Create shapes without knowing concrete classes
        Shape circle = ShapeFactory.createShape("circle");
        Shape rectangle = ShapeFactory.createRectangle(10, 20);

        // Use polymorphism
        List<Shape> shapes = new ArrayList<>();
        shapes.add(circle);
        shapes.add(rectangle);

        for (Shape shape : shapes) {
            System.out.println("Area: " + shape.calculateArea());
        }
    }
}
Benefit: To add a new shape, only modify the factory - not every place that creates shapes

Builder Pattern

  • Category: Creational
  • Problem: Construct complex objects step by step
  • Use cases:
    • Objects with many optional parameters
    • Immutable objects that need complex construction
    • Avoiding "telescoping constructors"
Key idea: Separate construction from representation using a builder class

Builder: The Problem

public class User {
    // Many fields, some required, some optional
    public User(String firstName, String lastName) { }
    public User(String firstName, String lastName, String email) { }
    public User(String firstName, String lastName, String email,
                int age) { }
    public User(String firstName, String lastName, String email,
                int age, String phone) { }
    public User(String firstName, String lastName, String email,
                int age, String phone, String address) { }
    // ... telescoping constructors!
}

// Usage is confusing:
User user = new User("John", "Doe", null, 25, null, "Paris");
// What do all these parameters mean?
Problems:
  • Too many constructor variants
  • Hard to read and maintain
  • Easy to mix up parameter order

Builder: Implementation

public class User {
    private final String firstName;  // required
    private final String lastName;   // required
    private final String email;      // optional
    private final int age;           // optional
    private final String phone;      // optional

    private User(Builder builder) {
        this.firstName = builder.firstName;
        this.lastName = builder.lastName;
        this.email = builder.email;
        this.age = builder.age;
        this.phone = builder.phone;
    }

    // Getters...
    public String getFirstName() { return firstName; }
    public String getLastName() { return lastName; }
    public String getEmail() { return email; }
    public int getAge() { return age; }
    public String getPhone() { return phone; }

    // Static inner Builder class (see next slide)
}

Builder: The Builder Class

public class User {
    // ... fields and constructor from previous slide

    public static class Builder {
        private final String firstName;  // required
        private final String lastName;   // required
        private String email = "";       // optional with default
        private int age = 0;
        private String phone = "";

        public Builder(String firstName, String lastName) {
            this.firstName = firstName;
            this.lastName = lastName;
        }

        public Builder email(String email) {
            this.email = email;
            return this;  // Return this for chaining
        }

        public Builder age(int age) {
            this.age = age;
            return this;
        }

        public Builder phone(String phone) {
            this.phone = phone;
            return this;
        }

        public User build() {
            return new User(this);
        }
    }
}

Builder: Usage

public class Application {
    public static void main(String[] args) {
        // Fluent, readable object construction
        User user1 = new User.Builder("John", "Doe")
            .email("john@example.com")
            .age(25)
            .phone("555-1234")
            .build();

        // Only required fields
        User user2 = new User.Builder("Jane", "Smith")
            .build();

        // Some optional fields
        User user3 = new User.Builder("Bob", "Wilson")
            .email("bob@example.com")
            .build();

        System.out.println(user1.getFirstName() + " " + user1.getLastName());
        System.out.println("Email: " + user1.getEmail());
    }
}
Benefits: Readable, flexible, and the User object can be immutable

Strategy Pattern

  • Category: Behavioral
  • Problem: Define a family of algorithms and make them interchangeable
  • Use cases:
    • Different sorting algorithms
    • Different payment methods
    • Different validation strategies
    • Different pricing calculations
Key idea: Encapsulate algorithms in separate classes that implement a common interface

Strategy: The Problem

public class PaymentProcessor {
    public void processPayment(String method, double amount) {
        if (method.equals("credit_card")) {
            // Credit card logic
            System.out.println("Processing credit card payment: " + amount);
            // Validate card, connect to payment gateway, etc.
        } else if (method.equals("paypal")) {
            // PayPal logic
            System.out.println("Processing PayPal payment: " + amount);
            // Redirect to PayPal, handle callback, etc.
        } else if (method.equals("bank_transfer")) {
            // Bank transfer logic
            System.out.println("Processing bank transfer: " + amount);
            // Generate IBAN, initiate transfer, etc.
        }
        // Adding new payment methods = modifying this class
    }
}
Problems: Violates Open/Closed Principle, hard to test, hard to extend

Strategy: Implementation

// Strategy interface
public interface PaymentStrategy {
    void pay(double amount);
}

// Concrete strategies
public class CreditCardPayment implements PaymentStrategy {
    private String cardNumber;

    public CreditCardPayment(String cardNumber) {
        this.cardNumber = cardNumber;
    }

    @Override
    public void pay(double amount) {
        System.out.println("Paid " + amount + " using credit card: "
            + cardNumber.substring(cardNumber.length() - 4));
    }
}

public class PayPalPayment implements PaymentStrategy {
    private String email;

    public PayPalPayment(String email) {
        this.email = email;
    }

    @Override
    public void pay(double amount) {
        System.out.println("Paid " + amount + " using PayPal: " + email);
    }
}

Strategy: Context and Usage

// Context class that uses the strategy
public class ShoppingCart {
    private PaymentStrategy paymentStrategy;
    private double total;

    public void setPaymentStrategy(PaymentStrategy strategy) {
        this.paymentStrategy = strategy;
    }

    public void checkout() {
        paymentStrategy.pay(total);
    }
}

// Usage
public static void main(String[] args) {
    ShoppingCart cart = new ShoppingCart();
    cart.addItem("Book", 29.99);

    // Pay with credit card
    cart.setPaymentStrategy(new CreditCardPayment("1234-5678-9012-3456"));
    cart.checkout();

    // Or pay with PayPal
    cart.setPaymentStrategy(new PayPalPayment("user@email.com"));
    cart.checkout();
}
Adding new payment methods: Just create a new class implementing PaymentStrategy

Observer Pattern

  • Category: Behavioral
  • Problem: Define a one-to-many dependency so that when one object changes state, all dependents are notified
  • Use cases:
    • Event handling systems
    • GUI frameworks (button clicks)
    • Notification systems
    • Real-time data updates
Key idea: Subject maintains a list of observers and notifies them of state changes

Observer: Implementation

// Observer interface
public interface Observer {
    void update(String message);
}

// Subject (Observable) class
public class NewsAgency {
    private List<Observer> observers = new ArrayList<>();
    private String news;

    public void addObserver(Observer observer) {
        observers.add(observer);
    }

    public void removeObserver(Observer observer) {
        observers.remove(observer);
    }

    public void setNews(String news) {
        this.news = news;
        notifyObservers();
    }

    private void notifyObservers() {
        for (Observer observer : observers) {
            observer.update(news);
        }
    }
}

Observer: Concrete Observers

public class EmailSubscriber implements Observer {
    private String email;

    public EmailSubscriber(String email) {
        this.email = email;
    }

    @Override
    public void update(String news) {
        System.out.println("Email sent to " + email + ": " + news);
    }
}

public class SMSSubscriber implements Observer {
    private String phoneNumber;

    public SMSSubscriber(String phoneNumber) {
        this.phoneNumber = phoneNumber;
    }

    @Override
    public void update(String news) {
        System.out.println("SMS sent to " + phoneNumber + ": " + news);
    }
}

public class AppNotification implements Observer {
    @Override
    public void update(String news) {
        System.out.println("Push notification: " + news);
    }
}

Observer: Usage

public class Application {
    public static void main(String[] args) {
        // Create the subject
        NewsAgency agency = new NewsAgency();

        // Create observers
        Observer emailSub = new EmailSubscriber("user@email.com");
        Observer smsSub = new SMSSubscriber("+33612345678");
        Observer appNotif = new AppNotification();

        // Register observers
        agency.addObserver(emailSub);
        agency.addObserver(smsSub);
        agency.addObserver(appNotif);

        // When news is published, all observers are notified
        agency.setNews("Breaking: Java 22 released!");

        // Output:
        // Email sent to user@email.com: Breaking: Java 22 released!
        // SMS sent to +33612345678: Breaking: Java 22 released!
        // Push notification: Breaking: Java 22 released!
    }
}

MVC Pattern (Model-View-Controller)

  • Category: Architectural (combines multiple patterns)
  • Problem: Separate concerns in applications with user interfaces
  • Components:
    • Model: Data and business logic
    • View: User interface (display)
    • Controller: Handles user input, updates Model and View
MVC is widely used in web frameworks (Spring MVC) and desktop applications (Swing)

MVC: Diagram

    +-------------+       +----------------+
    |    VIEW     |<------|   CONTROLLER   |
    | (Display)   |       | (Handle Input) |
    +-------------+       +----------------+
          ^                      |
          |                      v
          |               +-------------+
          +---------------|    MODEL    |
                          |   (Data)    |
                          +-------------+
            
  • User interacts with the View
  • Controller receives input and updates the Model
  • Model notifies View of changes (Observer pattern)
  • View displays updated data

MVC: Model

// Model - represents data and business logic
public class StudentModel {
    private String name;
    private int grade;

    public StudentModel(String name, int grade) {
        this.name = name;
        this.grade = grade;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getGrade() {
        return grade;
    }

    public void setGrade(int grade) {
        this.grade = grade;
    }

    // Business logic
    public boolean isPassing() {
        return grade >= 10;
    }
}

MVC: View

// View - displays data to user
public class StudentView {

    public void displayStudentDetails(String name, int grade,
                                      boolean passing) {
        System.out.println("=== Student Details ===");
        System.out.println("Name: " + name);
        System.out.println("Grade: " + grade + "/20");
        System.out.println("Status: " + (passing ? "PASSING" : "FAILING"));
        System.out.println("=======================");
    }

    public void displayError(String message) {
        System.out.println("ERROR: " + message);
    }

    public void displaySuccess(String message) {
        System.out.println("SUCCESS: " + message);
    }
}
The View only displays - it doesn't contain business logic

MVC: Controller

// Controller - handles user input, coordinates Model and View
public class StudentController {
    private StudentModel model;
    private StudentView view;

    public StudentController(StudentModel model, StudentView view) {
        this.model = model;
        this.view = view;
    }

    public void setStudentName(String name) {
        model.setName(name);
    }

    public void setStudentGrade(int grade) {
        if (grade < 0 || grade > 20) {
            view.displayError("Grade must be between 0 and 20");
            return;
        }
        model.setGrade(grade);
        view.displaySuccess("Grade updated");
    }

    public void updateView() {
        view.displayStudentDetails(
            model.getName(),
            model.getGrade(),
            model.isPassing()
        );
    }
}

MVC: Usage

public class Application {
    public static void main(String[] args) {
        // Create Model
        StudentModel model = new StudentModel("Alice", 15);

        // Create View
        StudentView view = new StudentView();

        // Create Controller
        StudentController controller = new StudentController(model, view);

        // Display initial data
        controller.updateView();

        // User interaction: update grade
        controller.setStudentGrade(18);
        controller.updateView();

        // Invalid input
        controller.setStudentGrade(25);  // ERROR: Grade must be between 0 and 20
    }
}

Design Patterns Summary

Pattern When to Use
Singleton Need exactly one instance globally (config, logging)
Factory Creating objects without specifying exact class
Builder Complex objects with many optional parameters
Strategy Need interchangeable algorithms at runtime
Observer One-to-many notifications (events, subscriptions)
MVC Separating UI from business logic

Exercise 1: Singleton - Application Settings

Create a DatabaseConfig singleton that stores database connection settings:

  1. Implement the Singleton pattern with:
    • Private constructor
    • Static getInstance() method
  2. Store these settings:
    • host (default: "localhost")
    • port (default: 3306)
    • database (default: "myapp")
    • username, password
  3. Add a method getConnectionString() that returns: "jdbc:mysql://host:port/database"
  4. Test that multiple calls to getInstance() return the same object

Exercise 2: Factory - Shape Creation

Extend the geometry system with a Factory pattern:

  1. Create a ShapeFactory class with:
    • createShape(String type) - creates shape by name
    • createRandomShape() - creates a random shape with random dimensions
  2. Support these shapes: Circle, Rectangle, Triangle
  3. Add a createShapeFromArea(String type, double area) method that creates a shape with the specified area
  4. Test by creating 5 random shapes and calculating their total area
Hint: For createShapeFromArea, you'll need to calculate dimensions from the area (e.g., for a circle: radius = sqrt(area / PI))

Exercise 3: Observer - Stock Price Monitor

Create a stock price notification system:

  1. Create a Stock class (Subject) with:
    • Symbol (e.g., "AAPL")
    • Price (can change)
    • Methods to add/remove observers
  2. Create an StockObserver interface with update(String symbol, double price)
  3. Implement these observers:
    • PriceAlert - prints warning if price exceeds a threshold
    • PriceLogger - logs all price changes to console
    • PortfolioTracker - tracks total portfolio value
  4. Simulate price changes and verify all observers are notified

Exercise 4: Complete Project - Bank System with Patterns

Refactor the Bank Account system using design patterns:

  1. Singleton: Create a BankConfig for interest rates and fees
  2. Factory: Create AccountFactory to create different account types (Savings, Checking, Investment)
  3. Strategy: Implement different interest calculation strategies:
    • SimpleInterest
    • CompoundInterest
    • NoInterest (for checking accounts)
  4. Observer: Notify when:
    • Balance goes below minimum
    • Large transaction occurs
Bonus: Add MVC to separate the console UI from the business logic

Key Takeaways

  • Design patterns are tools, not rules
    • Use them when they solve a real problem
    • Don't over-engineer simple solutions
  • Patterns promote good design principles:
    • Single Responsibility Principle
    • Open/Closed Principle
    • Dependency Inversion
  • Common vocabulary: When you say "Factory" or "Observer", other developers understand immediately
  • Practice recognizing when to use each pattern - it becomes intuitive with experience

Slide Overview