0

Encapsulation — hiding internal state behind a public interface

Beginner5 min read·eng-12-009
interviewcompare

Concept

Encapsulation — bundling data and the methods that operate on that data together in one unit (a class), and controlling access to the internals via visibility modifiers.

What it is: The object hides its internal state. External code cannot directly read or modify the internals. All interaction goes through a defined public interface.

Why it matters:

  • Internal representation can change freely: If BankAccount stores balance in cents internally but exposes it in dollars publicly, you can change the storage format without breaking any code that uses the class.
  • Invariants are maintained: The class can validate all modifications to its state. withdraw($amount) can check that balance doesn't go negative. Direct $account->balance -= $amount bypasses the check.
  • Reduces coupling: Code that depends on a class's public interface doesn't break when you change private implementation.

Visibility in PHP:

  • public: Accessible from anywhere.
  • protected: Accessible from the class and its subclasses.
  • private: Accessible only from within the class. Tightest encapsulation.

PHP 8.1+ readonly: readonly properties can only be set once (in the constructor). Enforces immutability — a stronger form of encapsulation.

Getters and setters: A naive approach that adds encapsulation syntax without the benefit. getBalance() + setBalance($x) is worse than just public $balance. Prefer behavior methods: deposit($amount), withdraw($amount) — they maintain invariants.

Law of Demeter ("tell, don't ask"): Don't reach into an object's internals — tell it what to do. $order->confirm() not $order->getStatus()->setConfirmed(true).

Code Example

php
<?php
// ❌ No encapsulation — public fields, no invariant protection
class BankAccountBad
{
    public float $balance = 0;
    // Anyone can do: $account->balance = -9999; — no protection!
}

// ✅ Encapsulated — private state, public behavior
class BankAccount
{
    private float $balanceCents;   // stored in cents internally
    private array $transactions = [];

    public function __construct(float $initialBalance = 0.0)
    {
        if ($initialBalance < 0) throw new \InvalidArgumentException('Initial balance cannot be negative');
        $this->balanceCents = (int) ($initialBalance * 100);
    }

    public function deposit(float $amount): void
    {
        if ($amount <= 0) throw new \InvalidArgumentException('Deposit amount must be positive');
        $this->balanceCents     += (int) ($amount * 100);
        $this->transactions[] = ['type' => 'deposit', 'amount' => $amount, 'at' => now()];
    }

    public function withdraw(float $amount): void
    {
        if ($amount <= 0)                        throw new \InvalidArgumentException('Amount must be positive');
        if ($amount * 100 > $this->balanceCents) throw new \DomainException('Insufficient funds');
        $this->balanceCents     -= (int) ($amount * 100);
        $this->transactions[] = ['type' => 'withdrawal', 'amount' => $amount, 'at' => now()];
    }

    // Getter — read-only view of internal state, in dollars
    public function getBalance(): float { return $this->balanceCents / 100; }

    // Read-only view of history — returns a COPY, not the internal array
    public function getTransactions(): array { return $this->transactions; }

    // Behavior method — tells the account to do something
    public function transfer(BankAccount $to, float $amount): void
    {
        $this->withdraw($amount); // validates here
        $to->deposit($amount);
    }
}

// External code can only interact through the public interface
$account = new BankAccount(100.00);
$account->deposit(50);
$account->withdraw(30);
echo $account->getBalance(); // 120.00

// $account->balanceCents = -9999; // Fatal error — private!
// $account->transactions[] = [...]; // Fatal error — private!

// PHP 8.1 readonly — encapsulated via immutability
class Money
{
    public function __construct(
        public readonly int    $amount,   // cannot be changed after construction
        public readonly string $currency,
    ) {}
}

$price = new Money(100, 'USD');
// $price->amount = 200; // Fatal error: Cannot modify readonly property