3 Encapsulation and Abstraction
A practical guide to controlling object state, hiding implementation details, preserving invariants, and designing public interfaces that are safe and predictable to use.
Protecting State with
Object-oriented design becomes safer when an object controls its own state instead of exposing arbitrary storage to every caller. The central idea is to combine state with the operations that preserve the rules for that state.
For example, a bank account should not let callers assign any balance they choose. It should provide operations such as deposit and withdraw, each of which checks its input and updates the balance only when the account's rules are satisfied.
has two closely related goals:
State protection: prevent invalid or unauthorized changes.
Behavior coordination: ensure that every change goes through operations that preserve the object's rules.
A class is not effectively encapsulated merely because it contains fields and methods. If its fields are publicly mutable, callers can bypass validation and become dependent on the internal representation. Strong gives callers a controlled interface while keeping implementation details private.
Takeaway: Keep state and its rule-preserving behavior together, and expose controlled operations instead of unrestricted mutation.
Separating Responsibilities from Implementation
A useful design separates what clients need to know from how the object accomplishes its work. describes the important capabilities of an object in terms of its responsibilities. A file-store might promise that documents can be saved and loaded without exposing whether the implementation uses a local file, a database, or a cloud service.
is the deliberate prevention of client dependencies on details that may change. The storage mechanism, database schema, helper fields, and internal algorithms can remain private while the supported capabilities stay stable.
These ideas answer different questions:
: What does the object provide?
: How are state and behavior organized and controlled?
: Which implementation details should clients be unable to depend on?
A good is small, understandable, and expressed in terms of responsibilities rather than representation. The is leaking when clients must know internal fields, call methods in an undocumented order, or reproduce validation logic themselves.
Takeaway: Present stable responsibilities to clients and hide mechanisms that may change.
Choosing Appropriate Visibility
is a language-supported way to limit which code can use a type or member. Common levels include:
Public: available to code that can access the type.
Private: available only within the declaring class or type.
Protected: available to the declaring type and, depending on the language, derived types.
Internal or package-private: available within a module, assembly, or package.
The exact rules differ among programming languages, so the language's visibility model must be checked when designing a class. The general design principle is consistent: use the narrowest visibility that supports the required behavior.
A private temperature field, for example, can be changed only through operations that reject values below degrees Celsius when that is the domain rule. More precisely, the lower-bound condition can be written as . The public operations then form the supported interface while the stored representation remains protected.
is a design tool, not a complete security system. Private members can reduce accidental misuse through the type system, but sensitive applications still require authentication, authorization, safe storage, and other security controls.
Takeaway: Restrict visibility deliberately, but do not confuse language-level privacy with system security.
Providing Controlled Access with Properties
A gives clients convenient, field-like syntax while allowing the class to control reads and writes. Depending on the language and design, a can be read-only, write-only, or read-write. Its implementation can validate input, normalize acceptable input, compute a value, or trigger controlled behavior.
For example, a Name can reject an empty or whitespace-only value and trim surrounding whitespace before storing the result. A balance can be publicly readable while allowing assignment only from inside the class. This exposes useful information without allowing ordinary callers to replace the balance directly.
Use a when access needs logic or when a stable boundary may need implementation flexibility later. Do not automatically create a getter and setter for every field. Ask whether the caller should read the value, change it, or perform a meaningful domain operation instead. An operation such as withdraw(amount) expresses a rule more clearly than a general setBalance(value) operation.
Validation and normalization are different:
Validation rejects unacceptable input.
Normalization transforms acceptable but variable input, such as trimming surrounding whitespace.
If normalization changes the meaning of input or hides an error, rejection is safer.
Takeaway: Use properties and methods to express controlled access, and prefer meaningful operations when a state change has domain rules.
Maintaining Invariants
An is a condition that must remain true for every valid object state. Typical examples include the following:
An account balance stays above its permitted minimum.
A date range has an end date no earlier than its start date.
A username is nonempty and has an allowed length.
A collection contains no duplicate identifiers.
Validation should happen at every boundary where invalid data could enter: construction, a factory method, a setter, a command method, deserialization, or a public collection. The checks should be complete, consistent, local to the state they protect, explicit about failure, and impossible to bypass through another public path.
A object checks its required conditions before it becomes available to callers. For example, a date-range constructor can accept the object only when the start date is no later than the end date. After successful construction, other methods can rely on that instead of repeatedly repairing invalid state.
A class must preserve its through every state-changing operation, not just through its most obvious entry point. Returning an internal mutable collection can undermine validation just as surely as exposing a public field.
Takeaway: Identify each , validate it at every state-changing boundary, and prevent alternate paths from bypassing the rule.
Designing Reliable Public Interfaces
A is the collection of types, methods, properties, and documented behaviors that client code may depend on. Its design determines how easy the class is to use correctly and how much freedom remains to change its implementation.
Reliable interfaces follow several principles:
Expose operations rather than mutable representation. A shopping cart can provide an
addoperation and a read-only view instead of returning its internal mutable list.Keep the interface minimal. Every public member can become a long-term client dependency.
Make illegal states difficult to represent by using validated constructors or specialized value types.
Document preconditions, postconditions, side effects, failure behavior, ownership, and object lifetime.
Avoid surprising setters. An operation that performs expensive input/output, changes unrelated state, or applies unexpected rounding should have an explicit name.
Preserve substitutability. An implementation used through an interface or base-class reference should not impose stronger preconditions, provide weaker postconditions, or unexpectedly change documented behavior.
Common mistakes include public mutable fields, automatic accessors for every field, validation in only one entry point, leaked mutable helper objects, and confusing privacy with a well-designed . A private field does not by itself make a coherent.
Before exposing a class, ask what responsibility it owns, which state must remain valid, whether callers can bypass validation, whether returned objects are mutable, which details may change, and whether the interface is small enough to understand and test.
Takeaway: Make correct use easy, invalid use difficult, and future implementation changes independent of client code.