Skip to content
IRC-CodingIRC-Coding
OOPEncapsulationInformation HidingVisibility ModifiersGetter MethodsSetter MethodsImmutabilityAccess Control

OOP Encapsulation Basics: Information Hiding

Master OOP encapsulation with information hiding, visibility modifiers, getters/setters, and immutability. Java examples included.

S

schutzgeist

10 min read
OOP Encapsulation Basics: Information Hiding

OOP Encapsulation Fundamentals: Information Hiding & Visibility

Encapsulation is one of the four fundamental principles of object-oriented programming. It hides the internal state and implementation details of an object, exposing 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 boundaries of responsibility.

Core Encapsulation Principles

  • Information Hiding: Internal details remain hidden
  • Interface Control: Only defined access points are available
  • Invariant Protection: Object state stays consistent
  • Reduced Coupling: Fewer dependencies between components

Visibility Modifiers

Java Visibility Levels

public class VisibilityDemo {
    
    // public: accessible from anywhere
    public String publicField = "public";
    
    // protected: accessible within the class and subclasses
    protected String protectedField = "protected";
    
    // package-private: accessible only within the package
    String packageField = "package-private";
    
    // private: accessible only within the class
    private String privateField = "private";
    
    // 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); // Call private method
        // Processing...
    }
}

C# Access Modifiers

public class BankAccount
{
    // public: accessible from anywhere
    public string AccountNumber { get; }
    
    // private: accessible only within the class
    private decimal balance;
    
    // protected: accessible within the class and derived classes
    protected string AccountType { get; set; }
    
    // internal: accessible only within the assembly
    internal string BankCode { get; set; }
    
    // protected internal: accessible 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

Effective 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("Ungültige 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("Konto ist deaktiviert");
        }
        if (balanceInCents < 0) {
            throw new IllegalArgumentException("Negativer Kontostand nicht erlaubt");
        }
        this.balanceInCents = balanceInCents;
    }
    
    // Business method instead of simple setter
    public void deposit(int cents) {
        if (cents <= 0) {
            throw new IllegalArgumentException("Betrag muss positiv sein");
        }
        this.balanceInCents += cents;
    }
    
    public boolean withdraw(int cents) {
        if (cents <= 0) {
            throw new IllegalArgumentException("Betrag muss positiv sein");
        }
        if (cents > balanceInCents) {
            return false; // Insufficient funds
        }
        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("Kontostand darf nicht negativ sein")
        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("Betrag muss positiv sein")
        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 darf nicht null sein");
        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: the list itself 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':
        """Returns a new instance with updated age"""
        return ImmutablePerson(self.name, new_age, list(self.hobbies))
    
    def add_hobby(self, hobby: str) -> 'ImmutablePerson':
        """Returns a new instance with an additional hobby"""
        new_hobbies = list(self.hobbies) + [hobby]
        return ImmutablePerson(self.name, self.age, new_hobbies)

Defensive Copies

Protecting 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 a copy
    }
    
    // Defensive copy on parameter
    public void addItems(List<Item> newItems) {
        if (newItems != null) {
            this.items.addAll(new ArrayList<>(newItems)); // Store a 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 a defensive copy"""
        return self._items.copy()
    
    def add_items(self, items):
        """Store a defensive copy"""
        if items:
            self._items.extend(items.copy())
    
    def get_items_immutable(self):
        """Return an immutable view"""
        return tuple(self._items)
    
    def items_iterator(self):
        """Iterator for safe access"""
        return iter(self._items)

Law of Demeter

Avoiding Chained Method 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 is too long");
        }
    }
    
    public String getValue() {
        return value;
    }
    
    @Override
    public String toString() {
        return value;
    }
}

Design by Contract

Preconditions 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 has insufficient funds
     * @throws IllegalStateException if either account is inactive
     * @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 funds in source account");
        }
        
        // Store old state for postcondition verification
        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 state 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");
        }
        
        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
    }
    
    // Computed 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();
    }
}

Benefits of Encapsulation

1. Reduced Coupling

  • Components depend less on each other
  • Changes have limited ripple effects
  • Refactoring becomes simpler

2. Higher Cohesion

  • Related functionality is bundled together
  • Clear responsibilities
  • Easier to understand

3. Improved Security

  • Controlled access to data
  • Validation at boundaries
  • Protection against inconsistent states

4. Better Testability

  • Clear interfaces for unit tests
  • Easier to create mocks
  • Focus on behavior rather than implementation

Drawbacks and Challenges

1. Extra Effort

  • More code for getters and setters
  • Boilerplate in simple data classes
  • Higher implementation overhead

2. Over-Encapsulation

  • Too many small methods
  • Unnecessary abstraction layers
  • Complexity without benefit

3. Learning Curve

  • Understanding good encapsulation takes practice
  • Balancing openness and closure isn’t intuitive
  • Experience needed to get granularity right

Exam-Relevant Questions

Typical IHK Questions

  1. What’s the difference between encapsulation and abstraction?

    • Encapsulation hides implementation details; abstraction reduces complexity
  2. When do getters and setters make sense?

    • Only when there’s a business reason, not for every private field
  3. Why are public fields problematic?

    • They bypass validation and break invariants
  4. How does immutability support encapsulation?

    • It guarantees stable invariants and simplifies concurrent use
  5. What does the Law of Demeter say?

    • Talk only to your direct neighbors; avoid chained method calls

Summary

Encapsulation is a fundamental principle for robust, maintainable software. It protects internal state, defines clear interfaces, and enables safe refactoring. Good encapsulation requires:

  • Careful visibility planning
  • Validation at boundaries
  • Defensive programming
  • Deliberate immutability
  • Clear responsibilities

Striking the right balance between sufficient encapsulation and practical usability is key to successful software architecture.

Continue on the OOP Learning Path

All OOP articles are now complete. Return to the first article: Object-Oriented Programming OOP Fundamentals.

Back to Blog
Share:

Nächster Artikel in Object-Oriented Programming

Weiterlesen
OOP Inheritance & Polymorphism Basics

Related Posts