في الجزء الأول من هذه السلسلة تحدثنا عن الأمن السيبراني، وعن ضرورة ألا نثق في الواجهة أو المستخدم أو الطلبات أو حتى مخرجات نموذج الذكاء الاصطناعي دون تحقق.
ثم انتقلنا في الجزء الثاني إلى قواعد البيانات والأداء، وتناولنا تصميم الجداول والعلاقات، والاستعلامات، والفهارس، والمعاملات، ومشكلات التزامن، وConnection Pooling، والتخزين المؤقت.
وفي الجزء الثالث تحدثنا عن الاعتمادية والاستعداد لبيئة الإنتاج: التوسع، وموازنات الحمل، والطوابير، وRetry، وCircuit Breaker، والمراقبة، وخطط التراجع واستعادة البيانات.
وبذلك وصلنا إلى سؤال آخر لا يقل أهمية:
ماذا عن الكود نفسه؟
قد يكون التطبيق آمنًا، وقاعدة بياناته مصممة بصورة جيدة، ولديه خطة للتعامل مع الأعطال، لكن الكود من الداخل أصبح متشابكًا إلى درجة أن إضافة ميزة صغيرة تؤدي إلى كسر ثلاث ميزات أخرى.
قد يحتوي المشروع على:
- ملفات ضخمة تقوم بعشرات المسؤوليات.
- قواعد عمل موزعة بين الواجهة والخادم.
- تكرار لنفس الحساب في أكثر من مكان.
- اعتماديات مرتبطة مباشرة ببعضها.
- شروط متداخلة يصعب تتبعها.
- أسماء غامضة لا توضح مقصد الكود.
- طبقات كثيرة لا يعرف أحد لماذا أُضيفت.
- أو كلاس واحد يعرف كل شيء عن النظام.
- الأمن السيبراني وحماية التطبيق — انتهينا منه في الجزء الأول.
- قواعد البيانات والأداء — انتهينا منه في الجزء الثاني.
- الاعتمادية والاستعداد للإنتاج — انتهينا منه في الجزء الثالث.
- كتابة كود نظيف وقابل للصيانة بدل كود السباغيتي — وهو موضوع هذا الجزء.
- اختبار ما بناه الذكاء الاصطناعي وإثبات جودته بالأدلة.
في هذا الجزء سننتقل من سؤال:
هل الميزة تعمل؟
إلى سؤال أكثر نضجًا:
هل يستطيع مطور آخر فهمها وتعديلها بعد ستة أشهر دون أن يخاف من كسر المشروع؟
١. سرعة الشهر الأول قد تصبح عبء الشهر الثالث
أحد أكبر أسباب انتشار الفايب كودنق أن الذكاء الاصطناعي يستطيع إنتاج كميات كبيرة من الكود بسرعة.
يمكنه إنشاء:
- شاشة كاملة.
- خدمة API.
- نموذج بيانات.
- عملية تسجيل دخول.
- ربط قاعدة بيانات.
- نظام إشعارات.
- أو ميزة جديدة داخل مشروع قائم.
النماذج الحديثة أصبحت أفضل في العمل داخل المشاريع الموجودة مسبقًا، وليس فقط في إنشاء مشاريع جديدة من الصفر. ومع زيادة قدرتها على استخدام الأدوات وتنفيذ التعديلات، قد ينشغل المطور بكتابة المتطلبات والتأكد من أن الميزة تعمل، بينما يقل الوقت الذي يقضيه في مراجعة طريقة بناء الكود نفسه.
في الشهر الأول تبدو النتيجة ممتازة:
- الميزات تُنجز بسرعة.
- الشاشات تظهر.
- الأزرار تعمل.
- العميل يرى تقدمًا يوميًا.
- وكل طلب جديد يتحول إلى كود خلال ساعات.
لكن الكود المولد لا يعيش منفصلًا.
كل تعديل جديد يُبنى فوق التعديلات السابقة.
إذا كان الأساس منظمًا، يستطيع الذكاء الاصطناعي مواصلة البناء عليه.
أما إذا كان الأساس يحتوي على قرارات خاطئة ومسؤوليات مختلطة، فسيبدأ النموذج في بناء طبقات جديدة فوق أرضية ضعيفة.
تصف مقالة Aikido هذه المشكلة بوضوح: تتراكم التغييرات المولدة بالذكاء الاصطناعي فوق تغييرات أخرى مولدة بالطريقة نفسها، وتفترض كل طبقة أن ما تحتها صحيح. وعندما لا يكون ذلك صحيحًا، تبدأ الأخطاء بالظهور في أماكن غريبة، وتصبح التعديلات الصغيرة سببًا في آثار جانبية يصعب تتبعها.
بعد عدة أشهر قد تلاحظ أن:
- تعديل اسم حقل يكسر عدة شاشات.
- إضافة حالة جديدة للطلب تتطلب تعديل عشرة ملفات.
- تغيير مزود دفع يحتاج إلى إعادة كتابة منطق الطلبات.
- إصلاح Bug في مكان يؤدي إلى ظهور Bug في مكان آخر.
- المساعد البرمجي يكرر حلولًا مختلفة للمشكلة نفسها.
- ولا تعرف أين توجد النسخة المعتمدة من قاعدة العمل.
السرعة التي كانت مرتفعة في البداية تبدأ بالانخفاض، لأن كل تغيير جديد يحمل وزن جميع القرارات السابقة.
وهنا تظهر المفارقة:
استخدمنا الذكاء الاصطناعي لتسريع التطوير، ثم أصبح الكود الذي أنتجه هو ما يبطئ إضافة الميزات.
٢. ما هو كود السباغيتي؟
مصطلح Spaghetti Code يصف كودًا أصبحت العلاقات داخله متشابكة مثل خيوط السباغيتي.
لا يعني ذلك أن الكود مكتوب بلغة معينة، أو أنه لا يستخدم Classes، أو أنه لا يحتوي على مجلدات منظمة.
قد يكون المشروع مليئًا بأسماء مثل:
controllers
services
repositories
use_cases
managers
helpers
ومع ذلك يبقى كود سباغيتي.
المشكلة ليست في أسماء المجلدات، بل في طريقة تدفق المسؤوليات والاعتماديات.
من علامات كود السباغيتي:
- الواجهة تتصل بقاعدة البيانات مباشرة.
- قواعد العمل مكتوبة داخل أزرار الشاشة.
- الملف الواحد يعرض الواجهة ويرسل البريد ويحسب السعر ويحفظ البيانات.
- كل Feature تستطيع الوصول إلى التفاصيل الداخلية لبقية Features.
- تغيير كلاس واحد يحتاج إلى تعديل عدد كبير من الكلاسات.
- المنطق نفسه مكرر في أكثر من مكان.
- الأخطاء تُبتلع أو تتحول جميعها إلى رسالة واحدة.
- لا يمكن اختبار جزء صغير دون تشغيل التطبيق كاملًا.
- لا يعرف المطور أين يضع الكود الجديد.
- وكل تعديل يوضع في أول مكان يستطيع فيه جعله يعمل.
المثال الأبسط
تخيل زرًا لإنشاء طلب:
async function onCreateOrderPressed() {
const user = await database.getCurrentUser();
if (!user) {
showError("Login required");
return;
}
const product = await database.getProduct(selectedProductId);
const discount =
user.isPremium ? product.price * 0.2 : 0;
const finalPrice = product.price - discount;
const order = await database.insertOrder({
userId: user.id,
productId: product.id,
finalPrice
});
await emailService.sendOrderConfirmation(
user.email,
order.id
);
analytics.track("order_created");
navigateToOrder(order.id);
}
الكود قد يعمل، لكنه يخلط بين:
- منطق الواجهة.
- المصادقة.
- قراءة البيانات.
- حساب الخصم.
- إنشاء الطلب.
- إرسال البريد.
- التحليلات.
- والتنقل.
إذا تغيرت قاعدة الخصم، نعدل الشاشة.
إذا تغير مزود البريد، نعدل الشاشة.
إذا أردنا إنشاء الطلب من تطبيق آخر أو مهمة مجدولة، علينا نسخ المنطق.
وإذا أردنا اختبار حساب الخصم، نحتاج إلى تهيئة قاعدة البيانات والبريد والتنقل.
الكود لم يفشل بعد، لكنه بدأ يراكم تكلفة مستقبلية.
٣. الكود النظيف ليس كودًا جميلًا فقط
قد يعتقد البعض أن Clean Code يعني:
- تنسيقًا مرتبًا.
- أسماء متناسقة.
- ملفات قصيرة.
- تعليقات كثيرة.
- أو استخدام Design Patterns.
هذه أمور مفيدة، لكنها ليست الهدف النهائي.
- يوضح نيته.
- يضع قواعد العمل في موضع معروف.
- يقلل عدد الأشياء التي تحتاج إلى فهمها لتعديل جزء منه.
- يحدد حدودًا واضحة بين المكونات.
- يمكن اختباره بصورة مستقلة.
- يسمح بتغيير التفاصيل الخارجية دون إعادة كتابة قلب المنتج.
- ويقلل احتمالات أن يؤدي تغيير صغير إلى آثار بعيدة.
قد يكون الكود مرتبًا بصريًا لكنه سيئ معماريًا.
وقد تكون الدالة قصيرة، لكنها تستدعي عشر خدمات عالمية وتغير حالات مخفية.
وقد يحتوي المشروع على عشرين Interface، لكنها لا تحل أي مشكلة حقيقية.
لذلك لا نقيس جودة الكود بعدد الملفات أو الكلاسات أو الأنماط المستخدمة.
نقيسها بالسؤال:
ما مقدار المعرفة التي أحتاج إليها حتى أعدل هذه الميزة بأمان؟
٤. فصل المسؤوليات — Separation of Concerns
يعد فصل المسؤوليات من أهم المبادئ التي تمنع تشابك الكود.
الفكرة أن كل جزء من النظام يتولى نوعًا واضحًا من العمل.
في تطبيق نموذجي قد توجد مسؤوليات مثل:
- عرض الواجهة.
- إدارة حالة الشاشة.
- تنفيذ حالة استخدام.
- تطبيق قواعد العمل.
- قراءة البيانات وكتابتها.
- الاتصال بخدمات خارجية.
- تحويل النماذج.
- وتسجيل الأحداث.
إرشادات Flutter الرسمية تضع فصل المسؤوليات في مقدمة مبادئ تصميم التطبيقات، وتوصي بفصل طبقة الواجهة عن طبقة البيانات، ثم تقسيم كل طبقة إلى مكونات ذات مسؤوليات وحدود واعتماديات واضحة.
مثال على تقسيم عملية إنشاء الطلب
بدل أن تقوم الشاشة بكل شيء:
CreateOrderScreen
يمكن أن تكون المسؤوليات:
CreateOrderScreen
→ تعرض الحقول والنتيجة
CreateOrderViewModel
→ يدير حالة الشاشة
CreateOrderUseCase
→ ينسق عملية إنشاء الطلب
PricingPolicy
→ يحسب السعر والخصم
OrderRepository
→ يحفظ الطلب ويقرأه
NotificationService
→ يرسل الإشعار
الهدف ليس إضافة طبقات لمجرد زيادة الملفات.
الهدف أن نعرف:
- أين توجد قاعدة الخصم؟
- من يقرر أن الطلب صالح؟
- من يتعامل مع قاعدة البيانات؟
- من يرسل الإشعار؟
- ومن يتعامل مع فشل كل خطوة؟
قاعدة عملية:
الواجهة تعرض المعلومات وتستقبل تفاعل المستخدم، لكنها لا ينبغي أن تكون المكان المعتمد لقواعد العمل أو الوصول المباشر إلى البيانات.
لا تحوّل فصل المسؤوليات إلى فصل مبالغ فيه
قد يقرأ المساعد عبارة:
افصل المسؤوليات.
ثم ينشئ لكل عملية بسيطة:
- Interface.
- Abstract Class.
- Implementation.
- Factory.
- Manager.
- Coordinator.
- Adapter.
- Provider.
- Mapper.
- وملف إعداد.
فتصبح عملية قراءة اسم المستخدم موزعة على عشرة ملفات.
هذا ليس كودًا نظيفًا.
إنه تعقيد منظم شكليًا.
الفصل الجيد يحقق أحد الأهداف التالية:
- عزل قاعدة عمل مهمة.
- تسهيل الاختبار.
- تبديل اعتماد خارجي.
- منع الواجهة من معرفة تفاصيل البيانات.
- توضيح حدود Feature.
- أو تقليل أثر التغيير.
إذا لم تستطع شرح المشكلة التي تحلها الطبقة، فقد لا تحتاج إليها.
٥. البرمجة كائنية التوجه — OOP
Object-Oriented Programming هي طريقة لتنظيم البرنامج حول كائنات تجمع البيانات والسلوك المرتبط بها.
تقوم عادةً على مفاهيم رئيسية:
- Encapsulation.
- Abstraction.
- Polymorphism.
- Inheritance.
لكن OOP ليست شرطًا لكل كود نظيف، وليست هدفًا بحد ذاتها.
يمكن كتابة كود سيئ جدًا باستخدام Classes.
ويمكن كتابة كود منظم باستخدام Functions وModules دون وراثة معقدة.
المهم هو استخدام الأدوات بما يخدم تصميم المنتج.
Encapsulation — حماية الحالة والسلوك
تعني Encapsulation ألا تسمح لكل جزء من المشروع بتعديل الحالة الداخلية للكائن بلا قواعد.
لنفترض أن لدينا حسابًا ماليًا:
class Account {
balance: number;
}
يمكن لأي جزء من النظام كتابة:
account.balance = -5000;
الأفضل أن تتحكم الوحدة نفسها في التغيير:
class Account {
private balanceInHalalas: number;
withdraw(amountInHalalas: number) {
if (amountInHalalas <= 0) {
throw new InvalidAmountError();
}
if (amountInHalalas > this.balanceInHalalas) {
throw new InsufficientBalanceError();
}
this.balanceInHalalas -= amountInHalalas;
}
getBalanceInHalalas() {
return this.balanceInHalalas;
}
}
الآن لا يستطيع الجزء الخارجي تغيير الرصيد إلا عبر عملية تطبق قواعده.
لكن تذكر ما شرحناه في جزء قواعد البيانات: حماية الكائن داخل التطبيق لا تكفي وحدها أمام العمليات المتزامنة. ما زلنا نحتاج إلى Transaction أو عملية ذرية في قاعدة البيانات.
الكود النظيف لا يلغي طبقات الأمان والبيانات، بل ينظم المسؤوليات بينها.
Abstraction — إظهار ما تحتاج إليه وإخفاء التفاصيل
تعني Abstraction أن يتعامل الجزء الأعلى مع مفهوم واضح، دون معرفة التفاصيل التقنية كاملة.
مثلًا، عملية إنشاء الطلب تحتاج إلى حفظ الطلب:
interface OrderRepository {
create(order: Order): Promise<Order>;
}
لا تحتاج عملية إنشاء الطلب إلى معرفة:
- هل البيانات في PostgreSQL؟
- أم Supabase؟
- أم Firebase؟
- أم خدمة HTTP؟
- أم قاعدة محلية للاختبار؟
التنفيذ التفصيلي يمكن أن يكون:
class PostgresOrderRepository
implements OrderRepository {
// Database details
}
الفائدة أن قاعدة العمل تعتمد على عقد يعبر عن حاجتها، لا على تفاصيل قد تتغير.
لكن لا تنشئ Interface لكل دالة صغيرة بصورة تلقائية.
التجريد مفيد عندما توجد حدود حقيقية، أو اعتماد متغير، أو حاجة إلى الاختبار والعزل.
Polymorphism — سلوك واحد بتنفيذات مختلفة
يسمح Polymorphism لعدة تنفيذات بالالتزام بعقد واحد.
مثلًا:
interface PaymentGateway {
charge(request: PaymentRequest): Promise<PaymentResult>;
}
ثم:
class StripePaymentGateway
implements PaymentGateway {}
class LocalPaymentGateway
implements PaymentGateway {}
class FakePaymentGateway
implements PaymentGateway {}
تستطيع عملية الدفع استخدام العقد نفسه دون معرفة المزود.
هذا يسهل:
- تبديل المزود.
- الاختبار.
- إضافة مزود جديد.
- واختيار التنفيذ حسب البيئة.
لكن إذا كان المشروع لن يستخدم إلا تنفيذًا واحدًا بسيطًا، ولا توجد حاجة إلى عزله، فقد لا تحتاج إلى بنية معقدة منذ البداية.
Inheritance — الوراثة ليست الحل الافتراضي
الوراثة تسمح لكلاس أن يرث سلوكًا من كلاس آخر.
لكن سلاسل الوراثة الطويلة قد تجعل السلوك موزعًا بين طبقات يصعب فهمها:
BaseUser
→ PaidUser
→ PremiumPaidUser
→ CorporatePremiumPaidUser
→ InternationalCorporatePremiumPaidUser
لتعرف ما الذي يفعله الكلاس الأخير، تحتاج إلى قراءة السلسلة كاملة.
وقد يؤدي تعديل الكلاس الأساسي إلى تغيير سلوك جميع الأبناء.
لذلك يفضّل غالبًا استخدام:
Composition over Inheritance
أي تركيب السلوك من مكونات أصغر بدل بناء شجرة وراثة طويلة.
مثلًا، بدل إنشاء نوع مستخدم جديد لكل مجموعة خصائص، يمكن أن يملك المستخدم:
- SubscriptionPolicy.
- PricingPolicy.
- PermissionSet.
- NotificationPreference.
الوراثة ليست خطأ، لكنها لا ينبغي أن تكون الحل الأول لكل علاقة.
٦. قواعد SOLID
SOLID ليست مكتبة أو Framework.
إنها مجموعة مبادئ تساعد على تقليل الترابط وتحسين قابلية التغيير والاختبار.
تجمع SOLID خمسة مبادئ: المسؤولية الواحدة، والانفتاح والإغلاق، واستبدال ليسكوف، وفصل الواجهات، وعكس الاعتماديات. وتوضح وثائق Microsoft أن الهدف منها يرتبط بتصميم الطبقات الداخلية وفصل اعتمادياتها، وأن Dependency Injection إحدى وسائل تطبيق Dependency Inversion.
لكن SOLID ليست قوانين تُطبق حرفيًا في كل دالة.
هي أدوات للتفكير في المخاطر الناتجة عن التغيير.
S — Single Responsibility Principle
مبدأ المسؤولية الواحدة لا يعني أن الكلاس يجب أن يحتوي على دالة واحدة.
المعنى الأقرب:
ينبغي أن يكون للوحدة سبب منطقي واحد للتغيير.
لنفترض أن لدينا:
class InvoiceService {
calculateInvoice() {}
saveInvoice() {}
generatePdf() {}
sendEmail() {}
uploadToStorage() {}
recordAnalytics() {}
}
قد يتغير هذا الكلاس بسبب:
- تغيير قواعد التسعير.
- تغيير قاعدة البيانات.
- تغيير مكتبة PDF.
- تغيير البريد.
- تغيير التخزين.
- تغيير التحليلات.
لديه أسباب كثيرة للتغيير.
يمكن فصل المسؤوليات:
InvoiceCalculator
InvoiceRepository
InvoicePdfGenerator
InvoiceMailer
InvoiceStorage
لكن لا تفصل كل سطر في كلاس مستقل.
المعيار هو وجود مسؤوليات تتغير لأسباب مختلفة.
إذا كان Constructor يحتاج إلى عشر أو خمس عشرة Dependency، فقد تكون هذه إشارة إلى أن الكلاس يقوم بأكثر مما ينبغي. تشير وثائق Microsoft إلى أن كثرة الاعتماديات المحقونة قد تكون Code Smell يدل على انتهاك مبدأ المسؤولية الواحدة.
O — Open/Closed Principle
يعني أن يكون المكوّن قابلًا للتوسعة دون الحاجة إلى تعديل قلبه مع كل حالة جديدة.
لنفترض أن حساب الخصم مكتوب هكذا:
function calculateDiscount(userType: string) {
if (userType === "regular") return 0;
if (userType === "premium") return 0.10;
if (userType === "corporate") return 0.20;
if (userType === "student") return 0.15;
}
كلما أضفت نوعًا، تعدّل الدالة المركزية.
يمكن في بعض الحالات فصل الاستراتيجيات:
interface DiscountPolicy {
calculate(context: PricingContext): number;
}
ثم إضافة سياسة جديدة دون تعديل السياسات السابقة.
لكن لا تطبق Strategy Pattern على شرطين لن يتغيرا.
مبدأ Open/Closed يفيد عندما توجد منطقة يتكرر فيها التوسع وتزداد الحالات.
L — Liskov Substitution Principle
المعنى العملي:
أي تنفيذ لعقد يجب أن يحافظ على التوقعات التي وعد بها العقد.
لنفترض أن لدينا:
interface FileStorage {
save(file: File): Promise<SavedFile>;
}
تنفيذ محلي يحفظ الملف ويعيد النتيجة.
لكن تنفيذًا آخر يعيد نجاحًا قبل أن يحفظ الملف فعلًا، ثم قد يفشل لاحقًا دون إبلاغ العميل.
رغم أن الاثنين يطبقان Interface نفسها، فإن سلوكهما ليس متوافقًا.
أو لدينا Repository يعد بأن:
getById(id)
يعيد null عند عدم وجود السجل.
لكن تنفيذًا آخر يرمي Exception.
هذا الاختلاف يكسر توقعات المستهلك.
العقود ليست أسماء دوال فقط.
يجب أن توضح:
- المدخلات المقبولة.
- النتيجة.
- حالات الخطأ.
- الآثار الجانبية.
- والتوقعات الزمنية عند أهميتها.
I — Interface Segregation Principle
لا تجعل Interface ضخمة تجبر كل تنفيذ على دعم وظائف لا يحتاج إليها.
مثال سيئ:
interface UserRepository {
login();
register();
uploadAvatar();
sendEmail();
exportPdf();
clearCache();
deleteAccount();
generateReport();
}
هذا ليس Repository للمستخدم، بل مجموعة مسؤوليات غير مترابطة.
يمكن تقسيم العقود وفق الحاجة:
UserRepository
AuthenticationService
AvatarStorage
AccountDeletionService
UserReportExporter
كذلك لا تجعل كل مستهلك يعتمد على Interface كاملة إذا كان يحتاج إلى عملية واحدة فقط.
العقود الأصغر والواضحة تقلل الترابط، لكن كثرة العقود الصغيرة جدًا قد تزيد الضوضاء.
D — Dependency Inversion Principle
يعني أن منطق الأعمال المهم لا ينبغي أن يعتمد مباشرة على تفاصيل تقنية منخفضة المستوى.
بدل:
class CreateOrderUseCase {
private database =
new SupabaseClient(...);
async execute() {
// Business rules and Supabase details
}
}
يعتمد على عقد:
class CreateOrderUseCase {
constructor(
private orderRepository: OrderRepository
) {}
}
ثم تمرر التنفيذ من الخارج.
توضح إرشادات Microsoft أن عكس الاعتماديات يجعل التفاصيل التقنية تعتمد على Abstractions التي تحددها الطبقات العليا، مما يحسن الفصل وقابلية الاختبار والتبديل.
٧. Dependency Injection وتماسك الوحدات
Dependency Injection — DI
Dependency Injection تعني أن الكلاس لا ينشئ اعتمادياته بنفسه، بل يحصل عليها من الخارج.
بدل:
class ReportService {
private database =
new PostgresDatabase();
private email =
new SendGridEmailService();
}
استخدم:
class ReportService {
constructor(
private database: ReportRepository,
private email: EmailService
) {}
}
ماذا نستفيد؟
- نستطيع تمرير تنفيذ مختلف.
- نستطيع استخدام Fake أثناء الاختبار.
- تصبح الاعتماديات ظاهرة.
- يقل الاعتماد على Global State.
- ويصبح الكلاس أكثر وضوحًا بشأن ما يحتاج إليه.
تصف وثائق Microsoft DI بأنها تقنية لتحقيق Loose Coupling عبر تزويد الكلاس بالاعتماديات بدل إنشائها داخليًا، وغالبًا تُعلن الاعتماديات عبر Constructor.
كما توصي إرشادات Flutter المعمارية باستخدام Dependency Injection لتجنب الكائنات المتاحة عالميًا وتقليل الأخطاء، وتوصي باستخدام Repository Abstractions عندما تحتاج إلى تنفيذات مختلفة للبيئات أو الاختبارات.
يمكن تطبيق DI يدويًا:
const repository =
new PostgresOrderRepository(database);
const service =
new NotificationService(emailClient);
const useCase =
new CreateOrderUseCase(
repository,
service
);
لا تحتاج دائمًا إلى مكتبة أو Container.
والـContainer نفسه قد يصبح Service Locator يخفي الاعتماديات إذا استُخدم داخل كل كلاس:
const database =
container.resolve("database");
الاعتمادية الآن موجودة، لكنها غير ظاهرة في Constructor.
الأفضل أن يعرف قارئ الكلاس ما يحتاج إليه من توقيعه نفسه.
كما أن DI ليست علاجًا لكل شيء. استخدامها بصورة مفرطة قد ينتج عددًا كبيرًا من العقود والـMocks ويجعل تدفق البرنامج أصعب فهمًا. يشير Martin Fowler إلى أن DI أداة فعالة عند استخدامها بحكمة، لكنها ليست حلًا شاملًا لكل مشكلة.
High Cohesion وLow Coupling
High Cohesion — تماسك مرتفع
يعني أن العناصر داخل الوحدة مرتبطة بهدف واحد واضح.
مثلًا، قد يحتوي PricingPolicy على:
- حساب السعر الأساسي.
- تطبيق الخصم.
- التحقق من حدود الخصم.
- حساب الضريبة حسب السياسة.
جميعها مرتبطة بالتسعير.
Low Coupling — ترابط منخفض
يعني ألا تحتاج الوحدة إلى معرفة تفاصيل كثيرة عن بقية النظام.
مثلًا، PricingPolicy لا تحتاج إلى معرفة:
- شكل شاشة الدفع.
- نوع قاعدة البيانات.
- مزود البريد.
- أو نظام التنقل.
الكود الجيد يجمع الوظائف المترابطة داخل حدود واضحة، ويقلل المعرفة المتبادلة بين الحدود المختلفة.
٨. DRY وKISS وYAGNI
DRY — لا تكرر نفسك
مبدأ Don't Repeat Yourself يهدف إلى منع تكرار المعرفة أو قاعدة العمل نفسها.
لنفترض أن حساب الرسوم مكتوب في:
- صفحة إنشاء الطلب.
- صفحة تعديل الطلب.
- شاشة الإدارة.
- وخدمة التصدير.
عندما تتغير الرسوم، قد تحدّث ثلاثة أماكن وتنسى الرابع.
يجب أن توجد قاعدة الرسوم المعتمدة في مكان واضح.
لكن DRY من أكثر المبادئ التي تُساء ممارستها.
ليس كل تشابه تكرارًا يجب توحيده
تخيل دالتين متشابهتين اليوم، لكنهما تخصان قاعدتي عمل مختلفتين.
إذا جمعتهما في Abstraction واحدة لمجرد تشابه الكود، فقد تصبح الدالة الجديدة مليئة بالشروط:
calculateValue(
type,
mode,
useLegacyRules,
skipValidation,
applySpecialCase
)
أصبحت أقل تكرارًا، لكنها أصعب فهمًا.
تطرح مقالة Aikido هذه النقطة بوضوح: بعض الفرق تفضل إزالة التكرار بقوة، بينما تفضل فرق أخرى قبول قدر محدود منه إذا كان التجريد سيضر بسهولة القراءة. وتصف هذه المنطقة بأنها ليست قرارًا أبيض أو أسود، بل تعتمد على أسلوب الفريق والسياق.
وحّد المعرفة المشتركة، لا مجرد الأسطر المتشابهة. وقد يكون تكرار بسيط ومؤقت أفضل من Abstraction خاطئة تربط مفهومين مختلفين.
KISS — اجعل الحل بسيطًا
Keep It Simple لا تعني كتابة كود بدائي.
تعني اختيار أبسط حل يحقق المتطلبات الحالية بصورة صحيحة.
قد تطلب من الذكاء الاصطناعي:
أنشئ بنية احترافية وقابلة للتوسع عالميًا.
فيقترح:
- Microservices.
- Event Bus.
- CQRS.
- Factories.
- Plugin System.
- عدة قواعد بيانات.
- Layers كثيرة.
- وInterfaces لكل شيء.
بينما المنتج لا يزال نموذجًا أوليًا يستخدمه عشرون شخصًا.
كل طبقة جديدة لها تكلفة:
- فهم.
- صيانة.
- اختبار.
- Debugging.
- وتوثيق.
البساطة الجيدة لا تعني جمع المشروع في ملف واحد.
تعني أن يكون لكل تعقيد سبب واضح.
YAGNI — لن تحتاج إليه الآن
You Aren't Gonna Need It يحذر من بناء مزايا وبنى تحتية لمجرد احتمال الحاجة إليها مستقبلًا.
مثلًا:
- دعم خمسة مزودي دفع قبل إطلاق الأول.
- تصميم عشرين دورًا لا يستخدم منها إلا اثنان.
- إضافة Sharding لتطبيق بلا مستخدمين.
- نظام Plugins دون Plugins.
- Event Sourcing لعملية CRUD بسيطة.
- أو إنشاء طبقات لتبديل مكتبة لا توجد نية لتبديلها.
قد تحتاج إليها لاحقًا، لكن بناءها الآن قد يضيف افتراضات خاطئة ويزيد تكلفة كل تغيير.
القابلية للتوسع لا تعني بناء جميع الاحتمالات.
تعني بناء حدود واضحة تجعل إضافة الحاجة المستقبلية ممكنة عندما تصبح حقيقية.
٩. لا تطلب من الذكاء الاصطناعي استخدام جميع Design Patterns
من الأوامر الخطرة:
طبّق أفضل Design Patterns على المشروع.
لا يوجد Pattern أفضل بصورة مطلقة.
كل Pattern يحل نوعًا محددًا من المشكلات.
Strategy Pattern — مناسب عندما توجد خوارزميات أو سياسات قابلة للتبديل.
Adapter Pattern — مناسب عندما تحتاج إلى توحيد واجهة خدمة خارجية مع عقد النظام.
Factory Pattern — مناسب عندما تكون عملية إنشاء الكائن معقدة أو تختلف حسب السياق.
Observer أو Events — مناسب لفصل من يحتاج إلى معرفة وقوع حدث، لكن قد يصعب تتبع التدفق إذا أُفرط فيه.
Repository Pattern — مناسب عندما تحتاج إلى حد واضح للوصول إلى البيانات، لكنه قد يصبح مجرد Wrapper يكرر ORM بلا قيمة.
السؤال ليس:
أي Pattern يمكنني إضافته؟
بل:
ما المشكلة المحددة التي تجعل هذا Pattern مفيدًا؟
١٠. تنظيم المعمارية
Layered Architecture
يمكن تقسيم التطبيق إلى طبقات ذات مسؤوليات مختلفة.
Presentation Layer — الشاشات، Components أو Widgets، حالة الواجهة، استقبال تفاعل المستخدم، عرض الأخطاء والتحميل.
Application Layer — Use Cases، تنسيق العمليات، ترتيب الخطوات، التحكم في تدفق حالة الاستخدام.
Domain Layer — Entities، Value Objects، قواعد العمل الأساسية، السياسات التي لا تعتمد على Framework.
Data أو Infrastructure Layer — قاعدة البيانات، API Clients، التخزين، البريد، الملفات، مزودات الدفع.
توصي إرشادات Flutter الرسمية بتقسيم التطبيق إلى طبقات ومكونات ذات مسؤوليات منفصلة، وتوضح أن الواجهة ومنطق البيانات ينبغي ألا يمتزجا داخل Widgets نفسها.
لكن عدد الطبقات يعتمد على حجم المنتج.
قد يحتاج تطبيق صغير إلى:
UI
Data
وقد يحتاج تطبيق أكبر إلى:
UI
Application
Domain
Data
لا تضف Domain Layer فارغة لا تحتوي على قواعد مجال حقيقية.
Feature-First Structure
في المشاريع الكبيرة قد يصبح التنظيم بحسب النوع فقط مشكلة:
screens/
services/
models/
repositories/
widgets/
لأن ملفات الميزة الواحدة تصبح موزعة في جميع المشروع.
يمكن التنظيم بحسب Feature:
features/
orders/
ui/
application/
domain/
data/
payments/
ui/
application/
domain/
data/
الفائدة أن حدود الميزة تصبح أوضح.
لكن لا تجعل كل Feature جزيرة معزولة تكرر كل شيء.
يمكن أن توجد وحدات مشتركة حقيقية مثل:
core/
shared/
بشرط ألا يتحول shared إلى مكان نضع فيه أي ملف لا نعرف أين يذهب.
Use Cases: سمِّ العمليات بلغة المنتج
بدل خدمات عامة مثل:
DataManager
AppService
ProcessHelper
استخدم أسماء تعبّر عن العمليات الحقيقية:
CreateOrder
ApproveMembershipRequest
CancelSubscription
TransferOwnership
WithdrawFunds
GenerateMonthlyStatement
الاسم يوضح:
- لماذا توجد الوحدة؟
- ما العملية التي تنفذها؟
- وأين نبحث عن قواعدها؟
كل Use Case قد:
- يتحقق من المدخلات.
- يجلب البيانات المطلوبة.
- يطبق قواعد العمل.
- يطلب الحفظ.
- يطلق حدثًا أو إشعارًا.
لكن لا تحوله إلى God Use Case يقوم بكل شيء مباشرة.
يمكنه تنسيق مكونات أصغر.
Repository Pattern
الـRepository يمثل حدًا للوصول إلى البيانات.
مثلًا:
interface OrderRepository {
findById(id: OrderId): Promise<Order | null>;
save(order: Order): Promise<void>;
}
الفكرة ليست إخفاء SQL فقط.
بل تقديم عمليات مرتبطة بالمجال.
مثال ضعيف:
repository.query(table, filters);
repository.insert(table, data);
repository.update(table, data);
هذا مجرد Database Client باسم آخر.
مثال أوضح:
findActiveSubscriptionForUser()
reserveInventory()
saveApprovedRequest()
لكن انتبه ألا تضع كل جداول المشروع داخل:
AppRepository
فهو يتحول إلى God Object.
لا تمرر نموذج قاعدة البيانات في كل مكان
من الأخطاء الشائعة استخدام الكائن نفسه بوصفه:
- صف قاعدة بيانات.
- استجابة API.
- نموذج الواجهة.
- مدخل العملية.
- وكائن المجال.
لنفترض أن جدول المستخدم يحتوي:
id
email
password_hash
role
internal_notes
created_at
updated_at
لا ينبغي أن يصل هذا النموذج كاملًا إلى الواجهة أو كل طبقات المشروع.
يمكن استخدام:
- Database Model.
- Domain Model.
- Request DTO.
- Response DTO.
- UI Model.
توصي إرشادات Flutter المعمارية بفصل نماذج API عن نماذج المجال في التطبيقات الكبيرة عندما يمنع ذلك انتقال التعقيد إلى ViewModels وUse Cases.
لكن هذا الفصل يزيد عدد التحويلات والملفات، لذلك يُستخدم عندما يحل مشكلة فعلية، لا بصورة تلقائية لكل كائن بسيط.
١١. الأنواع والقيم والحالة
الأنواع القوية — Strong Types
من أسرع الطرق إلى كود هش استخدام any أو Map<String, dynamic> في كل مكان.
مثلًا:
function updateOrder(data: any) {}
لا يعرف القارئ:
- ما الحقول المطلوبة؟
- ما الأنواع؟
- ما الحالات؟
- وما الذي تعيده الدالة؟
الأفضل استخدام عقد واضح:
type UpdateOrderRequest = {
orderId: string;
status: OrderStatus;
expectedVersion: number;
};
واستخدام حالات محددة:
type OrderStatus =
| "pending"
| "approved"
| "cancelled";
بدل:
status: string;
يساعد نظام الأنواع على اكتشاف فئة من الأخطاء أثناء التطوير، لكنه لا يغني عن Validation عند حدود النظام؛ فالبيانات القادمة من المستخدم أو الشبكة ما زالت غير موثوقة.
توفر Dart نظام أنواع ساكنًا وتحليلًا يستطيع اكتشاف مشكلات عديدة أثناء التطوير، وتوصي وثائقها باستخدام أنواع واضحة للمجموعات والعقود بدل تركها غامضة.
Value Objects بدل القيم المبعثرة
بدل تمرير أرقام ونصوص لا يعرف معناها:
transfer(
"123",
"456",
10000,
"SAR"
);
يمكن استخدام أنواع أكثر وضوحًا:
transfer({
fromAccountId,
toAccountId,
amount: Money.sarInHalalas(10000)
});
يساعد Value Object على فرض قواعد مثل:
- العملة.
- عدم قبول مبلغ سالب.
- طريقة التقريب.
- المقارنة.
- والتنسيق.
كما يمنع الخلط بين قيم متشابهة شكليًا، مثل: User ID وOrder ID وTenant ID.
جميعها قد تكون Strings، لكنها ليست الشيء نفسه.
Immutable Data
عندما تستطيع أي جهة تعديل الكائن المشترك، يصبح تتبع الحالة صعبًا.
مثلًا:
order.status = "paid";
order.total = 0;
order.items.push(...);
قد يحدث التعديل من أماكن متعددة.
يمكن استخدام كائنات Immutable أو عمليات تعيد نسخة جديدة:
const paidOrder =
order.markAsPaid(paymentReference);
الفوائد:
- وضوح انتقال الحالة.
- سهولة التراجع والمقارنة.
- تقليل الآثار الجانبية.
- تحسين الاختبار.
- وتسهيل State Management.
توصي إرشادات Flutter باستخدام نماذج Immutable في كثير من حالات إدارة البيانات، مع الإشارة إلى أن أدوات Code Generation قد تضيف تكلفة بناء ينبغي أخذها في الحسبان.
Pure Functions
الدالة النقية تعتمد على مدخلاتها فقط، ولا تعدّل حالة خارجية، ولا تتصل بقاعدة بيانات، ولا ترسل بريدًا، وتعيد النتيجة نفسها للمدخلات نفسها.
مثلًا:
function calculateDiscount(
subtotal: number,
membership: MembershipType
): number {
if (membership === "premium") {
return subtotal * 0.10;
}
return 0;
}
هذه الدالة سهلة الفهم والاختبار.
يمكن عزل الحسابات وقواعد التحويل في Pure Functions، ثم ترك الآثار الخارجية لمكونات واضحة.
لكن لا تحول كل النظام إلى Functions بلا حدود أو كائنات مجال عندما يكون وجود كائن يحمي الحالة أكثر وضوحًا.
Command Query Separation
قاعدة مفيدة:
العملية إما تغيّر الحالة، أو تعيد معلومات، ولا تحاول الجمع بين الاثنين دون وضوح.
Query — getOrderById(id) — تعيد بيانات ولا يفترض أن تغير النظام.
Command — approveOrder(id) — تغير حالة النظام.
إذا كانت دالة باسم getUser() تقوم أيضًا بتحديث last_seen، وإرسال Event، وإنشاء سجل، فقد يصبح سلوكها مفاجئًا.
لا يعني هذا منع جميع الآثار الجانبية في عمليات القراءة، لكن يجب أن تكون واضحة ومقصودة.
١٢. الوظائف والأسماء
الوظائف الصغيرة والواضحة
لا يوجد رقم سحري لطول الدالة.
الدالة التي تحتوي 40 سطرًا قد تكون واضحة.
والدالة التي تحتوي 8 أسطر قد تكون غامضة.
اسأل:
- هل للدالة هدف واضح؟
- هل اسمها يشرح ما تفعله؟
- هل تعمل على مستوى تجريد واحد؟
- هل تحتوي على خطوات لا تنتمي إليها؟
- هل يمكن اختبارها؟
- هل تغير حالة خارجية مخفية؟
- وهل تحتاج إلى عدد كبير من المعاملات؟
الـBooleans الكثيرة تجعل السلوك غير واضح:
processData(data, type, mode, flag, option, skip, force)
processData(data, true, false, true);
الأفضل استخدام كائن طلب أو عمليات منفصلة ذات أسماء واضحة.
Guard Clauses بدل التعشيق
بدل:
if (user) {
if (user.isActive) {
if (user.canPurchase) {
if (stock > 0) {
// Create order
}
}
}
}
استخدم:
if (!user) {
throw new UserNotFoundError();
}
if (!user.isActive) {
throw new InactiveUserError();
}
if (!user.canPurchase) {
throw new PermissionDeniedError();
}
if (stock <= 0) {
throw new OutOfStockError();
}
// Create order
هذا يعيدنا إلى فكرة الجزء الأول:
الخطأ قد يكون في شرط الحماية الذي لم يُكتب.
Guard Clauses تجعل شروط الرفض ظاهرة، وتبقي المسار الرئيسي أوضح.
الأسماء جزء من تصميم النظام
من أكثر الأسماء شيوعًا في الكود المولد:
data
item
value
result
temp
manager
helper
service
process
handle
execute
هذه الكلمات ليست خاطئة دائمًا، لكنها قد تكون بلا معنى دون سياق.
قارن:
const value = calculate(data);
مع:
const renewalPriceInHalalas =
calculateSubscriptionRenewalPrice(
activeSubscription,
selectedPlan
);
الاسم الثاني أطول، لكنه يشرح: ما القيمة؟ ما وحدتها؟ وما القاعدة التي أنتجتها؟
تؤكد إرشادات Effective Dart أن التسمية عنصر مهم في كتابة كود مقروء وقابل للصيانة، كما أن الاتساق في التنسيق والتسمية يجعل قراءة المشروع أسهل.
إذا كان اسم الدالة validateOrder() لكنها تحفظ الطلب، وترسل إشعارًا، وتخصم المخزون، فالاسم يخفي آثارًا مهمة. اختر اسمًا يعبر عن السلوك، أو افصل المسؤوليات.
Magic Numbers وMagic Strings
بدل:
if (attempts >= 5) {
lockAccount();
}
استخدم:
const MAX_LOGIN_ATTEMPTS = 5;
وبدل:
if (status === "aprvd") {}
استخدم Enum أو Type واضحًا:
OrderStatus.approved
الفائدة ليست مجرد الاسم، بل تحديد مصدر القرار.
لكن لا تحول كل رقم بسيط إلى Constant عالمي، مثل:
items.take(1)
قد يكون واضحًا في سياقه. استخدم الثوابت عندما تحمل القيمة معنى أو سياسة قابلة للتغيير.
١٣. إدارة الأخطاء
الكود النظيف لا يعني منع الأخطاء فقط، بل جعلها واضحة وقابلة للتعامل.
من الأنماط السيئة:
try {
await createOrder();
} catch (error) {
console.log(error);
}
ثم تعرض الواجهة نجاحًا أو تستمر العملية.
هذا يسمى أحيانًا ابتلاع الخطأ — Swallowed Error.
حدث الفشل، لكن النظام فقد المعلومة.
يجب التمييز بين أخطاء مثل:
- Validation Error.
- Authentication Error.
- Permission Error.
- Not Found.
- Conflict.
- External Service Failure.
- Timeout.
- Unexpected Error.
يمكن لطبقة التطبيق تحويلها إلى نتائج معروفة، ثم تعرض الواجهة رسالة مناسبة، بينما تصل التفاصيل إلى نظام المراقبة.
catch (_) { return null; } — الـnull الآن قد يعني: السجل غير موجود، أو قاعدة البيانات تعطلت، أو المستخدم لا يملك الصلاحية، أو حصل Timeout، أو هناك Bug. تجميع جميع الحالات في نتيجة واحدة يسبب قرارات خاطئة لاحقًا.
Fail Fast
عندما تكون حالة النظام غير صالحة، ارفضها مبكرًا بدل السماح لها بالانتقال إلى طبقات بعيدة.
مثلًا:
- إعداد إلزامي مفقود.
- Dependency لم تُمرر.
- قيمة خارج النطاق.
- حالة طلب لا تسمح بالعملية.
- أو Payload غير مطابق للعقد.
الفشل المبكر يجعل مكان الخطأ أقرب إلى سببه.
لكن يجب التمييز بين فشل يجب أن يوقف العملية، وحالة متوقعة يمكن التعامل معها.
لا تجعل أي خطأ بسيط يؤدي إلى سقوط التطبيق كاملًا.
التعليقات ليست علاجًا للكود الغامض
قد يكتب المساعد:
// Get user
const user = await getUser();
// Check if active
if (user.isActive) {
// Process order
}
هذه التعليقات تكرر ما يقوله الكود.
التعليق المفيد يشرح: لماذا اتخذنا قرارًا غير واضح؟ ما القيد الخارجي؟ لماذا لا يمكن تبسيط العملية؟ ما الـInvariant؟ وما المخاطر عند التغيير؟
مثلًا:
// Keep this write before publishing the event.
// Consumers may read the order immediately after
// receiving the event.
لكن حتى التعليق المفيد قد يصبح قديمًا.
استخدم أسماء وبنية واضحة أولًا، ثم أضف تعليقًا عندما توجد معرفة لا يستطيع الكود التعبير عنها وحده.
تشير إرشادات Effective Dart إلى أن توثيق الأنواع ينبغي أن يوضح Invariants والمصطلحات والسياق، وليس فقط تكرار أسماء الأعضاء.
١٤. علامات الترابط الزائد
God Class
الـGod Class هو كلاس يعرف ويفعل أشياء كثيرة جدًا.
مثلًا، AppManager يقوم بـ: تسجيل الدخول، إدارة المستخدم، قراءة الإعدادات، الدفع، الإشعارات، التخزين، التنقل، التحليلات، إدارة Cache، واستدعاء الذكاء الاصطناعي.
علاماته:
- Constructor ضخم.
- ملف طويل.
- كل Feature تعتمد عليه.
- أي تعديل فيه قد يؤثر في النظام كله.
- لا يمكن اختباره دون عشرات الـMocks.
- ولا يمكن وصف مسؤوليته بجملة واحدة.
لا تقسمه دفعة واحدة بطريقة عشوائية.
ابدأ بتحديد مجموعات المسؤوليات، ثم انقل كل مجموعة إلى حد واضح مع اختبارات تحمي السلوك.
God Function
قد تحتوي الدالة على: التحقق من المستخدم، قراءة قاعدة البيانات، حساب القيم، تحديث عدة جداول، استدعاء API، إرسال بريد، تسجيل Analytics، إنشاء ملف، وتحويل النتيجة.
حتى إذا كان اسمها handleSubmit()، فهي في الحقيقة Workflow كامل.
يمكن أن يبقى التنسيق في Use Case، لكن التفاصيل تُنقل إلى مكونات واضحة:
CreateOrderUseCase
→ OrderValidator
→ PricingPolicy
→ OrderRepository
→ PaymentGateway
→ NotificationService
مع الحرص على ألا يصبح كل سطر خدمة مستقلة.
Circular Dependencies
تظهر عندما تعتمد الوحدات على بعضها في دائرة:
Orders depends on Payments
Payments depends on Users
Users depends on Orders
أو:
Feature A imports Feature B
Feature B imports Feature C
Feature C imports Feature A
النتيجة: صعوبة معرفة اتجاه النظام، مشكلات في التهيئة، اختبارات معقدة، تغييرات تنتشر بين Features، وأحيانًا أخطاء Runtime.
الحل لا يكون دائمًا نقل الملفات إلى مجلد shared. قد تحتاج إلى: استخراج عقد أصغر، نقل قاعدة إلى Domain مشترك حقيقي، عكس اتجاه الاعتماد، استخدام Event، أو إعادة تحديد ملكية المسؤولية.
Global State
قد يكون من السهل وضع كل شيء في Singleton أو Global Store:
global.currentUser
global.database
global.settings
global.cache
لكن هذا يجعل: الاعتماديات مخفية، الاختبارات تؤثر في بعضها، الحالة قابلة للتعديل من أي مكان، ترتيب التشغيل مهمًا، والأخطاء صعبة التتبع.
لا يعني ذلك أن كل Global State محظور. قد توجد إعدادات أو خدمات ذات عمر تطبيق كامل.
لكن يجب أن يكون الوصول إليها منظمًا، وأن تكون المسؤولية واضحة، وألا تتحول إلى طريق مختصر يربط كل شيء بكل شيء.
State Management ليس مكانًا لوضع التطبيق كاملًا
في Flutter أو React قد يبدأ Store بسيطًا ثم يتحول إلى AppState يحتوي: المستخدم، الطلبات، الإشعارات، الدفع، النماذج، الإعدادات، كل Loading State، وكل Error.
أي تغيير يعيد بناء أجزاء كثيرة، ولا يعرف أحد من يملك البيانات.
افصل بين: UI State، Form State، Server State، Session State، Cached State، Domain State.
واجعل كل Feature تملك حالتها بقدر الإمكان.
لكن لا تنشئ Store منفصلًا لكل Text Field إذا كان State محليًا كافيًا.
١٥. الديون التقنية وإعادة الهيكلة
Technical Debt ليست كل كود غير مثالي
الديون التقنية ليست شتيمة.
أحيانًا يختار الفريق حلًا أسرع بصورة واعية للوصول إلى السوق.
المشكلة عندما: لا يُسجل القرار، لا تُعرف التكلفة، لا يوجد موعد أو شرط للمراجعة، أو تُبنى طبقات جديدة فوقه وكأنه تصميم نهائي.
مثال:
سنستخدم هذا التنفيذ المؤقت حتى نثبت حاجة المستخدم، لكنه لا يدعم التزامن وسنستبدله قبل إطلاق الدفع الحقيقي.
هذا دين معروف.
أما أن تبني العملية المؤقتة، ثم تضيف فوقها عشر ميزات مالية دون أن يعرف أحد حدودها، فهو خطر متراكم.
Refactoring: تنظيف الكود دون تغيير سلوكه الخارجي
Refactoring يعني تحسين البنية الداخلية للكود مع المحافظة على السلوك الخارجي.
يصف Martin Fowler الـRefactoring بأنه سلسلة من تغييرات صغيرة ومنضبطة تعيد تنظيم البنية الداخلية دون تغيير السلوك المرئي، مما يقلل خطر التعديل الكبير دفعة واحدة.
أمثلة: استخراج دالة، تغيير اسم غامض، نقل مسؤولية، تقسيم God Class، إزالة تكرار حقيقي، إدخال Interface عند حد خارجي، استبدال شرط معقد بسياسة، أو إزالة اعتماد دائري.
من أخطر أوامر الفايب كودنق: «أعد بناء المشروع بالكامل باستخدام Clean Architecture». قد ينتج المساعد آلاف التعديلات دفعة واحدة، وقد يغير السلوك، أو يحذف حالات نادرة، أو يفسد Migrations، أو يكسر التوافق، أو ينقل الأخطاء إلى بنية أجمل.
الأفضل:
- تحديد المشكلة المؤكدة.
- كتابة أو تثبيت الاختبارات.
- إجراء تعديل صغير.
- تشغيل الاختبارات.
- ثم الانتقال إلى الخطوة التالية.
لا تنتظر حملة تنظيف ضخمة
توضح مقالة Aikido أن تكلفة تجاهل المراجعات، خصوصًا في التغييرات المعمارية، تظهر بعد أشهر، وعندها تصبح عملية التنظيف كبيرة ومكلفة. وتقترح التعامل مع الجودة مبكرًا عبر فحص التغييرات الجديدة ومراجعة المستودع بصورة مستمرة بدل انتظار تراكم المشكلة.
هذه فكرة أساسية لمطور الفايب كودنق:
لا تجعل المساعد يبني عشرين ميزة، ثم تطلب منه في النهاية «نظف المشروع».
راجع مع كل Feature: أين وُضعت قاعدة العمل؟ ما الاعتماديات الجديدة؟ هل تكرر منطق موجود؟ هل خالف نمط المشروع؟ هل أنشأ طبقة بلا حاجة؟ وهل زاد الترابط بين Features؟
الإصلاح الصغير اليوم أرخص من إعادة كتابة النظام بعد سنة.
١٦. كيف تراجع الجودة مع الذكاء الاصطناعي
لا تطلب: «راجع جودة المستودع كاملًا»
قد يبدو الأمر جيدًا:
Review the entire repository for code quality.
لكن السؤال واسع جدًا. أي جودة تقصد؟ الأسماء؟ التكرار؟ حدود الطبقات؟ الأخطاء؟ الاعتماديات؟ الملفات الضخمة؟ الاختبارات؟ أم الأمن؟
تشرح مقالة Aikido أن مطالبة نموذج لغوي بمراجعة مستودع كامل في مرور واحد تضع أمامه نطاقًا واسعًا يصعب معه التركيز والوصول إلى نتائج دقيقة. وتقترح فحص الجودة على أساس قواعد محددة، بحيث يركز كل فحص على سؤال واحد واضح، مع إضافة سياق يتوافق مع أسلوب المشروع.
بدل مراجعة واحدة عامة، نفذ مراجعات منفصلة:
- افحص قواعد العمل الموجودة داخل الواجهة.
- افحص الدوال التي تبتلع الأخطاء.
- افحص التكرار في قواعد العمل.
- افحص الاعتماديات الدائرية.
- افحص الكلاسات ذات المسؤوليات المتعددة.
- افحص الوصول المباشر إلى الخدمات الخارجية.
- افحص الأسماء العامة والغامضة.
- افحص الطبقات والـInterfaces التي لا تضيف قيمة.
- افحص DTOs التي تكشف نماذج قاعدة البيانات.
- افحص الملفات التي تغير نمط المشروع المعتمد.
كل سؤال محدد يمنح المساعد فرصة أفضل لتقديم نتيجة قابلة للتحقق.
قواعد الجودة تعتمد على سياق المشروع
لا يوجد معيار واحد يناسب جميع الفرق.
فريق قد يفضّل: Abstractions أكثر، Interfaces مبكرة، منع أي تكرار.
وفريق آخر يفضّل: كودًا مباشرًا، تكرارًا بسيطًا، وإضافة التجريد بعد ظهور الحاجة مرتين أو ثلاثًا.
توضح مقالة Aikido أن بعض قواعد الجودة تقع في منطقة رمادية، مثل مقدار التكرار المقبول، وأن الحكم يتأثر بثقافة المشروع وتفضيلات الفريق.
لذلك يجب أن يملك مشروعك قواعد واضحة، مثل:
- الواجهة لا تتصل بقاعدة البيانات مباشرة.
- كل Use Case رئيسي له اختبارات.
- الأخطاء لا تُبتلع.
- لا نضيف Interface دون حد قابل للتبديل أو حاجة للاختبار.
- نقبل التكرار البسيط إذا كان التجريد سيخلط مفهومين مختلفين.
- قواعد الصلاحيات تبقى في الخادم.
- Features لا تستورد تفاصيل داخلية من Features أخرى.
- لا يزيد الملف عن حجم معين دون مراجعة، لا بوصفه قانونًا آليًا بل إشارة.
- أسماء الحالات والقيم تأتي من أنواع محددة.
هذه القواعد تعطي المساعد السياق الذي يحتاج إليه.
الجودة ليست هي اكتشاف Bugs فقط
قد يعمل الكود حاليًا دون Bug ظاهر، لكنه يحتوي على بنية تجعل الخطأ القادم أكثر احتمالًا.
هناك فرق بين:
مراجعة صحة السلوك — هل العملية تحسب المبلغ الصحيح؟ هل الصلاحيات تعمل؟ هل Race Condition ممكنة؟ هل البيانات محفوظة؟
مراجعة جودة التصميم — هل قاعدة الحساب في مكان واحد؟ هل يمكن تبديل المزود؟ هل المسؤوليات مختلطة؟ هل الاسم يوضح المقصد؟ هل الميزة تزيد الترابط؟
توضح مقالة Aikido أن فحص جودة الكود يختلف عن الفحص العميق للأخطاء المنطقية أو الأمنية؛ فالجودة تهتم بملاحظات سريعة ومتكررة حول البنية والأسلوب، بينما تحتاج الأخطاء المنطقية والصلاحيات إلى مراجعة أعمق وأبطأ.
وهذا يمهد للجزء الخامس:
الكود النظيف لا يثبت أن الكود صحيح، والاختبارات الناجحة لا تثبت أن التصميم جيد. نحتاج إلى الاثنين.
لا تجعل الذكاء الاصطناعي القاضي الوحيد
المساعد يستطيع: كتابة الكود، مراجعته، اقتراح Refactoring، وكتابة الاختبارات.
لكن إذا طلبت منه كل ذلك في جلسة واحدة، فقد يكرر الافتراضات نفسها.
مثلًا:
- يفهم المتطلب بطريقة خاطئة.
- يبني الميزة وفق الفهم الخاطئ.
- يكتب اختبارًا يطابق تنفيذه.
- يراجع التصميم بناءً على البنية التي اختارها.
- ثم يعلن النجاح.
الأفضل فصل الأدوار: وكيل يبني، وكيل مستقل يراجع المعمارية، فحص قواعد محددة، اختبارات آلية، ومراجعة بشرية للمناطق الحساسة.
لا تطلب من المراجع:
حسّن الكود بالطريقة التي تراها.
اطلب أولًا تقريرًا قائمًا على الأدلة، ثم قرر ما الذي يستحق التعديل.
١٧. أوامر عملية جاهزة
أمر لبناء ميزة جديدة بكود قابل للصيانة
Act as a senior software engineer.
Implement this feature as maintainable production code,
not as a demo.
Before writing code:
1. Identify the business rules and invariants.
2. Identify UI, application, domain, and data responsibilities.
3. List external dependencies.
4. Map the feature boundaries and ownership.
5. Propose the simplest architecture that keeps business logic
out of the UI and infrastructure details out of the domain.
6. Identify any existing project patterns that must be followed.
During implementation:
- Apply separation of concerns.
- Follow SOLID only where it solves a concrete problem.
- Prefer composition over inheritance.
- Use dependency injection for replaceable or external dependencies.
- Keep dependencies explicit.
- Avoid global mutable state.
- Avoid God classes and oversized workflows.
- Avoid circular dependencies.
- Avoid duplicated business rules.
- Use clear names and strongly typed contracts.
- Do not expose database models directly to the UI.
- Handle errors explicitly.
- Do not swallow exceptions.
- Do not add abstractions or design patterns without a current need.
After implementation:
1. List every new class, interface, and abstraction.
2. Explain the concrete problem each one solves.
3. Report remaining coupling and technical debt.
4. Identify what was intentionally kept simple.
5. Add tests for business rules and failure paths.
6. Run formatting, static analysis, and the relevant tests.
Do not claim the feature is maintainable without showing
the architectural boundaries and validation evidence.
أمر لمراجعة كود سباغيتي
Act as a senior software architect.
Review this feature for maintainability.
Do not modify code yet.
Inspect for:
- mixed responsibilities,
- business logic inside UI components,
- direct database or API calls from presentation code,
- God classes,
- oversized functions,
- hidden global state,
- duplicated business rules,
- tight coupling,
- low cohesion,
- circular dependencies,
- misused inheritance,
- missing or excessive dependency injection,
- broad interfaces,
- unnecessary abstractions,
- premature design patterns,
- ambiguous naming,
- magic values,
- weak types,
- swallowed errors,
- database models crossing architectural boundaries,
- and code that violates established project patterns.
For every finding, provide:
1. Exact file and symbol.
2. Evidence from the code.
3. Why it increases maintenance risk.
4. A realistic change that would become difficult.
5. The smallest safe refactor.
6. Tests required before refactoring.
7. Whether the finding is confirmed or subjective.
8. The remaining risk after the proposed change.
Do not propose a full rewrite.
Provide an incremental refactoring sequence.
أمر لمراجعة قاعدة واحدة فقط
هذه الطريقة مستوحاة من فكرة الفحص المحدد لكل قاعدة بدل المراجعة العامة:
Review this repository for one rule only:
"No business logic may live inside UI components."
Define business logic as:
- pricing and discount calculations,
- permission decisions,
- feature gating,
- workflow state transitions,
- financial rules,
- validation required for security,
- and decisions that must remain consistent across clients.
For every confirmed violation:
1. Identify the exact file and lines.
2. Quote or summarize the affected logic.
3. Explain why it is a business rule rather than UI behavior.
4. Identify the appropriate destination layer.
5. Propose the smallest safe extraction.
6. List tests needed before moving it.
Do not report styling preferences.
Do not inspect unrelated quality rules.
Do not modify code.
يمكن تكرار الأسلوب نفسه مع قواعد أخرى، مثل:
Review this repository for swallowed errors only.
أو:
Review this repository for circular dependencies only.
أو:
Review this repository for duplicated business rules only.
كل مراجعة تركز على سؤال واضح.
أمر لمنع الإفراط الهندسي
Review this architecture for overengineering.
Identify every:
- interface,
- abstract class,
- factory,
- manager,
- coordinator,
- adapter,
- event,
- layer,
- repository,
- and service.
For each one, state:
1. The concrete current requirement it solves.
2. Whether more than one implementation exists.
3. Whether it creates a meaningful test boundary.
4. Whether removing it would make the code clearer.
5. Whether it exists only for hypothetical future needs.
Recommend removing or simplifying abstractions that do not
solve a current problem.
Prefer the simplest design that:
- keeps business rules testable,
- separates UI from data access,
- makes external dependencies replaceable where needed,
- and preserves clear feature boundaries.
Do not justify complexity with vague phrases such as
"scalability", "enterprise-ready", or "best practice".
أمر لإعادة تنظيم الكود تدريجيًا
Create an incremental refactoring plan.
The external behavior must remain unchanged.
Before proposing changes:
1. Identify the current behavior and existing tests.
2. Identify missing characterization tests.
3. Find the highest-risk coupling or mixed responsibility.
4. Determine the smallest behavior-preserving change.
For each refactoring step:
- change one architectural concern only,
- keep the diff reviewable,
- preserve API and database compatibility,
- run relevant tests,
- report the evidence,
- and stop if behavior changes unexpectedly.
Do not perform a big-bang rewrite.
Do not change business requirements.
Do not change tests merely to match a broken implementation.
١٨. قائمة فحص الكود النظيف
قبل اعتبار الميزة جاهزة من ناحية جودة الكود، اسأل:
المسؤوليات
- هل يمكن وصف مسؤولية كل كلاس بجملة واضحة؟
- هل الواجهة تحتوي قواعد عمل؟
- هل يوجد اتصال مباشر بقاعدة البيانات من الواجهة؟
- هل يوجد كلاس يتغير لأسباب كثيرة؟
- هل توجد دالة تنسق عددًا كبيرًا من العمليات مباشرة؟
- هل يمكن نقل مسؤولية دون كسر أجزاء بعيدة؟
الحدود والطبقات
- هل اتجاه الاعتماديات واضح؟
- هل منطق المجال يعرف تفاصيل Framework؟
- هل Features تستورد تفاصيل داخلية من بعضها؟
- هل توجد Circular Dependencies؟
- هل Models قاعدة البيانات تصل إلى الواجهة؟
- هل العقود بين الطبقات واضحة؟
OOP وSOLID
- هل الحالة الداخلية محمية؟
- هل الوراثة مستخدمة لحاجة حقيقية؟
- هل يمكن استخدام Composition بصورة أوضح؟
- هل Interfaces صغيرة ومركزة؟
- هل التنفيذات تحافظ على توقعات العقود؟
- هل الطبقات العليا تعتمد مباشرة على تفاصيل خارجية؟
- هل عدد Dependency في Constructor يدل على مسؤوليات كثيرة؟
DI والاعتماديات
- هل الاعتماديات معلنة بوضوح؟
- هل توجد Services تُنشئ اعتمادياتها داخليًا؟
- هل يوجد Global State قابل للتعديل؟
- هل DI Container يُستخدم بوصفه Service Locator؟
- هل يمكن استبدال الخدمات الخارجية في الاختبارات؟
- هل توجد Abstractions بلا حاجة حالية؟
التكرار والتبسيط
- هل تكررت قاعدة عمل واحدة؟
- هل التجريد المقترح يوحد معرفة حقيقية أم تشابهًا شكليًا؟
- هل الحل أبسط مما يحتاج إليه المتطلب؟
- هل توجد بنية مستقبلية لا يستخدمها المنتج؟
- هل كل Pattern مضاف يحل مشكلة محددة؟
الأسماء والأنواع
- هل الأسماء تعبّر عن لغة المنتج؟
- هل توجد أسماء عامة مثل Manager وHelper بلا معنى؟
- هل الوحدات المالية والزمنية واضحة؟
- هل الحالات تستخدم Types أو Enums؟
- هل يوجد استخدام واسع لـ
anyأو Dynamic Maps؟ - هل معاملات الدوال واضحة؟
- هل أسماء الدوال تكشف آثارها الجانبية؟
الأخطاء
- هل توجد
catchفارغة؟ - هل يتحول كل خطأ إلى
null؟ - هل الأخطاء المتوقعة مميزة عن الأخطاء غير المتوقعة؟
- هل رسالة المستخدم منفصلة عن تفاصيل السجل؟
- هل الفشل يمنع استمرار العملية في حالة غير صحيحة؟
- هل توجد عمليات تعرض نجاحًا بعد فشل داخلي؟
الحالة
- هل الحالة Mutable من جهات كثيرة؟
- هل يوجد Store عالمي لكل التطبيق؟
- هل انتقالات الحالة واضحة؟
- هل يمكن أن تصبح الحالة غير صالحة؟
- هل توجد Pure Functions لقواعد الحساب المناسبة؟
المراجعة والصيانة
- هل اتبع التغيير نمط المشروع المعتمد؟
- هل توجد اختبارات تحمي السلوك قبل Refactoring؟
- هل التعديل صغير وقابل للمراجعة؟
- هل أُضيف دين تقني جديد؟
- هل تم تسجيله؟
- هل يستطيع مطور جديد العثور على قاعدة العمل؟
- هل يستطيع المساعد تفسير سبب كل Abstraction أضافها؟
١٩. لا تقبل عبارة: «تم تنظيف الكود»
كما لم نقبل في الأجزاء السابقة:
تم تأمين الميزة. / تم تحسين قاعدة البيانات. / النظام جاهز للإنتاج.
لا نقبل هنا:
تم تطبيق Clean Architecture وتنظيف الكود.
تقرير ضعيف:
Refactored the feature using SOLID, clean architecture,
dependency injection, and best practices.
لا يخبرنا: ماذا كانت المشكلة؟ ماذا تغير؟ ولماذا؟ وهل بقي السلوك نفسه؟
تقرير أقوى:
Confirmed findings:
1. Pricing rules existed in three UI components.
2. OrderScreen directly instantiated SupabaseClient.
3. OrderManager had 14 dependencies and handled pricing,
persistence, notifications, and analytics.
4. Two catch blocks swallowed database failures.
Refactoring performed:
- Extracted PricingPolicy as a pure domain service.
- Added OrderRepository boundary around data access.
- Injected OrderRepository into CreateOrderUseCase.
- Split notifications into an independent post-order action.
- Replaced swallowed errors with typed failures.
- Removed one unused abstraction.
Evidence:
- Existing 18 tests passed before the change.
- Added 9 pricing and failure-path tests.
- All 27 tests passed after the change.
- Static analysis completed without new issues.
- UI API and database schema remained unchanged.
- The largest class was reduced from four responsibilities
to one coordination responsibility.
Remaining risk:
- Analytics is still coupled to the use case and should be
reviewed if additional event consumers are introduced.
التقرير الثاني يقدم: مشكلة مؤكدة، تغييرًا محددًا، سببًا، دليلًا، ومخاطر متبقية.
لا تجعل مقاييس الجودة قوانين عمياء
قد تستخدم أدوات تقيس: طول الملف، Cyclomatic Complexity، عدد الاعتماديات، نسبة التكرار، تغطية الاختبارات، أو عدد الأسطر في الدالة.
هذه مؤشرات مفيدة، لكنها لا تفهم المنتج كاملًا.
قد تكون دالة طويلة لكنها واضحة.
وقد تكون عشر دوال قصيرة تنقل القارئ بينها دون فائدة.
وقد يكون التكرار مقصودًا لفصل سياقين مختلفين.
وقد تكون تغطية الاختبارات 100% لكنها لا تختبر حالات مهمة.
استخدم المقاييس لتحديد مناطق تحتاج إلى مراجعة، لا لإصدار حكم آلي نهائي.
وهذا يتفق مع فكرة مقالة Aikido حول المناطق الرمادية في قواعد الجودة والحاجة إلى ضبط القواعد بحسب ثقافة المشروع وسياقه.
متى يكون الكود نظيفًا بما يكفي؟
لن يصل المشروع إلى حالة مثالية نهائية.
كل منتج يحتوي على: قرارات قديمة، حلول مؤقتة، أجزاء أفضل من غيرها، وتنازلات بين الوقت والجودة.
السؤال ليس:
هل كل الكود مثالي؟
بل:
هل يمكننا تطوير أهم أجزاء المنتج دون خوف؟ وهل نعرف أماكن الديون والمخاطر؟ وهل يمنع التصميم الحالي التقدم؟ وهل تكلفة تحسينه أقل من تكلفة تركه؟
ركز على: قواعد الأعمال الحساسة، الأجزاء التي تتغير كثيرًا، النقاط التي تسبب Bugs متكررة، المكونات التي يصعب اختبارها، والحدود التي تعطل إضافة Features جديدة.
لا تقضِ شهرًا في تنظيف كود لم يتغير منذ سنوات ويعمل بصورة مستقرة، بينما توجد ميزة حرجة تُبنى كل أسبوع فوق تصميم هش.
٢٠. الخلاصة: لا تجعل السرعة تكتب مستقبل المشروع نيابة عنك
الفايب كودنق يمنحنا سرعة لم تكن متاحة سابقًا.
يستطيع الذكاء الاصطناعي أن: ينشئ الملفات، يكتب Classes، يقترح Architecture، يطبق OOP، يضيف DI، ويعيد تنظيم المشروع.
لكن النموذج يميل إلى حل الطلب الموجود أمامه.
عندما تقول:
أضف زرًا يحسب الخصم ويحفظ الطلب.
قد يضع كل شيء داخل الشاشة لأنه أسرع طريق يجعل الميزة تعمل.
وعندما تقول:
طبّق Clean Architecture.
قد ينشئ عشرات الطبقات والواجهات لأنك لم تحدد المشكلة التي تريد حلها.
المشكلة ليست أن الذكاء الاصطناعي لا يعرف SOLID.
المشكلة أنه لا يعرف تلقائيًا: أي المبادئ يحتاج إليها مشروعك، ما مقدار التعقيد المقبول، ما القواعد التي يعتمدها فريقك، ما الديون التي تقبلها مؤقتًا، وأين تريد أن تكون حدود المنتج بعد سنة.
لهذا يجب أن توجهه بوضوح:
- افصل الواجهة عن قواعد العمل.
- اجعل الاعتماديات ظاهرة.
- لا تضف Abstraction دون حاجة.
- لا تستخدم الوراثة إذا كان التركيب أوضح.
- لا تكرر قاعدة العمل نفسها.
- لكن لا توحّد كودًا متشابهًا إذا كان يمثل مفهومين مختلفين.
- التزم بنمط المشروع.
- راجع كل قاعدة جودة بصورة مستقلة.
- ونفّذ Refactoring تدريجيًا مع اختبارات تحمي السلوك.
السرعة الحقيقية ليست عدد الملفات التي يستطيع المساعد إنتاجها اليوم.
السرعة الحقيقية هي قدرتك على إضافة ميزة بعد ستة أشهر دون أن تضطر إلى إعادة بناء المشروع من الصفر.
فالذكاء الاصطناعي قد يكتب آلاف الأسطر خلال ساعات.
لكن مسؤوليتك هي أن تتأكد من أن هذه الأسطر لا تتحول إلى خيوط سباغيتي تربط كل جزء من التطبيق بكل جزء آخر.
في الجزء الخامس
أنهينا حتى الآن:
- الجزء الأول: الأمن السيبراني.
- الجزء الثاني: قواعد البيانات والأداء.
- الجزء الثالث: الاعتمادية والاستعداد للإنتاج.
- الجزء الرابع: كتابة كود نظيف وقابل للصيانة.
لكن يبقى السؤال الأخير:
من يثبت أن كل ما بنيناه يعمل فعلًا؟
في الجزء الخامس والأخير سنتحدث عن اختبار ما بناه الذكاء الاصطناعي:
- هل يستطيع اختبار الكود الذي كتبه؟
- ما الفرق بين مراجعة الكود وكتابة الاختبارات وتنفيذها؟
- كيف نطلب منه اختبار حالات الرفض والفشل؟
- كيف نمنعه من كتابة اختبار يوافق تنفيذه الخاطئ؟
- ما الفرق بين Unit وIntegration وEnd-to-End Tests؟
- كيف نختبر الصلاحيات وقاعدة البيانات والتزامن والأعطال؟
- لماذا لا نقبل عبارة «تم الاختبار» دون أثر تنفيذي؟
- وما الأدلة التي يجب أن يقدمها قبل أن نعتبر الميزة جاهزة؟
فالذكاء الاصطناعي يستطيع كتابة الميزة، وكتابة اختبارها، وتشغيل الاختبار.
لكن لا ينبغي أن يكون وحده من: يحدد المتطلب، ويكتب التنفيذ، ويقرر شكل الاختبار، ثم يعلن أن كل شيء صحيح.
في الجزء الخامس سنتعلم كيف نطلب منه الأدلة، لا الوعود.
اعتمد هذا الجزء على مقالة Aikido المنشورة في 13 يوليو 2026 حول جودة الكود المولد بالذكاء الاصطناعي، وتراكم الديون التقنية، وأهمية مراجعة Pull Requests والمستودعات باستخدام قواعد محددة بدل مراجعة عامة واسعة. والمقالة مصدر مهني صادر عن شركة تقدم منتجًا تجاريًا في هذا المجال، لذلك استُخدمت أفكارها التحليلية دون تبني الجزء التسويقي المتعلق بمنتج الشركة بوصفه الحل الوحيد.
كما اعتمد على إرشادات Flutter الرسمية لفصل المسؤوليات، والطبقات، وDependency Injection، وتنظيم نماذج البيانات؛ وعلى وثائق Microsoft الرسمية لمبادئ SOLID وDependency Inversion وDI؛ وعلى إرشادات Effective Dart للتسمية والأنواع والتوثيق؛ وعلى تعريف Martin Fowler للـRefactoring والتحسين التدريجي للبنية الداخلية مع المحافظة على السلوك الخارجي.