Skip to main content
Lecture 6: Changeability I: Modularity and Information Hiding

CS 3100: Program Design and Implementation II

Lecture 6: Changeability I — Modularity and Information Hiding

©2025 Jonathan Bell & Ellen Spertus, CC-BY-SA

Poll: What are your goals for CS 3100 (revised, anonymous)?

A. Learning as much as possible

B. Getting a high grade

C. Getting a strong letter of recommendation

D. Having time for other projects

E. Being ready for co-op

F. Behaving with integrity

Poll Everywhere QR Code or Logo

Text espertus to 22333 if the
URL isn't working for you.

https://pollev.com/espertus

This slide is totally anonymous. Feel free to use incognito mode.

Poll: What should you do if you are having trouble with an assignment?

A. Give up

B. Keep grinding away

C. Go to office hours

D. Ask a friend for help

E. View Pawtograder posts

F. Ask AI for help with your problem

G. Ask AI how to download the autograder code

Poll Everywhere QR Code or Logo

Text espertus to 22333 if the
URL isn't working for you.

https://pollev.com/espertus

Learning Objectives

After this lecture, you will be able to:

  1. Describe the importance of changeability as a goal of program design and implementation
  2. Describe the relevance of modularity to changeability
  3. Describe the role of information hiding and immutability in enabling effective modularity
  4. Apply Java language features to achieve information hiding and immutability

The Tire Swing Meme

The classic tire swing meme showing how requirements get lost in translation through each stage of a software project

See: History of the Tree Swing

Example: A "Simple" Requirement

"The Pawtograder platform should allow graders to annotate student submissions with feedback on the quality of the code."

Questions we'll have to answer:

  • Do annotations directly affect the score? Positive or negative scoring?
  • Is there a rubric? How detailed? Structured with categories and levels?
  • Are annotations associated with part of a line, or the whole line?
  • How does the grader specify which lines to annotate?
  • ...

The Response: Design for Changeability

A core principle of modern software design:

Favor rapid prototyping and iteration over getting requirements perfect.

But throwing away a design and starting over is expensive...

So our goal is to instill a sense of changeability in our designs.

What is Modularity?

The core idea: design systems as relatively independent modules.

A module is a self-contained unit of code:

LEGO bricks with same interface (studs) but different hidden implementations
  • Has a well-defined interface that specifies behavior
  • Implementation is hidden and can be independently compiled
  • Does not depend on implementation details of other modules

A module could be a class, a package, or even a whole program.

Modules Exist at Every Scale

A module is any unit of code with a well-defined interface. The same principles apply at every scale:

ScaleModuleInterface
MethodA single functionIts signature and Javadoc
ClassA single typeIts public methods
PackageA group of related classesIts public classes
LibraryA group of packagesIts exported packages
ServiceA whole programIts API

Today's challenge: Why modules, and how to enforce their boundaries? Next lecture's challenge: How do we decide what belongs in a module? Where are the boundaries?

Benefits of Modularity

Why break systems into independent modules?

Efficiency

