What is Encapsulation in Object-Oriented Programming
Encapsulation is one of the fundamental pillars of object-oriented programming (OOP) and represents the principle of hiding implementation details of a class, exposing only what is necessary through a well-defined public interface. This concept is crucial for creating maintainable, secure, and modular software.
🎯 Goals of Encapsulation
- Information Hiding: Hide implementation details
- Data Protection: Prevent unauthorized access and modifications
- Flexibility: Change implementation without impacting external code
- Maintainability: Reduce coupling between components
Access Modifiers in TypeScript
TypeScript offers three access modifiers to implement encapsulation:
private, protected, and public. Choosing the right modifier
determines who can access class members.
1. Private: Internal Access Only
private members are accessible only within the class that defines them.
This is the most restrictive level of encapsulation and should be the default for all
internal class data.
class BankAccount {{ '{' }}
// Private properties - internal access only
private balance: number;
private accountNumber: string;
private transactionHistory: Transaction[] = [];
private isLocked: boolean = false;
constructor(accountNumber: string, initialBalance: number) {{ '{' }}
this.accountNumber = accountNumber;
this.balance = initialBalance;
this.logTransaction('Account created', initialBalance);
{{ '}' }}
// Public method with validation
public deposit(amount: number): void {{ '{' }}
if (this.isLocked) {{ '{' }}
throw new Error('Account is locked');
{{ '}' }}
if (amount <= 0) {{ '{' }}
throw new Error('Amount must be positive');
{{ '}' }}
this.balance += amount;
this.logTransaction('Deposit', amount);
{{ '}' }}
public withdraw(amount: number): boolean {{ '{' }}
if (this.isLocked) {{ '{' }}
throw new Error('Account is locked');
{{ '}' }}
if (amount > this.balance) {{ '{' }}
console.log('Insufficient funds');
return false;
{{ '}' }}
if (amount <= 0) {{ '{' }}
throw new Error('Amount must be positive');
{{ '}' }}
this.balance -= amount;
this.logTransaction('Withdrawal', amount);
return true;
{{ '}' }}
// Public getter - read-only access
public getBalance(): number {{ '{' }}
return this.balance;
{{ '}' }}
public getAccountNumber(): string {{ '{' }}
return this.accountNumber;
{{ '}' }}
// Private method - internal helper
private logTransaction(type: string, amount: number): void {{ '{' }}
const transaction: Transaction = {{ '{' }}
id: Date.now().toString(),
type,
amount,
timestamp: new Date(),
balanceAfter: this.balance
{{ '}' }};
this.transactionHistory.push(transaction);
{{ '}' }}
// Method to get copy of history (not the original array!)
public getTransactionHistory(): readonly Transaction[] {{ '{' }}
return Object.freeze([...this.transactionHistory]);
{{ '}' }}
// Administrative methods
public lockAccount(): void {{ '{' }}
this.isLocked = true;
{{ '}' }}
public unlockAccount(adminCode: string): void {{ '{' }}
if (this.verifyAdminCode(adminCode)) {{ '{' }}
this.isLocked = false;
{{ '}' }}
{{ '}' }}
private verifyAdminCode(code: string): boolean {{ '{' }}
// Secure verification logic
return code === 'ADMIN_SECRET_CODE';
{{ '}' }}
{{ '}' }}
interface Transaction {{ '{' }}
id: string;
type: string;
amount: number;
timestamp: Date;
balanceAfter: number;
{{ '}' }}
// Usage
const account = new BankAccount('IT60X0542811101000000123456', 1000);
account.deposit(500); // ✅ OK
console.log(account.getBalance()); // ✅ OK - 1500
// account.balance = 999999; // ❌ ERROR: balance is private
// account.logTransaction(); // ❌ ERROR: logTransaction is private
✨ Advantages of Private Members
- Total Control: Only the class can modify internal state
- Centralized Validation: All modifications go through validated public methods
- Safe Refactoring: Can change private implementation without breaking external code
- Easy Debugging: Know exactly where state is modified
2. Protected: Access for Subclasses
protected members are accessible from the class and its subclasses.
Useful when you want to allow extension but maintain encapsulation from the outside.
abstract class Vehicle {{ '{' }}
protected engineStatus: 'on' | 'off' = 'off';
protected currentSpeed: number = 0;
protected readonly maxSpeed: number;
constructor(maxSpeed: number) {{ '{' }}
this.maxSpeed = maxSpeed;
{{ '}' }}
// Protected method - usable by subclasses
protected startEngine(): void {{ '{' }}
if (this.engineStatus === 'off') {{ '{' }}
this.engineStatus = 'on';
console.log('Engine started');
{{ '}' }}
{{ '}' }}
protected stopEngine(): void {{ '{' }}
if (this.currentSpeed === 0) {{ '{' }}
this.engineStatus = 'off';
console.log('Engine stopped');
{{ '}' }} else {{ '{' }}
console.log('Cannot stop engine while moving');
{{ '}' }}
{{ '}' }}
// Public method
public getSpeed(): number {{ '{' }}
return this.currentSpeed;
{{ '}' }}
// Abstract method - must be implemented
abstract accelerate(amount: number): void;
{{ '}' }}
class Car extends Vehicle {{ '{' }}
private gear: number = 1;
constructor() {{ '{' }}
super(200); // maxSpeed for car
{{ '}' }}
accelerate(amount: number): void {{ '{' }}
// Can access protected members of parent
if (this.engineStatus === 'off') {{ '{' }}
this.startEngine(); // ✅ OK - protected method
{{ '}' }}
if (this.currentSpeed + amount <= this.maxSpeed) {{ '{' }}
this.currentSpeed += amount;
this.adjustGear();
{{ '}' }}
{{ '}' }}
brake(amount: number): void {{ '{' }}
this.currentSpeed = Math.max(0, this.currentSpeed - amount);
if (this.currentSpeed === 0) {{ '{' }}
this.stopEngine(); // ✅ OK - protected method
{{ '}' }}
{{ '}' }}
private adjustGear(): void {{ '{' }}
// Private gear shifting logic
if (this.currentSpeed > 80) this.gear = 5;
else if (this.currentSpeed > 60) this.gear = 4;
else if (this.currentSpeed > 40) this.gear = 3;
else if (this.currentSpeed > 20) this.gear = 2;
else this.gear = 1;
{{ '}' }}
{{ '}' }}
// Usage
const car = new Car();
car.accelerate(50);
console.log(car.getSpeed()); // 50
// car.startEngine(); // ❌ ERROR: startEngine is protected
// car.engineStatus; // ❌ ERROR: engineStatus is protected
3. Public: Free Access
public members are accessible from anywhere in the code. These
constitute the class's public interface and should be
minimized and well-documented.
⚠️ Principle of Least Privilege
Always start with private and only relax when necessary. It's easier to make something public later than to make private something that was public (breaking change).
- Private by default
- Protected only if necessary for subclasses
- Public only for essential interface
Getters and Setters: Controlled Access
Getters and setters in TypeScript offer an elegant way to control property access, allowing validation and custom logic.
class User {{ '{' }}
private _email: string;
private _age: number;
private _passwordHash: string;
constructor(email: string, age: number, password: string) {{ '{' }}
this._email = email;
this._age = age;
this._passwordHash = this.hashPassword(password);
{{ '}' }}
// Getter - read-only access with transformation
get email(): string {{ '{' }}
return this._email.toLowerCase();
{{ '}' }}
// Setter with validation
set email(value: string) {{ '{' }}
if (!this.isValidEmail(value)) {{ '{' }}
throw new Error('Invalid email format');
{{ '}' }}
this._email = value;
{{ '}' }}
// Getter
get age(): number {{ '{' }}
return this._age;
{{ '}' }}
// Setter with validation
set age(value: number) {{ '{' }}
if (value < 0 || value > 150) {{ '{' }}
throw new Error('Invalid age');
{{ '}' }}
this._age = value;
{{ '}' }}
// Computed property - getter only
get isAdult(): boolean {{ '{' }}
return this._age >= 18;
{{ '}' }}
get ageGroup(): string {{ '{' }}
if (this._age < 18) return 'minor';
if (this._age < 65) return 'adult';
return 'senior';
{{ '}' }}
// Public method to change password
changePassword(oldPassword: string, newPassword: string): boolean {{ '{' }}
if (!this.verifyPassword(oldPassword)) {{ '{' }}
return false;
{{ '}' }}
if (newPassword.length < 8) {{ '{' }}
throw new Error('Password must be at least 8 characters');
{{ '}' }}
this._passwordHash = this.hashPassword(newPassword);
return true;
{{ '}' }}
// Private methods - helpers
private isValidEmail(email: string): boolean {{ '{' }}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
{{ '}' }}
private hashPassword(password: string): string {{ '{' }}
// Hash simulation (use bcrypt in production!)
return `hash_${{ '{' }}password.split('').reverse().join(''){{ '}' }}`;
{{ '}' }}
private verifyPassword(password: string): boolean {{ '{' }}
return this.hashPassword(password) === this._passwordHash;
{{ '}' }}
{{ '}' }}
// Usage
const user = new User('john@example.com', 25, 'password123');
console.log(user.email); // "john@example.com"
console.log(user.isAdult); // true
console.log(user.ageGroup); // "adult"
user.age = 30; // ✅ OK - passes validation
// user.age = -5; // ❌ Error: Invalid age
// user.email = 'invalid'; // ❌ Error: Invalid email format
user.changePassword('password123', 'newSecurePass123'); // ✅ OK
// user._passwordHash; // ❌ ERROR: _passwordHash is private
✅ When to Use Getters/Setters:
- Data validation needed
- Value transformation
- Computed properties
- Logging modifications
- Lazy initialization
❌ When NOT to Use Them:
- Simple access without logic
- Just to "look OOP"
- Heavy computations in getter
- Side effects in getter
Readonly: Immutability
The readonly modifier ensures that a property can only be
assigned during initialization (constructor) and cannot be modified afterwards.
class Configuration {{ '{' }}
public readonly appName: string;
public readonly version: string;
private readonly apiKey: string;
public readonly createdAt: Date;
constructor(appName: string, version: string, apiKey: string) {{ '{' }}
this.appName = appName;
this.version = version;
this.apiKey = apiKey;
this.createdAt = new Date(); // Can only be assigned here
{{ '}' }}
// Shorthand syntax in constructor
constructor(
public readonly appName: string,
public readonly version: string,
private readonly apiKey: string
) {{ '{' }}
// TypeScript automatically creates properties
{{ '}' }}
getApiKey(): string {{ '{' }}
return this.apiKey;
{{ '}' }}
{{ '}' }}
const config = new Configuration('MyApp', '1.0.0', 'secret_key');
console.log(config.appName); // "MyApp"
// config.appName = 'NewName'; // ❌ ERROR: readonly property
Encapsulation Best Practices
🎯 Professional Guidelines
1. Tell, Don't Ask
Instead of asking for state and then deciding, tell the object what to do:
❌ Bad (Ask):
if (account.getBalance() > amount) {{ '{' }}
account.setBalance(
account.getBalance() - amount
);
{{ '}' }}
✅ Good (Tell):
account.withdraw(amount);
2. Law of Demeter (LoD)
Don't talk to strangers - an object should only call:
- Its own methods
- Parameters passed to the method
- Objects it creates
- Direct components
❌ LoD Violation:
user.getAddress()
.getCity()
.getZipCode()
✅ Respects LoD:
user.getCityZipCode()
3. Immutability When Possible
Favor immutable objects - easier to reason about and thread-safe:
class Point {{ '{' }}
constructor(
public readonly x: number,
public readonly y: number
) {{ '{' }}{{ '}' }}
// Instead of modifying, return new object
move(dx: number, dy: number): Point {{ '{' }}
return new Point(this.x + dx, this.y + dy);
{{ '}' }}
{{ '}' }}
Common Mistakes to Avoid
⚠️ Encapsulation Anti-Patterns
1. Useless Getter/Setter (JavaBeans Anti-Pattern)
// ❌ Bad - adds no value
class Product {{ '{' }}
private name: string;
getName(): string {{ '{' }} return this.name; {{ '}' }}
setName(name: string) {{ '{' }} this.name = name; {{ '}' }}
{{ '}' }}
// ✅ Better - public property if no logic needed
class Product {{ '{' }}
public name: string;
{{ '}' }}
2. Exposing Mutable Collections
// ❌ Bad - array can be modified externally
getItems(): Item[] {{ '{' }}
return this.items;
{{ '}' }}
// ✅ Good - return immutable copy
getItems(): readonly Item[] {{ '{' }}
return Object.freeze([...this.items]);
{{ '}' }}
3. Too Many Getters/Setters
If you have 20 getters and setters, the class probably does too much (violates Single Responsibility).
Conclusion
Encapsulation is fundamental for creating quality OOP code. By hiding implementation details and exposing only well-defined interfaces, you achieve more secure, maintainable, and flexible code. Remember: private by default, and only relax when strictly necessary.
🎯 Key Points
- Use
privatefor all internal details - Use
protectedonly for controlled extension - Keep public interface minimal and stable
- Always validate input data
- Prefer getters/setters when logic is needed
- Use
readonlyfor data that shouldn't change - Apply "Tell, Don't Ask" and Law of Demeter
📚 Next in the OOP Series
In the next article we'll explore Inheritance and Polymorphism, seeing how classes can extend and how to leverage polymorphism for flexible and extensible code.
Related deep dive: 01 - Introduzione alla Programmazione Orientata agli Oggetti.
Related deep dive: 03 - Ereditarietà e Composizione: Riutilizzo del Codice in OOP.
Frequently asked questions
What is encapsulation in object-oriented programming?
Encapsulation is the principle of hiding implementation details of a class, exposing only what is necessary through a well-defined public interface.
How many access modifiers does TypeScript offer to implement encapsulation?
TypeScript offers three access modifiers: private, protected, and public.
What is the purpose of using getters and setters in TypeScript?
Getters and setters in TypeScript allow for controlled property access, enabling validation and custom logic.
Why should properties be made private by default in encapsulation?
Properties should be made private by default because it's easier to make something public later than to make private something that was public (breaking change).
Commenti
Caricamento commenti...
Sign in to comment