ð oop-encapsulation
Use when applying encapsulation and information hiding principles in object-oriented design. Use when controlling access to object state and behavior.
Overview
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
// Strong encapsulation with validation
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<>();
}
// Read-only access to account number
public String getAccountNumber() {
return accountNumber;
}
// Read-only access to balance
public BigDecimal getBalance() {
return balance;
}
// Defensive copy for collection
public List<Transaction> getTransactions() {
return Collections.unmodifiableList(transactions);
}
// Controlled mutation with validation
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));
}
// Controlled mutation with business logic
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 # Protected attribute
self.__salary = salary # Private attribute (name mangling)
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 self.__salary
@salary.setter
def salary(self, value: float) -> None:
"""Controlled mutation with validation."""
if value < 0:
raise ValueError("Salary cannot be negative")
if value < self.__salary * 0.9:
raise ValueError("Salary cannot decrease by more than 10%")
self.__salary = value
@property
def performance_rating(self) -> float:
"""Read-only access to performance rating."""
return self.__performance_rating
def update_performance(self, rating: float) -> None:
"""Controlled update with validation and side effects."""
if not 0 <= rating <= 5:
raise ValueError("Rating must be between 0 and 5")
self.__performance_rating = rating
# Business logic: automatic raise for high performers
if rating >= 4.5:
self.__salary *= 1.10
def give_raise(self, percentage: float) -> None:
"""Apply percentage raise with validation."""
if percentage < 0:
raise ValueError("Raise percentage cannot be negative")
if percentage > 20:
raise ValueError("Single raise cannot exceed 20%")
self.__salary *= (1 + percentage / 100)
def __repr__(self) -> str:
return f"Employee(name={self._name}, department={self._department})"
TypeScript Encapsulation
// Class-based encapsulation with private fields
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;
}
// Read-only access
get id(): string {
return this.#id;
}
get username(): string {
return this.#username;
}
get email(): string {
return this.#email;
}
get lastLoginAt(): Date | null {
return this.#lastLoginAt;
}
get isLocked(): boolean {
return this.#isLocked;
}
// Controlled mutation with validation
updateEmail(newEmail: string): void {
if (!this.isValidEmail(newEmail)) {
throw new Error("Invalid email format");
}
this.#email = newEmail;
}
// Business logic encapsulated
attemptLogin(password: string): boolean {
if (this.#isLocked) {
throw new Error("Account is locked");
}
if (this.verifyPassword(password)) {
this.#lastLoginAt = new Date();
this.#failedLoginAttempts = 0;
return true;
}
this.#failedLoginAttempts++;
if (this.#failedLoginAttempts >= 3) {
this.#isLocked = true;
}
return false;
}
// Private helper methods
private isValidEmail(email: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
private verifyPassword(password: string): boolean {
// Hash comparison logic
return true; // Simplified
}
unlock(): void {
this.#isLocked = false;
this.#failedLoginAttempts = 0;
}
}
C# Encapsulation
// Strong encapsulation with properties and backing fields
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)
};
}
// Read-only property
public Guid Id => _id;
// Property with validation
public string Name
{
get => _name;
set
{
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentException("Product name is required");
_name = value;
}
}
// Property with side effects
public decimal Price
{
get => _price;
set
{
if (value <= 0)
throw new ArgumentException("Price must be positive");
if (value != _price)
{
_price = value;
_priceHistory.Add(new PriceHistory(value, DateTime.UtcNow));
}
}
}
public int StockQuantity => _stockQuantity;
// Defensive copy for collection
public IReadOnlyList<PriceHistory> PriceHistory => _priceHistory.AsReadOnly();
// Encapsulated business logic
public bool TryReserveStock(int quantity)
{
if (quantity <= 0)
throw new ArgumentException("Quantity must be positive");
if (_stockQuantity >= quantity)
{
_stockQuantity -= quantity;
return true;
}
return false;
}
public void RestockItems(int quantity)
{
if (quantity <= 0)
throw new ArgumentException("Quantity must be positive");
_stockQuantity += quantity;
}
public decimal GetAveragePrice()
{
return _priceHistory.Average(h => h.Price);
}
}
public record PriceHistory(decimal Price, DateTime ChangedAt);
Data Hiding Patterns
Information Hiding in Java
// Module pattern with package-private implementation
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 interface - what clients need to know
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 implementation - hidden from clients
private void validateOrder(Order order) {
if (!validator.isValid(order)) {
throw new ValidationException("Order validation failed");
}
}
private void reserveInventory(Order order) {
for (OrderItem item : order.getItems()) {
if (!inventory.reserve(item.getProductId(), item.getQuantity())) {
throw new InventoryException("Insufficient inventory");
}
}
}
private void processPayment(Order order) {
PaymentRequest request = createPaymentRequest(order);
PaymentResponse response = payment.charge(request);
if (!response.isSuccessful()) {
throw new PaymentException("Payment processing failed");
}
}
private void releaseInventory(Order order) {
for (OrderItem item : order.getItems()) {
inventory.release(item.getProductId(), item.getQuantity());
}
}
private PaymentRequest createPaymentRequest(Order order) {
return PaymentRequest.builder()
.orderId(order.getId())
.amount(order.getTotalAmount())
.customerId(order.getCustomerId())
.build();
}
}
Closure-Based Encapsulation in TypeScript
// Factory function with closures for private state
function createCounter(initialValue = 0) {
// Private state - not accessible outside
let count = initialValue;
const listeners: Array<(value: number) => void> = [];
// Private functions
function notifyListeners(): void {
listeners.forEach(listener => listener(count));
}
// Public interface
return {
// Read-only access
getValue(): number {
return count;
},
// Controlled mutation
increment(): void {
count++;
notifyListeners();
},
decrement(): void {
count--;
notifyListeners();
},
reset(): void {
count = initialValue;
notifyListeners();
},
// Observer pattern
subscribe(listener: (value: number) => void): () => void {
listeners.push(listener);
// Return unsubscribe function
return () => {
const index = listeners.indexOf(listener);
if (index > -1) {
listeners.splice(index, 1);
}
};
}
};
}
// Usage
const counter = createCounter(10);
const unsubscribe = counter.subscribe(value => console.log(`Count: ${value}`));
counter.increment(); // Logs: Count: 11
counter.increment(); // Logs: Count: 12
unsubscribe();
counter.increment(); // No log
Module Pattern in Python
# Module with private implementation details
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 self.__is_expired(entry):
self.__remove(key)
self.__misses += 1
return None
self.__hits += 1
entry.access_count += 1
return entry.value
def set(self, key: str, value: any, ttl_seconds: int = 3600) -> None:
"""Set value in cache with TTL."""
if len(self.__entries) >= self.__max_size:
self.__evict_least_used()
expires_at = datetime.now() + timedelta(seconds=ttl_seconds)
self.__entries[key] = CacheEntry(value, expires_at)
def delete(self, key: str) -> bool:
"""Remove entry from cache."""
return self.__remove(key)
def clear(self) -> None:
"""Clear all cache entries."""
self.__entries.clear()
self.__hits = 0
self.__misses = 0
def get_stats(self) -> Dict[str, int]:
"""Get cache statistics."""
return {
'size': len(self.__entries),
'hits': self.__hits,
'misses': self.__misses,
'hit_rate': self.__calculate_hit_rate()
}
# Private methods - implementation details
def __is_expired(self, entry: CacheEntry) -> bool:
return datetime.now() > entry.expires_at
def __remove(self, key: str) -> bool:
if key in self.__entries:
del self.__entries[key]
return True
return False
def __evict_least_used(self) -> None:
if not self.__entries:
return
least_used = min(
self.__entries.items(),
key=lambda item: item[1].access_count
)
self.__remove(least_used[0])
def __calculate_hit_rate(self) -> float:
total = self.__hits + self.__misses
return self.__hits / total if total > 0 else 0.0
Access Control Levels
Java Access Modifiers
// Demonstrating all access levels
public class AccessControlExample {
// Private - only within this class
private String secretKey;
// Package-private (default) - within same package
String packageData;
// Protected - within package and subclasses
protected String inheritableData;
// Public - everywhere
public String publicData;
// Private constructor for factory pattern
private AccessControlExample(String key) {
this.secretKey = key;
}
// Public factory method
public static AccessControlExample create(String key) {
return new AccessControlExample(key);
}
// Private helper method
private boolean validateKey(String key) {
return key != null && key.length() >= 10;
}
// Protected method for subclasses
protected void performSecureOperation() {
if (validateKey(secretKey)) {
// Operation logic
}
}
// Public interface method
public String getPublicInfo() {
return "Public information";
}
}
// Nested class for internal implementation
class InternalHelper {
// Package-private - only used within package
static void helperMethod() {
// Implementation
}
}
C# Access Levels
// Comprehensive access control
public class PaymentProcessor
{
// Private field - only within class
private readonly IPaymentGateway _gateway;
// Protected field - class and derived classes
protected readonly ILogger _logger;
// Internal - within same assembly
internal readonly string AssemblyId;
// Protected internal - within assembly OR derived classes
protected internal readonly DateTime CreatedAt;
// Private protected - within class AND derived classes in same assembly
private protected readonly string ProcessorId;
// Public constructor
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 method - external interface
public async Task<PaymentResult> ProcessPaymentAsync(PaymentRequest request)
{
ValidateRequest(request);
return await ExecutePaymentAsync(request);
}
// Protected method - for derived classes
protected virtual void ValidateRequest(PaymentRequest request)
{
if (request == null)
throw new ArgumentNullException(nameof(request));
if (request.Amount <= 0)
throw new ArgumentException("Amount must be positive");
}
// Private method - internal implementation
private async Task<PaymentResult> ExecutePaymentAsync(PaymentRequest request)
{
_logger.LogInformation($"Processing payment: {request.Id}");
try
{
var response = await _gateway.ChargeAsync(request);
return ConvertToResult(response);
}
catch (Exception ex)
{
_logger.LogError(ex, "Payment processing failed");
return PaymentResult.Failed(ex.Message);
}
}
// Internal method - used by assembly
internal void ResetGateway()
{
// Reset logic
}
// Private helper
private static string GenerateProcessorId()
{
return $"PROC-{Guid.NewGuid():N}";
}
// Private conversion
private PaymentResult ConvertToResult(GatewayResponse response)
{
return response.Success
? PaymentResult.Succeeded(response.TransactionId)
: PaymentResult.Failed(response.ErrorMessage);
}
}
Immutability and Encapsulation
Immutable Objects in Java
// Immutable class - encapsulation through immutability
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);
}
// All getters return copies or immutable values
public BigDecimal getAmount() {
return amount;
}
public Currency getCurrency() {
return currency;
}
// Operations return new instances
public Money add(Money other) {
if (!currency.equals(other.currency)) {
throw new IllegalArgumentException("Currency mismatch");
}
return new Money(amount.add(other.amount), currency);
}
public Money subtract(Money other) {
if (!currency.equals(other.currency)) {
throw new IllegalArgumentException("Currency mismatch");
}
return new Money(amount.subtract(other.amount), currency);
}
public Money multiply(BigDecimal factor) {
return new Money(amount.multiply(factor), currency);
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof Money)) return false;
Money other = (Money) obj;
return amount.equals(other.amount) && currency.equals(other.currency);
}
@Override
public int hashCode() {
return Objects.hash(amount, currency);
}
@Override
public String toString() {
return String.format("%s %s", 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
- Effective Java by Joshua Bloch (Encapsulation chapters)
- Clean Code by Robert Martin (Objects and Data Structures)
- Design Patterns: Elements of Reusable Object-Oriented Software
- Python Data Model: https://docs.python.org/3/reference/datamodel.html
- C# Properties: https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/properties
- Java Access Control: https://docs.oracle.com/javase/tutorial/java/javaOO/accesscontrol.html
- TypeScript Private Fields: https://www.typescriptlang.org/docs/handbook/2/classes.html#private