9.4 KiB
id, title, category, priority, tags, related
| id | title | category | priority | tags | related | ||||||
|---|---|---|---|---|---|---|---|---|---|---|---|
| core-encapsulation | Encapsulation | core-principles | critical |
|
|
Encapsulation
Hide internal implementation details and expose only what's necessary through a well-defined interface. Protect data integrity by controlling access to internal state.
Bad Example
// Anti-pattern: Exposed internals, no encapsulation
class BankAccount {
// Public fields - anyone can modify directly
public accountNumber: string;
public balance: number;
public transactions: Transaction[];
public overdraftLimit: number;
public isLocked: boolean;
constructor(accountNumber: string, initialBalance: number) {
this.accountNumber = accountNumber;
this.balance = initialBalance;
this.transactions = [];
this.overdraftLimit = 0;
this.isLocked = false;
}
}
// External code can violate business rules
const account = new BankAccount('12345', 1000);
// Direct modification bypasses validation
account.balance = -999999; // Negative balance without check!
account.balance = account.balance + 1000; // No transaction record!
// Can manipulate transaction history
account.transactions.push({
id: 'fake',
amount: 1000000,
type: 'deposit'
}); // Fraudulent transaction!
// Can unlock locked accounts
account.isLocked = false; // Bypasses security!
// Can change overdraft without authorization
account.overdraftLimit = 100000; // Unauthorized overdraft!
// Other classes depend on internal structure
class AccountReport {
generate(account: BankAccount): Report {
// Directly accesses internal array
const deposits = account.transactions.filter(t => t.type === 'deposit');
const withdrawals = account.transactions.filter(t => t.type === 'withdrawal');
// Depends on internal structure of Transaction
const totalDeposits = deposits.reduce((sum, t) => sum + t.amount, 0);
return {
balance: account.balance,
totalDeposits,
transactionCount: account.transactions.length
};
}
}
// Problems:
// 1. Anyone can modify balance without recording transaction
// 2. Business rules can be bypassed
// 3. No audit trail for changes
// 4. Internal structure changes break external code
// 5. No way to add validation later without breaking changes
Good Example
// Correct approach: Proper encapsulation
class BankAccount {
private readonly _accountNumber: string;
private _balance: number;
private readonly _transactions: Transaction[] = [];
private _overdraftLimit: number = 0;
private _isLocked: boolean = false;
private _lockReason: string | null = null;
constructor(accountNumber: string, initialBalance: number) {
if (!accountNumber || accountNumber.length < 5) {
throw new Error('Invalid account number');
}
if (initialBalance < 0) {
throw new Error('Initial balance cannot be negative');
}
this._accountNumber = accountNumber;
this._balance = initialBalance;
this._transactions.push(
Transaction.createInitial(initialBalance)
);
}
// Read-only access to account number
get accountNumber(): string {
return this._accountNumber;
}
// Read-only access to balance
get balance(): number {
return this._balance;
}
// Read-only access to lock status
get isLocked(): boolean {
return this._isLocked;
}
// Controlled deposit with validation and audit trail
deposit(amount: number, description: string = 'Deposit'): Transaction {
this.ensureNotLocked();
if (amount <= 0) {
throw new InvalidAmountError('Deposit amount must be positive');
}
this._balance += amount;
const transaction = Transaction.createDeposit(amount, description, this._balance);
this._transactions.push(transaction);
return transaction;
}
// Controlled withdrawal with business rules
withdraw(amount: number, description: string = 'Withdrawal'): Transaction {
this.ensureNotLocked();
if (amount <= 0) {
throw new InvalidAmountError('Withdrawal amount must be positive');
}
const availableBalance = this._balance + this._overdraftLimit;
if (amount > availableBalance) {
throw new InsufficientFundsError(amount, availableBalance);
}
this._balance -= amount;
const transaction = Transaction.createWithdrawal(amount, description, this._balance);
this._transactions.push(transaction);
return transaction;
}
// Transfer with proper validation
transferTo(recipient: BankAccount, amount: number): TransferResult {
this.ensureNotLocked();
recipient.ensureNotLocked();
if (amount <= 0) {
throw new InvalidAmountError('Transfer amount must be positive');
}
const withdrawalTx = this.withdraw(amount, `Transfer to ${recipient.accountNumber}`);
const depositTx = recipient.deposit(amount, `Transfer from ${this._accountNumber}`);
return { withdrawalTx, depositTx };
}
// Controlled overdraft limit modification
setOverdraftLimit(limit: number, authorizedBy: string): void {
if (limit < 0) {
throw new Error('Overdraft limit cannot be negative');
}
if (limit > 10000) {
throw new Error('Overdraft limit exceeds maximum allowed');
}
this._overdraftLimit = limit;
// Audit trail for limit changes
this._transactions.push(
Transaction.createAdministrative(
`Overdraft limit set to ${limit} by ${authorizedBy}`
)
);
}
// Security control with audit
lock(reason: string): void {
this._isLocked = true;
this._lockReason = reason;
this._transactions.push(
Transaction.createAdministrative(`Account locked: ${reason}`)
);
}
unlock(authorizedBy: string): void {
this._isLocked = false;
this._lockReason = null;
this._transactions.push(
Transaction.createAdministrative(`Account unlocked by ${authorizedBy}`)
);
}
// Return copy of transactions, not the internal array
getTransactionHistory(): ReadonlyArray<Transaction> {
return [...this._transactions];
}
// Provide summary without exposing internals
getSummary(): AccountSummary {
const deposits = this._transactions.filter(t => t.type === 'deposit');
const withdrawals = this._transactions.filter(t => t.type === 'withdrawal');
return {
accountNumber: this._accountNumber,
balance: this._balance,
isLocked: this._isLocked,
totalDeposits: deposits.reduce((sum, t) => sum + t.amount, 0),
totalWithdrawals: withdrawals.reduce((sum, t) => sum + t.amount, 0),
transactionCount: this._transactions.length
};
}
// Private helper method
private ensureNotLocked(): void {
if (this._isLocked) {
throw new AccountLockedError(this._lockReason || 'Account is locked');
}
}
}
// Transaction is also encapsulated
class Transaction {
private constructor(
public readonly id: string,
public readonly type: TransactionType,
public readonly amount: number,
public readonly description: string,
public readonly balanceAfter: number,
public readonly timestamp: Date
) {}
static createDeposit(amount: number, description: string, balanceAfter: number): Transaction {
return new Transaction(
generateId(),
'deposit',
amount,
description,
balanceAfter,
new Date()
);
}
static createWithdrawal(amount: number, description: string, balanceAfter: number): Transaction {
return new Transaction(
generateId(),
'withdrawal',
amount,
description,
balanceAfter,
new Date()
);
}
static createInitial(balance: number): Transaction {
return new Transaction(
generateId(),
'initial',
balance,
'Account opened',
balance,
new Date()
);
}
static createAdministrative(description: string): Transaction {
return new Transaction(
generateId(),
'administrative',
0,
description,
0,
new Date()
);
}
}
// Usage - business rules are enforced
const account = new BankAccount('12345', 1000);
// Proper deposit with audit trail
account.deposit(500, 'Paycheck');
// Withdrawal validates funds
try {
account.withdraw(2000); // Will throw InsufficientFundsError
} catch (error) {
console.log('Cannot withdraw more than available');
}
// Cannot manipulate balance directly
// account.balance = 999999; // Error: Property 'balance' is read-only
// Cannot manipulate transactions
// account.getTransactionHistory().push(fakeTx); // Original array unaffected
// Account summary provides what reports need
const summary = account.getSummary();
console.log(`Balance: ${summary.balance}, Transactions: ${summary.transactionCount}`);
Why
-
Data Integrity: Balance can only change through proper deposit/withdraw methods that maintain consistency.
-
Business Rules: All rules (positive amounts, sufficient funds, locked accounts) are enforced in one place.
-
Audit Trail: Every change is recorded. Cannot modify history without proper methods.
-
Flexibility: Internal representation can change without affecting clients. Switch from array to database later.
-
Security: Cannot bypass validation or manipulate internal state directly.
-
Testing: Can verify all edge cases through the public interface. Internal state is controlled.
-
Documentation: The public interface documents what operations are allowed and how to use them.