Object-Oriented Design Patterns
Lecture 9
Reusable solutions to common software design problems
2026 WayUp
| 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 |
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;
}
}
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
}
}
new AppSettings() is not allowed - the constructor is private!
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;
}
}
synchronized or the enum approach for thread safety// 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();
}
}
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);
}
}
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());
}
}
}
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?
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)
}
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);
}
}
}
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());
}
}
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
}
}
// 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);
}
}
// 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();
}
// 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);
}
}
}
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);
}
}
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!
}
}
+-------------+ +----------------+
| VIEW |<------| CONTROLLER |
| (Display) | | (Handle Input) |
+-------------+ +----------------+
^ |
| v
| +-------------+
+---------------| MODEL |
| (Data) |
+-------------+
// 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;
}
}
// 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);
}
}
// 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()
);
}
}
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
}
}
| 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 |
Create a DatabaseConfig singleton that stores database connection settings:
getInstance() methodhost (default: "localhost")port (default: 3306)database (default: "myapp")username, passwordgetConnectionString() that returns: "jdbc:mysql://host:port/database"getInstance() return the same objectExtend the geometry system with a Factory pattern:
ShapeFactory class with:
createShape(String type) - creates shape by namecreateRandomShape() - creates a random shape with random dimensionscreateShapeFromArea(String type, double area) method that creates a shape with the specified areaCreate a stock price notification system:
Stock class (Subject) with:
StockObserver interface with update(String symbol, double price)PriceAlert - prints warning if price exceeds a thresholdPriceLogger - logs all price changes to consolePortfolioTracker - tracks total portfolio valueRefactor the Bank Account system using design patterns:
BankConfig for interest rates and feesAccountFactory to create different account types (Savings, Checking, Investment)SimpleInterestCompoundInterestNoInterest (for checking accounts)