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._centsclass 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 centsclass 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 works0
AttributeError: 'Account' object has no attribute '__pin'
1234class 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
500Step-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 startInterview 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).