OOP Encapsulation Fundamentals: Information Hiding & Visibility
Encapsulation is one of the four fundamental principles of object-oriented programming. It hides the internal states and implementation details of an object and provides only well-defined interfaces to the outside world.
What is Encapsulation?
Encapsulation bundles data and behavior into a single unit and protects the internal representation from unauthorized access. The goal is robust, maintainable, and secure software through clear responsibility boundaries.
Core Principles of Encapsulation
- Information Hiding: Internal details are hidden
- Interface Control: Only defined accesses are possible
- Invariant Preservation: Object state remains consistent
- Coupling Reduction: Fewer dependencies between components
Visibility Modifiers
Java Visibility Levels
public class VisibilityDemo {
// public: accessible from everywhere
public String publicField = "öffentlich";
// protected: within the class and subclasses
protected String protectedField = "geschützt";
// package-private: only within the package
String packageField = "paket-privat";
// private: only within the class
private String privateField = "privat";
// Private method - internal logic
private void validateInput(String input) {
if (input == null || input.trim().isEmpty()) {
throw new IllegalArgumentException("Input darf nicht leer sein");
}
}
// Public method with validation
public void processData(String data) {
validateInput(data); // Use private method
// Verarbeitung...
}
}
C# Access Modifiers
public class BankAccount
{
// public: accessible from everywhere
public string AccountNumber { get; }
// private: only within the class
private decimal balance;
// protected: within the class and derived classes
protected string AccountType { get; set; }
// internal: only within the assembly
internal string BankCode { get; set; }
// protected internal: within assembly or derived classes
protected internal string BranchCode { get; set; }
// Public property with private setter
public decimal Balance
{
get { return balance; }
private set { balance = value; }
}
}
Python Access Control
class BankAccount:
def __init__(self, account_number: str):
# Public attribute
self.account_number = account_number
# Protected attribute (convention)
self._balance = 0.0
# Private attribute (name mangling)
self.__transaction_history = []
def deposit(self, amount: float):
"""Public method with validation"""
if amount <= 0:
raise ValueError("Amount must be positive")
self._balance += amount
self.__add_transaction("deposit", amount)
def _validate_amount(self, amount: float):
"""Protected method for subclasses"""
return amount > 0
def __add_transaction(self, transaction_type: str, amount: float):
"""Private method - internal use only"""
self.__transaction_history.append({
'type': transaction_type,
'amount': amount,
'timestamp': datetime.now()
})
Getters and Setters
Meaningful Getter/Setter Implementation
public class BankAccount {
private String iban;
private int balanceInCents;
private boolean isActive = true;
// Constructor with validation
public BankAccount(String iban) {
if (iban == null || !isValidIban(iban)) {
throw new IllegalArgumentException("Invalid IBAN");
}
this.iban = iban;
this.balanceInCents = 0;
}
// Getter for read access
public int getBalanceInCents() {
return balanceInCents;
}
// Getter with formatting
public String getFormattedBalance() {
return String.format("€%.2f", balanceInCents / 100.0);
}
// Setter with validation and business logic
public void setBalanceInCents(int balanceInCents) {
if (!isActive) {
throw new IllegalStateException("Account is deactivated");
}
if (balanceInCents < 0) {
throw new IllegalArgumentException("Negative account balance not allowed");
}
this.balanceInCents = balanceInCents;
}
// Business method instead of simple setter
public void deposit(int cents) {
if (cents <= 0) {
throw new IllegalArgumentException("Amount must be positive");
}
this.balanceInCents += cents;
}
public boolean withdraw(int cents) {
if (cents <= 0) {
throw new IllegalArgumentException("Amount must be positive");
}
if (cents > balanceInCents) {
return false; // Insufficient balance
}
this.balanceInCents -= cents;
return true;
}
// Private validation method
private boolean isValidIban(String iban) {
// IBAN validation logic
return iban != null && iban.matches("[A-Z]{2}[0-9]{20}");
}
}
Python Properties
class BankAccount:
def __init__(self, iban: str):
self._iban = iban
self._balance = 0.0
@property
def balance(self) -> float:
"""Getter for account balance"""
return self._balance
@property
def iban(self) -> str:
"""Read-only property for IBAN"""
return self._iban
@balance.setter
def balance(self, value: float):
"""Setter with validation"""
if value < 0:
raise ValueError("Account balance cannot be negative")
self._balance = value
@property
def formatted_balance(self) -> str:
"""Computed property"""
return f"€{self._balance:.2f}"
def deposit(self, amount: float):
"""Business method instead of direct setter"""
if amount <= 0:
raise ValueError("Amount must be positive")
self._balance += amount
Immutability
Immutable Objects in Java
// Immutable class with final fields
public final class ImmutablePerson {
private final String name;
private final int age;
private final List<String> hobbies; // Defensive copy!
public ImmutablePerson(String name, int age, List<String> hobbies) {
this.name = Objects.requireNonNull(name, "Name cannot be null");
this.age = age;
// Defensive copy for mutable parameters
this.hobbies = List.copyOf(Objects.requireNonNull(hobbies));
}
// Getters - no setters!
public String getName() {
return name;
}
public int getAge() {
return age;
}
// Defensive copy for mutable returns
public List<String> getHobbies() {
return new ArrayList<>(hobbies);
}
// Methods create new instances
public ImmutablePerson withAge(int newAge) {
return new ImmutablePerson(this.name, newAge, this.hobbies);
}
public ImmutablePerson addHobby(String hobby) {
List<String> newHobbies = new ArrayList<>(this.hobbies);
newHobbies.add(hobby);
return new ImmutablePerson(this.name, this.age, newHobbies);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ImmutablePerson that = (ImmutablePerson) o;
return age == that.age &&
Objects.equals(name, that.name) &&
Objects.equals(hobbies, that.hobbies);
}
@Override
public int hashCode() {
return Objects.hash(name, age, hobbies);
}
@Override
public String toString() {
return "ImmutablePerson{name='" + name + "', age=" + age + ", hobbies=" + hobbies + "}";
}
}
Python Dataclasses for Immutability
from dataclasses import dataclass
from typing import List
import copy
@dataclass(frozen=True)
class ImmutablePerson:
name: str
age: int
hobbies: List[str] # Warning: List is still mutable!
def __post_init__(self):
# Validation after initialization
if self.age < 0:
raise ValueError("Age cannot be negative")
if not self.name:
raise ValueError("Name cannot be empty")
# Defensive copy for mutable fields
object.__setattr__(self, 'hobbies', tuple(self.hobbies))
def with_age(self, new_age: int) -> 'ImmutablePerson':
"""Creates new instance with changed age"""
return ImmutablePerson(self.name, new_age, list(self.hobbies))
def add_hobby(self, hobby: str) -> 'ImmutablePerson':
"""Creates new instance with additional hobby"""
new_hobbies = list(self.hobbies) + [hobby]
return ImmutablePerson(self.name, self.age, new_hobbies)
Defensive Copies
Protection Against External Mutation
public class ShoppingCart {
private final List<Item> items = new ArrayList<>();
private final Customer customer;
public ShoppingCart(Customer customer) {
this.customer = Objects.requireNonNull(customer);
}
// Defensive copy on return
public List<Item> getItems() {
return new ArrayList<>(items); // Return copy
}
// Defensive copy on parameter
public void addItems(List<Item> newItems) {
if (newItems != null) {
this.items.addAll(new ArrayList<>(newItems)); // Store copy
}
}
// Unmodifiable view
public List<Item> getItemsUnmodifiable() {
return Collections.unmodifiableList(items);
}
// Stream API for safe access
public Stream<Item> itemStream() {
return items.stream();
}
}
Python Defensive Copies
class ShoppingCart:
def __init__(self, customer):
self._customer = customer
self._items = []
def get_items(self):
"""Return defensive copy"""
return self._items.copy()
def add_items(self, items):
"""Store defensive copy"""
if items:
self._items.extend(items.copy())
def get_items_immutable(self):
"""Return unmodifiable view"""
return tuple(self._items)
def items_iterator(self):
"""Iterator for safe access"""
return iter(self._items)
Law of Demeter
Avoiding Cascading Calls
// Bad - Violates Law of Demeter
public void processOrder(Order order) {
// Too many dots - tight coupling
String city = order.getCustomer().getAddress().getCity();
double tax = order.getCustomer().getAddress().getTaxRate();
// ...
}
// Good - Decoupled
public void processOrder(Order order) {
String city = order.getCustomerCity();
double tax = order.getCustomerTaxRate();
// ...
}
// Better implementation
public class Order {
private Customer customer;
public String getCustomerCity() {
return customer.getAddress().getCity();
}
public double getCustomerTaxRate() {
return customer.getAddress().getTaxRate();
}
}
Validation and Invariants
Robust Validation Strategy
public class EmailAddress {
private final String value;
private static final Pattern EMAIL_PATTERN =
Pattern.compile("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+$");
public EmailAddress(String email) {
String normalized = normalize(email);
validate(normalized);
this.value = normalized;
}
private String normalize(String email) {
if (email == null) {
throw new IllegalArgumentException("Email cannot be null");
}
return email.trim().toLowerCase();
}
private void validate(String email) {
if (email.isEmpty()) {
throw new IllegalArgumentException("Email cannot be empty");
}
if (!EMAIL_PATTERN.matcher(email).matches()) {
throw new IllegalArgumentException("Invalid email format");
}
if (email.length() > 254) {
throw new IllegalArgumentException("Email too long");
}
}
public String getValue() {
return value;
}
@Override
public String toString() {
return value;
}
}
Design by Contract
Pre- and Postconditions
public class BankTransfer {
private final BankAccount fromAccount;
private final BankAccount toAccount;
public BankTransfer(BankAccount fromAccount, BankAccount toAccount) {
this.fromAccount = Objects.requireNonNull(fromAccount);
this.toAccount = Objects.requireNonNull(toAccount);
}
/**
* Transfers an amount from one account to another
*
* @param amount Transfer amount in cents
* @throws IllegalArgumentException if amount <= 0
* @throws IllegalStateException if source account is insufficiently funded
* @throws IllegalStateException if one of the accounts is disabled
* @post fromAccount.getBalance() == old(fromAccount.getBalance()) - amount
* @post toAccount.getBalance() == old(toAccount.getBalance()) + amount
*/
public void transfer(int amount) {
// Check preconditions
if (amount <= 0) {
throw new IllegalArgumentException("Amount must be positive");
}
if (!fromAccount.isActive() || !toAccount.isActive()) {
throw new IllegalStateException("Both accounts must be active");
}
if (fromAccount.getBalanceInCents() < amount) {
throw new IllegalStateException("Insufficient balance on source account");
}
// Store old states for postconditions
int oldFromBalance = fromAccount.getBalanceInCents();
int oldToBalance = toAccount.getBalanceInCents();
try {
// Atomic operation
fromAccount.withdraw(amount);
toAccount.deposit(amount);
// Verify postconditions
assert fromAccount.getBalanceInCents() == oldFromBalance - amount;
assert toAccount.getBalanceInCents() == oldToBalance + amount;
} catch (Exception e) {
// Rollback on error
throw new RuntimeException("Transfer failed", e);
}
}
}
Encapsulation in Practice
Example: E-Commerce Order System
public class Order {
private final String orderId;
private final Customer customer;
private final List<OrderItem> items;
private OrderStatus status;
private final LocalDateTime createdAt;
private LocalDateTime shippedAt;
// Private constructor - use factory method
private Order(String orderId, Customer customer) {
this.orderId = Objects.requireNonNull(orderId);
this.customer = Objects.requireNonNull(customer);
this.items = new ArrayList<>();
this.status = OrderStatus.PENDING;
this.createdAt = LocalDateTime.now();
}
// Factory method for object creation
public static Order create(Customer customer) {
if (customer == null || !customer.isActive()) {
throw new IllegalArgumentException("Invalid customer");
}
String orderId = generateOrderId();
return new Order(orderId, customer);
}
// Business method with status transitions
public void addItem(Product product, int quantity) {
if (product == null) {
throw new IllegalArgumentException("Product cannot be null");
}
if (quantity <= 0) {
throw new IllegalArgumentException("Quantity must be positive");
}
if (status != OrderStatus.PENDING) {
throw new IllegalStateException("Order cannot be modified anymore");
}
OrderItem item = new OrderItem(product, quantity);
items.add(item);
}
// State transition with validation
public void ship() {
if (status != OrderStatus.CONFIRMED) {
throw new IllegalStateException("Order must be confirmed");
}
if (items.isEmpty()) {
throw new IllegalStateException("Order is empty");
}
this.status = OrderStatus.SHIPPED;
this.shippedAt = LocalDateTime.now();
// Publish event
EventPublisher.publish(new OrderShippedEvent(orderId));
}
// Safe getters with defensive copies
public List<OrderItem> getItems() {
return new ArrayList<>(items);
}
public Customer getCustomer() {
return customer; // Immutable, no copy needed
}
// Calculated property
public BigDecimal getTotalAmount() {
return items.stream()
.map(OrderItem::getTotalPrice)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
// Status getter - immutable
public OrderStatus getStatus() {
return status;
}
// Private helper method
private static String generateOrderId() {
return "ORD-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase();
}
}
Advantages of Encapsulation
1. Reduced Coupling
- Components are less dependent on each other
- Changes have fewer effects
- Easier refactoring possible
2. Higher Cohesion
- Related functionality is bundled together
- Clear responsibilities
- Better understandability
3. Improved Security
- Controlled access to data
- Validation at boundaries
- Protection against inconsistent states
4. Better Testability
- Clear interfaces for unit tests
- Easier mock objects possible
- Focus on behavior instead of implementation
Disadvantages and Challenges
1. Additional Effort
- More code for getters/setters
- Boilerplate for simple data classes
- Higher implementation effort
2. Possible Over-encapsulation
- Too many small methods
- Unnecessary abstraction layers
- Complexity without benefit
3. Learning Curve
- Understanding of good encapsulation needed
- Balance between openness and closure
- Experience for correct granularity
Exam-Relevant Questions
Typical IHK Questions
-
What is the difference between encapsulation and abstraction?
- Encapsulation hides implementation details, abstraction reduces complexity
-
When are getters and setters useful?
- Only when functionally necessary, not for every private value
-
Why are public fields problematic?
- They bypass validation and violate invariants
-
How does immutability support encapsulation?
- Guarantees stable invariants and simplifies concurrent usage
-
What does the Law of Demeter state?
- Talk only to direct friends, avoid cascading calls
Summary
Encapsulation is a fundamental principle for robust and maintainable software. It protects internal states, defines clear interfaces, and enables safe refactoring. Good encapsulation requires:
- Careful visibility planning
- Validation at boundaries
- Defensive programming
- Intentional immutability
- Clear responsibilities
The balance between sufficient encapsulation and practical usability is crucial for successful software architecture.