| name | oop-encapsulation |
| user-invocable | false |
| description | Use when applying encapsulation and information hiding principles in object-oriented design. Use when controlling access to object state and behavior. |
| allowed-tools | ["Bash","Read"] |
OOP Encapsulation
Master encapsulation and information hiding to create robust, maintainable object-oriented systems. This skill focuses on controlling access to object internals and exposing well-defined interfaces.
Understanding Encapsulation
Encapsulation is the bundling of data and methods that operate on that data within a single unit, while restricting direct access to some of the object's components. This principle protects object integrity and reduces coupling.
Java Encapsulation
public class BankAccount {
private String accountNumber;
private BigDecimal balance;
private final List<Transaction> transactions;
public BankAccount(String accountNumber, BigDecimal initialBalance) {
if (accountNumber == null || accountNumber.isEmpty()) {
throw new IllegalArgumentException("Account number required");
}
if (initialBalance.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("Initial balance cannot be negative");
}
this.accountNumber = accountNumber;
this.balance = initialBalance;
this.transactions = new ArrayList<>();
}
public String getAccountNumber() {
return accountNumber;
}
public BigDecimal getBalance() {
return balance;
}
public List<Transaction> getTransactions() {
return Collections.unmodifiableList(transactions);
}
public void deposit(BigDecimal amount) {
if (amount.compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException("Deposit amount must be positive");
}
balance = balance.add(amount);
transactions.add(new Transaction(TransactionType.DEPOSIT, amount));
}
public void withdraw(BigDecimal amount) {
if (amount.compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException("Withdrawal amount must be positive");
}
if (amount.compareTo(balance) > 0) {
throw new InsufficientFundsException("Insufficient balance");
}
balance = balance.subtract(amount);
transactions.add(new Transaction(TransactionType.WITHDRAWAL, amount));
}
}
Python Encapsulation
class Employee:
"""Employee with encapsulated salary information."""
def __init__(self, name: str, salary: float, department: str):
if not name:
raise ValueError("Name is required")
if salary < 0:
raise ValueError("Salary cannot be negative")
self._name = name
self.__salary = salary
self._department = department
self.__performance_rating = 0.0
@property
def name(self) -> str:
"""Read-only access to name."""
return self._name
@property
def department(self) -> str:
"""Read-only access to department."""
return self._department
@property
def salary(self) -> float:
"""Controlled access to salary."""
return .__salary
() -> :
value < :
ValueError()
value < .__salary * :
ValueError()
.__salary = value
() -> :
.__performance_rating
() -> :
<= rating <= :
ValueError()
.__performance_rating = rating
rating >= :
.__salary *=
() -> :
percentage < :
ValueError()
percentage > :
ValueError()
.__salary *= ( + percentage / )
() -> :
TypeScript Encapsulation
class UserAccount {
readonly #id: string;
#username: string;
#email: string;
#passwordHash: string;
#lastLoginAt: Date | null = null;
#failedLoginAttempts = 0;
#isLocked = false;
constructor(username: string, email: string, passwordHash: string) {
if (!username || username.length < 3) {
throw new Error("Username must be at least 3 characters");
}
if (!this.isValidEmail(email)) {
throw new Error("Invalid email format");
}
this.#id = crypto.randomUUID();
this.#username = username;
this.#email = email;
this.#passwordHash = passwordHash;
}
get id(): string {
return .#id;
}
(): {
.#username;
}
(): {
.#email;
}
(): | {
.#lastLoginAt;
}
(): {
.#isLocked;
}
(: ): {
(!.(newEmail)) {
();
}
.#email = newEmail;
}
(: ): {
(.#isLocked) {
();
}
(.(password)) {
.#lastLoginAt = ();
.#failedLoginAttempts = ;
;
}
.#failedLoginAttempts++;
(.#failedLoginAttempts >= ) {
.#isLocked = ;
}
;
}
(: ): {
.(email);
}
(: ): {
;
}
(): {
.#isLocked = ;
.#failedLoginAttempts = ;
}
}
C# Encapsulation
public class Product
{
private readonly Guid _id;
private string _name;
private decimal _price;
private int _stockQuantity;
private readonly List<PriceHistory> _priceHistory;
public Product(string name, decimal price, int initialStock)
{
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException("Product name is required", nameof(name));
if (price <= 0)
throw new ArgumentException("Price must be positive", nameof(price));
if (initialStock < 0)
throw new ArgumentException("Stock cannot be negative", nameof(initialStock));
_id = Guid.NewGuid();
_name = name;
_price = price;
_stockQuantity = initialStock;
_priceHistory = new List<PriceHistory>
{
new PriceHistory(price, DateTime.UtcNow)
};
}
public Guid Id => _id;
public Name
{
=> _name;
{
(.IsNullOrWhiteSpace())
ArgumentException();
_name = ;
}
}
Price
{
=> _price;
{
( <= )
ArgumentException();
( != _price)
{
_price = ;
_priceHistory.Add( PriceHistory(, DateTime.UtcNow));
}
}
}
StockQuantity => _stockQuantity;
IReadOnlyList<PriceHistory> PriceHistory => _priceHistory.AsReadOnly();
{
(quantity <= )
ArgumentException();
(_stockQuantity >= quantity)
{
_stockQuantity -= quantity;
;
}
;
}
{
(quantity <= )
ArgumentException();
_stockQuantity += quantity;
}
{
_priceHistory.Average(h => h.Price);
}
}
;
Data Hiding Patterns
Information Hiding in Java
public class OrderProcessor {
private final OrderValidator validator;
private final InventoryService inventory;
private final PaymentGateway payment;
public OrderProcessor(
OrderValidator validator,
InventoryService inventory,
PaymentGateway payment
) {
this.validator = validator;
this.inventory = inventory;
this.payment = payment;
}
public OrderResult processOrder(Order order) {
try {
validateOrder(order);
reserveInventory(order);
processPayment(order);
return OrderResult.success(order.getId());
} catch (ValidationException e) {
return OrderResult.validationError(e.getMessage());
} catch (InventoryException e) {
return OrderResult.inventoryError(e.getMessage());
} catch (PaymentException e) {
releaseInventory(order);
return OrderResult.paymentError(e.getMessage());
}
}
private void validateOrder(Order order) {
if (!validator.isValid(order)) {
throw new ValidationException("Order validation failed");
}
}
{
(OrderItem item : order.getItems()) {
(!inventory.reserve(item.getProductId(), item.getQuantity())) {
();
}
}
}
{
createPaymentRequest(order);
payment.charge(request);
(!response.isSuccessful()) {
();
}
}
{
(OrderItem item : order.getItems()) {
inventory.release(item.getProductId(), item.getQuantity());
}
}
PaymentRequest {
PaymentRequest.builder()
.orderId(order.getId())
.amount(order.getTotalAmount())
.customerId(order.getCustomerId())
.build();
}
}
Closure-Based Encapsulation in TypeScript
function createCounter(initialValue = 0) {
let count = initialValue;
const listeners: Array<(value: number) => void> = [];
function notifyListeners(): void {
listeners.forEach(listener => listener(count));
}
return {
getValue(): number {
return count;
},
increment(): void {
count++;
notifyListeners();
},
decrement(): void {
count--;
notifyListeners();
},
reset(): void {
count = initialValue;
notifyListeners();
},
subscribe(listener: (value: number) => void): {
listeners.(listener);
{
index = listeners.(listener);
(index > -) {
listeners.(index, );
}
};
}
};
}
counter = ();
unsubscribe = counter.( .());
counter.();
counter.();
();
counter.();
Module Pattern in Python
from typing import Dict, List, Optional
from dataclasses import dataclass
from datetime import datetime
@dataclass
class CacheEntry:
"""Internal representation - not exported."""
value: any
expires_at: datetime
access_count: int = 0
class Cache:
"""Public cache interface."""
def __init__(self, max_size: int = 100):
self.__entries: Dict[str, CacheEntry] = {}
self.__max_size = max_size
self.__hits = 0
self.__misses = 0
def get(self, key: str) -> Optional[any]:
"""Get value from cache."""
entry = self.__entries.get(key)
if entry is None:
self.__misses += 1
return None
if .__is_expired(entry):
.__remove(key)
.__misses +=
.__hits +=
entry.access_count +=
entry.value
() -> :
(.__entries) >= .__max_size:
.__evict_least_used()
expires_at = datetime.now() + timedelta(seconds=ttl_seconds)
.__entries[key] = CacheEntry(value, expires_at)
() -> :
.__remove(key)
() -> :
.__entries.clear()
.__hits =
.__misses =
() -> [, ]:
{
: (.__entries),
: .__hits,
: .__misses,
: .__calculate_hit_rate()
}
() -> :
datetime.now() > entry.expires_at
() -> :
key .__entries:
.__entries[key]
() -> :
.__entries:
least_used = (
.__entries.items(),
key= item: item[].access_count
)
.__remove(least_used[])
() -> :
total = .__hits + .__misses
.__hits / total total >
Access Control Levels
Java Access Modifiers
public class AccessControlExample {
private String secretKey;
String packageData;
protected String inheritableData;
public String publicData;
private AccessControlExample(String key) {
this.secretKey = key;
}
public static AccessControlExample create(String key) {
return new AccessControlExample(key);
}
private boolean validateKey(String key) {
return key != null && key.length() >= 10;
}
protected void performSecureOperation() {
if (validateKey(secretKey)) {
}
}
public String getPublicInfo() {
return ;
}
}
{
{
}
}
C# Access Levels
public class PaymentProcessor
{
private readonly IPaymentGateway _gateway;
protected readonly ILogger _logger;
internal readonly string AssemblyId;
protected internal readonly DateTime CreatedAt;
private protected readonly string ProcessorId;
public PaymentProcessor(IPaymentGateway gateway, ILogger logger)
{
_gateway = gateway ?? throw new ArgumentNullException(nameof(gateway));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
AssemblyId = Guid.NewGuid().ToString();
CreatedAt = DateTime.UtcNow;
ProcessorId = GenerateProcessorId();
}
public async Task<PaymentResult> ProcessPaymentAsync(PaymentRequest request)
{
ValidateRequest(request);
return await ExecutePaymentAsync(request);
}
{
(request == )
ArgumentNullException((request));
(request.Amount <= )
ArgumentException();
}
{
_logger.LogInformation();
{
response = _gateway.ChargeAsync(request);
ConvertToResult(response);
}
(Exception ex)
{
_logger.LogError(ex, );
PaymentResult.Failed(ex.Message);
}
}
{
}
{
;
}
{
response.Success
? PaymentResult.Succeeded(response.TransactionId)
: PaymentResult.Failed(response.ErrorMessage);
}
}
Immutability and Encapsulation
Immutable Objects in Java
public final class Money {
private final BigDecimal amount;
private final Currency currency;
private Money(BigDecimal amount, Currency currency) {
this.amount = amount;
this.currency = currency;
}
public static Money of(BigDecimal amount, Currency currency) {
Objects.requireNonNull(amount, "Amount required");
Objects.requireNonNull(currency, "Currency required");
return new Money(amount, currency);
}
public static Money zero(Currency currency) {
return new Money(BigDecimal.ZERO, currency);
}
public BigDecimal getAmount() {
return amount;
}
public Currency getCurrency() {
return currency;
}
public Money add(Money other) {
if (!currency.equals(other.currency)) {
throw ();
}
(amount.add(other.amount), currency);
}
Money {
(!currency.equals(other.currency)) {
();
}
(amount.subtract(other.amount), currency);
}
Money {
(amount.multiply(factor), currency);
}
{
( == obj) ;
(!(obj Money)) ;
(Money) obj;
amount.equals(other.amount) && currency.equals(other.currency);
}
{
Objects.hash(amount, currency);
}
String {
String.format(, currency.getSymbol(), amount);
}
}
When to Use This Skill
Apply encapsulation principles when:
- Designing classes and modules with internal state
- Creating domain objects with business rules
- Building APIs and public interfaces
- Protecting object invariants
- Hiding implementation details
- Preventing invalid state transitions
- Managing complex internal structures
- Implementing data validation
- Creating defensive copies of mutable objects
- Controlling access to sensitive data
- Implementing access control policies
- Building frameworks and libraries
- Refactoring procedural code to OOP
- Designing immutable value objects
- Creating thread-safe classes
Best Practices
- Make fields private by default, expose through methods
- Use the principle of least privilege for access levels
- Validate all inputs in public methods
- Return defensive copies of mutable internal objects
- Make classes immutable when possible
- Use final/readonly for fields that don't change
- Encapsulate collections, never expose them directly
- Keep implementation details private
- Use properties/getters for controlled access
- Implement validation in setters/mutators
- Group related data and behavior together
- Hide complexity behind simple interfaces
- Use package-private/internal for implementation classes
- Avoid getter/setter pairs for every field
- Design for change by hiding what might vary
Common Pitfalls
- Creating getter/setter for every field (JavaBeans antipattern)
- Exposing mutable internal collections directly
- Making fields public "for convenience"
- Returning references to mutable internal objects
- Using protected fields instead of protected methods
- Overusing inheritance to access protected members
- Ignoring validation in constructors
- Allowing objects to be created in invalid states
- Mixing business logic in getters/setters
- Using static mutable state
- Forgetting to make defensive copies
- Exposing implementation details through exceptions
- Not considering thread safety for mutable state
- Breaking encapsulation with friend classes
- Using reflection to access private members
Resources