Encapsulation
Encapsulation broadly means bundling related data and behavior inside a unit such as a Class. Its design quality depends on how that unit controls access to its representation and protects its rules. Visibility is a tool for this; making a Property private does not by itself guarantee a good design.
Why unrestricted setters are insufficient
Section titled “Why unrestricted setters are insufficient”Replacing public balance access with setBalance() still lets callers choose
arbitrary balances. Even a setter that rejects negative values cannot explain
why money was added or removed. Operations such as deposit() and withdraw()
express intent and provide a place for each operation’s rules.
Tell, Don’t Ask: request an operation from its owner
Section titled “Tell, Don’t Ask: request an operation from its owner”The principle encourages callers to request behavior from the Object that owns the relevant responsibility and let it apply its rules. Don’t Ask does not ban all questions or getters. The concern is retrieving an Object’s data to implement its own rules elsewhere.
Consider this checkout code. It assumes a query and an unrestricted setter to illustrate a weak design:
$balance = $wallet->balance();
if ($amount <= 0) { throw new InvalidArgumentException('Amount must be positive.');}
if ($amount > $balance) { throw new LogicException('Insufficient balance.');}
$wallet->setBalance($balance - $amount);The caller reads the balance, decides whether withdrawal is valid, computes the new balance, and writes it back. The Wallet holds data while withdrawal rules live outside it. Another caller must repeat the checks and might omit one. Adding a rule such as preventing withdrawal from a suspended Wallet would require finding and changing these callers.
With Tell, Don’t Ask, external code expresses its request:
$wallet->withdraw($amount);The Wallet validates the amount and available balance before changing its
State. Every caller uses the same protected operation. Rules can evolve inside
withdraw() while its meaning remains consistent for callers. Tell does not
mean a request always succeeds: the Object may reject an invalid operation.
How this supports encapsulation
Section titled “How this supports encapsulation”The caller knows which operation it needs without knowing the balance-update calculation or every withdrawal rule. The Object keeps its information and the decisions that govern it together behind a meaningful entry point. Renaming getters and setters does not help if the rules remain external.
For example, canWithdraw() followed by setBalance() still permits callers
to bypass checks:
if ($wallet->canWithdraw($amount)) { $wallet->setBalance($wallet->balance() - $amount);}canWithdraw() may be useful for display, but withdraw() must enforce the
rules at execution time instead of relying on a prior caller check.
When asking is appropriate
Section titled “When asking is appropriate”Displaying a balance is a legitimate query:
$displayBalance = $wallet->balance();Similarly, isPaid() can support displaying an Order’s state. When reviewing
code, ask whether it reads information for a legitimate consumer need or
extracts information to implement the Object’s own rules externally. The
principle guides responsibility placement; it does not prohibit queries.
It also does not put every responsibility into Wallet. Checkout coordinates components, Wallet protects withdrawal, and a notification component delivers confirmation. Broader applications of this principle have a later lesson; here the focus is protecting State through encapsulation.
Protect a rule throughout the object’s lifetime
Section titled “Protect a rule throughout the object’s lifetime”An invariant is a condition that must remain true for a valid Object. For this educational Wallet, the invariant is that its balance never becomes negative. The constructor and withdrawal operation both protect it:
<?php
declare(strict_types=1);
final class Wallet{ public function __construct(private int $balance) { if ($balance < 0) { throw new InvalidArgumentException('Negative initial balance.'); } }
public function withdraw(int $amount): void { if ($amount <= 0) { throw new InvalidArgumentException('Amount must be positive.'); }
if ($amount > $this->balance) { throw new LogicException('Insufficient balance.'); }
$this->balance -= $amount; }
public function balance(): int { return $this->balance; }}Amounts are integers in a single currency’s smallest unit. This example teaches object rules; it is not a complete payment or persistent wallet system.
Starting with 100, withdrawing 30 leaves 70. A subsequent withdrawal of 90 throws before mutation, leaving 70. An Exception does not automatically undo previous successful operations.
Queries can hide representation
Section titled “Queries can hide representation”$order->isPaid() expresses a meaningful question and can hide how the Order
represents payment state. Its result must retain the same meaning when the
implementation changes. Reading state through a focused query is useful;
getters are not inherently bad.
Mistakes and trade-offs
Section titled “Mistakes and trade-offs”- Adding getters and setters for every Property without considering rules.
- Checking rules only in callers, where checks may be forgotten or duplicated.
- Validating operations but allowing invalid constructor state.
- Making internal state public and allowing callers to bypass behavior.
- Hiding useful information unnecessarily: choose access boundaries according to the rules and responsibilities of the Object.
Summary and knowledge check
Section titled “Summary and knowledge check”Good encapsulation keeps related state and behavior together, hides details that callers need not depend on, and protects valid state through meaningful operations.
- Why can a private balance with an unrestricted setter still be unsafe?
- Why must constructor validation allow zero but reject a negative balance?
- Does a failed operation undo an earlier successful operation?
- What does
isPaid()hide from its caller?