Home

Week 11

Introduction to Design Patterns

Software Design and Architecture

Instructor: Baber Sheikh (Visiting Faculty)

UML diagrams and code samples use Java. Dotted terms have quick definitions.

11.1–11.6 Foundations
11.7–11.12 Creational patterns

Why are we starting Design Patterns now?

Because you already learned the big picture

In the first 10 weeks you studied architecture, quality attributes, UML modeling, and architectural styles. Now we zoom in — from the big system structure to the small, reusable design solutions inside that structure.

🏠 Think of it like building a house

Architecture tells you: "Put 3 bedrooms here, kitchen there, garage on the left."
Design Patterns tell you: "Use this type of door hinge so the door can open both ways."
Both are needed — one is about the whole building, the other is about smart solutions for individual parts.

Already learned Why it matters for Week 11
Architecture fundamentals Patterns are placed inside the components of an architecture
Cohesion and coupling Patterns are practical tools to improve cohesion and reduce coupling
Quality attributes Patterns directly affect maintainability, flexibility, and testability

Course journey so far — Weeks 1 to 10

Week Main topic Key idea to remember
Week 1 Architecture basics Elements, connectors, constraints, cohesion, coupling, quality attributes
Week 2 Design space Static, runtime, and management perspectives; connector types
Week 3 Models and views UML, 4+1 view model, architectural description languages (ADL)
Week 4 Data flow architecture Batch sequential, pipe-and-filter, process control
Week 5 Data-centered architecture Repository and blackboard styles, central data, knowledge sources
Weeks 6–10 Midterm + deeper architectural styles Hierarchy, layered systems, event-driven, microservices
Week 11 → Design Patterns Reusable solutions at the code design level

What was missing until now?

✅ We learned system-level design

  • How to break a system into parts
  • How parts communicate
  • How to document architecture
  • How to compare styles using trade-offs

❓ We still need code-level design wisdom

  • How to create objects cleanly
  • How to connect parts that don't naturally fit
  • How to add features without rewriting core code
  • How objects should collaborate at runtime

Simple one-liner

Architecture tells us what parts the system has.  Design patterns tell us how to design those parts internally.

🚗 Car analogy

Architecture = deciding how many seats, where the engine goes, what type of car (SUV, sedan).
Design Patterns = deciding how the gear mechanism works, how brakes are connected — the internal engineering solutions.

From architecture to patterns — the natural next step

Level 1: Architecture

Choose system shape:

  • Layered
  • Repository
  • Pipe-and-filter

Big picture blueprint

Level 2: Modules / Services

Define responsibilities and interfaces:

  • Service layer
  • Data layer
  • UI layer

Component boundaries

Level 3: Design Patterns

Solve recurring local design problems:

  • Factory
  • Adapter
  • Observer

This is Week 11

⚠️ Important point

Patterns do not replace architecture. They work inside architecture. You can't use a design pattern to decide if a system should be layered or event-driven — that's architecture's job.

What is a Design Pattern?

Core definition

A design pattern is a reusable, named solution to a commonly recurring problem in software design. It is not finished code — it is a template or idea that you apply to your specific situation.

