Encapsulaton & Information Hiding

Encapsulation and Information Hiding

Encapsulation bundles an object’s data (attributes) and behaviour (methods) into a single unit, and information hiding restricts access to that internal state.

By controlling who can read or modify an object’s internals, we preserve its integrity, prevent invalid states, and improve maintainability.

Access Modifiers

  • public: Everyone can access this member. Use for methods intended as part of the class’s public API.
  • protected: Accessible within the class and its subclasses (and same package in Java). Use for members needed by derived classes but hidden from the outside world.
  • private: Only the defining class can access. Use for internal data that must never be modified directly.

Choosing the right access level helps reduce unintended interference or modification with an object’s internals, and helps ensure the object remains in a valid state.

Scope / Example Private Protected Public
Python Example self.__isbn self._isbn self.isbn
Java Example private String isbn; protected String isbn; public String isbn;
Accessible to:
Class Yes Yes Yes
Subclass No Yes Yes
Package No Yes (Java) Yes
Other Classes No No Yes

Notes: In Python, underscores signal intent (_isbn internal; __isbn is name-mangled to _ClassName__isbn), not strict access control. In Java, protected is visible to subclasses and to classes in the same package.

Encapsulation with Parent → Child Classes

Python uses naming conventions: public (no underscore), _protected (by convention), __private (name-mangled). Child class accesses _balance but not __pin.

class Account:
    def __init__(self, owner: str, opening: float):
        self.owner = owner        # public
        self._balance = opening   # "protected" by convention
        self.__pin = "1234"       # private (name-mangled)

    # public API
    def deposit(self, amt: float) -> bool:
        if amt > 0:
            self._balance += amt
            return True
        return False

    def withdraw(self, amt: float) -> bool:
        if 0 < amt <= self._balance:
            self._balance -= amt
            return True
        return False

    def get_balance(self) -> float:
        return self._balance

class SavingsAccount(Account):
    def __init__(self, owner: str, opening: float, rate: float):
        super().__init__(owner, opening)
        self._rate = rate  # "protected" detail

    # public behavior that legitimately uses the protected state
    def apply_interest(self):
        self._balance += self._balance * self._rate

# --- Demo ---
if __name__ == "__main__":
    a = Account("Alice", 1000)
    a.deposit(250); a.withdraw(300)
    print(a.owner, "balance:", a.get_balance())  # a.owner: public access ... not recommended

    s = SavingsAccount("Bob", 500, 0.05)
    s.apply_interest()
    s.withdraw(50)
    print(s.owner, "balance:", s.get_balance())

    # print(a.__pin)  # AttributeError: name-mangled (private)
    # print(s._balance)  # possible but discouraged: "protected" by convention

Java enforces modifiers: private hides state, protected for subclasses, public for API. Includes a psvm to instantiate and test.

class Account {
    public String owner;        // public access
    protected double balance;    // visible to subclasses

    public Account(String owner, double opening) {
        this.owner = owner;
        this.balance = opening;
    }

    // public API
    public String getOwner() { return owner; }
    public double getBalance() { return balance; }

    public boolean deposit(double amt) {
        if (amt <= 0) return false;
        balance += amt;
        return true;
    }
    public boolean withdraw(double amt) {
        if (amt <= 0 || amt > balance) return false;
        balance -= amt;
        return true;
    }
}

class SavingsAccount extends Account {
    private double interestRate;  // subclass-specific detail

    public SavingsAccount(String owner, double opening, double rate) {
        super(owner, opening);
        this.interestRate = rate;
    }

    public void applyInterest() { // public behavior using protected balance
        balance += balance * interestRate;
    }
}

public class EncapsulationDemo {
    public static void main(String[] args) {
        Account a = new Account("Alice", 1000);
        a.deposit(250);
        a.withdraw(300);
        System.out.printf("%s balance: %.2f%n", a.getOwner(), a.getBalance());

        SavingsAccount s = new SavingsAccount("Bob", 500, 0.05);
        s.applyInterest();
        s.withdraw(50);
        System.out.printf("%s balance: %.2f%n", s.getOwner(), s.getBalance());

        a.owner = "Eve";        //  a.owner: public access ... not recommended
        // The following would NOT compile if uncommented:
        // s.interestRate = 0.10;  // interestRate is private

        // Accessing 'balance' directly outside subclasses is discouraged.
        // It's 'protected': allowed only to subclasses (and same-package), not general API.
    }
}

Risks of Bypassing Encapsulation

  • Someone might set your balance to a negative value, breaking your business rules.
  • Direct access to “private” members can lead to fragile code that breaks when internals change.
  • Encapsulation prevents unintended side-effects and makes refactoring safer.

 Key Takeaways

  • Encapsulation hides internal state; information hiding enforces class invariants.
  • public, protected, and private control who can see or modify members.
  • Always expose a minimal, well-defined public interface to reduce coupling.
  • By preventing direct access, you guard against invalid states and simplify maintenance.