Skip to content
IRC-Coding IRC-Coding
OOP Encapsulation Information Hiding Visibility Modifiers Getters Setters Immutability Java

OOP Encapsulation Basics: Information Hiding

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

S

schutzgeist

2 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 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

  1. What is the difference between encapsulation and abstraction?

    • Encapsulation hides implementation details, abstraction reduces complexity
  2. When are getters and setters useful?

    • Only when functionally necessary, not for every private value
  3. Why are public fields problematic?

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

    • Guarantees stable invariants and simplifies concurrent usage
  5. 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.

Back to Blog
Share:

Related Posts