Access Specifiers and Encapsulation

Learning Objectives

Use access specifiers in C++, Java, C#, Python and TypeScript.

Define encapsulation and invariants.

Explain why private fields alone do not guarantee good encapsulation.

Decide which fields need getters, setters or neither.

Prerequisites: Module 2.

Intuition and Definition

Encapsulation means bundling state with the operations that change it and *controlling* those changes so the object can never enter an invalid state. The rules that must always hold are the class invariants — "balance is never negative", "end date is after start date".

Access specifiers are the language tool that makes encapsulation enforceable: they decide which code may touch which members.

public, protected (class + derived classes), private (class + friends)

public, protected (subclasses + package), package-private (default), private

public, protected, internal (same assembly), protected internal, private protected, private

Convention only: name public, _name "internal", __name name-mangled to _Class__name — not security

public, protected, private (compile-time only), and ECMAScript #private (enforced at run time)

Why It Matters

If any code can write account.cents = -500, then every function that reads the balance must defend against corrupted data. With encapsulation, the class checks once and everyone else can trust it.

Syntax and Language Notes

class Account {
public:      // anyone
    long long balance() const { return cents_; }
protected:   // this class and derived classes
    void audit() const {}
private:     // only this class (and friends)
    long long cents_ = 0;
};
class Account:
    def __init__(self):
        self._cents = 0          # "please don't touch" by convention
        self.__pin = "1234"      # name-mangled to _Account__pin

    @property
    def balance(self):           # read-only view: no setter defined
        return self._cents
