What's changed: Initial version
3.2Object-oriented design and UML
Covers the three pillars of object-oriented design—encapsulation, inheritance, and polymorphism—the SOLID principles for change-resilient design (single responsibility, open/closed, Liskov substitution, interface segregation, dependency inversion), and how to choose among UML diagrams (class, sequence, state-transition) to visualize different design decisions, together with judging how to respond to a real requirement change.
The value of object-oriented design lies not in "creating classes" per se, but in how little existing code needs to be touched when a future requirement changes. What matters to a system architect is being able to judge how to combine encapsulation, inheritance, and polymorphism for change resilience, and to spot which SOLID principle is being violated when a design has become rigid.
3.2.1The three pillars of OO
- Encapsulation bundles data with the operations that act on it into a single object, preventing external code from directly manipulating internal state and forcing access only through a published interface (methods). The goal is to let internal implementation change without affecting callers.
- Inheritance lets a new class (child) take on the properties of an existing class (parent) and add or override only the differences. It is useful for reusing common processing, but because a change to the parent class ripples to every child class, undisciplined multi-level inheritance has the side effect of raising coupling.
- Polymorphism lets the same interface (method call) produce different behavior depending on the actual type of the object. Because the caller does not need to be aware of the concrete type, its greatest benefit is that adding a new type (behavior) requires no change to the caller's code.
3.2.2The SOLID principles
| Principle | Gist |
|---|---|
| Single Responsibility (SRP) | A class should have only one reason to change. A class with multiple concerns risks a change to one concern unintentionally affecting the other. |
| Open/Closed (OCP) | A module should be **open for extension but closed for modification**. New behavior should be added by adding new classes, not by rewriting the internals of existing ones. |
| Liskov Substitution (LSP) | A variable of the parent type must behave correctly no matter which child-class instance it is substituted with. A child class must not break the parent's contract (preconditions/postconditions). |
| Interface Segregation (ISP) | Clients should not be forced to depend on methods they do not use. Prefer **several small, purpose-specific interfaces over one large general-purpose interface**. |
| Dependency Inversion (DIP) | High-level modules should not depend directly on low-level concrete implementations; both should **depend on an abstraction (interface)**, keeping implementation details swappable later. |
Most-tested: "the open/closed principle means open for extension, closed for modification," "the Liskov substitution principle means a child class must not break the parent's contract," and "the dependency inversion principle means high-level modules depend on abstractions, not low-level concrete implementations." Note the direct link between polymorphism and the open/closed principle: adding a new type requires no change to the caller's existing code.
Suppose a system architect is reviewing the design of a class, PaymentProcessor, that handles payment processing. The current implementation lists per-method-type processing inside a single method using a chain of conditionals: if (type "credit") {...} else if (type "bank_transfer") {...} else if (type "e_wallet") {...}, so every time a new payment method (say, QR-code payment) is added, this method itself must be rewritten. This is a textbook violation of the open/closed principle (OCP)—because every extension (adding a new payment method) requires modifying the existing branching logic, each modification carries the risk of accidentally breaking the existing methods (credit card, bank transfer). The fix is to define a common interface, PaymentMethod (with a pay() method), and split the logic into concrete classes that leverage polymorphism—CreditCardPayment, BankTransferPayment, EWalletPayment. If PaymentProcessor merely calls pay() through a variable of type PaymentMethod (the dependency inversion principle (DIP): the high-level module, PaymentProcessor, depends on the abstraction PaymentMethod, not on low-level concrete implementations), then adding a new payment method (QR-code payment) requires only adding a new class, QrCodePayment—no existing code in PaymentProcessor needs to change at all. This satisfies the open/closed principle. That said, this redesign also requires care with the Liskov substitution principle (LSP): if EWalletPayment were to add behavior not part of PaymentMethod's contract—say, "always throws an exception once the payment amount exceeds a limit"—PaymentProcessor could no longer treat EWalletPayment the same as the other payment methods, breaking the premise of polymorphism. Whenever you see the symptom of "rewriting existing branching logic every time a requirement changes," the architect's job is to restructure the design using polymorphism-based decomposition and DIP to satisfy the open/closed principle, while keeping LSP intact.
3.2.3Choosing among UML diagrams
- A class diagram expresses the static structure among classes—attributes, operations, inheritance, associations, and dependencies. SOLID violations (a bloated interface, a direct dependency on a concrete class, etc.) can often be spotted just by looking at the association lines in a class diagram.
- A sequence diagram expresses the time-ordered exchange of messages between objects within a specific use case (scenario). Use it when you need to verify the dynamic flow of collaboration—which object calls which method, in what order.
- A state-transition diagram expresses the states a single object can take and the events that move it between states—for example, a payment's "unprocessed -> processing -> completed/failed." It suits designing and verifying objects where the state-transition rules themselves are complex or important.
Trap: "Making heavy use of inheritance to reduce code volume is always good design" is wrong—it creates coupling where a parent-class change ripples to every child, so composition (delegation leveraging polymorphism) is often preferable to inheritance for reusing common processing. Also wrong: "use a class diagram to verify the time-ordered exchange of messages between classes"—time-ordered collaboration is the role of a sequence diagram; a class diagram expresses static structure.
3.2.4Section summary
- Leveraging polymorphism lets you add a new type without changing the caller's existing code, making it easier to satisfy the open/closed principle (OCP)
- The dependency inversion principle (DIP) has high-level modules depend on an abstraction rather than a low-level concrete implementation; the Liskov substitution principle (LSP) requires that a child class not break the parent's contract
- Visualize static structure with a class diagram, time-ordered collaboration with a sequence diagram, and complex state-transition rules with a state-transition diagram
Sign in to track progress — Log in.
Quick check
(just a quick review)Q1. Per-payment-method processing is listed with if-else branches in one method, and every time a new payment method is added, this method is rewritten directly. Which principle is violated, and what is the most appropriate fix?
Q2. PaymentProcessor (a high-level module) was redesigned to depend on a common interface, PaymentMethod (an abstraction), rather than directly on the concrete payment-method classes (low-level modules). Which SOLID principle does this directly embody?
Q3. A payment-processing object has complex state-transition rules such as "unprocessed -> processing -> completed" and "unprocessed -> processing -> failed," and the design review needs to verify the validity of those rules themselves. Which UML diagram is most appropriate?

