التصميم المدفوع بالمجال في مشاريع Rust: عندما تصبح الأنواع حلفاءك
هل سبق لك أن كتبت تطبيقًا بلغة Rust غرقت فيه منطق الأعمال في بحر من الهياكل والسمات والماكرو؟ شعور مألوف عندما تنظر إلى الكود ولا تستطيع أن تقول بسرعة ما تفعله هذه الوظيفة أو تلك في سياق المجال؟ إذا كان الأمر كذلك - مرحبًا بك في النادي. التصميم المدفوع بالمجال (DDD) مصمم لحل هذه المشكلة، وRust مع نظام أنواعها هي ساحة مثالية لتطبيق مبادئ DDD.
في هذه المقالة، لن نعيد سرد كتاب إيفانز. بدلاً من ذلك، سأوضح كيفية تطبيق التصميم المدفوع بالمجال في مشاريع Rust بأمثلة حقيقية. ستتعلم كيف تجعل كود المجال نظيفًا وآمنًا ومفهومًا باستخدام أنواع التغليف والهندسة المعيارية والهياكل التعبيرية. هيا بنا!
لماذا Rust و DDD زوج مثالي
التصميم المدفوع بالمجال يتطلب عزلًا صارمًا لمنطق المجال عن البنية التحتية. توفر Rust أدوات قوية لذلك:
- نظام الأنواع - يسمح بنمذجة قواعد الأعمال على مستوى المترجم. خطأ؟ المترجم لن يسمح به.
- الملكية والاقتراض - يضمنان عدم تعديل التجميعات والكيانات بشكل عشوائي في مكان غير متوقع.
- النظام المعياري - يفصل بوضوح طبقة المجال والبنية التحتية والتطبيق.
لكن هناك أيضًا عوائق. Rust لا يغفر الكائنات "الضخمة" مع الكثير من الحالة القابلة للتغيير. لذلك، يتطلب DDD في Rust نهجًا مختلفًا للنمذجة عن، على سبيل المثال، Java أو C#.
الفكرة الرئيسية: اجعل الحالات غير الصالحة غير قابلة للتمثيل
في DDD، نسعى إلى أن تكون قواعد الأعمال مشفرة في بنية البيانات نفسها. في Rust، يتحول هذا إلى شعار: "إذا كانت الحالة غير مسموح بها - اجعل من المستحيل إنشاؤها".
بدلاً من كتابة:
struct User { name: String, email: String, age: u8,}نقوم بإنشاء أنواع تضمن الصحة بالفعل في مرحلة التجميع:
pub struct Email(String);pub struct Age(u8);pub struct UserName(String);
pub struct User { name: UserName, email: Email, age: Age,}يبدو أنها مجرد أغلفة؟ في الواقع، هذه هي الخطوة الأولى نحو DDD على Rust، حيث تصبح الأنواع وثائق وحارسًا للثوابت.
نمذجة المجال: Value Object و Entity
المفاهيم المركزية لـ DDD هي Entity (كيان) و Value Object (كائن القيمة). في Rust، يتم تنفيذهما بشكل مختلف.
Value Object في Rust: الثبات والمساواة حسب القيمة
Value Object ليس له هوية. كائنان متساويان إذا كانت حقولهما متساوية. في Rust، يتناسب تمامًا مع الهياكل باستخدام #[derive(PartialEq)] والحقول غير القابلة للتغيير.
use std::fmt;
#[derive(Debug, Clone, PartialEq)]pub struct Money { amount: i64, currency: Currency,}
#[derive(Debug, Clone, PartialEq)]pub enum Currency { USD, EUR, RUB,}
impl Money { pub fn new(amount: i64, currency: Currency) -> Self { // قاعدة الأعمال: لا يمكن أن يكون المبلغ سالبًا assert!(amount >= 0, "Amount must be non-negative"); Self { amount, currency } }
pub fn add(&self, other: &Money) -> Result<Self, String> { if self.currency != other.currency { return Err("Cannot add different currencies".into()); } Ok(Self::new(self.amount + other.amount, self.currency.clone())) }}لاحظ: Money غير قابل للتغيير. أي عملية تُرجع كائنًا جديدًا. هذا يتوافق مع فلسفة DDD في Rust: لا يوجد setters، فقط طرق تنشئ مثيلات جديدة.