Rust والهندسة المعمارية السداسية: نكتب كودًا لا نخاف من تغييره
\n\nكل مطور واجه مرة واحدة على الأقل موقفًا تحول فيه تغيير بسيط في قاعدة البيانات أو تغيير إطار عمل HTTP إلى إعادة هيكلة جهنمية. يتشابك منطق الأعمال مع تفاصيل التنفيذ، ويتحول الكود إلى "معكرونة". بالنسبة لـ Rust، مع تركيزها على الأمان والأداء، هذا الخلط هو جريمة حقيقية.
\n\nحل هذه المشكلة هو الهندسة المعمارية السداسية (المعروفة أيضًا باسم Ports and Adapters). في هذه المقالة، سنشرح كيفية تطبيق هذا النمط في Rust، باستخدام قوة نظام الأنواع والـ traits. ستتعلم كيفية عزل منطق المجال عن العالم الخارجي، وكتابة كود قابل للاختبار، وتغيير برامج تشغيل قاعدة البيانات أو خادم الويب بسهولة. هذه مهارة أساسية لأي شخص يريد بناء أنظمة معقدة وقابلة للصيانة في Rust.
\n\nما هي الهندسة المعمارية السداسية؟ (باختصار شديد)
\n\nتخيل شكلًا سداسيًا. في المركز - منطق أعمالك (المجال). على الحواف - "منافذ" (واجهات) يتواصل من خلالها المجال مع العالم الخارجي. وفي الخارج - "محولات" (تطبيقات محددة) تتصل بالمنافذ.
\n\nالفكرة الأساسية: المجال لا يعرف شيئًا عن كيفية حفظ البيانات أو إرسال الطلبات. إنه يعرف فقط عن المنافذ - traits مع دوال. المحولات (مثل مستودع PostgreSQL أو وحدة تحكم HTTP) تقوم بتنفيذ هذه الـ traits.
\n\nفي Rust، يتناسب هذا المفهوم بشكل مثالي بفضل الـ traits (ما يعادل الواجهات) و الـ generics. يقوم المترجم (Compiler) بالتحقق من جميع الاتصالات في مرحلة البناء، وليس في وقت التشغيل.
\n\nهيكل المشروع في Rust
\n\nللبدء، سننشئ هيكل مجلدات يفصل بصريًا المجال عن البنية التحتية:
\n\nsrc/\n├── domain/ # منطق أعمال نقي (لا تبعيات!)\n│ ├── mod.rs\n│ ├── models.rs # الكيانات وقيم الكائنات\n│ └── services.rs # الخدمات التي تعمل مع المنافذ\n├── ports/ # Traits (واجهات) للعالم الخارجي\n│ ├── mod.rs\n│ └── repositories.rs\n├── adapters/ # تطبيقات المنافذ (قواعد بيانات، HTTP، إلخ.)\n│ ├── mod.rs\n│ ├── postgres_repo.rs\n│ └── in_memory_repo.rs # للاختبارات\n├── main.rs # بناء التطبيق (Wiring)\n└── lib.rs # واجهة برمجة التطبيقات العامة للمكتبة\n\nالخطوة 1: المجال - قلب النظام
\n\nلنبدأ بنموذج بسيط لمستخدم. لا توجد في المجال أي تبعية للمكتبات الخارجية (حتى serde، إذا لم يكن ذلك مطلوبًا من قبل منطق الأعمال).
\n\n// src/domain/models.rs\npub struct User {\n pub id: String,\n pub name: String,\n pub email: String,\n}\n\nimpl User {\n pub fn new(id: String, name: String, email: String) -> Self {\n // يمكن أن يكون هناك تحقق هنا\n Self { id, name, email }\n }\n}\n\nالآن سنحدد خدمة المجال. ستتعامل مع المستخدمين، لكنها لن تعرف من أين يأتون.
\n\n// src/domain/services.rs\nuse crate::ports::repositories::UserRepository;\n\npub struct UserService {\n repo: Box<dyn UserRepository>,\n}\n\nimpl UserService {\n pub fn new(repo: Box<dyn UserRepository>) -> Self {\n Self { repo }\n }\n\n pub fn create_user(&mut self, name: String, email: String) -> Result<User, String> {\n let id = uuid::Uuid::new_v4().to_string(); // هل هي تبعية بالفعل؟ نعم، لكنها تفصيل\n let user = User::new(id, name, email);\n self.repo.save(&user)?;\n Ok(user)\n }\n\n pub fn get_user(&self, id: &str) -> Result<Option<User>, String> {\n self.repo.find_by_id(id)\n }\n}\n\nهام: نحن نستخدم Box<dyn UserRepository> للتوزيع الديناميكي. في Rust، هذا طبيعي للتطبيقات المعقدة. للحالات البسيطة، يمكن استخدام الـ generics.