encapsulation

encapsulation

Use when exposing internal state. Use when making fields public for convenience. Use when external code modifies object internals.

4stars
0forks
Updated 1/23/2026
SKILL.md
readonlyread-only
name
encapsulation
description

Use when exposing internal state. Use when making fields public for convenience. Use when external code modifies object internals.

Encapsulation

Overview

Hide internal state. Expose behavior, not data. Control access through methods.

Public fields let anyone modify your object's internals, bypassing validation and breaking invariants. Encapsulation protects data integrity.

When to Use

  • Designing classes with state
  • Tempted to make fields public
  • External code directly modifies object state
  • Invariants can be violated by direct access

The Iron Rule

NEVER expose internal state directly. Always use methods to control access.

No exceptions:

  • Not for "it's simpler"
  • Not for "we trust callers"
  • Not for "it's just data"
  • Not for "getters/setters are verbose"

Detection: Exposed State Smell

If internal state is directly accessible, STOP:

// ❌ VIOLATION: Public state
class BankAccount {
  public balance: number = 0;  // Anyone can modify!
  public transactions: Transaction[] = [];
}

// Callers can break invariants
const account = new BankAccount();
account.balance = -1000000;  // Negative balance!
account.transactions = [];   // Audit trail destroyed!

Problems:

  • No validation on changes
  • Invariants easily violated
  • No audit trail
  • Can't change internal representation

The Correct Pattern: Private State, Public Methods

// ✅ CORRECT: Encapsulated state
class BankAccount {
  private _balance: number = 0;
  private _transactions: Transaction[] = [];
  
  get balance(): number {
    return this._balance;
  }
  
  deposit(amount: number): void {
    if (amount <= 0) {
      throw new Error('Deposit must be positive');
    }
    
    this._balance += amount;
    this._transactions.push({
      type: 'deposit',
      amount,
      timestamp: new Date()
    });
  }
  
  withdraw(amount: number): void {
    if (amount <= 0) {
      throw new Error('Withdrawal must be positive');
    }
    if (amount > this._balance) {
      throw new Error('Insufficient funds');
    }
    
    this._balance -= amount;
    this._transactions.push({
      type: 'withdrawal',
      amount,
      timestamp: new Date()
    });
  }
  
  getTransactionHistory(): ReadonlyArray<Transaction> {
    return [...this._transactions];  // Return copy
  }
}

// Now invariants are protected
const account = new BankAccount();
account.deposit(100);     // ✅ Validated, logged
account.withdraw(50);     // ✅ Validated, logged
account.balance = -1000;  // ❌ Error: Cannot set

Encapsulation Techniques

1. Private Fields

class User {
  private _password: string;
  
  setPassword(newPassword: string): void {
    if (newPassword.length < 8) throw new Error('Too short');
    this._password = hash(newPassword);
  }
  
  checkPassword(attempt: string): boolean {
    return verify(attempt, this._password);
  }
}

2. Readonly for Read-Only Access

class Config {
  readonly apiUrl: string;
  readonly timeout: number;
  
  constructor(apiUrl: string, timeout: number) {
    this.apiUrl = apiUrl;
    this.timeout = timeout;
  }
}

3. Return Copies, Not References

class Order {
  private _items: OrderItem[] = [];
  
  // ❌ BAD: Returns reference
  getItems(): OrderItem[] {
    return this._items;  // Caller can modify!
  }
  
  // ✅ GOOD: Returns copy
  getItems(): OrderItem[] {
    return [...this._items];
  }
  
  // ✅ ALSO GOOD: Return readonly
  getItems(): ReadonlyArray<OrderItem> {
    return this._items;
  }
}

4. Validate in Setters

class Product {
  private _price: number = 0;
  
  get price(): number {
    return this._price;
  }
  
  set price(value: number) {
    if (value < 0) throw new Error('Price cannot be negative');
    if (value > 1000000) throw new Error('Price too high');
    this._price = value;
  }
}

Pressure Resistance Protocol

1. "It's Simpler"

Pressure: "Public fields are less code"

Response: Less code now, more bugs later. Encapsulation prevents invalid states.

Action: Private fields + methods. The extra code is validation.

2. "We Trust Callers"

Pressure: "Our team won't misuse public fields"

Response: Teams grow. Code evolves. Mistakes happen. Protect invariants in code.

Action: Don't rely on caller discipline. Enforce in class.

3. "It's Just Data"

Pressure: "This class is just a data container"

Response: Even data has rules. Emails have formats. Ages have ranges.

Action: Use DTOs/interfaces for pure data. Classes = behavior + encapsulation.

4. "Getters/Setters Are Verbose"

Pressure: "Java-style getters/setters are boilerplate"

Response: TypeScript has concise get/set syntax. Use it.

Action: get balance() is not verbose.

Red Flags - STOP and Reconsider

  • public keyword on mutable fields
  • Direct property assignment from outside
  • Arrays/objects returned by reference
  • No validation on state changes
  • Invariants only documented, not enforced

All of these mean: Encapsulate the state.

Quick Reference

Exposed (Bad) Encapsulated (Good)
public balance private _balance + deposit()/withdraw()
return this.items return [...this.items]
Direct mutation Method with validation
Trust callers Enforce in class

Common Rationalizations (All Invalid)

Excuse Reality
"Simpler" Simpler to write, harder to maintain.
"We trust callers" Code should enforce, not trust.
"Just data" Data has constraints. Enforce them.
"Verbose" TypeScript getters are concise.
"Over-engineering" It's just engineering.

The Bottom Line

Private state. Public methods. Validate on every change.

Never expose internal state directly. Return copies of collections. Validate in setters. Encapsulation protects invariants and enables safe evolution.

You Might Also Like

Related Skills

coding-agent

coding-agent

179Kdev-codegen

Run Codex CLI, Claude Code, OpenCode, or Pi Coding Agent via background process for programmatic control.

openclaw avataropenclaw
Get
add-uint-support

add-uint-support

97Kdev-codegen

Add unsigned integer (uint) type support to PyTorch operators by updating AT_DISPATCH macros. Use when adding support for uint16, uint32, uint64 types to operators, kernels, or when user mentions enabling unsigned types, barebones unsigned types, or uint support.

pytorch avatarpytorch
Get
at-dispatch-v2

at-dispatch-v2

97Kdev-codegen

Convert PyTorch AT_DISPATCH macros to AT_DISPATCH_V2 format in ATen C++ code. Use when porting AT_DISPATCH_ALL_TYPES_AND*, AT_DISPATCH_FLOATING_TYPES*, or other dispatch macros to the new v2 API. For ATen kernel files, CUDA kernels, and native operator implementations.

pytorch avatarpytorch
Get
skill-writer

skill-writer

97Kdev-codegen

Guide users through creating Agent Skills for Claude Code. Use when the user wants to create, write, author, or design a new Skill, or needs help with SKILL.md files, frontmatter, or skill structure.

pytorch avatarpytorch
Get

Implements JavaScript classes in C++ using JavaScriptCore. Use when creating new JS classes with C++ bindings, prototypes, or constructors.

oven-sh avataroven-sh
Get

Creates JavaScript classes using Bun's Zig bindings generator (.classes.ts). Use when implementing new JS APIs in Zig with JSC integration.

oven-sh avataroven-sh
Get