M2 — Abstraction, Types, and Encapsulation

SENG 365 — Software Engineering

Neil Ernst

University of Victoria

2026-10-01

Module Goals

Design Judgement

By the end of this module, you should be able to:

  • compare representations by the changes they localize;
  • identify exposed representation in code and object diagrams;
  • choose between a query, a copy, and an unmodifiable view;
  • distinguish input validation from a client precondition; and
  • defend an encapsulation decision using evidence from an artifact.

Abstraction and Encapsulation

Preparation

Before class: You read Robillard, 3rd ed., Chapter 2, Sections 2.1–2.3.

You should arrive able to recognize:

  • abstraction and information hiding;
  • primitive obsession;
  • classes, records, and enumerated types; and
  • public and private accessibility.

Bring one question or point of confusion from the reading.

Entry Decision

A program represents a playing card in one of these ways:

int card = 13;
String card = "ACE_OF_HEARTS";
Card card = new Card(Rank.ACE, Suit.HEARTS);

A Joker is added to the game. Which representation makes that change safest (and what do we mean by safe?)?

Write down:

  1. your choice;
  2. one assumption behind it; and
  3. one place you expect the change to reach.

The Existing Artifact

// 0–12 clubs, 13–25 hearts, 26–38 spades, 39–51 diamonds
public final class Card {
    public int code;

    public Card(int code) {
        this.code = code;
    }
}

Client code knows the encoding:

boolean isHeart(Card card) {
    return card.code / 13 == 1;
}

int rank(Card card) {
    return card.code % 13;
}

What can client code know, change, or accidentally corrupt?

Change Pressure

The next release must:

  • reject values that do not represent a card;
  • display ranks and suits in more than one language (der Bube);
  • support games that include Jokers; and
  • preserve existing game logic where possible.

Mark every line in the previous code artifact that might change.

What other client code would you search for?

Pair Design Studio

With a partner, design one representation for Card.

Your design must address:

  • ordinary ranked and suited cards;
  • Jokers;
  • invalid construction;
  • localized display;

Sketch out in pseudocode:

  1. the representation;
  2. the public interface;
  3. one example of client code; and
  4. one requirement your design handles poorly.

Compare Two Designs

We will compare two proposals from the class.

Question Design A Design B
What is stored?
What do clients see?
Can invalid cards exist?
How is a Joker represented?
Where does localization belong?
Which change is expensive?

Synthesis

  • An abstraction names a concept and the operations meaningful for it.
  • Encapsulation limits the contact points between parts of a system.
  • Information hiding keeps design decisions from becoming client dependencies.
  • A type can prevent meaningless values and make domain intent visible.

Exit Note

Complete both sentences:

Clients of Card should know __________.

Clients of Card should not know __________.

Types, Visibility, and Aliasing

Preparation

Before class: Read all of Robillard, 3rd ed., Chapter 2. Focus on Sections 2.4–2.9.

You should arrive able to recognize:

  • object diagrams and shared references;
  • escaping references and inappropriate intimacy;
  • immutability;
  • copies and unmodifiable views; and
  • input validation and design by contract.

Entry Decision

Which return statement best preserves the encapsulation of a Deck?

return cards;
return new ArrayList<>(cards);
return List.copyOf(cards);
return Collections.unmodifiableList(cards);

Choose one, then identify the missing fact that could change your answer.

The Leaky Deck

Assume Card is an immutable record.

public final class Deck {
    private List<Card> cards;

    public Deck(List<Card> cards) {
        this.cards = cards;
    }

    public List<Card> cards() {
        return cards;
    }

    public Card draw() {
        return cards.removeLast();
    }
}

The field is private. Is the representation hidden?

Follow the References

List<Card> source = standardCards();
Deck deck = new Deck(source);

source.clear();

List<Card> exposed = deck.cards();
exposed.add(new Card(Rank.ACE, Suit.HEARTS));

Without running the code:

  1. Predict the size of the deck after each mutation.
  2. Draw an object diagram after the constructor call.
  3. Add exposed to the diagram.
  4. Mark every reference that crosses the boundary of Deck.

Diagnose

Using the object diagram, find two different routes by which a reference escapes.

For each route, state:

  • who owns the mutable list;
  • which code can mutate it;
  • which Deck invariant could be broken; and
  • why declaring the field private did not prevent the problem.

Interface Refactoring

Revise the public interface of Deck. Your design must let a client:

  • discover how many cards remain;
  • inspect the remaining cards when necessary; and
  • draw a card without directly mutating the internal collection.

Produce:

  1. the revised constructor and public method signatures;
  2. an object diagram showing the references after cards() is called; and
  3. one sentence stating who can mutate each object in the diagram.

Design Choices

Possible ways to expose information include:

public int size()
public Card cardAt(int index)

// Alternative A: unmodifiable snapshot
public List<Card> cards() {
    return List.copyOf(cards);
}

// Alternative B: unmodifiable live view
public List<Card> cards() {
    return Collections.unmodifiableList(cards);
}

Differences

These are not interchangeable:

  • A query can reveal less but may make common client work awkward.
  • A snapshot separates later changes in the deck from the returned value.
  • An unmodifiable view can still reflect later changes in the deck.
  • A shallow copy still shares references to its elements.

Things to consider

What behaviour does your client actually need?

And what behaviour should you anticipate, accommodate, or leave to future redesigns?

A Refactoring Is a Claim

Write a small check that would distinguish the old and new designs:

List<Card> source = standardCards();
Deck deck = new Deck(source);

// Mutate source or a value returned by deck.
// What observation would show that Deck is still in control of its state?

Your check should expose an ownership or aliasing decision, not an implementation detail.

New Information

The requirements change: Card is now mutable.

public final class Card {
    private Suit suit;
    public void setSuit(Suit suit) { this.suit = suit; }
    // ...
}

Re-evaluate your design:

  • Does a shallow copy still protect Deck?
  • Does an unmodifiable list protect its elements?
  • Would you make Card immutable, perform a deep copy, or expose a narrower query?
  • What does each option cost?

Contract at the Boundary

What should happen when an empty deck receives draw()?

Client precondition

/** @pre !isEmpty() */
public Card draw()

Validated operation

/** @throws IllegalStateException if this deck is empty */
public Card draw()

Who is responsible in each design? Which choice fits the expected clients of this class?

Compare Alternatives

  • Did you copy incoming data as well as outgoing data?
  • Is the returned value a snapshot or a live view?
  • Are shared element references safe?
  • Which invariant does the interface preserve?
  • What must a caller do before drawing?
  • What future change will your design absorb well?

Synthesis

  • A private field can still be exposed through aliases.
  • References can escape outward through return values or inward through parameters.
  • Immutability makes sharing safer, but it must hold for the entire reachable object graph that matters.
  • Validation and preconditions assign responsibility differently.
  • An interface is a promise to clients and a boundary around future change (how long do we promise it?)

Exit Note

Write one decision rule you would use next time:

Return a copy, view, or query when __________, because __________.