Skip to main content

Encapsulation and Information Hiding as Design Goals

What This Concept Is

Encapsulation is hiding a decision behind an interface so that the rest of the system does not depend on it.

In most introductions, encapsulation collapses to "use private fields and write getters." That misses the point. The real question is: which design decisions are likely to change, and how can they change without the rest of the code caring?

Information hiding (Parnas's term) is the clearer name. A module hides what it knows and exposes what it can do. Good modules export small, stable operations and keep volatile decisions invisible.

Why It Matters Here

Bad encapsulation is the quiet cause of most expensive-to-change codebases. When every client knows the shape of a data structure, the ordering of its fields, the meaning of its null values, or the identity of its backing store, that is what you cannot change without a trail of edits.

Most refactorings in later modules (Encapsulate Record, Encapsulate Collection, Encapsulate Variable, Replace Primitive with Object) exist to buy back encapsulation that was lost by treating an object as a bag of fields.

Concrete Example

Leaky encapsulation -- private field, public getter, same thing:

class Account {
private List<Transaction> transactions = new ArrayList<>();
public List<Transaction> getTransactions() { return transactions; }
}

// elsewhere
account.getTransactions().add(new Transaction(...));
account.getTransactions().sort(...);

The field is private, but the list is exposed by identity. Callers mutate it, sort it, and rely on it being an ArrayList. Nothing inside Account is actually hidden.

Real encapsulation -- Account hides the representation and the invariants:

class Account {
private final List<Transaction> transactions = new ArrayList<>();

public void record(Transaction t) {
validate(t);
transactions.add(t);
}

public Money balance() { return Money.sum(transactions); }
public List<Transaction> history() { return List.copyOf(transactions); }
}

Now Account decides how transactions are stored, how they are validated, and how they are exposed. The storage is no longer a contract with the rest of the world.

Common Confusion / Misconception

"My fields are private, so the class is encapsulated." Privacy of syntax is not privacy of decision. If every field has a getter and setter and your clients rebuild all invariants outside, you have just renamed struct to class.

A second confusion: encapsulation is not about secrecy. It is about stability. You are choosing which parts of the design can change quietly. Anything you expose, you are promising to keep stable; anything you hide, you are free to rework.

How To Use It

Before finishing a class, run three sweeps:

  1. Decision sweep. For each design decision (data layout, algorithm, storage backend, error style), ask: is this exposed? If yes, commit to it as a contract; if no, hide it.
  2. Return-value sweep. Do you return mutable collections or internal objects by identity? Either return copies / unmodifiable views or do not return them at all.
  3. Behavior sweep. Are callers composing multiple calls to enforce invariants (get / validate / set)? Pull the composition into the class as one method.

Check Yourself

  1. Why is "private fields with getters and setters" often still unencapsulated?
  2. What is the difference between encapsulation and security?
  3. Why is information hiding closer to the real goal than "data hiding"?

Mini Drill or Application

Pick a class with at least three fields that are exposed through getters. Do all four:

  1. For each getter, ask what the caller does with the result.
  2. For any caller that transforms the result, move that transformation into the class as a named method.
  3. Replace any mutable returned collection with an unmodifiable view or a copy.
  4. Rewrite the class's public contract in one paragraph: "this class lets callers X, Y, and Z, and hides A, B, and C."

Read This Only If Stuck