1 Object-Oriented Programming Foundations
A structured guide to the core concepts, relationships, and design principles of object-oriented programming, from objects and classes through polymorphism and composition.
Objects and the Object-Oriented Paradigm
organizes software around objects rather than treating data and operations as unrelated elements. An object combines three central characteristics:
State: information stored in fields, attributes, or properties.
Behavior: operations supplied by methods.
Identity: a distinct existence, even when two objects contain equal data.
A bank-account object, for example, may store an account number and balance while providing operations such as depositing and withdrawing money. The object controls how its state changes, which helps keep related rules in one place.
OOP is intended to support several design goals:
Modularity: dividing a large system into understandable components.
Information hiding: keeping implementation details private while exposing intended operations.
Reuse: sharing behavior through classes, libraries, or components.
Extensibility: adding new kinds of objects without rewriting all existing client code.
Maintainability: localizing changes so that one implementation has limited effects elsewhere.
Procedural programming primarily organizes a program around procedures or functions that operate on data. OOP instead treats entities and their responsibilities as central design units. This is a difference in emphasis, not an absolute separation: object-oriented programs still use functions, loops, conditionals, and data structures, while procedural programs can still use modules and data abstraction.
Takeaway: OOP is a set of design choices for organizing state, behavior, and responsibilities; merely using a language with classes does not guarantee good design.
Classes, Objects, and Responsibilities
A describes a kind of object, including the state it may hold and the operations it may perform. An object, also called an instance, is a concrete value created from that . Multiple objects can come from one while retaining separate identities and state.
For a BankAccount , a field such as balance represents state. Methods such as deposit() and getBalance() represent behavior. A constructor establishes the initial valid state of a new object, such as rejecting a negative initial balance.
A well-designed represents a meaningful concept and has a focused set of responsibilities. A that simultaneously manages billing, user authentication, file storage, and screen layout is likely too broad. Keeping responsibilities focused makes the easier to understand, test, and change.
Takeaway: A defines a type of object, while an object is a concrete instance with its own state and identity.
, Abstraction, and Invariants
combines state and behavior while controlling access to internal representation. Instead of allowing outside code to assign arbitrary values directly to a balance, a can require callers to use operations that validate deposits and withdrawals.
protects an object's invariants: conditions that should remain true whenever the object is observable. For a bank account, an invariant might be that the balance cannot become negative unless overdrafts are explicitly supported.
Common access levels include:
Public: available to client code.
Private: available only within the defining .
Protected: available to the and, depending on the language, its subclasses.
is more than making fields private. It also requires a useful public and enough separation between the and implementation that internal representation can change without breaking clients.
Abstraction complements by exposing important concepts and operations while omitting unnecessary implementation detail. A caller needs to know that withdraw(25) requests a withdrawal, but does not need to know whether the account uses a memory field, a database, or a remote service internally.
Takeaway: controls how state changes, while abstraction controls which details clients need to understand.
and Substitutability
allows a subclass, or derived , to reuse and specialize selected state and behavior from a superclass, or base . A subclass may add members or override inherited methods.
is appropriate when there is a genuine is-a relationship. A savings account is a bank account, so a savings-account object can be used where a bank account is expected. The derived type must still honor the promises made by the general type.
can reduce duplication, but it creates a strong dependency between base and derived classes. A change to a superclass can affect many subclasses. Therefore, should not be chosen merely to reuse a few lines of code; the relationship should be conceptually stable and behaviorally substitutable.
Takeaway: Use for a stable, substitutable specialization, not as a general-purpose code-reuse shortcut.
and Common Contracts
allows one general type or to represent objects with different concrete implementations. Client code can invoke the same operation while the actual object determines which implementation runs.
For example, a general Shape reference may refer to a circle or a rectangle. Calling area() through that reference uses the circle-specific or rectangle-specific implementation as appropriate. A collection of shapes can therefore be processed uniformly without a long conditional that checks every concrete type.
commonly appears through:
Method overriding: a subclass supplies a specialized implementation of an inherited operation.
: different classes implement the same and can be used interchangeably.
Subtype : an object of a derived type is usable where its base type is expected.
Polymorphic design depends on substitutability. A specialized object should preserve the expectations established by the general type rather than surprising client code with incompatible behavior.
Takeaway: separates what client code asks for from the concrete implementation that performs the operation.
Interfaces and Low Coupling
An defines a contract: a set of operations that an implementing promises to provide. Unlike a hierarchy, an describes what an object can do without requiring all implementers to share the same implementation or ancestry.
Suppose an application needs to calculate payments. An Employee and a Contractor can both implement a Payable even though their payment calculations differ. Code that performs payment calculations can depend on Payable rather than either concrete .
This supports the design rule program to an , not an implementation. Depend on the smallest contract a client actually needs. Interfaces are especially valuable at boundaries between subsystems, including storage, notification, payment, and authentication services.
An reduces coupling and makes it easier to add another implementation or substitute a test implementation. The client remains focused on the capability it needs rather than on the details of one provider.
Takeaway: Interfaces make capabilities explicit and allow implementations to vary behind a stable contract.
and Flexible Reuse
builds a by giving it objects of other classes as parts. It expresses a has-a relationship. A car may have an engine, and an order may have a collection of line items.
A car can delegate starting behavior to its engine object. The car keeps its public role while the engine implementation can potentially be replaced without changing how clients use the car. also allows a to combine capabilities that do not belong in one hierarchy.
is often more flexible than because the contained object's behavior can vary independently. This is why a common design rule is to favor over when reuse, rather than a genuine subtype relationship, is the main goal. The two mechanisms are not mutually exclusive: a subclass may inherit a general identity while composing services or helper objects.
Takeaway: Prefer when a needs replaceable parts or delegated behavior rather than a true specialized type.
Object-Oriented Design in Practice
Effective object-oriented design translates requirements into collaborating objects and contracts. A practical process is:
Identify domain concepts. Look for important nouns, such as
Customer,Order, orInvoice, and important behaviors, such as placing an order or issuing a refund.Assign responsibilities. Give each operation to the object with the information needed to perform it.
Define boundaries. Keep internal state private and expose focused operations.
Prefer small, cohesive classes. Give each a clear purpose instead of making it a container for unrelated logic.
Use interfaces at change points. Introduce an where multiple implementations are likely or independent testing is important.
Prefer for variable behavior. Delegate to contained objects when behavior may change independently.
Use carefully. Require a genuine, substitutable "is-a" relationship.
Minimize dependencies. Make each know as little as practical about the internal details of other classes.
Keep public contracts stable. Let internal code change safely behind abstractions.
For example, an OrderService should not need to know how a payment provider stores transactions. It can depend on a PaymentProcessor and receive a concrete processor through a constructor. This is an example of , which makes the service easier to replace and test.
The overall quality of an object-oriented design depends on clear responsibilities, low coupling, high cohesion, stable abstractions, and careful selection among , , , interfaces, and .
Takeaway: Good OOP design is less about using every language feature and more about assigning responsibilities clearly while isolating change.