Teams work in parallel without coordination (Brooks' Law)

Readability

Easier to understand when modules are independent

Changeability

Change one module without affecting others

Testability

Test modules in isolation; easier to debug failures

Note: All benefits require that modules are truly independent.

What Happens Without Modularity: The Big Ball of Mud

The most common software architecture in practice is... no architecture at all.

"A Big Ball of Mud is a haphazardly structured, sprawling, sloppy, duct-tape-and-baling-wire, spaghetti-code jungle."

Brian Foote and Joseph Yoder, 1997

It happens gradually: shortcuts, tight deadlines, "temporary" hacks that become permanent.

"Life of a Software Engineer" by Manu Cornet

A Brief History of Information Hiding

David Parnas proposed information hiding in 1972 — before most programming languages supported it.

Back then:

  • You could organize code into modules...
  • ...but nothing prevented access to internal details
  • Discipline was the only enforcement

Important Terminology

  • invariant: a property that always holds
  • precondition: a property that must hold before an action is performed
  • postcondition: a property guaranteed to hold after an action is completed

Without Information Hiding: Everything is Accessible

Imagine Java without private — all fields are accessible:

// Counter "module" — intended to only increment
class Counter {
int count = 0; // count will never be negative

void increment() { count++; }
int getCount() { return count; }
}
// Client code in another file...
Counter c = new Counter();
c.increment(); // Intended use
c.count = -999; // Nothing stops this!
c.count = c.count * 2; // Or this!

The "module" can't enforce its own invariants. Any client can break it.

The Python Approach

Lisa Simpson standing in front of a sign reading:'KEEP OUT...OR ENTER. I'm a sign, not a cop.

The Java Approach

Gandalf, fiercely blocking the way, with caption: 'YOU SHALL NOT PASS!'

Information Hiding: Python vs Java

Python: Convention-Based

    class GradeBook:
def __init__(self):
self._grades = [] # "please don't"
self.__secret = [] # name-mangled

book._GradeBook__secret # still works!

"We're all consenting adults here."

Java: Compiler-Enforced

    public class GradeBook {
private List<Double> grades;
// Compiler error if accessed
// from outside the class
}

The language prevents violations.

The Cure: Language-Enforced Boundaries

This course focuses on Java's approach. Why?

  • Conventions don't scale — "please don't" becomes "someone did"
  • Deadlines happen — under pressure, developers take shortcuts
  • Teams change — new members don't know the unwritten rules

Java's enforcement tools: private, final, sealed, modules

The compiler is your boundary guard. Invariants become enforceable.

Hyrum's Law: Why Information Hiding Matters

Even with good design, developers will find unintended ways to use your module...

Hyrum Wright
"With a sufficient number of users of an API, it does not matter what you promise in the contract: all observable behaviors of a system will be depended on by somebody."

Hyrum Wright

Hyrum's Law in Action (XKCD)

XKCD 1172: A user complains that a bug fix broke their workflow because they relied on the bug

XKCD 1172 — "Every change breaks someone's workflow"

Hyrum's Law in Action (LED traffic lights)

Headline from CBC article: 'LED traffic lights trouble in winter because they don't melt snow

Source: CBC

Java Language Features for Information Hiding

In OO design, these features enable encapsulation:

  1. Access modifiers — control who can see what
  2. Immutable classes — prevent state changes after construction
  3. Sealed classes — control who can extend a type
  4. The module system — hide entire packages from library consumers

Access Modifiers in Java

Every class, method, and field has an access modifier:

ModifierVisibility
publicAccessible from anywhere
protectedAccessible from package and subclasses
(default)Package-private: same package only
privateAccessible only within the class

Rule: Minimize accessibility of classes and members.

Immutable Classes

Immutable classes: instances cannot be changed after construction.

  • Simpler to reason about—behavior determined by constructor
  • Safe to pass between modules—no surprise mutations
  • Thread-safe by default

Default to immutable. Only make mutable if there's a good reason.

Example: An Immutable PhoneNumber Class

public final class PhoneNumber {
private final short areaCode;
private final short centralOfficeCode;
private final short number;

public PhoneNumber(short areaCode, short centralOfficeCode, short number) {
this.areaCode = areaCode;
this.centralOfficeCode = centralOfficeCode;
this.number = number;
}

public short getAreaCode() { return areaCode; }
public short getCentralOfficeCode() { return centralOfficeCode; }
public short getNumber() { return number; }
}

This looks a lot like a record from L5! And indeed, record PhoneNumber(short areaCode, short centralOfficeCode, short number) {} would give us the same immutability guarantees with less boilerplate.

Can you break the invariant from a client? 1

/**
* A phone number consisting of 10 digits in the range 0-9 inclusive.
*/
public class PhoneNumber {
private final int[] digits;

public PhoneNumber(int[] digits) {
Objects.requireNonNull(digits);
if (digits.length != 10) {
throw new IllegalArgumentException("numbers length must be 10");
}
for (int i = 0; i < digits.length; i++) {
if (digits[i] < 0 || digits[i] > 9) {
throw new IllegalArgumentException("digits must be between 0 and 9");
}
}
this.digits = digits;
}
}
  public static void main(String[] args) {
int[] digits = {5, 1, 0, 4, 3, 0, 5, 5, 5, 5};
PhoneNumber phoneNumber = new PhoneNumber(digits);
digits[0] = -20; // breaks invariant
}

Shared References Let Clients Break Invariants

int[] digits = {5, 1, 0, 4, 3, 0, 5, 5, 5, 5};
PhoneNumber phoneNumber = new PhoneNumber(digits);
digits[0] = -20; // breaks invariant

Can you break the invariant from a client? 2

/**
* A phone number consisting of 10 digits in the range 0-9 inclusive.
*/
public class PhoneNumber {
private final int[] digits;

public PhoneNumber(int[] digits) {
// validation code hidden
this.digits = digits.clone(); // make a defensive copy
}

public int[] getDigits() {
return digits;
}
}
  public static void main(String[] args) {
int[] digits = {5, 1, 0, 4, 3, 0, 5, 5, 5, 5};
PhoneNumber phoneNumber = new PhoneNumber(digits);
digits[0] = -20; // does not affect phoneNumber.digits
phoneNumber.getDigits()[0] = -20; // breaks invariant
}

Defensive Copy Protects Input—But Getter Still Leaks

int[] digits = {5, 1, 0, 4, 3, 0, 5, 5, 5, 5};
PhoneNumber phoneNumber = new PhoneNumber(digits);
phoneNumber.getDigits()[0] = -20

Correct Implementation

/**
* A phone number consisting of 10 digits in the range 0-9 inclusive.
*/
public class PhoneNumber {
private final int[] digits;

public PhoneNumber(int[] digits) {
Objects.requireNonNull(digits);
if (digits.length != 10) {
throw new IllegalArgumentException("numbers length must be 10");
}
for (int i = 0; i < digits.length; i++) {
if (digits[i] < 0 || digits[i] > 9) {
throw new IllegalArgumentException("digits must be between 0 and 9");
}
}
this.digits = digits.clone(); // make a defensive copy
}

public int[] getDigits() {
return digits.clone(); // make a defensive copy
}
}

Defensive Copies Protect Reference Fields

For reference types, make copies on the way in and out:

public final class PhoneNumber {
private final short[] number;

public PhoneNumber(short[] number) {
// Defensive copy on the way in
this.number = new short[number.length];
System.arraycopy(number, 0, this.number, 0, number.length);
}

public short[] getNumber() {
// Defensive copy on the way out
short[] copy = new short[number.length];
System.arraycopy(number, 0, copy, 0, number.length);
return copy;
}
}

"But isn't copying slow?" — Choose safety. Hardware gets faster; bugs from aliased references don't fix themselves.

Poll: Can a Client Break the Invariant?

/**
* A Northeastern student whose entrance year is no earlier than
* NU's founding and no later than the current year.
*/
public class NortheasternStudent {
private static final int FOUNDING_YEAR = 1898;
private String name;
private int entranceYear;

public NortheasternStudent(String name, int entranceYear) {
this.name = name;
if (entranceYear < FOUNDING_YEAR || entranceYear > Year.now().getValue()) {
throw new IllegalArgumentException("entranceYear " + entranceYear +
" is out of range " + FOUNDING_YEAR + "..." + Year.now().getValue());
}
this.entranceYear = entranceYear;
}
}

A. yes, both fields are vulnerable

B. yes, name is vulnerable

C. yes, entranceYear is vulnerable

D. no, it's safe

E. I don't know

Poll Everywhere QR Code or Logo

Text espertus to 22333 if the
URL isn't working for you.

https://pollev.com/espertus

Recipe for Safety

  • Do not provide any mutators (methods that change state)
  • Make the class final to prevent subclasses
  • Make all fields final
  • Make all fields private
  • For reference fields, make defensive copies of mutable objects in constructors and getters
    • Copies are automatically made with primitive assignment (this.year = year;)
    • There's no need to make copies of immutable object types, such as String

Even without a specific invariant to protect, prefer immutability. It makes code easier to understand and maintain.

Uncontrolled Subtyping

In the left panel, three devices subtype IoTDevice: DimmableLight, Thermostat, and MaliciousDevice. In the right panel, all are admitted into a trusting method, provideAccess(List<IoTDevice>)

The Problem: Uncontrolled Inheritance

Imagine you're building a smart home system with different device types:

public class IoTDevice {
public void sendCommand(String cmd) { /* ... */ }
}

public class DimmableLight extends IoTDevice { /* ... */ }
public class Thermostat extends IoTDevice { /* ... */ }

But someone else adds this their deployment of your code:

public class MaliciousDevice extends IoTDevice {
@Override
public void sendCommand(String cmd) {
uploadToHackerServer(cmd); // Oops!
super.sendCommand(cmd);
}
}

final on IoTDevice would prevent this—but then you can't have any subclasses!

Sealed Classes: Controlled Inheritance

Sealed classes let you specify exactly which classes can extend:

public sealed class IoTDevice
permits DimmableLight, SwitchedLight, Thermostat {
// ...
}
Bouncer with VIP list representing sealed class permits clause

Subclasses of Sealed Classes

Classes extending a sealed class must declare themselves as one of:

finalCannot be extended further
sealedMust specify its own permitted subclasses
non-sealedReopens the hierarchy—anyone can extend
public sealed class IoTDevice permits DimmableLight, SwitchedLight, Thermostat { }

public final class DimmableLight extends IoTDevice { } // Cannot be extended

public final class SwitchedLight extends IoTDevice { } // Cannot be extended

public non-sealed class Thermostat extends IoTDevice { } // Open for extension

Why Sealed Classes Matter

1. Controlled evolution

Add new permitted subclasses in future versions without breaking existing code.

2. Exhaustive pattern matching

The compiler knows all possible subtypes:

public String describe(IoTDevice device) {
if (device instanceof DimmableLight d) {
return "Dimmable light at " + d.getBrightness() + "%";
} else if (device instanceof SwitchedLight s) {
return "Switched light: " + (s.isOn() ? "on" : "off");
} else if (device instanceof Thermostat t) {
return "Thermostat set to " + t.getTargetTemp() + "°";
}
// Compiler warns if we miss a case (unless non-sealed in hierarchy)
return "Unknown device";
}

Sealed Classes for Domain Modeling

Sealed classes model domains with a closed set of possibilities:

public sealed interface PaymentMethod
permits CreditCard, BankTransfer, DigitalWallet { }

public final class CreditCard implements PaymentMethod { ... }
public final class BankTransfer implements PaymentMethod { ... }
public final class DigitalWallet implements PaymentMethod { ... }

The type system now enforces that a PaymentMethod is one of exactly three things. Adding a new subtype requires a change to PaymentMethod

Scaling Up: The Java Module System

Access modifiers work at the class level. But what about library level?

The problem:

  • Internal classes must be public for your packages to use them
  • But that makes them visible to library consumers too!
  • "Internal API - do not use" doesn't stop determined developers

This is Hyrum's Law at the library scale.

The module-info.java File

Declare which packages are part of your public API:

// module-info.java for a hypothetical grading library
module com.pawtograder.grading {
// These packages are our public API - consumers can use them
exports com.pawtograder.grading.api;
exports com.pawtograder.grading.model;

// com.pawtograder.grading.impl is NOT exported
// Classes there can be public but invisible to consumers
}

Unexported packages are module-private—even public classes are hidden!

Encapsulation at Every Level

ScopeMechanismWhat it hides
Fieldprivate keywordImplementation state within a class
ClassPackage-private (default)Helper classes within a package
LibraryModule system (exports)Internal packages within a library

Same principle at different scales:

  • Reduced coupling
  • Clearer contracts
  • Safer evolution

Key Takeaways

  1. Changeability is essential — most software cost is maintenance
  2. Modularity enables change — independent modules evolve independently
  3. Information hiding protects modularity — Hyrum's Law warns us
  4. Java provides tools at every scale: Access modifiers, immutability, sealed classes, modules

Looking Ahead

The module system is our first glimpse of how design principles scale beyond individual classes.

Later in the course:

  • Software Architecture (Lecture 19) — System-level modularity
  • Open Source Ecosystems (Lecture 23) — Library boundaries
  • Sustainability (Lecture 36) — Long-term technical health

Information hiding is fractal.
The same principle that makes a well-designed class easier to change also makes a well-designed system easier to change.