Encapsulation, Abstraction & OOP Design
Move from language mechanics to design judgment — how encapsulation, abstraction, cohesion, coupling, and composition combine to produce classes that are easy to use correctly and hard to use incorrectly.
Classes, inheritance, and virtual functions are C++ mechanisms; encapsulation and abstraction are the design ideas that decide how to use those mechanisms well. This topic steps back from syntax to ask what makes a class's public interface trustworthy, what keeps a class's internal invariants intact, and when composition or inheritance is the right relationship between two classes. Detailed named design patterns (Strategy, Observer, Factory, and so on) belong to the dedicated Design Patterns topic — this page covers the design principles that patterns are built on top of.
What Is Encapsulation?
Encapsulation is the bundling of data together with the operations that act on it, combined with restricting direct access to that data from outside the bundle. A class is C++'s primary encapsulation mechanism: its private members hold the data, and its public member functions define the only sanctioned ways to observe or change that data.
celsiusValue is private — external code cannot read or write it directly. All interaction goes through the constructor and the two accessor functions.
Encapsulation is what makes it possible to change celsiusValue's internal representation later (for example, storing Kelvin instead) without breaking any code that only ever used the public celsius() and fahrenheit() functions.
class Temperature
{
public:
explicit Temperature(double celsius) : celsiusValue(celsius) {}
double celsius() const { return celsiusValue; }
double fahrenheit() const { return celsiusValue * 9.0 / 5.0 + 32.0; }
private:
double celsiusValue;
};Encapsulation in Practice: Bundling Data and Behavior
Here any code anywhere can modify balance directly, bypassing withdraw() entirely, or call withdraw() with an amount that leaves balance negative.
balance can now only change through withdraw() (or other member functions Account chooses to provide), so Account can guarantee balance never goes negative through withdrawal — a guarantee RawAccount could never make.
This is the practical payoff of encapsulation: correctness rules live in one place, next to the data they protect, instead of being the responsibility of every piece of code that happens to touch that data.
// Without encapsulation: data and the logic that manipulates it are separate
struct RawAccount
{
double balance;
};
void withdraw(RawAccount& account, double amount)
{
account.balance -= amount; // nothing stops amount from exceeding balance
}// With encapsulation: the class itself enforces its own rule
class Account
{
public:
void withdraw(double amount)
{
if (amount <= balance)
{
balance -= amount;
}
}
double getBalance() const { return balance; }
private:
double balance = 0.0;
};What Is Abstraction?
Abstraction is presenting a simplified, purpose-focused view of something, deliberately omitting details that are not relevant to that purpose. A class's public interface is an abstraction of its behavior: callers reason about what the class does, not how it does it internally.
Callers of Stack think in terms of push, pop, and empty — a last-in-first-out abstraction. Whether it happens to be backed by a std::vector, a linked list, or something else is deliberately hidden.
Abstraction is about the model presented to the user of the type; encapsulation is the mechanism (data hiding via access control) that protects and enables that model. The next section makes this distinction concrete.
class Stack
{
public:
void push(int value);
int pop();
bool empty() const;
private:
std::vector<int> data; // an implementation detail
};Abstraction vs Encapsulation
The two ideas work together but answer different questions: encapsulation is about access; abstraction is about the mental model the interface offers.
A class can be encapsulated (all data private) without being well abstracted (if its public functions still leak implementation detail, for example a function named useVectorPushBack()) — and the reverse is largely impossible in C++, since a good abstraction generally needs encapsulation to actually be enforced.
Aspect | Encapsulation | Abstraction
What it is | Bundling data with behavior, restricting access | Presenting a simplified, purpose-focused interface
Primary question | Who is allowed to touch this data? | What does this type let its users think about?
C++ mechanism | Access specifiers (private, protected, public) | The chosen shape of the public interface itself
Focus | Protecting internal state and invariants | Hiding implementation complexity from callers
Example | balance is private | withdraw()/deposit() instead of exposing raw arithmeticInformation Hiding
Information hiding is the broader design principle behind both ideas: a module (here, a class) should expose only what its clients need to use it correctly, and hide everything else, including exactly how it is implemented and why particular internal decisions were made.
Only log() is exposed. writeToFile, rotateIfNeeded, and the underlying file handle are all hidden — callers cannot depend on their existence, so Logger is free to restructure them later.
Information hiding is what makes a class's implementation genuinely changeable later — anything not exposed publicly can be rewritten without breaking a single caller, because no caller could have depended on it in the first place.
class Logger
{
public:
void log(const std::string& message);
private:
void writeToFile(const std::string& formatted); // hidden helper
void rotateIfNeeded(); // hidden helper
std::ofstream file; // hidden resource
};Public Interface vs Private Implementation
A well-designed class has a clear line between two things: its public interface (the contract callers rely on) and its private implementation (everything the class uses internally to fulfil that contract). Only the public interface should be treated as a promise; the implementation should be treated as free to change.
Switching entries to a different container, or renaming evictOldest, cannot break any code outside Cache, because neither was ever part of the public interface.
protected members sit in between: hidden from the outside world, but visible to derived classes, forming an extension point for a class hierarchy rather than a fully public promise.
class Cache
{
public:
void insert(const std::string& key, int value); // interface: the promise
int lookup(const std::string& key) const; // interface: the promise
private:
std::unordered_map<std::string, int> entries; // implementation: free to change
void evictOldest(); // implementation: free to change
};Class Invariants
An invariant is a condition about an object's state that is always expected to hold true between calls to its public member functions — for example, 'balance is never negative' for Account, or 'size never exceeds capacity' for a fixed-size buffer. Encapsulation exists largely to let a class establish and protect its own invariants.
push() checks the invariant (items.size() <= capacity) before mutating, and refuses the operation rather than allowing it to be violated.
A class's invariant should be established by every constructor and preserved by every public member function — if any one of them can leave the object in a state that breaks the invariant, the invariant does not actually hold.
class BoundedBuffer
{
public:
explicit BoundedBuffer(std::size_t cap) : capacity(cap) {}
bool push(int value)
{
if (items.size() >= capacity)
{
return false; // invariant protected: never exceed capacity
}
items.push_back(value);
return true;
}
private:
std::vector<int> items;
std::size_t capacity;
};Enforcing Invariants Through Constructors and Member Functions
Both the constructor and set() route through the same clamping logic, so a Percentage object can never observably hold a value outside [0, 100] — that is its invariant, enforced at every entry point that could change stored.
Every public entry point that can change an object's state is a place the invariant can be broken if it is not checked there — this is why constructors (which establish the invariant for the first time) and every mutating member function need to cooperate, usually by routing through shared private helper logic like set() above rather than duplicating the check.
class Percentage
{
public:
explicit Percentage(double value)
{
set(value);
}
void set(double value)
{
if (value < 0.0) value = 0.0;
if (value > 100.0) value = 100.0;
stored = value;
}
double value() const { return stored; }
private:
double stored;
};Why Public Data Members Break Invariants
Because value is public, no member function stands between external code and the data — there is no invariant PercentagePublic can enforce, because it has no control over how its data is modified.
This is the concrete reason private data matters: an invariant is only real if there is no way to bypass the code that enforces it. A public data member removes that guarantee entirely, regardless of how carefully the rest of the class is written.
struct PercentagePublic
{
double value; // public — anyone can set it to anything
};
PercentagePublic p{50.0};
p.value = 250.0; // invariant violated, and nothing detects or prevents itGetters and Setters: When They Help and When They Leak Implementation
A getter/setter pair that simply reads and writes a private member with no additional logic (get value, set value) provides encapsulation in name only — it re-exposes the field through two functions instead of directly, without adding any actual protection or abstraction.
setBalance() can be called with any value, including a negative one — this class has private data but effectively no invariant protection, since the setter enforces nothing.
A getter or setter earns its place when it does real work — validating, clamping, recomputing a derived value, or logging — or when read access and write access genuinely need to be controlled separately (for example, a read-only computed property with no setter at all). Where a bare pass-through pair is truly all that is needed, that is a signal the member itself could reasonably just be part of the public interface as a simple, well-considered value type instead.
class WeakEncapsulation
{
public:
double getBalance() const { return balance; }
void setBalance(double b) { balance = b; } // no validation at all
private:
double balance;
};Designing a Minimal, Cohesive Public Interface
A class's public interface should expose exactly the operations its clients genuinely need, and nothing incidental to its own implementation. A smaller, more focused interface is easier to learn, easier to keep invariants over, and easier to change later, since every additional public function is one more promise that must be kept indefinitely.
Stopwatch exposes exactly the three operations a caller needs to think in terms of a stopwatch, none of which reveal how time is actually measured internally.
A useful test when deciding whether to add a public member: would a reasonable client of this class actually need this operation, or does it only exist because it was convenient for the class's own internals?
class Stopwatch
{
public:
void start();
void stop();
double elapsedSeconds() const;
// no getInternalClockTicks(), no setRawState() — those are implementation, not interface
};Class Responsibilities
A well-designed class should have one clear, statable responsibility — a single reason a reasonable person would need to change it. A class that mixes unrelated responsibilities (for example, one class that both parses a file format and renders a user interface) tends to change for multiple, unrelated reasons, and each change risks disturbing unrelated behavior.
ReportMixed changes whenever the file format changes and whenever the console output format changes — two unrelated reasons living in one class.
ReportData is responsible only for holding and loading parsed data; ReportRenderer is responsible only for presenting it. Each has one reason to change.
// Mixed responsibility: parsing and rendering coupled into one class
class ReportMixed
{
public:
void loadFromFile(const std::string& path);
void renderToConsole() const;
private:
std::vector<std::string> rawLines;
};// Separated responsibilities
class ReportData
{
public:
static ReportData loadFromFile(const std::string& path);
const std::vector<std::string>& lines() const { return rawLines; }
private:
std::vector<std::string> rawLines;
};
class ReportRenderer
{
public:
void render(const ReportData& data) const;
};Cohesion
Cohesion describes how closely related a class's members are to one another and to its single stated responsibility. A highly cohesive class has data and functions that clearly belong together and are used together; a low-cohesion class bundles loosely related things that happen to share a class only for convenience.
These three functions share no data and no conceptual theme beyond both being 'small helper functions' — a caller reading Utility learns little about what it is actually for.
High cohesion is generally a sign of a good class boundary; consistently low cohesion is a sign a class should be split into several smaller, more focused ones, each with members that genuinely relate to each other.
// Low cohesion: unrelated concerns sharing one class arbitrarily
class Utility
{
public:
double celsiusToFahrenheit(double c) const;
std::string reverseString(const std::string& s) const;
int daysInMonth(int month, int year) const;
};Coupling
Coupling describes how much one class's implementation depends on another's internal details. Tight coupling means a change in one class is likely to force a change in the other; loose coupling means the two classes can change somewhat independently, as long as their agreed interface stays the same.
OrderProcessor depends on Inventory's exact storage type (an unordered_map) and its public exposure; changing Inventory's internal representation would break OrderProcessor.
OrderProcessor now depends only on Inventory's takeOne() operation — Inventory can change its internal storage freely without affecting OrderProcessor at all.
// Tight coupling: OrderProcessor reaches directly into Inventory's internals
class Inventory
{
public:
std::unordered_map<std::string, int> stock; // public data — an implementation detail exposed
};
class OrderProcessor
{
public:
bool fulfil(Inventory& inv, const std::string& item)
{
if (inv.stock[item] > 0) // reaches directly into Inventory's map
{
--inv.stock[item];
return true;
}
return false;
}
};// Loose coupling: OrderProcessor depends only on Inventory's interface
class Inventory
{
public:
bool takeOne(const std::string& item)
{
auto it = stock.find(item);
if (it != stock.end() && it->second > 0)
{
--it->second;
return true;
}
return false;
}
private:
std::unordered_map<std::string, int> stock;
};
class OrderProcessor
{
public:
bool fulfil(Inventory& inv, const std::string& item)
{
return inv.takeOne(item);
}
};Reducing Coupling Through Abstraction
Depending on an abstract interface rather than a specific concrete class reduces coupling further: a class that depends only on what an interface promises, not on which concrete type implements it, can work with any current or future implementation.
OrderProcessor now depends only on the Storage interface. Any class implementing Storage's contract — an in-memory inventory, a database-backed one, a test double — can be supplied without changing OrderProcessor at all.
This is the same interface-style polymorphism covered in the Polymorphism topic, applied here as a coupling-reduction design technique rather than purely a language mechanic.
class Storage
{
public:
virtual bool takeOne(const std::string& item) = 0;
virtual ~Storage() = default;
};
class OrderProcessor
{
public:
explicit OrderProcessor(Storage& storage) : backing(storage) {}
bool fulfil(const std::string& item) { return backing.takeOne(item); }
private:
Storage& backing;
};Composition: 'Has-A' Relationships
Composition models a class in terms of the other objects it is built from — its members. It expresses a 'has-a' relationship: an Engine object has a FuelInjector, a Car has an Engine, rather than a Car being a kind of Engine.
Car does not inherit from Engine; it owns one as a member and delegates to it. Car's public interface can be shaped independently of Engine's.
Composed member objects follow ordinary object lifetime: a Car's engine member is constructed when the Car is constructed and destroyed when the Car is destroyed, with no separate ownership management needed — this is the same RAII-friendly behavior covered in other topics, applied to class design.
class Engine
{
public:
void start() { std::cout << "engine starting\n"; }
};
class Car
{
public:
void start() { engine.start(); } // Car forwards to its Engine member
private:
Engine engine; // Car has-an Engine
};Composition vs Inheritance
Neither is universally 'better' — they express different relationships and have different coupling consequences.
Public inheritance should be reserved for genuine is-a relationships, especially ones that need polymorphism (a Circle really is a Shape). Composition should be the default choice whenever a class merely uses or is built from another type's functionality.
Aspect | Inheritance (is-a) | Composition (has-a)
Relationship expressed | Derived is a kind of Base | Class owns / is built from another object
Interface exposure | Base's public interface is inherited automatically | Only what the containing class chooses to forward is exposed
Coupling to the other type | Tight — derived depends on base's implementation and interface | Looser — only depends on the member's public interface
Flexibility to change later| Harder — changing Base can ripple through every derived class | Easier — the member type can often be swapped if the interface used stays the same
When it fits well | A genuine, stable is-a relationship, especially for polymorphism | Almost everything else, especially 'built from' or 'uses' relationships
Runtime type flexibility | Fixed at compile time per object (an object's own type never changes) | Can hold a pointer/reference to different implementations (e.g. via composition + polymorphism)Preferring Composition for Code Reuse
Inheriting from std::vector purely to reuse its storage exposes its entire public interface, including operations that break the 'stack' abstraction the class was supposed to provide — and std::vector does not even have a virtual destructor, making this pattern unsafe for polymorphic deletion as well.
Stack reuses std::vector's storage and algorithms internally, but exposes only the operations that preserve stack discipline — exactly the kind of abstraction discussed earlier in this topic.
This is a common, well-established guideline: prefer composition over inheritance when the goal is only to reuse an existing type's functionality, and reserve inheritance for genuine is-a relationships.
// Inheritance misused purely for code reuse — Stack is not really a kind of vector
class StackViaInheritance : public std::vector<int>
{
// now every std::vector operation is also (incorrectly) part of Stack's public interface,
// including operations that would let a caller violate stack discipline, like inserting
// in the middle
};// Composition — reuses the same functionality without leaking the interface
class Stack
{
public:
void push(int value) { data.push_back(value); }
void pop() { data.pop_back(); }
int top() const { return data.back(); }
bool empty() const { return data.empty(); }
private:
std::vector<int> data;
};Interface Design Principles
A well-designed interface is small and focused (each operation does one clear thing), consistent (similarly-named operations behave in similarly-shaped ways across the codebase), and hard to use incorrectly (its types and preconditions guide a caller toward correct use rather than merely documenting how to avoid misuse).
The second version makes the unit explicit in the constructing function's name and makes it structurally impossible to pass a negative or ambiguously-unitted raw number — the interface itself steers correct usage.
Consistent naming and behavior across a codebase's classes (for example, always naming a size-reporting function size(), always making empty() mean the same thing everywhere) also reduces the amount a caller needs to learn per class, and reduces the chance of misusing one class by assuming it behaves like another similarly-named one.
// Harder to use correctly: caller must remember units and valid range
void setSpeed(double value); // is value m/s? km/h? can it be negative?
// Easier to use correctly: the type and interface communicate the contract
class Speed
{
public:
static Speed metersPerSecond(double v) { return Speed(v < 0.0 ? 0.0 : v); }
double value() const { return stored; }
private:
explicit Speed(double v) : stored(v) {}
double stored;
};
void setSpeed(Speed value);Dependency Direction
Dependency direction asks: which classes know about which other classes' concrete implementations? A design where higher-level, policy-setting code depends only on abstractions (interfaces), and lower-level, detail-implementing code implements those abstractions, keeps the high-level code stable even as low-level details change.
OrderService, a high-level policy class, is directly coupled to a specific low-level implementation, SqlLogger. Swapping in a different logging mechanism means changing OrderService itself.
OrderService now depends only on the Logger abstraction. SqlLogger depends on (implements) that same abstraction. Any new Logger implementation can be supplied to OrderService with no change to OrderService itself.
This basic idea — depend on abstractions rather than on concrete, low-level details — is one of the most consequential structural choices in object-oriented design, and it is what makes the loose coupling described earlier actually achievable in a larger codebase.
// High-level policy depends directly on a low-level concrete detail
class SqlLogger { public: void write(const std::string& msg); };
class OrderService
{
public:
explicit OrderService(SqlLogger& logger) : log(logger) {} // tied to SqlLogger specifically
private:
SqlLogger& log;
};// High-level policy depends on an abstraction instead
class Logger { public: virtual void write(const std::string& msg) = 0; virtual ~Logger() = default; };
class SqlLogger : public Logger { public: void write(const std::string& msg) override; };
class OrderService
{
public:
explicit OrderService(Logger& logger) : log(logger) {} // depends only on the abstraction
private:
Logger& log;
};Designing Maintainable Classes: A Worked Walkthrough
Every field is public (no invariant possible, e.g. nothing stops email from being an empty string), and UserProfile mixes 'holding profile data' with 'knowing how to persist itself to a database' — two different responsibilities.
UserProfile now owns and protects its own data (loginCount can only ever increase by one, through recordLogin(), never be set to an arbitrary value), and it no longer knows anything about databases — that responsibility moved to UserProfileRepository, which depends on UserProfile rather than the reverse.
This before/after pair applies, in miniature, every idea covered on this page: encapsulation (private fields), invariant protection (recordLogin() instead of a public setter), a minimal cohesive interface, single responsibility, and reduced coupling between the data class and the persistence logic.
// Before: public data, mixed responsibilities, no enforced invariant
struct UserProfile
{
std::string name;
std::string email;
int loginCount;
void saveToDatabase(); // persistence mixed into the data class itself
};// After: encapsulated, invariant-enforcing, single-responsibility
class UserProfile
{
public:
UserProfile(std::string userName, std::string userEmail)
: name(std::move(userName)), email(std::move(userEmail)) {}
const std::string& getName() const { return name; }
const std::string& getEmail() const { return email; }
int getLoginCount() const { return loginCount; }
void recordLogin() { ++loginCount; }
private:
std::string name;
std::string email;
int loginCount = 0;
};
class UserProfileRepository
{
public:
void save(const UserProfile& profile); // persistence is a separate responsibility
};Best Practices
1. Keep data members private by default, and expose behavior through member functions that can validate, transform, or restrict access as needed to protect the class's invariants.
2. State a class's single responsibility in one sentence; if that sentence needs 'and,' consider splitting the class.
3. Keep the public interface as small as the class's actual clients require — an unused public function is still a promise that must be maintained.
4. Prefer composition for 'has-a'/'uses' relationships and reserve public inheritance for genuine, stable is-a relationships, especially where runtime polymorphism is actually needed.
5. Depend on abstractions (interfaces) rather than on other classes' concrete implementations wherever a class needs to remain stable while its collaborators evolve.
6. Give a getter or setter real behavior (validation, computation, access control) or reconsider whether the underlying member should simply be exposed as a straightforward public value type instead of wrapped in a pass-through pair that adds no protection.
7. Use consistent naming and behavior for similar operations across different classes in the same codebase, so callers can transfer what they already know from one class to another.
Complete Example
Account demonstrates encapsulation (private balance, protected by deposit/withdraw), abstraction (callers think in terms of deposit/withdraw, not raw arithmetic), an invariant (balance never negative), a single responsibility (managing one account's balance), and composition against an abstraction (Logger&) rather than a concrete logging class.
Account depends on the Logger interface, not on ConsoleLogger directly — a different Logger implementation could be substituted with no change to Account deposit(100.0) passes the amount > 0 check, so balance becomes 100 and a log line is written withdraw(30.0) passes both checks (positive, not more than balance), so balance becomes 70 and a log line is written getBalance() exposes the resulting balance through a read-only accessor
The program's output (exact decimal formatting may vary).
#include <iostream>
#include <string>
#include <vector>
class Logger
{
public:
virtual void write(const std::string& message) = 0;
virtual ~Logger() = default;
};
class ConsoleLogger : public Logger
{
public:
void write(const std::string& message) override
{
std::cout << "[log] " << message << '\n';
}
};
class Account
{
public:
Account(std::string ownerName, Logger& loggerRef)
: owner(std::move(ownerName)), logger(loggerRef) {}
void deposit(double amount)
{
if (amount <= 0.0)
{
return; // invariant: balance never decreases from a deposit, never negative amounts accepted
}
balance += amount;
logger.write(owner + " deposited " + std::to_string(amount));
}
bool withdraw(double amount)
{
if (amount <= 0.0 || amount > balance)
{
return false; // invariant: balance never goes negative
}
balance -= amount;
logger.write(owner + " withdrew " + std::to_string(amount));
return true;
}
double getBalance() const { return balance; }
private:
std::string owner;
double balance = 0.0;
Logger& logger; //
…[log] Alex deposited 100.000000
[log] Alex withdrew 30.000000
Balance: 70OOP Design Mental Model
Picture a well-designed class as a small, sealed capsule: the public interface is the only door in or out, the private data and invariant are sealed inside, and anything a caller does through the door is guaranteed to leave the capsule in a valid state.
The public interface is not just a convenience — it is the sole enforcement point for everything the class guarantees about its own state.
At the level of relationships between classes, prefer to picture composition ('this object is built from these parts, wired together through interfaces') as the default connective tissue of a design, with inheritance reserved for the specific, deliberate cases where one type genuinely is a more specific kind of another.
public interface (the only door)
| |
v v
+-----------------------+
| private data |
| (invariant protected |
| by every function |
| reachable through |
| the door) |
+-----------------------+
Callers never reach in directly — every interaction is mediated by a function
that can check, transform, or refuse the request.Quick Reference
A quick-reference table summarizing the core design vocabulary from this topic.
Idea | What it means | C++ tool
Encapsulation | Bundle data with behavior; restrict direct access | private/protected members, public member functions
Abstraction | Present a simplified, purpose-focused interface | The chosen shape of the class's public members
Information hiding | Expose only what clients need; hide the rest | Access specifiers, private helper functions
Invariant | A condition always true between public calls | Constructors + mutating member functions that enforce it together
Cohesion | How related a class's members are to its one responsibility | Design judgment, not a language feature
Coupling | How much one class depends on another's internals | Depending on interfaces instead of concrete types reduces it
Composition (has-a) | Class built from member objects | Member variables of another class type
Inheritance (is-a) | Derived is a specific kind of Base | public inheritance, typically with virtual functions
Depende
…Key Takeaways
Encapsulation bundles data with the operations that act on it and restricts direct access, so a class can guarantee and enforce its own invariants.
Abstraction presents a simplified, purpose-focused interface, letting callers reason about what a class does rather than how it is implemented.
A class invariant is only real if every entry point that can change the object's state — every constructor and every mutating member function — enforces it; public data members make invariants impossible to guarantee.
A getter or setter that merely passes a value through, with no validation or logic, provides little real encapsulation benefit; keep them only where they do genuine work.
A well-designed class has one clear responsibility, high cohesion among its own members, and loose coupling to other classes, ideally through abstractions rather than concrete implementation details.
Composition ('has-a') should be the default relationship between classes; reserve public inheritance ('is-a') for genuine, stable specialization relationships, especially where runtime polymorphism is actually needed.
Depending on abstractions rather than concrete low-level details keeps higher-level design stable while implementation details change underneath it.
A class exposes getBalance() and setBalance(double) where setBalance simply assigns its argument to the private balance member with no checks. In what sense is this class encapsulated, and in what sense does it fail to achieve the benefit encapsulation is meant to provide?
It is technically encapsulated in the narrow sense that balance is a private data member, accessible only through member functions. But because setBalance performs no validation, it fails to actually protect any invariant — any caller can still set balance to an arbitrary value (including a negative one) exactly as if the member were public. Encapsulation's real benefit — the ability to guarantee an invariant — requires the member functions guarding the data to actually enforce a rule, not merely mediate access to it.
Explain the difference between encapsulation and abstraction using a Stack class backed internally by a std::vector, with a public interface of push, pop, top, and empty.
Encapsulation is the fact that the underlying std::vector is a private member, inaccessible directly from outside Stack — it is the access-control mechanism. Abstraction is the fact that callers of Stack think in terms of push/pop/top/empty (a last-in-first-out model) rather than in terms of vector indices or iterators — it is the simplified conceptual model the interface presents. Encapsulation is what makes that abstraction enforceable: because the vector is private, no caller can bypass the stack-discipline interface to, say, insert into the middle.
Give an example of two classes that are tightly coupled, and describe a design change that would loosen that coupling.
Example: an OrderProcessor class that directly accesses an Inventory class's public std::unordered_map<std::string,int> stock member to check and decrement quantities. This is tight coupling because OrderProcessor depends on Inventory's exact internal storage type and structure. Loosening it means making stock private and giving Inventory a member function like takeOne(item) that OrderProcessor calls instead — OrderProcessor then depends only on that one operation's behavior, not on how Inventory stores its data, so Inventory's internals can change freely without affecting OrderProcessor.
A class Vehicle has a class Engine as a public inheritance base purely so Vehicle can reuse Engine's start() function. What is wrong with this design choice, and what alternative relationship would be more appropriate?
Public inheritance states an is-a relationship — Vehicle : public Engine claims 'a Vehicle is a kind of Engine,' which is not true; a vehicle has an engine, it is not one. Using inheritance here purely for code reuse also exposes the entirety of Engine's public interface as part of Vehicle's own interface, whether or not that is appropriate, and ties Vehicle's implementation to Engine's. Composition is the more appropriate relationship: Vehicle should have an Engine member and forward to it explicitly (e.g. void start() { engine.start(); }), exposing only the operations Vehicle actually wants to promise.
What does it mean for a design to depend on abstractions rather than on concrete low-level details, and why does this matter for maintainability? Use a Logger example.
It means a high-level class (for example OrderService) is written in terms of an abstract interface (an abstract Logger base class with a pure virtual write() function) rather than a specific concrete implementation (like a specific SqlLogger class). Because OrderService only calls operations the Logger interface promises, any class implementing that interface — SqlLogger, a console logger, a test double — can be substituted without any change to OrderService itself. This matters for maintainability because it means the high-level policy code stays stable even as low-level implementation details (like how or where logs are written) change or are replaced over time.
This Meter class is meant to represent a non-negative distance traveled, but nothing enforces that. Explain exactly why the design fails to guarantee its intended invariant, in terms of encapsulation.
distance is a public data member, so there is no member function mediating access to it — any code anywhere can assign any value directly, as the last line shows by setting a clearly invalid negative distance. Because there is no private data and no controlled entry point, Meter cannot guarantee that its intended invariant (distance is never negative) actually holds; the invariant exists only as an unenforced comment in the programmer's head, not as something the type structurally guarantees.
Redesign the Meter class from the previous example so distance is private and can only be increased through an addDistance(double delta) member function that rejects negative deltas, with a getDistance() accessor. Demonstrate that attempting to add a negative delta leaves distance unchanged.
distance is now private, so the only way to modify it is through addDistance(), which checks delta < 0.0 and refuses the call rather than applying a negative delta. After addDistance(10.0) distance is 10.0; addDistance(-500.0) is rejected and leaves distance unchanged. The program prints 10, and distance's invariant (never negative, since it starts at 0 and can only be increased by non-negative deltas) is now structurally guaranteed rather than merely intended.
Write an abstract PaymentGateway interface with a pure virtual bool charge(double amount) and a virtual destructor. Write a concrete TestGateway implementation that always returns true and prints the amount charged. Write a Checkout class that depends only on PaymentGateway (composition against the abstraction, stored as a reference) and has a method completePurchase(double amount) that calls charge() and prints whether it succeeded. Wire a TestGateway into a Checkout in main and call completePurchase(49.99).
Checkout depends only on the PaymentGateway abstraction (stored as a reference member — composition, not inheritance), not on TestGateway specifically, so any other PaymentGateway implementation could be substituted with no change to Checkout. completePurchase(49.99) calls gateway.charge(49.99), which dispatches dynamically to TestGateway::charge, printing 'charging 49.99' and returning true, after which Checkout prints 'purchase succeeded'.
setupInternalBuffer, validateChecksum, and applyDefaults are all declared public even though only configure() calls them internally. Explain the design problem with this, in terms of minimal public interfaces, and how you would fix it.
Making setupInternalBuffer, validateChecksum, and applyDefaults public exposes internal implementation steps as part of Widget's permanent interface contract, even though no external caller is meant to call them individually or out of order — a caller could call validateChecksum() before setupInternalBuffer() has run, leaving buffer in an inconsistent state, and Widget could never safely change how configure() is broken down internally without risking breaking some caller who depended on those steps directly. The fix is to make all three functions private (or move them into an unnamed-namespace-free private section, or into free functions in an implementation-only translation unit), leaving configure() as the only public entry point — the internal steps become free to reorder, merge, or rewrite entirely since nothing outside the class could ever have called them directly.
class Meter
{
public:
double distance; // public
};
void addDistance(Meter& m, double delta)
{
m.distance += delta;
}
int main()
{
Meter m{0.0};
addDistance(m, 10.0);
m.distance = -500.0; // allowed
return 0;
}class Widget
{
public:
void configure() { setupInternalBuffer(); validateChecksum(); applyDefaults(); }
void setupInternalBuffer();
void validateChecksum();
void applyDefaults();
private:
std::vector<int> buffer;
};