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.
A team with two rules
At most six players, and the captain is one of them.
1 · The situation
A five-a-side team with two rules
A five-a-side league keeps each team's squad. Two rules matter:
- a squad has at most six players, five on the pitch and one substitute;
- 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:
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;
}
}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:
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:
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.
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;
}
}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()andappointCaptain()are the only code that knows them, and no caller needs to. - The inside can change freely.
Teamcould keep its players in a set or a database row tomorrow; callers that only usesign()andplayers()wouldn't notice.
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 |
|---|---|---|
Read a value | A getter is fine |
|
Read a collection | A read-only view or a copy |
|
Change something that has a rule | A method named for the event, which checks the rule |
|
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 |
|
|
C++ |
| Return a |
JavaScript |
| A copy, frozen with |
Python | A leading | A |
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
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.