Added AI skills
This commit is contained in:
335
.agents/skills/clean-code-principles/rules/core-encapsulation.md
Normal file
335
.agents/skills/clean-code-principles/rules/core-encapsulation.md
Normal file
@@ -0,0 +1,335 @@
|
||||
---
|
||||
id: core-encapsulation
|
||||
title: Encapsulation
|
||||
category: core-principles
|
||||
priority: critical
|
||||
tags: [encapsulation, information-hiding, data-protection]
|
||||
related: [solid-srp-class, core-law-demeter, solid-isp-interfaces]
|
||||
---
|
||||
|
||||
# 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
|
||||
|
||||
```typescript
|
||||
// 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
|
||||
|
||||
```typescript
|
||||
// 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
|
||||
|
||||
1. **Data Integrity**: Balance can only change through proper deposit/withdraw methods that maintain consistency.
|
||||
|
||||
2. **Business Rules**: All rules (positive amounts, sufficient funds, locked accounts) are enforced in one place.
|
||||
|
||||
3. **Audit Trail**: Every change is recorded. Cannot modify history without proper methods.
|
||||
|
||||
4. **Flexibility**: Internal representation can change without affecting clients. Switch from array to database later.
|
||||
|
||||
5. **Security**: Cannot bypass validation or manipulate internal state directly.
|
||||
|
||||
6. **Testing**: Can verify all edge cases through the public interface. Internal state is controlled.
|
||||
|
||||
7. **Documentation**: The public interface documents what operations are allowed and how to use them.
|
||||
Reference in New Issue
Block a user