class Account {
  #cents = 0;                    // truly private at run time (ECMAScript)
  private note = "";             // private only for the type checker
  get balance(): number { return this.#cents; }
}
public class Account
{
    public long Balance { get; private set; }   // public read, private write
    internal void Reconcile() { }               // visible inside this assembly only
}

Complete Example

Python's privacy is a naming convention — the "private" attribute is still reachable under its mangled name:

TypeScript's private disappears at run time, while #private is enforced by the JavaScript engine:

#include <climits>
#include <iostream>
#include <stdexcept>

class BankAccount {
public:
    explicit BankAccount(long long openingCents) : cents_(openingCents) {
        if (openingCents < 0) throw std::invalid_argument("negative opening balance");
    }
    void deposit(long long amount) {
        if (amount <= 0) throw std::invalid_argument("deposit must be positive");
        if (amount > LLONG_MAX - cents_) throw std::overflow_error("balance overflow");
        cents_ += amount;
    }
    void withdraw(long long amount) {
        if (amount <= 0) throw std::invalid_argument("withdrawal must be positive");
        if (amount > cents_) throw std::runtime_error("insufficient funds");
        cents_ -= amount;
    }
    long long balance() const { return cents_; }

private:
    long long cents_;   // invariant: 0 <= cents_
};

int main() {
    BankAccount acc(10'000);          // $100.00
    acc.deposit(2'550);
    acc.withdraw(1'000);
    std::cout << "balance: " << acc.balance() << " cents\n";

    for (long long bad : {0LL, -5LL}) {
        try { acc.deposit(bad); }
        catch (const std::exception& e) { std::cout << "deposit(" << bad << ") -> " << e.what() << "\n"; }
    }

…
balance: 11550 cents
deposit(0) -> deposit must be positive
deposit(-5) -> deposit must be positive
withdraw -> insufficient funds
huge deposit -> balance overflow
constructor -> negative opening balance
balance unchanged: 11550 cents
class Account:
    def __init__(self):
        self._cents = 0
        self.__pin = "1234"

a = Account()
print(a._cents)                 # allowed, just discouraged
try:
    print(a.__pin)
except AttributeError as e:
    print("AttributeError:", e)
print(a._Account__pin)          # the mangled name still works
0
AttributeError: 'Account' object has no attribute '__pin'
1234
class Account {
  private note = "only the type checker protects me";
  #cents = 500;
  get balance(): number { return this.#cents; }
}
const a = new Account();
console.log((a as any).note);          // reachable at run time
console.log((a as any)["#cents"]);     // not a normal property: undefined
console.log(a.balance);
only the type checker protects me
undefined
500

Step-by-Step Walkthrough

The constructor refuses a negative opening balance — an object that fails its invariant is never created.

deposit checks positivity and overflow *before* modifying cents_; if a check fails, the state is untouched.

withdraw refuses overdrafts. All four failed operations leave the balance at 11550 cents.

There is no setBalance() — callers express *intent* (deposit, withdraw), not raw assignments.

Common Mistakes

Private fields + public setters for everything. setBalance(long long) makes the private field pointless; encapsulation is about *operations that preserve invariants*, not about hiding variables.

Returning a non-const reference or pointer to internal state (std::vector<Item>& items()), which lets callers bypass every check. Return a const view or a copy.

Using floating point for money. 0.1 + 0.2 != 0.3; use integer cents or a decimal type.

Believing Python's __name is security. It only prevents accidental clashes in subclasses.

Exposing protected data. Derived classes become coupled to the base class's representation.

Trade-offs and Alternatives

Immutable objects (all fields set once in the constructor) need no setters and are trivially thread-safe — but every change creates a new object.

Getters/setters vs public fields: for pure data carriers (DTOs, config structs) public fields or records are simpler and honest. Encapsulate when there are rules.

Read-only views: C# IReadOnlyList<T>, C++ std::span<const T>, Python tuples, TypeScript readonly T[] let callers read without modifying.

Real-World Use

Banking ledgers, inventory counts, sensor calibration tables and vehicle odometers all rely on encapsulated state: an odometer class exposes addDistance(km) but never setOdometer(), so mileage cannot be rolled back through the API.

Practice

Q1. (Easy) Which C++ access level lets derived classes see a member but hides it from other code?

Answer: protected.

Q2. (Medium) Why is int& count() returning a reference to a private field a leak?

Answer: The caller can write through the reference (obj.count() = -7) and break any invariant the class enforces; return int (a copy) or provide intent-revealing operations instead.

Q3. (Advanced) Write a DateRange class whose invariant is start <= end (use day numbers as int). Provide extendBy(days) and shiftBy(days); reject operations that would break the invariant.

#include <iostream>
#include <stdexcept>
class DateRange {
    int start_, end_;   // invariant: start_ <= end_
public:
    DateRange(int start, int end) : start_(start), end_(end) {
        if (start > end) throw std::invalid_argument("start after end");
    }
    void extendBy(int days) {
        if (days < 0 && end_ + days < start_) throw std::invalid_argument("would end before start");
        end_ += days;
    }
    void shiftBy(int days) { start_ += days; end_ += days; }   // keeps the length, always valid
    int length() const { return end_ - start_; }
    int start() const { return start_; }
};
int main() {
    DateRange r(10, 15);
    r.extendBy(5);
    r.shiftBy(-3);
    std::cout << r.start() << " +" << r.length() << "\n";
    try { r.extendBy(-20); } catch (const std::exception& e) { std::cout << e.what() << "\n"; }
}
7 +10
would end before start

Interview Question

Q1. "If all my fields are private and have getters and setters, is my class encapsulated?"

Answer: Not necessarily. Encapsulation means the object controls *how* its state changes so invariants always hold. Blind setters let callers put the object into any state, which is the same as public fields with extra syntax. A well-encapsulated class exposes intent-revealing operations (withdraw, checkout), validates them, and exposes read access only where needed — often without any setter at all.

Quick Quiz

Q1. What is an invariant?

Answer: A condition about an object's state that must always hold between operations (e.g. balance ≥ 0).

Q2. Which TypeScript privacy mechanism is enforced at run time?

Answer: ECMAScript #private fields; private is checked only by the compiler.

Q3. What does Python do with self.__pin inside class Account?

Answer: It renames (mangles) it to self._Account__pin.

Summary

Encapsulation = state + operations that keep it valid; access specifiers enforce it.

Validate in constructors and operations; never hand out mutable references to internals.

Getters/setters are not encapsulation by themselves.

Related: Module 4 (abstraction), Module 18 (value objects), Module 68 (secure object design).