Pattern IS:

  • A proven design idea or structure
  • Language-independent (same idea in Python, Java, C#)
  • A communication shorthand between developers
  • A solution to a recurring problem

Pattern is NOT:

  • Code to copy-paste directly
  • A magic fix for every situation
  • A replacement for thinking
  • Required in every project unconditionally

📖 The Recipe Book analogy

Think of design patterns like recipes in a cookbook. A recipe gives you the steps and method — but you still choose the ingredients based on what you have. You don't eat a recipe; you use it to make food. Similarly, you don't run a pattern; you use it to design your classes.

Where did Design Patterns come from?

The Gang of Four — 1994

In 1994, four computer scientists — Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides — published a book called "Design Patterns: Elements of Reusable Object-Oriented Software". They are nicknamed the Gang of Four (GoF).

What did they do?

  • Collected and named 23 recurring design solutions
  • Organized them into 3 categories: Creational, Structural, Behavioral
  • Documented each pattern with: name, intent, structure, example, trade-offs

Why does it matter to you?

  • These 23 patterns are the foundation of almost all modern software design discussions
  • When a developer says "use an Observer here" — they are referring to GoF
  • Every major language (Python, Java, C++) has examples of these patterns built in

📚 Think of it like this

Before GoF, developers solved the same problems over and over, in different ways, with no common names. GoF gave them a shared dictionary. Instead of saying "I'll create a central object that manages all instances," a developer can just say "Singleton" and everyone understands.

Patterns you use in real life (you just don't know it yet)

Design patterns exist everywhere — not only in software.

🏭

Factory

A car factory produces cars without the buyer knowing all the internal assembly steps.
→ In code: a factory creates objects without exposing how.

🔌

Adapter

A plug adapter lets a UK appliance work in a US socket without changing either device.
→ In code: an adapter lets two incompatible classes work together.

📢

Observer

When you subscribe to a YouTube channel, you get notified when a new video is uploaded.
→ In code: observers are notified when a subject changes.

Key insight for beginners

Pattern names are just agreed labels for solutions we already understand intuitively. Learning patterns means learning to recognize those solutions and apply them deliberately in code.

Characteristics of Design Patterns

1. Problem–Solution Structure

Every pattern describes a specific recurring problem and provides a well-defined solution structure for it.

2. Reusability

Once understood, a pattern can be applied across many different projects and contexts.

3. Abstraction

Patterns focus on high-level design ideas, not one specific implementation. The same pattern looks slightly different in Java vs Python.

4. Common Vocabulary

Patterns give developers standard words: Factory, Observer, Singleton. One word replaces a paragraph of explanation.

5. Proven Solutions

They come from decades of real-world software development experience — not guesswork.

6. Context-Dependent

A pattern is only useful in the right context. Outside its context, it can make things worse.

🗺️ Pattern = a named map for a known territory

A map is useful only if you are going to that place. If you hand someone a map of Paris while they are in Tokyo, it's useless — even if the map itself is perfect.

How should students think about patterns?

Common wrong assumption Correct understanding
"Every program must use patterns" Patterns are tools for recurring design problems. Simple programs may not need them.
"Patterns are code to memorize exactly" Patterns are design ideas. They look different in different languages and contexts.
"More patterns = better code" Wrong patterns or too many patterns create unnecessary complexity — the opposite of the goal.
"I need to invent my own solutions" Patterns save you from reinventing the wheel. If a pattern fits, use it confidently.
"Patterns are only for senior developers" You start learning patterns now because recognizing design problems early is the best habit to build.

⚠️ Practical rule for beginners

To pick the right pattern: first understand the problem. If the problem is unclear, the pattern selection will also be wrong. Pattern names come after pattern understanding, not before.

The Three Types of Design Patterns

The GoF Classification

The Gang of Four organized all 23 patterns into three groups based on what kind of design problem they solve: Creational, Structural, and Behavioral.

🏗️

Creational

Question: How are objects created?

Focus: object creation and instantiation.

5 patterns total

🧱

Structural

Question: How are classes and objects composed into larger structures?

Focus: composition and connection.

7 patterns total

🤝

Behavioral

Question: How do objects communicate and divide responsibility?

Focus: collaboration and control flow.

11 patterns total

Memory aid

Creational = BIRTH of objects  |  Structural = SHAPE of objects  |  Behavioral = TALK between objects

Creational Design Patterns

Main idea

Creational patterns abstract the instantiation process. They make a system more independent of how objects are created, composed, and represented.

Two recurring themes

  • They hide which concrete class is used
  • They hide how instances are created and put together

Why this matters

If you hard-code new EmailNotifier() in 50 places, switching to SMS later breaks all 50 places. A creational pattern puts that decision in one place.

The 5 creational patterns (GoF)

  • Factory Method — let a subclass decide which object to create
  • Abstract Factory — create families of related objects
  • Singleton — ensure only one instance of a class exists
  • Prototype — create objects by cloning an existing one
  • Builder — build complex objects step-by-step

🍪 Cookie Cutter analogy

You have one cookie cutter (the pattern/factory). Each time you press it, a cookie comes out. You don't re-design the cookie shape every time — the cutter takes care of it. Creational patterns are the cookie cutter for your objects.

Structural Design Patterns

Main idea

Structural patterns deal with how classes and objects are assembled to form larger, more useful structures.

Three recurring themes

  • Help independently-developed parts work together
  • Describe ways to compose objects for new functionality
  • Object composition can often be changed at runtime

The 7 structural patterns (GoF)

  • Adapter — connect incompatible interfaces
  • Bridge — separate abstraction from implementation
  • Composite — treat single objects and groups uniformly
  • Decorator — add features to an object without changing its class
  • Facade — provide a simple interface to a complex subsystem
  • Flyweight — share data to reduce memory usage
  • Proxy — control access to another object

🔌 Power Adapter analogy

You travel from Pakistan (220V) to the USA (110V). Your charger won't fit or work directly. A travel adapter sits in between — it doesn't change your charger or the wall socket, it just makes them compatible.

The Adapter design pattern does exactly this: wraps an incompatible class so it works with your code.

Behavioral Design Patterns

Main idea

Behavioral patterns focus on communication and responsibility between objects — who does what, and how information flows between them.

Three recurring themes

  • Behavior can be distributed using inheritance
  • Behavior can be delegated at runtime via composition
  • Complex control flow becomes easier to organize and modify

The 11 behavioral patterns (GoF)

  • Chain of Responsibility
  • Command
  • Iterator
  • Mediator
  • Memento
  • Observer
  • State
  • Strategy
  • Template Method
  • Visitor
  • Interpreter

📺 YouTube Notification analogy (Observer)

When you subscribe to a YouTube channel, you don't keep checking it manually. YouTube notifies you when something new is posted. The Observer pattern works the same way: one object (Subject) notifies all its subscribers (Observers) automatically when its state changes.

Quick Comparison — All Three Categories

Category Answers the question Simple real-world analogy Example pattern
Creational How to create objects? Cookie cutter / Car factory Factory Method, Singleton
Structural How to assemble objects? Travel plug adapter / Lego bricks Adapter, Decorator, Facade
Behavioral How do objects talk to each other? YouTube subscription / WhatsApp group Observer, Strategy, Command

⚠️ A pattern can only belong to one category

Each pattern solves one type of design problem. If you're confused about which category a pattern belongs to, ask yourself: Is this about creating something, assembling something, or communicating something?

Total GoF pattern count

5 Creational + 7 Structural + 11 Behavioral = 23 design patterns total. This week we only study the concepts — specific patterns will be covered in detail in coming weeks.

What does a design pattern actually look like?

Every pattern has four essential parts

According to the Gang of Four, every design pattern has a name, a problem it solves, a solution structure, and known consequences (pros/cons).

Example: The Singleton Pattern

  • Name: Singleton
  • Problem: You need exactly one instance of a class in the entire program (e.g., a printer manager, a database connection)
  • Solution: Make the class control its own instantiation — only one instance allowed
  • Consequences: Easy global access ✅ — but hard to test in isolation ❌

🖨️ Printer Manager analogy

Imagine a school has one printer. If every student tried to create their own "printer manager" independently, they'd all think they own it and send jobs that conflict.

Singleton ensures: there is only ONE printer manager object in the whole program. Anyone who asks for it gets the same one.

Pseudocode idea (language-independent thinking)

Instead of: PrinterManager pm = new PrinterManager() everywhere in your code (creating many objects) …

Singleton says: PrinterManager pm = PrinterManager.getInstance() — always returns the same one object.

Advantages of Using Design Patterns

Advantage What it means in practice
Reusable solutions Don't solve the same design problem from scratch every time — apply known solutions
Shared vocabulary One developer says "Observer" — the whole team instantly understands the design intent
Faster development Proven templates reduce decision-making time during design
Better maintainability Organized, pattern-based code is easier to change, debug, and extend
Scalability Patterns provide flexible structures that grow with the system's needs
Team confidence Developers can trust a pattern-based design because it has been verified by years of real use

🍕 Pizza Recipe analogy

A pizza chef doesn't invent a new dough recipe every day. They use a proven recipe and tweak toppings as needed. Design patterns are your proven recipe — reliable, repeatable, and improvable.

Disadvantages of Design Patterns

Main challenges

  • Learning curve: understanding patterns takes time and seeing real examples
  • Overengineering: applying patterns where they are not needed creates unnecessary complexity
  • Rigidity risk: some pattern-heavy systems become hard to change because of added indirection
  • Misapplication: using the wrong pattern for a problem makes the design worse, not better
  • Team inconsistency: if only some developers understand patterns, the code becomes confusing for those who don't

The overengineering trap

This is the most common mistake beginners make after learning patterns — they try to "use" a pattern just to show they know it.

⚠️ Warning

Bad habit: "I'll use a Factory here because it sounds smart."
Good habit: "I have a recurring object creation problem — a Factory would solve this cleanly."

🔧 Wrong tool analogy

Using a pattern incorrectly is like using a screwdriver to hammer a nail. The tool is not wrong — it's the wrong tool for this task. Patterns are only useful when matched to the right problem.

Common Beginner Mistakes to Avoid

❌ Mistake 1: Pattern name without understanding

Using the word "Singleton" in an exam without knowing why Singleton exists is not helpful. Always tie the pattern name to the problem it solves.

❌ Mistake 2: Confusing architecture with patterns

"Layered architecture" is not a design pattern. Architecture is a system-level decision. Patterns live inside the components of that architecture.

❌ Mistake 3: Thinking patterns are language-specific

Factory exists in Python, Java, C#, and JavaScript. The idea is the same — only the syntax changes.

❌ Mistake 4: Mixing up categories

Observer is Behavioral (communication). Adapter is Structural (assembly). Singleton is Creational (birth). The category tells you what kind of problem is being solved.

🧠 Quick check — can you tell the difference?

For each scenario below, which category of pattern is probably needed?

  • You need to create different types of payment objects (PayPal, Credit Card, Bank Transfer) without hard-coding all of them. → Creational
  • An old library's interface doesn't match your new system's interface. → Structural
  • Many parts of the system need to be notified when a user logs in. → Behavioral

Creational Design Patterns

First family of GoF patterns — how objects are created, composed, and represented

What “creational” means

Creational patterns abstract the instantiation process. They hide how concrete classes are constructed so the rest of the system depends on interfaces and factories, not on new ConcreteType() scattered everywhere. Code samples in this deck use Java. Dotted terms explain jargon.

Examples in this family

  • Factory Method — subclasses decide which product to instantiate
  • Abstract Factory — families of related objects
  • Builder — step-by-step construction of complex objects
  • Prototype — clone a template instead of building from scratch
  • Singleton — exactly one instance (use sparingly)

Cookie cutter

You have one cutter (the factory). Each press produces a cookie of the right shape. You do not re-design the outline for every cookie — the cutter encapsulates creation.

This section: we cover every creational pattern with simple Java-friendly examples — starting with Factory Method.

Factory Method Design Pattern

Intent (GoF)

Define an interface for creating an object, but let subclasses decide which class to instantiate. Factory Method lets a class defer instantiation to subclasses.

When it shines

  • The exact type to create is not known until runtime
  • Creation logic is complex, repetitive, or should be encapsulated
  • You want Open/Closed: extend with new types without editing stable core code
  • Many similar objects — you might start with if-else / switch, but that grows into a maintenance trap

What it fixes

Factory Method lets you create different objects without coupling callers to every concrete class. You program to the Product interface and the Creator abstraction, not to five “notification” implementations.

Why naive code hurts: Notification example

You start with email only. Then SMS, push, Slack, WhatsApp… Each channel adds a branch to the same service. The class becomes a control tower that knows every concrete type.

NotificationService depends on Email, SMS, Push, Slack, and WhatsApp

Tight coupling: one service points at every concrete notification class

What goes wrong

  • Every new channel modifies the same core method
  • Testing is harder — creation and use are tangled
  • Violates Open/Closed — not open for extension without changing existing code

Dependency explosion

Adding a sixth channel means editing NotificationService again — regression risk and merge conflicts.

Simple Factory — a first cleanup (not GoF)

Extract creation into one class. The service asks the factory; it does not build new EmailNotification() itself. Better separation — but the factory still tends toward a switch that grows forever.

Centralized creation
class SimpleNotificationFactory {
    public static Notification createNotification(String type) {
        return switch (type) {
            case "EMAIL" -> new EmailNotification();
            case "SMS" -> new SMSNotification();
            case "PUSH" -> new PushNotification();
            default -> throw new IllegalArgumentException("Unknown type");
        };
    }
}
Factory dispatches to Object A, B, and C

One factory → many product types (still one place to edit)

Limitation

Each new notification type still means changing the factory’s switch. You are not yet open for extension without modification — you only moved the problem.

Factory Method — delegate creation to subclasses

Instead of one central factory deciding everything, the abstract Creator declares createNotification() (or createDocument()); each concrete Creator returns its paired concrete Product. Creation rules live in small classes, not one giant switch.

Food delivery analogy

Simple Factory = one central kitchen that decides pizza vs sushi vs burger.

Factory Method = each restaurant (Pizza Place, Sushi Bar, Burger Joint) has its own kitchen and knows how to prepare its food. The platform routes the order to the right restaurant.

Compared to if-else

Replace: if type == EMAIL → new Email…

With: EmailNotificationCreator always returns new EmailNotification() — one class, one responsibility, no shared branch.

Structure: Product, Creator, and subclasses

UML: Product interface, ConcreteProducts, abstract Creator with factoryMethod, ConcreteCreators

Classic Factory Method class diagram

Roles

  • Product — interface (e.g. Notification) with send()
  • ConcreteProductEmailNotification, SMSNotification, …
  • Creator — abstract; declares factory method + shared workflow (e.g. send() calls createNotification() then uses the product)
  • ConcreteCreator — overrides factory method to return a specific product

How it runs: sequence

Sequence: Client, EmailNotificationCreator, createNotification, EmailNotification

Creator defines the flow; subclass decides the product

Steps

  1. Client picks a ConcreteCreator (config, runtime choice, …)
  2. Client calls the high-level send() on the Creator
  3. Abstract logic calls createNotification() — resolved on the concrete subclass
  4. Factory returns a Notification (interface type)
  5. Creator uses the product: notification.send(message)

Java: Product + abstract Creator

Product interface
interface Notification {
    void send(String message);
}
Abstract Creator — shared workflow uses the factory method
abstract class NotificationCreator {
    public abstract Notification createNotification();

    public void send(String message) {
        Notification notification = createNotification();
        notification.send(message);
    }
}

The Creator defines how sending works; subclasses only supply which Notification to build.

Java: Concrete products + creators + client

Concrete products (excerpt)
class EmailNotification implements Notification {
    @Override
    public void send(String message) {
        System.out.println("Sending email: " + message);
    }
}
// ... SMSNotification, PushNotification, SlackNotification similarly
Concrete creators
class EmailNotificationCreator extends NotificationCreator {
    @Override
    public Notification createNotification() {
        return new EmailNotification();
    }
}
// ... one creator per channel
Client
NotificationCreator creator = new EmailNotificationCreator();
creator.send("Welcome to our platform!");

creator = new SMSNotificationCreator();
creator.send("Your OTP is 123456");

Extension without changing old code

Add WhatsApp by introducing two new classes — product + creator — and wiring the new creator where needed. No edits to existing creators or the abstract Creator.

Second domain: document export

Products: Document (PDF, HTML, CSV). Creators: PdfExportCreator, …

Shared export() in the abstract Creator walks header → rows → footer once; each Document supplies format-specific strings.

AbstractExportCreator, concrete creators, Document interface, concrete documents

Export system — creators and products paired

Document export — core Java

Abstract Creator + concrete creator
abstract class ExportCreator {
    public abstract Document createDocument();

    public void export(String[][] data) {
        Document doc = createDocument();
        System.out.println("Exporting to " + doc.getFileExtension() + " format...");
        String header = doc.getHeader();
        if (!header.isEmpty()) System.out.println(header);
        for (String[] row : data) {
            System.out.println(doc.formatRow(row));
        }
        String footer = doc.getFooter();
        if (!footer.isEmpty()) System.out.println(footer);
        System.out.println("Export complete.\n");
    }
}

class PdfExportCreator extends ExportCreator {
    @Override
    public Document createDocument() { return new PdfDocument(); }
}

What you gain

OCP: new format = new Document + new ExportCreator. SRP: each document class owns its formatting. Testing: each product in isolation.

More illustrations

Transport interface with Truck and Ship deliver()

Product family: same deliver(), different vehicles

Java vehicle example (idea)

Without Factory Method, client code might use if (type == 1) new TwoWheeler() else new FourWheeler() — tight coupling and mixed responsibilities.

With VehicleFactory and factories like TwoWheelerFactory / FourWheelerFactory, the client only uses Vehicle v = factory.createVehicle() — the concrete class stays hidden.

Vehicle interface, TwoWheeler, FourWheeler, VehicleFactory, concrete factories

Same structure in Java: interfaces + concrete factories (diagram labels may say C++; logic is identical)

Real-world uses & applicability

Where you see it

  • Browsers — renderers / plugins by content type
  • Android — system-driven creation around lifecycle
  • Payment gateways — Stripe vs PayPal behind one interface
  • Game engines — spawn entities by level or rules

Applicability (when to choose Factory Method)

  • Creation varies by subtype and you want that choice in subclasses
  • You need to enforce a shared algorithm (template) while varying the product
  • You expect many product types over time — avoid central switches
  • You want loose coupling from client code to concrete classes

Factory Method — pros & cons

Advantages

  • Encapsulation of creation — clients do not know construction details
  • Loose coupling to concrete products
  • Scalability — add types with new classes, not edits to a god-method
  • Reusability of creation/template logic in the abstract Creator
  • Testability — mock or stub factories and products
  • Aligns with Open/Closed — extend by adding subclasses

Disadvantages

  • More classes — one creator per product type
  • Indirection — steeper learning curve than “just new it”
  • Can be overkill for tiny or trivial creation
  • Wrong use adds complexity without benefit
Feature Benefit
Subclass decides product No central switch; each pairing is explicit
Template method + factory Shared workflow in Creator; variable product

Abstract Factory Design Pattern

Creational pattern — create families of related objects without naming concrete classes

Intent

Provide an interface for creating families of related or dependent objects without specifying their concrete classes. Often described as a “factory of factories”: one abstract factory interface, several concrete factories, each producing a consistent set of products.

How it behaves

  • A super-factory interface declares methods like createA(), createB() — one method per product role in the family
  • Concrete factories (chosen at runtime) return the actual product types for that family
  • The system stays independent of how products are created or represented
  • Switching whole families (e.g. North America vs Europe UI) means swapping the factory — not rewriting client logic

vs Factory Method

Factory Method — one product type, subclass chooses which concrete product.

Abstract Factorymultiple related product types per family; one factory object builds a matching set.

Abstract Factory — components

Role Responsibility
Abstract Factory Interface for creating each product in the family (e.g. createCar(), createSpecification())
Concrete Factory Implements that interface; encapsulates creation rules for one family (e.g. North America vs Europe)
Abstract Product Interface (or abstract class) for each kind of object in the family — clients depend on these types
Concrete Product Family-specific implementations; consistent with siblings from the same concrete factory
Client Uses only abstract factory + abstract products — never names concrete product classes directly

Features (summary)

Creates related objects together; exposes one interface for multiple product kinds; concrete factories pick actual classes at runtime; keeps products within a family consistent.

Example: global car manufacturing

You need cars and specifications for different regions (e.g. North America vs Europe): different regulations, features, and body styles — but within a region, car and spec must stay aligned.

Challenges without the pattern

  • Regions differ — easy to mix a European spec with a North American car by mistake
  • Consistency: production rules for “what goes together” scatter across the codebase
  • Change is painful: new region or new product role touches many places → bugs

What Abstract Factory fixes

  • Each region has its own factory — it always returns a matching car + spec
  • Design stays coherent per region; change one region without breaking others
  • New region → new concrete factory (+ products) — no edits to existing factories’ logic
  • Creation is separated from using cars and specs through shared interfaces

Car manufacturing — class diagram

Abstract Factory: CarFactory, NorthAmericaCarFactory, EuropeCarFactory, Car, CarSpecification, Sedan, Hatchback, regional specs

Client depends on CarFactory, Car, and CarSpecification — concrete factories wire each family

Java — abstract products & factory

Each region needs a matching family of objects. Interfaces keep the client independent of sedan vs hatchback details.

Abstract products + abstract factory (interfaces)
public interface Car {
    void assemble();
}

public interface CarSpecification {
    void display();
}

public interface CarFactory {
    Car createCar();
    CarSpecification createSpecification();
}
Concrete products (excerpt)
public class Sedan implements Car {
    @Override
    public void assemble() {
        System.out.println("Assembling Sedan car.");
    }
}

public class Hatchback implements Car {
    @Override
    public void assemble() {
        System.out.println("Assembling Hatchback car.");
    }
}
// NorthAmericaSpecification / EuropeSpecification implement display()

Java — concrete factories & client

Concrete factories
public class NorthAmericaCarFactory implements CarFactory {
    @Override
    public Car createCar() {
        return new Sedan();
    }
    @Override
    public CarSpecification createSpecification() {
        return new NorthAmericaSpecification();
    }
}

public class EuropeCarFactory implements CarFactory {
    @Override
    public Car createCar() {
        return new Hatchback();
    }
    @Override
    public CarSpecification createSpecification() {
        return new EuropeSpecification();
    }
}
Client
CarFactory northAmericaFactory = new NorthAmericaCarFactory();
Car northAmericaCar = northAmericaFactory.createCar();
CarSpecification northAmericaSpec = northAmericaFactory.createSpecification();
northAmericaCar.assemble();
northAmericaSpec.display();

CarFactory europeFactory = new EuropeCarFactory();
Car europeCar = europeFactory.createCar();
CarSpecification europeSpec = europeFactory.createSpecification();
europeCar.assemble();
europeSpec.display();

Expected console output

Assembling Sedan car. plus the North America spec line; then Assembling Hatchback car. plus the Europe spec line.

Second example: furniture “families”

Modern vs Victorian — each theme needs a matching chair and table. The client uses FurnitureFactory, Chair, and Table only; ModernFurnitureFactory returns ModernChair + ModernTable, and similarly for Victorian.

FurnitureFactory, Modern and Victorian factories, Chair and Table products

Same structure: abstract factory + two product interfaces + concrete families

Java — furniture (sketch)

Interfaces
public interface FurnitureFactory {
    Chair createChair();
    Table createTable();
}

public interface Chair { void sitOn(); }
public interface Table { void use(); }
Concrete factory + client
public class ModernFurnitureFactory implements FurnitureFactory {
    public Chair createChair() { return new ModernChair(); }
    public Table createTable() { return new ModernTable(); }
}

FurnitureFactory factory = new ModernFurnitureFactory();
Chair chair = factory.createChair();
Table table = factory.createTable();
chair.sitOn();
table.use();

Victorian factory returns VictorianChair / VictorianTable — client code stays identical except the chosen factory instance.

Real software & when to use

Real-life style uses

  • Multi-cloud — factories for AWS vs Azure vs GCP services (storage, compute, …)
  • Multi-database — connections, dialects, helpers per vendor behind one abstract API
  • Cross-platform GUI — widgets that match Windows / macOS / Linux look-and-feel per family
  • Themes — light vs dark: buttons, menus, colors created as a consistent set

Applicability

  • System must be independent of how products are created
  • Products are designed to be used together as a family
  • You switch whole families (theme, region, platform)
  • You need consistency among related objects

When to avoid

Very small apps; no real “families”; or when the extra interfaces/classes are not worth the indirection.

Abstract Factory — pros & cons

Advantages

  • Loose coupling — client depends on abstractions, not concrete classes
  • Family consistency — products from one factory belong together
  • Swap families — change factory instance, not scattered new calls
  • Encapsulates creation — factories own composition rules
  • Open/Closed — new family often means new classes, not editing old ones
  • Scales well in large modular systems

Disadvantages

  • Many types — interfaces + factories + products multiply
  • Can be heavy for trivial problems
  • Adding a new kind of product to every family requires changing the abstract factory and all concrete factories — awkward for evolution along that axis
  • Steep for beginners; risk of over-engineering

Builder Design Pattern

Creational pattern — build complex objects step-by-step, separating construction from the final representation

Intent

Construct objects with many optional parts incrementally. A dedicated Builder owns assembly; the Product can stay immutable and validated after build().

When it helps

  • Many optional fields — most callers only set a subset
  • You want to avoid telescoping constructors and long parameter lists
  • Assembly needs several steps, sometimes a specific order
  • Without it: overloaded constructors, wide setter APIs, easy to misuse

Core idea

Instead of one giant constructor, clients configure a builder with named methods, then call build(). Same construction process can yield different configurations.

Problem: configuring HttpRequest

Fields: URL (required), method, headers, query params, body, timeout — many optional. The naive approach is constructor overloading (telescoping): each new optional adds another constructor or more parameters.

Why telescoping hurts

  • Hard to read — many String / Map params; easy to swap positions
  • Error-prone — pass null for “unused” slots
  • Inflexible — to set only the 5th parameter you still thread nulls through 1–4
  • Poor scalability — new optionals churn constructors and callers
Telescoping style (idea)
new HttpRequestTelescoping(url);
new HttpRequestTelescoping(url, method, null, null, body);
new HttpRequestTelescoping(url, method, headers, null, body, timeout);

What is Builder?

Two central ideas

  • Step-by-step — set each part with its own method; only call what you need
  • Fluent interface — methods return this so calls chain: builder.a().b().build()

Participants: Builder (steps + build()), ConcreteBuilder (often fluent inner class), Product (complex result), Director (optional — orchestrates step order).

Client, Director, Builder interface, ConcreteBuilder, Product

Classic Builder structure (Director optional for fluent APIs)

Example: HTTP request — Director + Builder + Product

HttpRequestDirector uses Builder; Builder creates HttpRequest

Director encodes recipes; fluent Builder sets method, headers, body, timeout; build() yields HttpRequest

How it works at runtime

Sequence: Client chains Builder then build returns Product

Chaining returns this; build() constructs the product — often immutable after that

Four steps

  1. Create the builder (required args, e.g. URL)
  2. Configure optional fields — any order, named methods
  3. build() — validate, construct product (private constructor + builder state)
  4. Use the product — discard or reuse the builder for another build

Java: HttpRequest + nested Builder

Product + static nested Builder (excerpt)
public class HttpRequest {
    private final String url;
    private final String method;
    private final Map<String, String> headers;
    private final Map<String, String> queryParams;
    private final String body;
    private final int timeout;

    private HttpRequest(Builder builder) {
        this.url = builder.url;
        this.method = builder.method;
        this.headers = Collections.unmodifiableMap(new HashMap<>(builder.headers));
        this.queryParams = Collections.unmodifiableMap(new HashMap<>(builder.queryParams));
        this.body = builder.body;
        this.timeout = builder.timeout;
    }

    public static class Builder {
        private final String url;
        private String method = "GET";
        private Map<String, String> headers = new HashMap<>();
        private Map<String, String> queryParams = new HashMap<>();
        private String body;
        private int timeout = 30000;

        public Builder(String url) { this.url = url; }

        public Builder method(String method) { this.method = method; return this; }
        public Builder addHeader(String k, String v) { headers.put(k, v); return this; }
        public Builder body(String body) { this.body = body; return this; }
        public Builder timeout(int t) { this.timeout = t; return this; }

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

Client usage & what we gain

Readable, named configuration
HttpRequest get = new HttpRequest.Builder("https://api.example.com/users").build();

HttpRequest post = new HttpRequest.Builder("https://api.example.com/users")
        .method("POST")
        .addHeader("Content-Type", "application/json")
        .body("{\"name\":\"Alice\"}")
        .timeout(5000)
        .build();

Benefits

  • No positional null arguments — each field is explicit
  • Self-documenting chains
  • Immutable product after build()
  • Extend with new optional: add one builder method — callers unchanged
  • Flexible call order for independent options

Compare to telescoping

Every line names an aspect of the request. Mistakes are easier to spot than in a long constructor argument list.

Director (optional) — reusable “recipes”

When many call sites need the same builder steps (auth header, JSON content-type, timeout), duplicate chains become maintenance risk. A Director wraps those sequences in named methods.

HttpRequestDirector
class HttpRequestDirector {
    public HttpRequest buildSimpleGet(String url) {
        return new HttpRequest.Builder(url).method("GET").timeout(30000).build();
    }
    public HttpRequest buildAuthenticatedPost(String url, String token, String body) {
        return new HttpRequest.Builder(url).method("POST")
                .addHeader("Authorization", "Bearer " + token)
                .addHeader("Content-Type", "application/json")
                .body(body).timeout(10000).build();
    }
}
Approach When
Client uses Builder directly One-off configs; each site differs
Director Same preset in many places; named standard requests

Rule of thumb: if you copy-paste the same chain three times, consider a Director.

Practical pattern: SQL QueryBuilder

ORMs and DB APIs often use Builder to assemble SELECT … WHERE … ORDER BY … LIMIT without string concatenation bugs. Each method adds a clause; build() returns a query object or SQL string.

Fluent SQL builder (sketch)
SqlQuery q = new SqlQuery.Builder("users")
        .select("name", "email")
        .where("age > 18")
        .where("active = true")
        .orderBy("name", "ASC")
        .limit(10)
        .build();

// query2: orders table, filters, ORDER BY created_at DESC, LIMIT 20 OFFSET 40

Why it fits

Natural DSL shape: each call adds structure; invalid combinations can be caught in build().

Builder — applicability, pros & cons

Applicability

  • Object has many optional parameters or nested parts
  • You want immutable products after construction
  • Construction should be readable and safe from argument-order mistakes
  • Same construction algorithm, many representations (optional Director)

When not to use

  • Very simple objects with one or two fields
  • Overhead of extra class not justified

Advantages

  • Clear, fluent API; avoids telescoping
  • Immutable products; validation centralized in build()
  • Easy to extend with new optional steps
  • Director can DRY up common configurations

Disadvantages

  • More classes and boilerplate than a plain constructor
  • Indirection — readers must understand builder vs product
  • Possible overkill for trivial types

Prototype Design Pattern

Creational pattern — create new objects by cloning an existing instance instead of coupling to concrete constructors everywhere

Intent

Specify the kinds of objects to create using a prototypical instance, and create new objects by copying that prototype. Useful when instantiation is expensive or many objects share similar initial state.

The problem

  • Construction is costly — heavy I/O, DB, parsing, remote config
  • Many instances are almost the same — re-running full init is wasteful
  • Client code should not be tied to every concrete class just to “get another one like this”

The solution

Clone a template object that already holds the right state. Adjust the copy if needed. The original stays untouched unless you intend to change it.

Prototype — structure

Roles

  • Prototype — interface declaring clone() (or copy operation)
  • Concrete prototype — implements clone; defines how to duplicate (shallow vs deep)
  • Client — asks a prototype to clone; works through the Prototype type, not concrete classes
UML: Prototype interface, Shape implements clone, Main uses Shape

Prototype interface, concrete Shape, client Main

Java example — Shape

Prototype + concrete prototype
public interface Prototype {
    Prototype clone();
}

public class Shape implements Prototype {
    private String type;
    private String color;

    public Shape(String type, String color) {
        this.type = type;
        this.color = color;
    }

    public void setColor(String color) { this.color = color; }

    @Override
    public Prototype clone() {
        return new Shape(this.type, this.color);
    }

    @Override
    public String toString() {
        return "Shape [Type = " + type + ", Color = " + color + "]";
    }
}
Client
Shape originalShape = new Shape("Circle", "Red");
Shape clonedShape = (Shape) originalShape.clone();
clonedShape.setColor("Blue");
// original stays Red; clone is independent

Sample output

Original Shape : Shape [Type = Circle, Color = Red] — after changing clone color, original remains Red.

Analogy & when to use

Cookie cutter

Dough = prototype object with a known state. Cutter = clone() — each cookie matches the shape; you can decorate each copy without changing the template.

Same idea: duplicate structure fast, then specialize copies.

Applicability

  • Object creation is expensive or slow
  • You need many instances with similar starting state
  • Runtime needs new objects without hard-wiring concrete classes everywhere
  • Classes to instantiate are configured at runtime (e.g. from a registry of prototypes)

Prototype — pros & cons

Advantages

  • Performance — avoids repeating heavy initialization when a copy suffices
  • Decoupling — client can depend on Prototype, not specific concrete classes
  • Similar states — convenient when defaults are “like this one”
  • Can combine with registries (clone by key) for flexible object creation

Disadvantages

  • Cloning complexity — nested objects and references need deep copy discipline; easy to get aliasing bugs
  • Not always obvious — teams must agree on clone semantics (shallow vs deep)
  • Language support varies (Java’s Object.clone() is protected and awkward; custom interfaces are clearer)

Singleton Design Pattern

Creational pattern — ensure a class has only one instance and provide a global access point

Intent

Use when you need centralized control of a resource: configuration, logging, database connection pool handle, print spooler, etc. — one shared object, consistent state, controlled lifecycle.

What it enforces

  • Prevents accidental multiple instances
  • Controls resource usage (memory, connections)
  • One coordination point across subsystems

Use judiciously

Overuse → hidden dependencies, harder unit tests, global state. Prefer dependency injection where you can pass the instance explicitly.

Singleton — features & when to use

Features

  • Single instance — one object in the process (JVM / runtime)
  • Global access — typically getInstance()
  • Lazy or eager — create on first use vs at class load
  • Thread safety — must be designed in for concurrent code
  • Flexible implementation — eager, lazy, synchronized, double-checked locking, static holder, enum (Java)

Typical uses

  • Logging / audit facades
  • Configuration / feature flags
  • Shared DB connection factory or pool entry point
  • Hardware or OS handles (one printer manager)

Consider subclassing only when the design truly needs a replaceable singleton implementation — most teams prefer composition over singleton inheritance.

Singleton — components & memory

Three pieces

  • Static field — holds the sole instance (or pointer)
  • Private constructor — blocks new from outside
  • Static factory methodgetInstance() creates (once) or returns existing
Clients blocked from direct new; access via static method to static instance

No direct construction — access through the static gateway

Stack references Obj1 Obj2 point to one heap Singleton instance

Multiple references, one heap object

Lazy initialization & thread safety

Two threads both see null and create two instances

Classic lazy if (obj == null) — not safe under concurrency

Implementation options (summary)

  • Lazy (classic) — cheap startup; needs care for threads
  • Synchronized getInstance — simple, some lock overhead
  • Eager / static init — thread-safe by class loader; may waste work if never used
  • Double-checked locking — check, lock, check again (safe publication in Java with volatile)
  • Initialization-on-demand holder — static inner class in Java: loaded only on first use → lazy + thread-safe
  • Enum singleton (Java) — one constant; JVM guarantees single instance and serialization safety

Java — classic lazy Singleton

Lazy creation: simple for learning; add synchronization for real multi-threaded apps.

public class Singleton {
    private static Singleton instance;

    private Singleton() {
        System.out.println("Singleton is instantiated.");
    }

    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }

    public void doSomething() {
        System.out.println("Something is done.");
    }

    public static void main(String[] args) {
        Singleton.getInstance().doSomething();
    }
}

Console: “instantiated” prints once; later getInstance() returns the same object (single-threaded scenario).

Singleton — applications, applicability, trade-offs

Real-world style

  • Central logger
  • Config / environment reader
  • Single DB connection manager facade
  • Thread pool or scheduler entry point

Applicability

  • Exactly one shared instance is a correctness requirement
  • Controlled access beats scattering new
  • Not for “convenience globals” — that leads to test pain

Advantages

  • One instance → consistent state, less waste
  • Clear global access for true shared resources

Disadvantages

  • Tight coupling to a global
  • Testing — hard to mock without seams
  • Concurrency bugs if lazy init is naïve

Creational patterns — comparison

Pattern Core idea Typical use
Factory Method Subclass chooses which product to instantiate One product family; variation by subclass
Abstract Factory Factory interface builds a family of related objects Themes, platforms, regional stacks
Builder Step-by-step assembly; fluent build() Many optional parameters; immutable products
Prototype Clone an existing object as template Costly construction; similar starting state
Singleton One instance + global access Config, logging, shared resource gateways

Conclusion — Week 11: Creational patterns

What you covered

Five GoF creational patterns: how objects are created and wired — from factories and builders to cloning and a single shared instance. Each solves a different tension: flexibility, families, stepwise construction, duplication, or uniqueness.

Skills to keep

  • Match problem shape to pattern — not the other way around
  • Prefer simplest design that meets requirements
  • Know trade-offs: globals (Singleton), cloning depth (Prototype), factory evolution (Abstract Factory)

Next step

Next we move from birth to structure: how objects are composed and extended without rewriting cores — the Structural patterns.

Coming up — Structural design patterns

Focus: how classes and objects are composed to form larger structures — flexibility, reuse, and clean interfaces

Pattern Topic focus
Adapter Example · Applicability · Pros & Cons — make incompatible interfaces work together
Bridge Example · Applicability · Pros & Cons — separate abstraction from implementation
Decorator Example · Applicability · Pros & Cons — add responsibilities dynamically
Composite Example · Applicability · Pros & Cons — tree structures, part–whole hierarchies

How we’ll study each

For each pattern: intent, structure, a concrete example, when to apply, and trade-offs — same rhythm as creational patterns.