Low-level designCore6 min

Encapsulation

Keep an object's state to itself, and let it change only through methods that keep its rules. Making fields private is the start, not the whole job: a getter that hands out a live list, or a setter that skips the rules, opens the same hole.

Watch it evolve1 / 8
The problem

A team with two rules

At most six players, and the captain is one of them.

80
Start
Stage 1
Stage 2
Final

1 · The situation

A five-a-side team with two rules

A five-a-side league keeps each team's squad. Two rules matter:

  1. a squad has at most six players, five on the pitch and one substitute;
  2. the captain is one of the squad.

The first version writes the first rule into sign() and leaves the fields public, so the rest of the program can read the squad and the captain directly:

The rule is in sign(), and the fields are publicJava
class Team {
    static final int MAX_PLAYERS = 6;

    public final String name;
    public final List<Player> players = new ArrayList<>();
    public Player captain;

    Team(String name) {
        this.name = name;
    }

    // Five on the pitch and one substitute.
    boolean sign(Player p) {
        if (players.size() >= MAX_PLAYERS) return false;
        players.add(p);
        return true;
    }
}
*Player− name: String~ name(): StringTeam~ MAX_PLAYERS: int+ name: String+ players: List<Player>+ captain: Player~ sign(Player): boolean

Drawn from the code: the public fields show as +. Anyone holding a Team can reach in.

2 · The change

Code elsewhere goes around the rules

A season later, a new screen adds a late signing by appending to players, and a captain picker assigns captain directly. Each line looks reasonable on its own:

Two lines outside TeamJava
import java.util.ArrayList;
import java.util.List;

class Player {
    private final String name;

    Player(String name) {
        this.name = name;
    }

    String name() {
        return name;
    }
}

class Team {
    static final int MAX_PLAYERS = 6;

    public final String name;
    public final List<Player> players = new ArrayList<>();
    public Player captain;

    Team(String name) {
        this.name = name;
    }

    // Five on the pitch and one substitute.
    boolean sign(Player p) {
        if (players.size() >= MAX_PLAYERS) return false;
        players.add(p);
        return true;
    }
}

public class Main {
    public static void main(String[] args) {
        Team rovers = new Team("Rovers");
        for (String n : List.of("Ana", "Ben", "Cy", "Dee", "Eli", "Fay")) {
            rovers.sign(new Player(n));
        }
        rovers.captain = rovers.players.get(0);

        rovers.players.add(new Player("Gus"));
        rovers.captain = new Player("Zed");

        System.out.println(rovers.name + ": " + rovers.players.size() + " players, captain "
                + rovers.captain.name());
    }
}
Rovers: 7 players, captain Zed

Seven players, and a captain who isn't in the squad: both rules broken, and Team never knew.

sign() held the squad rule, but nothing forced anyone to use it. When state can be changed from outside, every rule about it must be remembered by every caller, and the one that forgets breaks the object. The practice checks flag this version: Team lets anyone replace its captain, and lets anyone change its players.

3 · Private isn't enough

Getters and setters that change nothing

The usual first fix makes the fields private and adds a getter and a setter for each. It looks encapsulated, and the checks no longer complain. The callers change only their syntax:

Private fields, the same holesJava
class Team {
    static final int MAX_PLAYERS = 6;

    private final String name;
    private final List<Player> players = new ArrayList<>();
    private Player captain;

    Team(String name) {
        this.name = name;
    }

    String name() {
        return name;
    }

    List<Player> players() {
        return players;
    }

    Player captain() {
        return captain;
    }

    void setCaptain(Player captain) {
        this.captain = captain;
    }

    // Five on the pitch and one substitute.
    boolean sign(Player p) {
        if (players.size() >= MAX_PLAYERS) return false;
        players.add(p);
        return true;
    }
}
Rovers: 7 players, captain Zed

The same result: seven players, and Zed as captain.

The getter returns the list itself, so a caller still appends to the squad, and the setter accepts any player at all. Encapsulation was never about the keyword private; it's about whether any path to the state can skip the rules. A tool that reads keywords sees nothing wrong here, which is why this is worth checking by hand.

4 · The fix

Hand out views, and change state only through rules

Close each path that skips a rule:

Nothing outside Team can assign players or captain any more. This is necessary but, as the last section showed, not enough on its own.

Java
class Team {
    static final int MAX_PLAYERS = 6;

    private final String name;
    private final List<Player> players = new ArrayList<>();
    private Player captain;

    Team(String name) {
        this.name = name;
    }

    String name() {
        return name;
    }

    List<Player> players() {
        return players;
    }

    Player captain() {
        return captain;
    }

    void setCaptain(Player captain) {
        this.captain = captain;
    }

    // Five on the pitch and one substitute.
    boolean sign(Player p) {
        if (players.size() >= MAX_PLAYERS) return false;
        players.add(p);
        return true;
    }
}
*Player− name: String~ name(): StringTeam~ MAX_PLAYERS: int− name: String− players: List<Player>− captain: Player~ name(): String~ players(): List<Player>~ captain(): Player~ setCaptain(Player)~ sign(Player): boolean
Gus: refused, the squad is full
Zed as captain: refused, not in the squad
Rovers: 6 players, captain Ana

The late signing and the outside captain are refused; the squad stays at six with Ana as captain.

5 · What it is

State kept private, changed only by rules

Encapsulation keeps an object's state inside it and lets the state change only through the object's own methods, which keep its rules. Two things follow:

  • The rules live in one place. sign() and appointCaptain() are the only code that knows them, and no caller needs to.
  • The inside can change freely. Team could keep its players in a set or a database row tomorrow; callers that only use sign() and players() wouldn't notice.
Public fields
2
Getter and setter
2
Encapsulated
0

Ways for code outside Team to change the squad or the captain without a check.

6 · Getters and setters

What to offer, case by case

Callers need to

Offer

On Team

Read a value

A getter is fine

name(), captain()

Read a collection

A read-only view or a copy

players()

Change something that has a rule

A method named for the event, which checks the rule

sign(), appointCaptain()

Change something with no rule

Ask first if it's really state; a plain data record may be enough

None here

A setter is a method named for a field rather than for something that happens. When a setter would need a check, give the change its real name instead, and the check has an obvious home.

7 · Across languages

How each language hides state

Language

Private state

A read-only collection

Java

private, enforced by the compiler

Collections.unmodifiableList() or List.copyOf()

C++

private, enforced by the compiler

Return a const reference; changing it won't compile

JavaScript

#name fields, enforced by the language: using one outside the class is a syntax error

A copy, frozen with Object.freeze()

Python

A leading _, by convention only

A tuple copy; nothing stops a determined caller

Python shows that encapsulation is a design choice more than a language feature: the underscore only asks callers to stay out. The design still decides which paths exist, and well-behaved callers follow them.

8 · In practice

The garage with a public field

Sam's draft of the parking lot has a public field that any caller can replace, and the checks flag it. Make the lot's state private, hand out read-only views, and let it change only through methods such as parking and checking out.

Fix the parking-lot draft and close its paths.

Check yourself

4 questions

1. players is private, but players() returns the list itself. What can a caller do?
2. Which is the better replacement for setCaptain(Player p)?
3. Why did the checks pass the getter-and-setter version while it still broke both rules?
4. In Python, _players can still be reached from outside. Is the class encapsulated?

Takeaways

Remember this

  • Encapsulation means state changes only through methods that keep the object's rules.
  • Private fields are the start; a getter that returns a live collection or a setter that skips a rule reopens the hole.
  • Hand out read-only views or copies of collections.
  • Replace setters that need a rule with methods named for the event, which check it.
  • Count the paths to the state, not the private keywords.