التطبيق العملي لمبادئ 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>منشورات مقترحة
مشاريع ذات صلة

- نكست جي إس
- ريأكت
- نود جي إس
- إكسبريس
- مونغو دي بي
- سوكيت أي أوه
- تايلويند سي إس إس
- تايب سكريبت
- CSS3
- مونجوز
- بي إم 2
- توثيق الهوية عبر JWT
- كلوديناري
- بيليز
منصة عقارية متكاملة تدعم دورة حياة الإعلانات، وإشعارات الواتساب المباشرة، والبحث الجغرافي بالخريطة في القاهرة والجيزة.

- نست جي إس
- تايب سكريبت
- بريزما
- مونغو دي بي
- ريأكت
- تايلويند سي إس إس
- زود
- نود جي إس
- إكسبريس
- CSS3
- نكست جي إس
- مونجوز
- بي إم 2
- توثيق الهوية عبر JWT
- كلوديناري
منظومة تشغيلية متكاملة لشركات السياحة لإدارة الرحلات والحجوزات وإصدار الفواتير متعددة العملات وكشوف المسافرين.

- نكست جي إس
- ريأكت
- تايب سكريبت
- تايلويند سي إس إس
- فرامير موشن
- زوستاند
- زود
- نود جي إس
- توثيق الهوية عبر JWT
- إكسبريس
- CSS3
- بوستغريس كيو إل
- بريزما
- إنجن إكس
- بي إم 2
- كلوديناري
- سوكيت أي أوه
- نست جي إس
- ريدكس تولكيت
منصة نادي سفر خاص قائمة على العضوية تجمع بين اكتشاف الفنادق الفاخرة المنتقاة، والأسعار المحمية للأعضاء، وطلبات الحجز المنظمة، ودورة عمل تشغيلية بمساعدة مستشار سفر مخصص.