
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

Text espertus to 22333 if the
URL isn't working for you.
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

Text espertus to 22333 if the
URL isn't working for you.
Learning Objectives
After this lecture, you will be able to:
- Describe the importance of changeability as a goal of program design and implementation
- Describe the relevance of modularity to changeability
- Describe the role of information hiding and immutability in enabling effective modularity
- Apply Java language features to achieve information hiding and immutability
The Tire Swing Meme

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:

- 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:
| Scale | Module | Interface |
|---|---|---|
| Method | A single function | Its signature and Javadoc |
| Class | A single type | Its public methods |
| Package | A group of related classes | Its public classes |
| Library | A group of packages | Its exported packages |
| Service | A whole program | Its 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

The Java Approach

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...

"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 — "Every change breaks someone's workflow"
Hyrum's Law in Action (LED traffic lights)

Source: CBC
Java Language Features for Information Hiding
In OO design, these features enable encapsulation:
- Access modifiers — control who can see what
- Immutable classes — prevent state changes after construction
- Sealed classes — control who can extend a type
- The module system — hide entire packages from library consumers
Access Modifiers in Java
Every class, method, and field has an access modifier:
| Modifier | Visibility |
|---|---|
public | Accessible from anywhere |
protected | Accessible from package and subclasses |
| (default) | Package-private: same package only |
private | Accessible 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

Text espertus to 22333 if the
URL isn't working for you.
Recipe for Safety
- Do not provide any mutators (methods that change state)
- Make the class
finalto 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

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 {
// ...
}

Subclasses of Sealed Classes
Classes extending a sealed class must declare themselves as one of:
final | Cannot be extended further |
sealed | Must specify its own permitted subclasses |
non-sealed | Reopens 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
publicfor 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
| Scope | Mechanism | What it hides |
|---|---|---|
| Field | private keyword | Implementation state within a class |
| Class | Package-private (default) | Helper classes within a package |
| Library | Module system (exports) | Internal packages within a library |
Same principle at different scales:
- Reduced coupling
- Clearer contracts
- Safer evolution
Key Takeaways
- Changeability is essential — most software cost is maintenance
- Modularity enables change — independent modules evolve independently
- Information hiding protects modularity — Hyrum's Law warns us
- 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.