التطبيق العملي لمبادئ SOLID: كود نظيف وقابل للتطوير دون مبالغة في التجريد
كيف تطبق مبادئ SOLID بطريقة واقعية تحافظ على سهولة القراءة والتعديل وتتجنب فخ التجريد المبكر والتعقيد غير المبرر.
🧠 SOLID vs Overengineering
<img src="https://cdn.simpleicons.org/solid/222222" alt="SOLID principles" width="110"/> <img src="https://img.shields.io/badge/Design-SOLID-blue" alt="SOLID"/> <img src="https://img.shields.io/badge/Architecture-Overengineering-orange" alt="Overengineering"/> <img src="https://img.shields.io/badge/Principle-Keep%20It%20Simple-green" alt="Keep It Simple"/>أول ما تتعلم SOLID...
IUserService→IUserRepository→IUserManager→IUserProvider→IUserFactory→IUserWhatever😂
المشكلة مش في SOLID. المشكلة لما نحوله من guidelines إلى religion.
</div>📚 Table of Contents
- الفكرة في دقيقة
- SOLID مش قوانين فيزيائية
- ليه الناس بتعمل Interface لكل حاجة؟
- Single Responsibility Principle
- Open/Closed Principle
- Liskov Substitution Principle
- Interface Segregation Principle
- Dependency Inversion Principle
- Abstraction Has a Cost
- Design for Change vs Imaginary Problems
- Overengineering
- Accidental Complexity
- When an Interface Is Actually Useful
- When an Interface Is Just Noise
- Practical Example
- Refactoring Example
- Decision Framework
- Common Misconceptions
- Production Checklist
- الخلاصة
- Further Reading
⚡ الفكرة في دقيقة
أول ما أي حد يتعلم:
SOLID
ممكن فجأة يحس إن كل class لازم يكون حواليه:
Interface
Abstract class
Factory
Strategy
Manager
Provider
Repository
Service
Adapter
😂
فتلاقي:
IUserService
↓
UserService
↓
IUserRepository
↓
UserRepository
↓
IUserProvider
↓
UserProvider
والـfeature نفسها:
return users;
لكن عشان توصل لها:
Controller
↓
Service
↓
Interface
↓
Factory
↓
Provider
↓
Repository
↓
Database
السؤال هنا:
هل كل abstraction أضاف قيمة فعلًا؟
مش كل مرة.
🧠 SOLID مش قوانين فيزيائية
SOLID عبارة عن مجموعة مبادئ تساعدك تصمم software قابل للفهم والتغيير والصيانة.
لكن:
SOLID = Principles / Guidelines
مش:
Law
ومش:
Religion
ومش:
"لو عندي class من غير interface يبقى أنا bad developer"
😂
الهدف مش إن الكود يحقق أكبر عدد من patterns.
الهدف:
Readable
+
Maintainable
+
Changeable
+
Testable
بتكلفة منطقية.
🧩 ليه الناس بتعمل Interface لكل حاجة؟
سبب شائع جدًا:
الشخص يتعلم:
Dependency Inversion
فيستنتج:
Every dependency
↓
Must be interface
لكن ده مش معنى DIP.
مثلاً:
class UserService {
constructor(
private readonly repository: IUserRepository
) {}
}
ممكن يكون ممتاز.
لكن لو عندك:
interface IUserRepository {
findById(id: string): User;
}
class UserRepository implements IUserRepository {
findById(id: string) {
// ...
}
}
والحقيقة:
One implementation
+
No meaningful boundary
+
No expected variation
+
No architectural benefit
فممكن تكون ضفت:
2 files
+
indirection
+
navigation cost
بدون benefit حقيقي.
💰 Abstraction Has a Cost
كل abstraction لها تكلفة.
مش بس:
Runtime cost
لكن كمان:
🧠 Cognitive Cost
المطور محتاج يفهم layers أكتر.
🧭 Navigation Cost
عشان تعرف implementation لازم تعدي على:
Interface
↓
Implementation
🐛 Debugging Cost
Stack traces وcall chains ممكن تبقى أطول.
👨💻 Onboarding Cost
Developer جديد محتاج يفهم architecture قبل ما يغير سطر.
🔧 Maintenance Cost
كل layer تحتاج update أو naming أو documentation.
📊 Abstraction Trade-off
| Benefit | Cost |
|---|---|
| Easier substitution | More indirection |
| Better isolation | More files |
| Better testing seams | More setup |
| Encapsulated boundaries | More concepts |
| Supports future variation | Potential speculative complexity |
| Clear architecture | Navigation overhead |
الهدف:
Benefit > Cost
مش:
Abstraction = Good
1️⃣ Single Responsibility Principle
SRP غالبًا يتفهم غلط.
مش معناه:
"كل class لازم يعمل حاجة واحدة فقط."
الأدق:
A module should have one reason to change.
يعني لو عندك:
class UserService {
createUser() {}
updateUser() {}
deleteUser() {}
sendWelcomeEmail() {}
generatePdf() {}
exportExcel() {}
}
ممكن يكون عندك أكثر من responsibility متداخلة.
لكن مش معنى كده إنك تعمل:
CreateUserService
UpdateUserService
DeleteUserService
SendWelcomeEmailService
GenerateUserPdfService
ExportUserExcelService
لمجرد إن كل method لازم يكون في class لوحده.
مرة أخرى:
Principle ≠ mechanical rule.
2️⃣ Open/Closed Principle
الـOCP:
Software entities should be open for extension, but closed for modification.
ناس كتير تفهمها:
❌ Never modify existing code.
وده مش المقصود.
الواقع:
Business changes
↓
Existing code sometimes MUST change
مثال:
function calculateShipping(order) {
if (order.country === "EG") {
// ...
}
if (order.country === "SA") {
// ...
}
}
لو كل أسبوع بتضيف country:
EG
SA
AE
US
UK
DE
...
ممكن abstraction مناسب يقلل التعديل المستمر.
لكن لو عندك:
2 countries
+
Stable requirements
+
No expected variation
مش لازم تعمل:
ShippingStrategy
ShippingFactory
ShippingProvider
ShippingResolver
ShippingRegistry
عشان احتمال feature تيجي بعد سنتين.
🧠 Design for Change vs Imaginary Problems
دي من أهم الأفكار.
في فرق بين:
Design for Change
أنت شايف variation حقيقي.
مثلاً:
Payment Provider
وعندك فعلًا:
Stripe
PayPal
Fawry
هنا abstraction ممكن تكون منطقية جدًا.
Design for Imaginary Problems
أنت بتقول:
"يمكن بعد 3 سنين نستخدم provider تاني."
فتعمل:
IPaymentProvider
PaymentProviderFactory
PaymentProviderStrategy
PaymentProviderResolver
PaymentProviderRegistry
مع إن عندك:
One provider
One use case
No evidence of variation
دي غالبًا:
Speculative abstraction
3️⃣ Liskov Substitution Principle
LSP ببساطة:
لو
Bsubtype منA، المفروض تقدر تستخدمBمكانAبدون ما تكسر توقعات النظام.
مثال مشهور:
Bird
├── Sparrow
└── Penguin
لو Bird فيها:
fly()
فـPenguin inheritance تصبح مشكلة.
لأن:
const bird: Bird = new Penguin();
bird.fly();
التصميم نفسه بيقول إن كل Bird يقدر fly، لكن الواقع لا.
المشكلة هنا ليست:
"مفيش interface."
المشكلة:
Wrong abstraction.
4️⃣ Interface Segregation Principle
الـISP يقول بشكل مبسط:
Clients should not be forced to depend on methods they don't use.
بدل:
interface IUserOperations {
create(): void;
update(): void;
delete(): void;
exportPdf(): void;
sendEmail(): void;
}
ممكن boundaries أصغر تكون أفضل لو clients مختلفة تحتاج subsets مختلفة.
لكن برضه:
مش معنى ISP إنك تعمل interface لكل method.
مش:
ICreateUser
IUpdateUser
IDeleteUser
ISendUserEmail
إلا لو فيه سبب حقيقي.
5️⃣ Dependency Inversion Principle
DIP من أكثر المبادئ اللي يحصل فيها overengineering.
الفكرة الأساسية:
High-level policy should not depend directly on low-level details. Both should depend on abstractions.
مثال:
OrderService
↓
Payment Gateway
لو business logic مربوط مباشرة بـimplementation محدد:
class OrderService {
private stripe = new Stripe();
}
فأنت ربطت high-level policy بتفصيل infrastructure.
ممكن abstraction هنا تكون مفيدة:
OrderService
↓
PaymentGateway
↑
┌───┴────┐
Stripe PayPal
لكن لو dependency نفسها stable ومش محتاجة boundary إضافية، abstraction قد تكون غير ضرورية.
🎯 Interface مش الهدف
الـInterface مجرد tool.
استخدمه لما تحتاج:
Boundary
+
Substitution
+
Isolation
+
Multiple implementations
+
Dependency inversion
+
Plugin architecture
+
External integration seam
مش لمجرد:
"أنا بتبع SOLID."
✅ When an Interface Is Actually Useful
1. Multiple Implementations
مثلاً:
Storage
├── S3Storage
├── CloudinaryStorage
└── LocalStorage
هنا:
interface Storage {
upload(file: File): Promise<string>;
}
ممكن تكون مفيدة جدًا.
2. External Providers
مثلاً:
PaymentGateway
├── Stripe
├── PayPal
└── Fawry
هنا boundary واضحة.
3. Infrastructure Boundary
مثلاً:
Business Logic
↓
EmailSender
↑
┌────┴─────┐
SMTP SES
الـbusiness logic لا يحتاج معرفة التفاصيل.
4. Plugin Architecture
لو النظام مصمم أصلًا ليستقبل plugins:
Core
↓
Plugin Interface
├── Plugin A
├── Plugin B
└── Plugin C
interface هنا جزء من architecture.
5. Testing Boundary
أحيانًا abstraction مفيدة لتغيير dependency أثناء الاختبارات.
لكن:
مش كل class يحتاج mock.
وأحيانًا integration test أو in-memory implementation يكون أفضل من mock-heavy architecture.
❌ When an Interface Is Just Noise
لو عندك:
One implementation
+
No expected variation
+
No meaningful boundary
+
No plugin system
+
No testing requirement
فممكن يكون:
class UserRepository {
findById(id: string) {
// ...
}
}
أفضل من:
IUserRepository
↓
UserRepository
مش لأن interface "غلط".
لكن لأن:
لا يوجد سبب قوي لها الآن.
🏗️ Practical Example
Overengineered Version
UserController
↓
IUserService
↓
UserService
↓
IUserManager
↓
UserManager
↓
IUserProvider
↓
UserProvider
↓
IUserRepository
↓
UserRepository
↓
IUserDataSource
↓
MongoUserDataSource
↓
MongoDB
وعندك feature:
GET /users/5
😂
✨ Simpler Version
UserController
↓
UserService
↓
UserRepository
↓
MongoDB
لو ده بالفعل كل اللي النظام محتاجه:
البساطة هنا ممكن تكون design أفضل.
🔄 Refactoring Example
بدل:
interface IUserService {
getUser(id: string): Promise<User>;
}
class UserService implements IUserService {
constructor(
private readonly repository: IUserRepository
) {}
getUser(id: string) {
return this.repository.findById(id);
}
}
مع:
IUserService
IUserRepository
UserService
UserRepository
ممكن تبدأ بـ:
class UserService {
constructor(
private readonly repository: UserRepository
) {}
getUser(id: string) {
return this.repository.findById(id);
}
}
ثم تضيف abstraction عندما يظهر سبب حقيقي.
دي فكرة مهمة:
You don't have to predict every future change.
🧠 "But What About Testing?"
دي حجة مشهورة:
"لازم interface عشان أعمل mock."
مش دائمًا.
ممكن تستخدم:
- Integration tests
- In-memory implementations
- Test containers
- Dependency injection
- Function-level mocking عند الحاجة
- Framework test utilities
والأهم:
Architecture لا يجب أن تُبنى بالكامل حول سهولة mocking.
اختار testing strategy مناسبة للـsystem.
🪜 Progressive Architecture
ممكن تبني architecture تدريجيًا:
Simple Code
↓
Real Change Appears
↓
Repeated Variation
↓
Extract Boundary
↓
Introduce Abstraction
↓
Multiple Implementations
بدل:
Imagine 20 future problems
↓
Build 20 abstractions
↓
Hope they become useful
🧩 Rule of Three
قاعدة عملية مفيدة:
Don't abstract the first example.
لو عندك implementation واحدة:
A
استنى.
ظهر variation تاني:
A
B
ابدأ تلاحظ common behavior.
ظهر الثالث:
A
B
C
غالبًا abstraction أصبحت أوضح.
مش قانون ثابت، لكن heuristic مفيد.
🧠 Abstraction Should Follow Understanding
أحيانًا أفضل abstraction لا تظهر من أول design.
تحتاج تشوف:
Real implementations
+
Real use cases
+
Real changes
وبعدها تستخرج:
Common concept
وده يقلل خطر إنك تعمل abstraction بناءً على تخمين.
🌀 Overengineering
Overengineering مش معناه:
"Architecture كبيرة."
المشكلة لما:
Complexity
>
Problem complexity
يعني عندك problem بسيطة:
CRUD users
وتبني:
Hexagonal Architecture
+
CQRS
+
Event Sourcing
+
Repository abstractions
+
Factory
+
Strategy
+
Mediator
+
Domain Events
+
20 interfaces
ممكن يكون فيه systems تحتاج بعض ده.
لكن السؤال:
هل المشكلة نفسها تستحقه؟
📈 Complexity Budget
كل system عنده complexity budget.
ممكن تفكر فيها كده:
Business Complexity
+
Technical Complexity
+
Team Complexity
+
Operational Complexity
لو الـbusiness بسيط، متضيفش technical complexity بدون سبب.
لو الـbusiness معقد جدًا، abstraction architecture قد تكون ضرورية.
🧠 Accidental Complexity
في فرق بين:
Essential Complexity
التعقيد اللي المشكلة نفسها تفرضه.
مثلاً:
Multi-tenant SaaS
+
Payments
+
Permissions
+
Distributed Systems
ده inherently complex.
Accidental Complexity
تعقيد أنت ضفته بنفسك.
مثلاً:
One database
+
One payment provider
+
Simple CRUD
ثم:
10 layers
+
15 interfaces
+
5 patterns
هنا ممكن تكون صنعت المشكلة بنفسك.
⚖️ Simple vs Flexible
مش دايمًا:
Simple = Better
ومش دايمًا:
Flexible = Better
الأصح:
Right amount of complexity
مثلاً:
| Situation | غالبًا الأنسب |
|---|---|
| Small CRUD | Simple architecture |
| Stable business rules | Minimal abstractions |
| Multiple providers | Explicit boundaries |
| Large team | Strong module boundaries |
| Plugin system | Interfaces/contracts |
| High business complexity | More deliberate architecture |
| Rapidly changing domain | Design for known variation |
| Unknown future | Avoid speculative abstractions |
🧭 Decision Framework
قبل ما تعمل interface اسأل:
1. هل عندي أكثر من implementation؟
Yes → Strong reason
No → Continue thinking
2. هل أتوقع variation حقيقي؟
مش:
"يمكن يومًا ما."
لكن:
"الـrequirements بتقول إن ده غالبًا هيحصل."
3. هل abstraction تفصل business logic عن infrastructure؟
مثلاً:
Business
↓
Payment Gateway
↓
Stripe
ده boundary قوية.
4. هل abstraction هتقلل coupling فعلًا؟
لو:
Interface
↓
One class
ومافيش benefit، اسأل نفسك ليه.
5. هل هتسهل التغيير فعلًا؟
لو تغيير implementation أصبح أسهل:
Stripe → PayPal
ففيه value.
6. هل هتخلي الكود أوضح؟
لو الإجابة:
No
ممكن تكون abstraction premature.
🚦 A Practical Decision Tree
flowchart TD
A["Need a new abstraction?"] --> B{"Multiple implementations?"}
B -->|Yes| C["Strong candidate"]
B -->|No| D{"Real expected variation?"}
D -->|Yes| C
D -->|No| E{"Clear architectural boundary?"}
E -->|Yes| C
E -->|No| F{"Would it reduce coupling?"}
F -->|Yes| C
F -->|No| G["Keep it simple"]
C --> H{"Benefit > complexity cost?"}
H -->|Yes| I["Introduce abstraction"]
H -->|No| G
🔥 The Senior Mindset
Junior mindset أحيانًا:
"إزاي أطبق SOLID؟"
Mid-level mindset:
"إيه الـpattern المناسب؟"
Senior mindset:
"هل أنا محتاج pattern أصلًا؟"
وده فرق كبير.
المهندس الشاطر مش اللي يعرف يعمل:
Factory
Strategy
Adapter
Repository
Decorator
Observer
Mediator
المهندس الشاطر يعرف:
إمتى يستخدمهم وإمتى ما يستخدمهمش.
🧠 Design for Change
لو عندك requirement واضح:
Payment providers will be multiple
صمم للـchange.
لو عندك:
Maybe someday we use another provider
متبنيش architecture كاملة على "maybe".
🚨 Warning Signs of Overengineering
لو لقيت:
- Interface لها implementation واحدة دائمًا
- Factory تعمل
newلكلاس واحد - Strategy لها strategy واحدة
- Wrapper لا يضيف behavior
- Manager يستدعي Service فقط
- Provider يستدعي Repository فقط
- أكثر من layer لنفس العملية بدون boundary واضح
- أسماء classes أطول من الكود اللي جواها 😂
- تحتاج 8 ملفات لفهم function واحدة
قف واسأل:
إيه المشكلة اللي الـabstraction دي بتحلها؟
لو الإجابة:
"عشان SOLID"
فدي مش إجابة كافية.
🧪 Code Smell: Pass-Through Layers
مثال:
class UserController {
getUser(id: string) {
return this.userManager.getUser(id);
}
}
class UserManager {
getUser(id: string) {
return this.userProvider.getUser(id);
}
}
class UserProvider {
getUser(id: string) {
return this.userRepository.getUser(id);
}
}
لو كل layer:
receives
↓
passes through
↓
returns
بدون business logic أو boundary:
ممكن يكون عندك indirection أكثر من اللازم.
🧹 Refactoring Question
قبل ما تضيف abstraction:
"What problem does this solve?"
بعد ما تضيفها:
"Did it actually solve it?"
ولو بعد فترة اكتشفت:
No variation
No boundary
No benefit
ممكن تشيلها.
Removing unnecessary abstraction is also good engineering.
📊 SOLID بدون Fanaticism
| Principle | سوء الفهم | الفهم الأفضل |
|---|---|---|
| SRP | Class يعمل حاجة واحدة حرفيًا | One reason to change |
| OCP | ممنوع تعديل الكود | Structure for meaningful variation |
| LSP | أي inheritance عادي | Subtypes preserve behavioral contracts |
| ISP | Interface لكل method | Clients depend only on what they need |
| DIP | كل dependency لازم interface | Depend on stable abstractions where useful |
🧠 SOLID + KISS + YAGNI
الثلاثة ممكن يشتغلوا مع بعض:
SOLID
Design principles
KISS
Keep It Simple
YAGNI
You Aren't Gonna Need It
مش لازم تختار:
SOLID OR Simple
الأصح:
SOLID
+
KISS
+
YAGNI
↓
Pragmatic Design
🎯 الخلاصة
SOLID مهم جدًا.
لكن:
SOLID مش هدف في حد ذاته.
الهدف:
Understandable Code
+
Maintainable Code
+
Appropriate Flexibility
+
Reasonable Complexity
كل abstraction لها cost.
قبل ما تعمل:
IUserWhatever
اسأل:
Why does this abstraction exist?
هل عندك:
Multiple implementations?
Real variation?
Clear boundary?
Coupling problem?
Testing/integration seam?
Plugin architecture?
لو آه:
اعملها.
لو لا:
الكود البسيط ممكن يكون التصميم الأفضل.
وأهم فرق بين:
Design for Change
و:
Design for Imaginary Problems
هو إن الأول مبني على evidence.
والثاني مبني على:
"يمكن بعد سنتين..." 😂
🏆 Final Mental Model
GOOD DESIGN
│
┌───────────┼───────────┐
▼ ▼ ▼
SOLID KISS YAGNI
│ │ │
└───────────┼───────────┘
▼
Pragmatic Balance
│
┌───────────┴───────────┐
▼ ▼
Real Complexity Imaginary Complexity
│ │
▼ ▼
Add abstraction Keep it simple
Senior engineering is not about maximizing abstraction.
It's about maximizing useful simplicity.
📚 Further Reading
SOLID
- Robert C. Martin — The Principles, Patterns, and Practices of Agile Software Development
- Martin Fowler — Refactoring
- Martin Fowler — YAGNI
Design & Complexity
- Martin Fowler — Is Design Dead?
- Martin Fowler — You Ain't Gonna Need It
- Martin Fowler — Technical Debt
Software Design
<div align="center">
🧠 SOLID gives you tools.
Experience teaches you when not to use them.
</div>منشورات مقترحة
مشاريع ذات صلة
منصة عقارية متكاملة تدعم دورة حياة الإعلانات، وإشعارات الواتساب المباشرة، والبحث الجغرافي بالخريطة في القاهرة والجيزة.
منظومة تشغيلية متكاملة لشركات السياحة لإدارة الرحلات والحجوزات وإصدار الفواتير متعددة العملات وكشوف المسافرين.
منصة نادي سفر خاص قائمة على العضوية تجمع بين اكتشاف الفنادق الفاخرة المنتقاة، والأسعار المحمية للأعضاء، وطلبات الحجز المنظمة، ودورة عمل تشغيلية بمساعدة مستشار سفر مخصص.