skip to content

Encapsulation & Information Hiding

Bundling state with the operations that govern it and hiding the representation behind a stable, mediated contract. Interviewers ask because most candidates conflate it with a getter per field.

on this pageshow

questions

13

A C library's public header contains only `typedef struct Conn Conn;` plus functions taking `Conn *` — the struct itself is defined in a .c file the caller never sees — even though the C language has no access-control keywords at all. Python is similar in spirit: a leading underscore is a convention, `__all__` names the exported surface, and an attribute written `self.__x` inside `class Cls` is simply stored under the name `_Cls__x` so that a subclass will not collide with it. Meanwhile, a class that declares every field private and adds a matching getter and setter for each satisfies every rule a language is able to check. Are encapsulation and information hiding the same thing? Argue using these cases.

level: middleimportance: must knowfreq 55%

answer

  1. mechanism vs secret — two axes
  2. C opaque struct: no keyword, total hiding
  3. getter+setter = republished representation
  4. `__x` mangling = collision avoidance
  5. Go/Rust hide per module, not per type

basics

~20 s

No. Encapsulation is a mechanism: bundle state with the operations that reach it. Information hiding is a design property: a module keeps a changeable decision secret. A C header can hide everything with no keywords; getter-and-setter classes hide nothing.

solid answer

~60 s

They are two different axes. - **Hiding with no mechanism.** The C header declares `typedef struct Conn Conn;` and defines the struct only in the .c file. C offers no access keyword, yet a client cannot name a field, cannot embed the struct by value, cannot even take its `sizeof`. The representation is a genuine secret: change it, rebuild that one file, and no caller changes. - **Mechanism with no secret.** A class with a private field plus a getter and setter for each field routes every access through a method — but it has republished the representation as the interface. Change a field's type or drop it and every client changes. Encapsulated, not hiding. - **Hiding by published surface.** Python's leading underscore and `__all__` state what the contract is; `__x` mangling exists to avoid subclass collisions, not to block anyone. - **A different unit entirely.** Go has no `private`: capitalisation exports, and the *package*, not the type, is the boundary. Parnas's test: which changeable decision does this module keep secret?

code

text · 11 lines
text
// public surface (what callers see)
type Conn        // name only; no fields, no size
open(host) -> Conn*
send(Conn*, bytes)
close(Conn*)

// private implementation (never shipped to callers)
struct Conn { socket; buffer; retries; deadline }

// caller cannot: read a field, embed Conn by value, take sizeof(Conn)
// implementer may: add/remove/reorder fields, rebuild one file, ship

go deeper

for a junior

Recall the two definitions and one clean counterexample: a class with a getter and setter for every field satisfies the language rules but hides nothing, because changing a field changes the interface.

for a middle

Own both directions. Show hiding with no mechanism (a C header that names a type without defining it) and mechanism with no hiding (accessor pairs), and state the operational test: can I change the decision without touching clients?

for a senior

Frame it as Parnas's question — which changeable decision is this module's secret — and connect it to real churn: which of your modules survives a representation change, and which forces a fan-out of edits. Note that the hiding unit differs by language.

for a principal

Treat it as a decomposition and blast-radius question. Decide where secrets sit relative to package/module boundaries, accept the costs hiding imposes (allocation moves inside the library, no inlining across the boundary, coarser APIs), and set conventions for the published surface where the language enforces nothing.

