Are OOP Design Principles actually useful, or just unnecessary complexity?
Let’s break them down and see.
1) Encapsulation
Encapsulation simply means:
Each object is responsible for managing and protecting its own state.
It hides internal details and enforces business rules so invalid data never enters the system.
For example:
Can an employee have a negative salary?
Logically no — unless the company is deducting loans or something unusual.
So the Employee object should prevent setting a negative salary unless explicitly allowed by business rules.
Can a person’s age be negative?
Of course not. So the Person object should reject invalid age values.
These validations are part of business logic and business rules.
Encapsulation ensures that each object protects its own data instead of relying on external code to behave correctly.
2) Inheritance
Inheritance is about grouping related objects and reusing shared logic.
If multiple classes share common behavior, inheritance can help reduce duplication.
Example:
A base PaymentMethod class with shared logic, and subclasses like:
CreditCardPayment
WalletPayment
BankTransferPayment
However — if you find yourself overriding most of the parent class methods, that’s a red flag 🚩
It usually means your design is wrong.
That’s why modern design advice says:
“Favor composition over inheritance.”
3) Polymorphism
As Dr. Ayman Ezzat jokingly says:
"Like a chameleon — it changes behavior depending on where you put it."
Polymorphism means the same method behaves differently based on the object implementing it.
Example:
All payment providers expose a method:
processPayment(amount)
But:
Stripe processes it one way
PayPal processes it another way
Cash on delivery processes it differently
Same method name. Different behavior.
Yet the system calls them all the same way.
4) Abstraction
Abstraction goes hand-in-hand with polymorphism.
There’s a famous quote:
“We design for abstraction, not for concrete implementations.”
Meaning:
We depend on interfaces or abstract definitions, not on specific classes.
This keeps systems flexible and easy to change.
Example:
Your checkout system depends on a PaymentGateway interface —
not on StripePaymentGateway directly.
Tomorrow, if you switch to another provider, minimal code changes are needed.
Final Thought For OOP, All these principles work together to:
- Reduce bugs
- Improve maintainability
- Make systems easier to extend
- Keep business rules safe
Used correctly, OOP principles don’t complicate your code —
they save you from future pain as a software engineer.
Why do we need SOLID if we already have OOP?
Think of it this way:
OOP gives you the tools. SOLID teaches you how to use them correctly.
Let’s quickly go through each principle and what it actually achieves.
S — Single Responsibility Principle
Each unit should have only one responsibility.
And by unit, we mean:
A method
A class
A module / namespace / package
The deeper you go (method level), the smaller the responsibility.
The higher you go (module or system level), the broader the responsibility.
Example:
Bad design:
UserService:
- Validate user data
- Save user to database
- Send welcome email
Good design:
UserValidator → validates data
UserRepository → handles database
EmailService → sends emails
Each unit has one clear reason to change.
O — Open/Closed Principle
A unit should be:
Open for extension & Closed for modification
If you got confused — here’s the simple rule:
Changing a contract (interface / public API) = Bad modification
Because everyone depending on you will break.
Changing internal implementation = Acceptable
Because no external code is affected.
Example:
Instead of:
if (paymentType == "stripe") ...
if (paymentType == "paypal") ...
Use:
PaymentGateway interface
StripeGateway implements it
PaypalGateway implements it
To add a new provider, you extend the system — without modifying existing code.
L — Liskov Substitution Principle
If you replace a parent object with any of its child objects,
the system should behave correctly without breaking.
If it breaks — your inheritance design is wrong 😄
Example:
If Bird has a method fly(),
and you create Penguin extends Bird,
but penguins can’t fly — you’ve violated LSP.
Better design:
Bird
FlyingBird extends Bird
Penguin extends Bird (no fly method)
I — Interface Segregation Principle
Don’t force classes to implement methods they don’t need.
Example:
Bad:
interface Worker {
work()
eat()
}
RobotWorker doesn’t eat — so it implements an empty method.
Good:
interface Workable { work() }
interface Eatable { eat() }
Each class implements only what it actually needs.
D — Dependency Inversion Principle
Always depend on abstractions, not concrete implementations.
High-level modules should not depend on low-level modules.
Both should depend on abstractions.
Example:
Bad:
class OrderService {
StripePaymentGateway gateway = new StripePaymentGateway();
}
Good:
class OrderService {
PaymentGateway gateway; // interface
}
Now you can inject any implementation without changing OrderService.
Final Thought:
- OOP is the language.
- SOLID is the grammar.
Finally lets bring a business use case
But first you must have a better knowledge, Static Factory Method Pattern, its a simple design pattern that's initiate an object in the runtime for the requested type
Use case:
As a product owner I'm asking you to implement a payment gateway factory to collect the order amount (due amount) from our customers depending on his selected/preferred method, but keep in mind we need it to be future scalable by adding/removing providers without affect other providers.
FYI the current providers are
- Stripe
- Paypal
- COD
Our needed contract something like that:
const gateway = PaymentFactory.create("stripe");
gateway.pay(100);
Without if/else everywhere. And easy to extend/modify later.
First lets provide the interface:
interface PaymentGateway {
pay(amount: number): void;
}
Second lets provide the concrete classes:
class StripeGateway implements PaymentGateway {
pay(amount: number): void {
console.log(`💳 Paid ${amount}$ using Stripe`);
}
}
class PaypalGateway implements PaymentGateway {
pay(amount: number): void {
console.log(`💰 Paid ${amount}$ using PayPal`);
}
}
class CashOnDeliveryGateway implements PaymentGateway {
pay(amount: number): void {
console.log(`📦 Cash on Delivery: ${amount}$ will be collected`);
}
}
Third lets provide the factory method:
class PaymentFactory {
static create(provider: string): PaymentGateway {
switch (provider) {
case "stripe":
return new StripeGateway();
case "paypal":
return new PaypalGateway();
case "cod":
return new CashOnDeliveryGateway();
default:
throw new Error("Payment provider not supported");
}
}
}
Now our client (code that's call our payment gateway) will invoke the
PaymentFactory only as we mentioned at the beginning of our use case.
At our current state we already covered all OOP principles:
- Encapsulation → Each gateway hides its internals
- Polymorphism → Same pay() different behavior
- Abstraction → Code depends on PaymentGateway
And of course we already keep our code compatible with the SRP.
Lets say that's our PO asks to add a new provider (e.g. Apple Pay),
our goal to avoid modification as we can (The OCP) is mandatory here to non break the whole system.
lets add the provider:
class ApplePayGateway implements PaymentGateway {
pay(amount: number): void {
console.log(`🍏 Paid ${amount}$ using Apple Pay`);
}
}
and of course we need to register it in our Static Factory Method
by adding this case:
case "applepay":
return new ApplePayGateway();
That’s it.
No existing business logic touched.
Open for extension, minimal modification.
Final Thought
This is exactly how static factory + SOLID + OOP come together in real production systems — especially in payment orchestration platforms.
Top comments (0)