100 companion flashcards · AI-assisted study content · Open the deck →
This deck walks through the foundational ideas behind object-oriented programming, from the four core pillars—encapsulation, inheritance, polymorphism, and abstraction—to the SOLID design principles that guide writing flexible, maintainable code. It also touches on practical design concepts like coupling, cohesion, composition versus inheritance, and the DRY principle, giving you a well-rounded view of how OOP is applied in real projects.
It's a great fit if you're just starting to learn OOP concepts, preparing for a technical interview, or looking to refresh and strengthen your understanding of software design fundamentals. Whether you're a student, a self-taught developer, or someone transitioning into a new language, these cards cover the vocabulary and reasoning you'll encounter again and again.
To get the most out of studying, try to connect each principle to a small piece of code you've written or seen. Abstract definitions become much easier to remember once you can point to a concrete example. Spacing out your review sessions over a few days works well for conceptual material like this—short, repeated exposure helps the terminology and trade-offs sink in naturally.
One helpful habit is to revisit the cards that compare similar ideas, like composition versus inheritance or interfaces versus abstract classes, since these distinctions often come up in interviews and design discussions. Pairing the flashcards with a bit of practice coding will reinforce what you learn here and make the principles feel less like memorized rules and more like useful tools.
Object-oriented programming rests on four foundational concepts that together shape how we model and organize software. Encapsulation is the practice of bundling data and the methods that operate on that data into a single unit, typically a class, while restricting direct access to some of the object's components. By hiding internal state and implementation details behind a controlled public interface, encapsulation protects object integrity and prevents unintended modification from outside code.
Inheritance allows a child class, or subclass, to acquire the properties and behaviors of a parent class, or superclass. In Java, declaring a Dog class with the extends keyword so that it inherits from Animal establishes a clear is-a relationship in which Dog is a specific kind of Animal. This mechanism promotes code reuse by allowing common behavior to live in a parent class, while also enabling polymorphic behavior when subclasses provide their own specialized implementations.
Polymorphism, literally meaning "many forms," is the ability of an object to take on different behaviors depending on its actual type. It comes in two main forms: compile-time polymorphism achieved through method overloading, where the compiler picks the right method based on argument types, and run-time polymorphism achieved through method overriding, where the call is resolved at runtime based on the object's actual class. A draw method on a Shape, for instance, will render a Circle differently from a Rectangle even when invoked through a common Shape reference.
Abstraction focuses on hiding complex implementation details and exposing only the essential features of an object. In practice, this is achieved through abstract classes that provide partial implementations, and through interfaces that define a pure contract with no implementation at all. A Vehicle interface, for example, might expose only start and stop methods, deliberately concealing the intricacies of how engines actually work.
The SOLID principles, introduced by Robert C. Martin (often called Uncle Bob), are five design guidelines that together promote maintainable, flexible, and testable object-oriented software. The Single Responsibility Principle states that a class should have only one reason to change, meaning it should have just one job or responsibility. A UserService class that handles authentication, email sending, and database queries violates this principle and should be split into separate AuthService, EmailService, and UserRepository classes, each with a clear and focused purpose.
The Open/Closed Principle holds that software entities should be open for extension but closed for modification. New behavior should be added by creating new classes or methods rather than altering existing tested code. Interfaces, abstract classes, and strategy patterns are the typical tools used to enable such extensions without modifying the original code.
The Liskov Substitution Principle requires that objects of a superclass be replaceable with objects of a subclass without breaking the program. A classic violation is modeling a Square class as extending Rectangle, because setting the width on a Square unexpectedly also changes its height. The deeper rule is that subclasses must honor the behavioral contract of the parent class.
The Interface Segregation Principle argues that clients should not be forced to depend on interfaces they do not use. A fat Worker interface that declares work, eat, and sleep methods becomes problematic when a Robot class is forced to implement eating and sleeping methods it does not need. The fix is to split such interfaces into smaller, more focused ones like Workable, Eatable, and Sleepable. Finally, the Dependency Inversion Principle states that high-level modules should not depend on low-level modules; both should depend on abstractions. In an OrderService, for instance, depending on a PaymentGateway interface rather than a concrete StripePayment class makes it trivial to swap implementations.
At the heart of OOP lies the distinction between a class and an object, and the related idea of object identity versus equality. A class is a blueprint or template that defines attributes and behaviors, while an object is a concrete instance of that class created at runtime. The analogy of an architectural plan versus an actual building captures the relationship well: many distinct buildings can be constructed from the same plan, just as many objects can be instantiated from a single class. In Java, the expression Car myCar = new Car(); creates a new object from the Car class definition. Closely related is the distinction between identity, where two references point to the same object in memory (a == b in Java or a is b in Python), and equality, where two distinct objects share the same value or state (a.equals(b) in Java or a == b in Python). When overriding equals in Java, hashCode must be overridden too, since equal objects must share a hash code even though identical hash codes do not imply equality.
Constructors are special methods invoked automatically when an object is created, responsible for initializing the object's state. They share the class name, have no return type, and can be overloaded with different parameter lists to provide multiple ways of constructing an object. If no constructor is defined, most languages provide a default constructor. The expression new Car("Toyota", 2024) invokes a constructor that accepts those arguments and sets up the new object accordingly. Destructors play the symmetric role on the other end of an object's life, releasing resources like file handles, memory, or network connections. In C++, a destructor declared with a tilde runs deterministically when an object goes out of scope, while Python uses __del__ invoked by the garbage collector, and Java relies on try-with-resources and AutoCloseable rather than true destructors.
Access modifiers control the visibility of class members and play a key role in enforcing encapsulation. The public modifier allows access from anywhere, private restricts access to within the declaring class only, and protected allows access within the class and its subclasses. Java also offers package-private visibility, the default when no modifier is specified, which restricts access to the same package. A common best practice is to default to private and widen access to protected only when subclasses genuinely need to access or override the member. The final keyword provides a complementary way to enforce immutability and communicate design intent: a final class cannot be subclassed (as with Java's String), a final method cannot be overridden, and a final variable cannot be reassigned after initialization. Using final thoughtfully can also help avoid the fragile base class problem by signaling which parts of a class are not designed for extension.
Static and instance members differ in how they belong to a program. Static members belong to the class itself, are shared across all instances, and are accessed via the class name such as Math.PI. They cannot access instance variables because they exist independently of any particular object. Instance members, by contrast, belong to each object individually, are accessed through an object reference like dog.name, and can freely access both static and instance members. Static methods work well for utility functions like Math.max, factory methods like List.of, or any logic that does not depend on instance state, but they should be avoided when polymorphic behavior is required or when they would create tight coupling.
Beyond the structure of individual classes, object-oriented systems are defined by how objects relate to one another. Association is the most general form of relationship, where one object uses or interacts with another without either owning the other. A Teacher and a Student, for example, are associated because they interact, yet neither contains the other. Associations can be unidirectional, where only one side knows about the relationship, or bidirectional, where both sides are aware of each other. Association is the weakest form of object relationship.
Aggregation is a specialized form of association that represents a whole-part relationship in which the part can exist independently of the whole. A Department containing Professor objects illustrates this idea: if the department were dissolved, the professors would still exist. In UML diagrams, aggregation is depicted with a hollow diamond on the whole side, signifying weak ownership. The relationship is therefore one of usage rather than true containment, representing a has-a relationship with weak ownership.
Composition represents the strongest form of whole-part relationship, where the part cannot exist without the whole. A House containing Room objects is a typical example, since rooms do not exist independently of their house. If the house is destroyed, so are its rooms. In UML, composition is shown with a filled diamond, and the parts share their lifecycle with the whole, representing strong ownership and a strict lifecycle dependency. The progression from association through aggregation to composition reflects an increasing strength of coupling, with association being the loosest and composition the tightest.
Polymorphism in object-oriented programming is enabled by two distinct mechanisms known as overloading and overriding. Method overloading, the form of compile-time polymorphism, occurs when multiple methods share the same name but differ in their parameter lists within the same class. For instance, a class might define both an add method accepting two integers and another accepting two doubles, and the compiler selects the correct version based on the argument types at the call site.
Method overriding is the run-time counterpart: a subclass provides a specific implementation of a method already declared in its superclass. When a Dog class extends Animal and overrides speak to print "Woof", the method called depends on the actual object type at runtime, not the declared reference type. This is what enables a single method call to produce different behavior depending on the underlying object.
The distinction between overloading and overriding runs deeper than their names suggest. Overloading involves compile-time binding within the same class with different parameters and possibly different return types, while overriding involves runtime binding across parent and child classes with identical signatures and return types that must be the same or covariant. Overloading is therefore static polymorphism, and overriding is dynamic polymorphism.
Underlying overriding is the concept of virtual methods. In C++, a method declared with the virtual keyword can be overridden by subclasses, with the call resolved at runtime through dynamic dispatch. In Java, all non-static, non-final methods are virtual by default, while in C# the virtual keyword must be used explicitly. A pure virtual function in C++, written as virtual void draw() = 0, has no implementation and forces subclasses to provide one, and a class containing such a function becomes abstract. These mechanisms together form the foundation of late binding, where the system checks the actual object type rather than the declared type when dispatching a method call, in contrast to early binding used for overloaded, static, and final methods. Virtual methods are also key to design patterns such as the Template Method.
Abstraction in OOP is most often expressed through abstract classes and interfaces, two related but distinct constructs. An abstract class is one that cannot be instantiated and may contain both abstract methods without implementation and concrete methods with full behavior. A typical Java example declares an abstract Shape class with an abstract area method that subclasses must implement, alongside a concrete describe method that provides shared behavior. Abstract classes are best used when subclasses share common code but must also implement specific behaviors.
An interface, by contrast, defines a contract of methods that implementing classes must provide, traditionally without any state. In Java, a class can implement multiple interfaces but extend only one abstract class. Methods in interfaces are implicitly public and abstract, although modern interfaces since Java 8 may also include default and static methods with concrete implementations. A Drawable interface, for example, declares a draw method that any implementing class, whether Circle or Rectangle, must provide on its own, specifying what a class does without revealing how.
The choice between an interface and an abstract class depends on what the design requires. Interfaces are well suited for defining pure contracts and capabilities without shared state, especially when classes need to combine several different roles. Abstract classes shine when there is shared state, common concrete behavior, constructors, or carefully chosen access modifiers to pass down to subclasses. The two are complementary tools in the OOP toolkit, each suited to a particular kind of abstraction.
Both composition and inheritance are ways to build classes out of other classes, but they capture fundamentally different relationships. Inheritance models an is-a relationship, as in Dog is an Animal, while composition models a has-a relationship, as in Car has an Engine. The widely cited principle to favor composition over inheritance reflects the fact that composition provides greater flexibility and avoids many of the pitfalls of inheritance hierarchies.
Composition is generally preferred for several reasons. It produces loose coupling, allowing components to be swapped at runtime. It sidesteps the fragile base class problem, where seemingly safe changes to a parent class unintentionally break subclasses. It offers greater flexibility by allowing a class to combine behaviors from multiple sources, and it avoids the deep, hard-to-understand hierarchies that complex inheritance trees tend to create. Inheritance remains the right choice only when a true is-a relationship genuinely exists and the behavioral contract is well understood.
Several modern mechanisms exist for code reuse that sidestep the limitations of single inheritance. Mixins are classes that provide reusable methods to other classes without serving as a standalone base, exemplified in Python by a JsonMixin that adds to_json behavior to any class that mixes it in. Traits are similar reusable sets of methods used in languages like Scala, PHP, and Rust, with explicit mechanisms for resolving conflicts when multiple traits provide the same method. Both approaches promote code reuse without forcing deep inheritance hierarchies.
Multiple inheritance, the ability for a class to inherit from more than one parent class, is supported in languages like C++ and Python but disallowed in Java and C#. Its main risk is the diamond problem, which arises when a class inherits from two classes that share a common ancestor, creating ambiguity about which version of an inherited method should be used. Languages solve this problem in different ways: Python uses C3 linearization through its method resolution order, C++ offers virtual inheritance, and Java sidesteps the issue entirely by disallowing multiple class inheritance and relying on interfaces instead. The fragile base class problem, where changes to a base class unexpectedly break subclasses, is another reason to favor composition, mark classes as final when not designed for extension, and document which methods are safe to override.
Beyond the SOLID principles, several general design principles guide object-oriented programming toward clearer, more maintainable code. The DRY principle, Don't Repeat Yourself, holds that every piece of knowledge should have a single authoritative representation in the system. Copy-pasted logic, duplicated validation rules, and repeated SQL queries are all violations; the fix is to extract shared logic into reusable methods, base classes, or utility functions so that any change happens in one place. The KISS principle, Keep It Simple Stupid, argues that systems work best when kept simple rather than made complex, counseling against over-engineering and premature abstraction in favor of straightforward, readable code. Closely related, the YAGNI principle, You Aren't Gonna Need It, warns against implementing functionality until it is actually needed, discouraging unused configuration options, abstract factories for a single implementation, and features built for hypothetical future requirements.
Coupling and cohesion describe two complementary qualities of well-designed systems. Coupling measures the degree of interdependence between modules: tight coupling, where classes depend heavily on each other's internals, makes code hard to change and test, while loose coupling, where classes interact through abstractions, enables modification and independent testing. Cohesion measures how closely related the responsibilities within a single module are: high cohesion means a class does one well-defined job, like an EmailSender focused solely on sending email, while low cohesion means a class handles unrelated tasks. The combined goal is to minimize coupling and maximize cohesion.
The Law of Demeter, also called the Principle of Least Knowledge, refines the goal of loose coupling with a specific rule: a method should only talk to its immediate friends, not strangers. An expression like order.getCustomer().getAddress().getCity() reaches deep into unrelated objects and violates this principle; the fix is to delegate the responsibility with a method like order.getShippingCity(). The principle formally states that a method may only invoke methods on itself, on its parameters, on objects it creates, and on its own direct fields. Closely related is Design by Contract, introduced by Bertrand Meyer, which formalizes the agreements between a method and its callers through preconditions that must hold before the method is called, postconditions it guarantees afterward, and invariants that must always hold for the class as a whole. A withdraw method, for example, might require a positive amount as a precondition and ensure a non-negative balance as a postcondition.
Finally, dependency injection is a practical technique that supports the Dependency Inversion Principle by providing an object's dependencies externally rather than constructing them internally. In constructor injection, dependencies are passed through the constructor; in setter injection, they are assigned through a setter method; and in interface injection, the dependency is supplied through a dedicated injector interface. Combined with the design principles above, dependency injection produces code that is testable, flexible, and easy to maintain as systems grow.
public — accessible from anywhereprivate — accessible only within the declaring classprotected — accessible within the class and its subclassesTeacher ↔ StudentLibrary → BookHouse → Roomfinal when not designed for extensionclass Car { String color; void drive() { ... } }BufferedReader br = new BufferedReader(new FileReader("file.txt")) adds buffering to file reading.final fields and no setters. In Python, use __slots__ or frozen dataclasses.AutoCloseable at the end of the statement. Example: try (BufferedReader br = new BufferedReader(...)) { ... }. Resources are closed in reverse order of declaration.String[] is subtype of Object[] in Java). Contravariance: reversing the order (used in generics with ? super T). Covariant return types allow a subclass method to return a more specific type.Drill this topic
100 flashcards on Oop Principles — free, no signup needed to start.
Study Oop Principles flashcardsLearnWiki pages are generated with AI assistance from LearnCoachAssist's reviewed study catalog and may contain errors — verify anything critical against your course materials.