## Two ideas that share one slogan **Encapsulation** is a language construct. State and the operations that mediate it are bundled into a single unit, and access to that state is routed through those operations. It answers *how*. **Information hiding** is David Parnas's 1972 decomposition criterion: split a system so that each module conceals a design decision that is likely to change, and design the interface to reveal as little of that decision as possible. It answers *what is secret*. In Parnas's own worked example, the interesting modularization was the one where no client could tell whether lines of text were stored expanded or computed on demand — the storage decision was the secret. They travel together in one popular language family, which is why they get taught as synonyms. Look at several languages at once and they come apart in both directions. ## The operational test Hiding has exactly one test: **can I change this decision without changing any client?** Not "is there a keyword", not "is there a method in front of it" — can the decision move. If changing a field's type, its units, its representation, or its very existence forces edits in calling code, that decision was never a secret, whatever the declaration said. ## Four quadrants - **Mechanism and secret.** An opaque handle behind a small function set; an OCaml module exposing an abstract `type t`. - **Mechanism, no secret.** The private-field-plus-getter-and-setter class. Every access is mediated, so the language is satisfied, but the field set *is* the interface. - **Secret, no mechanism.** The C opaque struct; a Python package whose internals churn freely between releases. - **Neither.** A struct in a shared header whose fields everyone reads and writes directly. ## C: hiding without any access mechanism The opaque-pointer idiom is the purest case. The header declares an *incomplete type*: the name exists, the layout does not. Because the layout is unknown to the compiler while compiling client code, the client cannot declare a variable of that type by value, cannot embed it in another struct, cannot compute its size, and cannot dereference a field. Everything must go through the declared functions. C spent zero syntax on access control and got total representational hiding — and it got it as a consequence of the compilation model rather than a permission list. The cost is the mirror image: allocation must move inside the library (`conn_open`/`conn_close`), and nothing can be stack-allocated or inlined by the client. Hiding is a trade, not a free win. ## Python: the published surface is the contract Python's conventions are a documentation system. A single leading underscore is a statement of intent. `__all__` narrows `from mod import *` and, more usefully, tells a reader which names the module considers its contract. Double leading underscores trigger *name mangling*: inside `class Cls`, `self.__x` is compiled to `self._Cls__x`. That mechanism was introduced so that a base class and a subclass writing the same attribute name would not clobber each other — collision avoidance in an inheritance chain, not a wall. And it works, because hiding is enforced by the dependency graph in practice rather than by the compiler. Mature Python libraries restructure internals across releases and users are fine — until someone reaches past the published surface into a private helper, at which point an upgrade breaks them. That is precisely the Parnas failure mode arriving with no access modifier involved. ## Go and Rust: the module, not the type, is the unit Go removed the keyword. An identifier beginning with a capital letter is exported from its package; anything else is not. Inside the package every file sees every lowercase field of every type, so Go has essentially *no per-type hiding* and very good package-level hiding — coherent, because Parnas's module was never a class. Go goes further with `internal/` directories, which restrict who may even import a package. Rust is module-scoped too: items and struct fields are private to their module by default, `pub` widens them outward, `pub(crate)` bounds them to the crate, and a child module can reach its ancestors' private items. A single Rust module can therefore contain several cooperating types that share representation freely while the crate sees only a narrow surface — a shape that a strictly per-class privacy model cannot express without leaking. ## ML-family signatures: the secret is a typing fact In OCaml or Standard ML, a `.mli` signature can declare `type t` with no right-hand side. Outside the module, `t` is abstract: values of it can be passed around but nothing about their representation is known. Violations are not access errors, they are *type* errors — the representation is unknowable rather than forbidden. Same goal, entirely different machinery. ## Why the accessor-pair class fails A getter and setter per field is the canonical anti-example. It satisfies every syntactic rule, but the client now depends on the exact field set, their types, and their units. Swapping two `int` coordinates for a polar representation, or a `Date` for an instant, changes the interface. Encapsulation was achieved; nothing was hidden. ## How to answer in an interview Define both in one sentence each. Then give the two-way counterexample: something with no mechanism that hides completely (the C opaque struct, or a Python package with a disciplined surface), and something with full mechanism that hides nothing (the accessor-pair class). Close on the operational test, and note that the unit of hiding differs by language — package in Go, module in Rust and ML, class in the Java/C# family — because that is where most real hiding decisions actually get made.

  • Give an example of a module with a fully access-controlled interface that still leaks a design decision.
    A repository whose method names and return types mirror the database table columns one-for-one: every field is private and reached through methods, but the schema is the interface, so a column rename ripples through every caller. Returning an internal mutable collection directly does the same, since callers can then depend on its identity and mutability. Exposing a raw number without stating units leaks the storage choice too — callers start hard-coding conversions.
  • Go and Rust make the package/module the privacy unit rather than the type. What does that buy and cost?
    It buys cohesion: several types that genuinely share one secret can cooperate over each other's internals without exposing anything outside the module, which a strictly per-class model can only do by widening visibility or adding friend-style escape hatches. The cost is that privacy inside a large package is purely a matter of discipline, so packages must be kept small to stay meaningful. Go's `internal/` directories and Rust's `pub(crate)` exist to add a second, coarser ring around that.
  • How would you check whether a module's secret is actually secret?
    Change the decision and see who moves. Grep for callers that name the internal concept, count how many files a representation change forces you to edit, and check whether the interface would still make sense under a different implementation. If the type signatures of the public surface mention the representation — the column names, the units, the concrete collection type — the secret already escaped.

A vending machine enforces access — you must use the slot — yet hides nothing: the glass front shows the exact stock and layout. An unlisted phone number enforces nothing at all, yet nobody calls. Mechanism and secrecy are separate properties.

saying these in an interview costs you the question

  • Saying encapsulation and information hiding are two words for the same thing.
  • Claiming a getter/setter for every field achieves information hiding — it republishes the representation as the contract.
  • Believing Python's `__x` exists to block access; it is name mangling to avoid subclass collisions.
  • Assuming hiding requires an access keyword, so languages without one (C, Go, Python) 'have no encapsulation'.
  • Assuming the class is universally the unit of hiding, when Go, Rust and ML hide at module/package level.

context

open as a page

Between fully private and fully public, languages offer intermediate visibility levels - protected, package-private, assembly-internal, crate-visible. What unit of code does each of these actually trust, and how would you choose one under a least-privilege rule?

level: middleimportance: must knowfreq 55%

basics

~20 s

Each trusts a different unit: subclasses (protected), one flat package (Java package-private), one compiled assembly (C# internal), one compilation module (Kotlin internal), a crate subtree (Rust pub(crate), pub(super)), one package plus the internal directory rule (Go). Pick the smallest unit that still compiles.

open as a page

Java convention says make every field private and add getX()/setX(); Python convention says expose the attribute and reach for @property only if you later need one. Explain what language feature makes these opposite conventions both correct, and what each community pays for its rule.

level: middleimportance: should knowfreq 45%

basics

~20 s

The uniform access principle: in Python, Ruby, C#, Kotlin and Swift a stored attribute can become a computed one without changing call sites, so accessors up front are noise. Java has no properties, so x.field cannot later become a computation - the getter buys future freedom, not encapsulation.

open as a page

Eiffel and Ada 2012 let a type declare an invariant that the runtime checks on your behalf, while Java, Python and C# leave you to re-check by hand in every mutator. For the languages that check automatically, at exactly which moments does the check fire — and why is it deliberately NOT evaluated during a call the object makes on itself?

level: middleimportance: should knowfreq 40%

basics

~20 s

An invariant constrains an object's stable states, not every instant. Eiffel evaluates the class invariant on entry to and exit from qualified calls (x.f); D runs invariant blocks around public member functions; Ada 2012 checks Type_Invariant when a value crosses the package's visible boundary. Internal self-calls are skipped so a routine can legally break and rebuild state.

open as a page

A constructor or setter accepts a mutable object from the caller and stores it. In some languages the caller is left with no usable handle on what it passed; in others it keeps one, and in at least one popular language a copy is made that still shares storage. Explain the mechanisms and where the trap is.

level: middleimportance: should knowfreq 40%

basics

~20 s

Rust moves ownership, so the caller's binding is unusable afterwards. Swift structs copy by value. Go copies a struct but its slice and map fields still point at the same backing storage. Java, C#, Python and JavaScript share by reference unless you copy by hand.

open as a page

Accessors are usually defended as legitimate at boundaries - DTOs, wire formats, persistence. Compare how Go's encoding/json, Rust's serde and reflective serializers such as Jackson or System.Text.Json get at an object's state, and what each implies for whether the domain type and the wire type can be the same type.

level: seniorimportance: should knowfreq 35%

basics

~20 s

Go's encoding/json sees only exported fields, so a serialized type must publish its state - which forces a separate wire struct. Rust's serde derive expands inside the type and reads private fields, so encapsulation survives but the wire shape silently tracks the representation. Jackson bypasses visibility reflectively.

open as a page

A C++ class declares every data member private, yet adding one more private member forces every translation unit that includes the header to recompile, and in a shipped shared library it breaks binary compatibility. An OCaml module, by contrast, can publish `type t` in its .mli with no representation at all. Explain how a type can be completely access-controlled and still hide nothing, and what techniques restore the secret.

level: seniorimportance: should knowfreq 40%

basics

~20 s

Access control governs who may name a member; hiding governs who may depend on it. C++ puts private members in the header, so their existence, size and layout become client dependencies. OCaml signatures, Ada private types and the pimpl idiom withhold the representation itself.

open as a page

An object stores a mutable collection and exposes it through an accessor. Across the languages you know, what mechanisms exist to stop a caller from mutating that internal state through the returned reference, and what does each mechanism cost?

level: seniorimportance: should knowfreq 45%

basics

~10 s

Rust forbids mutation through a shared borrow at compile time. Java's List.copyOf snapshots, while Collections.unmodifiableList is a live view. C#'s ReadOnlyCollection wraps a still-mutable list. Python and JavaScript enforce nothing; Object.freeze is shallow.

open as a page

In some languages one instance can read another instance's private state as long as both are the same class; in others privacy is per-object and even a sibling instance cannot reach it. Name concrete languages on each side, say exactly what rule each one enforces, and explain what the choice changes about how you design a type's operations.

level: seniorimportance: should knowfreq 38%

basics

~20 s

Java, C#, C++ and JavaScript #fields draw the boundary at the class: any instance may read a sibling's private state. Smalltalk draws it at the object. Ruby's private allows no receiver except a literal self, so sibling access needs protected.

open as a page

Instead of re-validating in every mutator, some languages let you make the invalid state impossible to construct: Rust's NonZeroU32, Ada's `subtype Percent is Integer range 0 .. 100`, an OCaml abstract type with a smart constructor, or a TypeScript branded type. Compare where each of those actually enforces the constraint, and what each one costs.

level: principalimportance: should knowfreq 30%

basics

~20 s

Push the check to the boundary and let the type carry the proof afterwards. Rust's NonZeroU32 checks once at construction and never again; Ada re-checks its range on every assignment; an OCaml abstract type makes the smart constructor the only door; TypeScript's brand is erased at compile time and enforces nothing at runtime.

open as a page

A calculation reads mostly another object's data, so the feature-envy smell says move it onto that object. Explain when that advice breaks down for an operation that needs the state of two different objects, and how languages that do not attach methods to a single receiver - Julia, Common Lisp's CLOS, Clojure multimethods - change the answer.

level: principalimportance: nice to knowfreq 22%

basics

~20 s

"Move it to the data" is well defined only when one object owns the data. For a genuinely binary operation, single-receiver languages force you to pick an owner and make the other object expose state, so the envy relocates. C++ friend functions, Swift's file-scoped private and CLOS or Julia generic functions let the operation belong to neither type.

open as a page

A team assumes that marking an object read-only protects the whole structure hanging off it. But in C++ a `const` member function can still mutate through a pointer member, and in Swift a struct bound with `let` is deep through nested structs yet stops at the first `class` reference it holds. How deep does a read-only guarantee actually reach in the languages you know, and how would you design an API that hands back nested structures in a language where it does not reach?

level: principalimportance: nice to knowfreq 22%

basics

~20 s

Read-only depth varies by language. C++ const stops at a pointer's target; Swift value semantics stop at the first class reference; Kotlin's List and C# IReadOnlyList are compile-time views only. Rust's shared borrow is transitive; Java's wrappers are not. So make leaf types immutable.

open as a page

"Access modifiers are not a security mechanism" is a claim you will hear in most code reviews. Decide where it is literally true: in which language runtimes can untrusted code in the same process reach a private member, and where is the boundary genuinely enforced?

level: principalimportance: nice to knowfreq 30%

basics

~20 s

True wherever privacy is compile-time only: C++ and Rust erase it entirely, and Python's double underscore only mangles the name. Java's reflection can open members, but module strong encapsulation blocks that across boundaries. JavaScript's # fields have no reflective back door.

open as a page