التصميم المبني على المجال في C#: كائنات القيمة غير القابلة للتغيير

في كتابه، التصميم الموجه بالمجال (DDD)، يشجع إريك إيفانز استخدام كائنات القيمة في نماذج المجال – الأنواع غير القابلة للتغيير التي يتم استخدامها كخصائص للكيانات.
يمكنك معرفة المزيد حول كائنات القيمة وDDD في الدورة التدريبية لأساسيات التصميم المستند إلى المجال والتي شاركت في تأليفها مع ستيف سميث.
لقد كان فريق التطوير في Pluralsight منذ فترة طويلة من المعجبين باستخدام نهج DDD في النمذجة، وهو كائنات القيمة النفوذية عبر طبقة المجال الخاصة بهم.
في منشور مدونة من عام 2012، أشار Keith Sparkjoy إلى أن هذا جعل كود C# الخاص بهم يبدو أكثر وظيفية بطبيعته، وهو ما له الكثير من الفوائد.
“تميل الحالة الأقل قابلية للتغيير إلى تسهيل اختبار الأشياء وتميل إلى تقليل الأخطاء.”
إن بناء الثبات في النوع الخاص بك هو أكثر من مجرد إخفاء محددات الخصائص.
مقترنًا بأحد أهم جوانب كائن القيمة – أن هويته مرتبطة بتكوين قيمه وليس خاصية هوية عشوائية – هناك أنماط أكثر صحة لضمان أن كائن القيمة الخاص بك غير قابل للتغيير. مفاتيح تصميم كائن القيمة هي:
- إنه ثابت
- استخدامه الوحيد هو كخاصية لكيانات واحدة أو أكثر أو حتى كائن قيمة آخر
- إنه يعرف كيفية استخدام جميع خصائصه عند إجراء أي نوع من التحقق من المساواة، بما في ذلك تلك التي تعتمد مقارنتها على رموز التجزئة
- يجب ألا يكون لمنطقه أي آثار جانبية على الحالة خارج كائن القيمة
كائن قيمة مثال: PersonFullName
دعونا نلقي نظرة على النوع الذي يمكن إعادة استخدامه عبر النظام، وهو النوع الذي يمثل اسم الشخص. يمكن استخدام هذا للمشتركين والمؤلفين وجهات الاتصال والمزيد.
سأطلق عليه اسم PersonFullName وأبدأ بإصدار بسيط ثم أقوم بتطويره إلى كائن قيمة.
لنفترض أن مهندسي Pluralsight قد حددوا قاعدة عمل تنص على أنه يجب أن يكون لكل فرد اسم معين (يُشار إليه غالبًا بالاسم الأول) ولقب (ويُعرف أيضًا باسم الاسم الأخير).
تحدد فئة PersonFullName الخاصة بي أولاً حقلين خاصين، _given و_surname. تفرض طريقة إنشاء المصنع أنه لا يمكنك أبدًا إنشاء PersonFullName دون تمرير هذين الجزأين من الاسم. وهذا يضمن أن الاسم لن يكون في حالة غير صالحة أبدًا.
يقوم المصنع بعد ذلك بتمرير تلك القيم إلى المُنشئ الذي يقوم بتعيين قيم الحقول. في هذه الحالة البسيطة، يمكنك القول بأن طريقة المصنع مبالغ فيها. ولكن هنا ترى مثالاً على هذا التنفيذ.
أخيرًا، أستخدم تعريفات نص التعبير (راجع منشور مدونة Dániel Szabóلمعرفة المزيد حول هذه) على الخصائص للتأكد من أن الخصائص للقراءة فقط.
الخصائص تعرف فقط كيفية إرجاع قيم الحقول. يتيح لي استخدام التعبيرات أيضًا الإشارة إلى إعجاب Keith برمز C# الذي يميل نحو البرمجة الوظيفية.
public class PersonFullName { private string _given; private string _surname; public static PersonFullName Create (string given, string surname) { return new PersonFullName (given, surname); } private PersonFullName(string given, string surname) { _given = given; _surname = surname; } public string Given => _given; public string Surname => _surname; }
لاحظ أنه لا توجد خاصية الهوية هنا. لن يتم تتبع الاسم من تلقاء نفسه أبدًا. لن يتم استخدام هذه الفئة إلا كخاصية من نوع آخر.
النوع بالفعل غير قابل للتغيير – بمجرد إنشاء الكائن، لا توجد طريقة لتعديله. إذا كتبت اسم الشخص بشكل خاطئ، فستقوم فقط بإنشاء مثيل جديد. تماما كما تفعل مع سلسلة.
هنا فئة المؤلف التي تستخدم كائن القيمة.
public class Author { private Guid _id; private PersonFullName _name; public Author (string given, string surname) { _id = Guid.NewGuid(); _name = PersonFullName.Create(given, surname); } public Guid Id => _id; public PersonFullName Name=>_name; public void FixAuthorName(string given, string surname) {_name = PersonFullName.Create(given, surname); } }
يتطلب مُنشئ المؤلف أيضًا توفير القيم المعطاة وقيم اللقب ثم استخدامها لإنشاء كائن PersonFullName بالإضافة إلى المرشد الذي تم إنشاؤه حديثًا لهويته.
إذا كنت بحاجة إلى تغيير الاسم، يمكنك استدعاء الأسلوب FixAuthorName لإعادة إنشاء هذا الاسم بسرعة.
يرشدنا DDD إلى التفكير في السلوكيات في كياناتنا، وليس فقط تحديد قيم الممتلكات والحصول عليها. يمثل FixAuthorName السلوك المطلوب لحل مشكلة معينة.
فقط لأن PersonFullName غير قابل للتغيير، لا يعني أن النوع لا يمكن أن يكون ذكيًا. غالبًا ما نحتاج إلى طرق أخرى لعرض الأسماء ويمكننا دمجها مباشرة في نوع PersonFullName.
على سبيل المثال، أريد أن أتمكن من رؤية الاسم بالكامل وترتيبه أبجديًا حسب اللقب. لقد أضفت المزيد من الخصائص للقراءة فقط، الاسم الكامل والعكس، مرة أخرى باستخدام تعريفات نص التعبير.
public string FullName => _given + " " + _surname; public string Reverse => _surname+", "+ _given;
يمكنني إضافة هذه الطريقة الذكية الصغيرة للسماح لنا بإنشاء اسم جديد لأحد أفراد العائلة يحمل نفس اللقب.
public PersonFullName FamilyMemberWithSameSurname(string newGivenName) => Create (newGivenName, _surname);
جعل كائن القيمة يقوم بإجراء مقارنات مناسبة
أصبح PersonFullname الآن نوعًا غير قابل للتغيير وله قيمتان ولا يوجد معرف. لكي يصبح هذا كائن قيمة، ما زلت بحاجة للتأكد من أنه يمكنني مقارنة نوعين من PersonFullName لتحديد المساواة.
وأحتاج إلى المرونة للقيام بذلك باستخدام طريقة Equals الموروثة من قبل كل كائن في C# وكذلك مع عوامل التشغيل == أو! =.
أريد أيضًا أن أفكر في اختبار المكان الذي قد أستخدم فيه MSTest Assert.AreEqual أو التأكيدات من أطر عمل أخرى.
إن مقارنة مثيلين لكائن القيمة هذا مع يساوي يعني مقارنة كل قيمة من القيم الموجودة في الكائنات. يتم إجراء بعض عمليات التحقق من المساواة باستخدام رموز التجزئة الخاصة بالكائن.
وهذا يعني إضافة المزيد من التعليمات البرمجية المعيارية. هناك طرق قليلة مثيرة للاهتمام لتحقيق ذلك.
يساوي Visual Studio والإجراء السريع لـ GetHashCode
إحدى الطرق هي الاستفادة من ميزة Visual Studio Quick Action. أحد إجراءات إعلان الفئة هو إنشاء (على سبيل المثال، تجاوز) Equals وGetHasCode.

سيسأل هذا عن الأعضاء الذين سيتم تضمينهم. أريد فقط إشراك الاسم واللقب وسأختار الخصائص وليس الحقول.
أحد الخيارات الأخرى هو تنفيذ IEquatable الذي تريد القيام به بحيث لا يضيف تجاوز يساوي فحسب، بل يضيف أيضًا أسلوب يساوي لنوع معين.
الخيار المهم الآخر هو توليد عوامل التشغيل. سيضمن ذلك أنه يمكنك استخدام == و != في التعليمات البرمجية الخاصة بك ومقارنة كائنين PersonFullName بشكل صحيح.
تبدو الفئة المحدثة الآن كما يلي:
public class PersonFullName : IEquatable<PersonFullName> { . . . original ctor, properties and methods are still here . . . public override bool Equals (object obj) { return Equals (obj as PersonFullName); } public bool Equals (PersonFullName other) { return other! = null && Given == other.Given && Surname == other.Surname; } public override int GetHashCode() { return HashCode.Combine(Given, Surname); } public static bool operator == (PersonFullName left, PersonFullName right) { return EqualityComparer<PersonFullName>.Default.Equals(left, right); } public static bool operator! = (PersonFullName left, PersonFullName right) { return! (left == right); } }
من الجميل جدًا أن يتم تنفيذ كل كود لوحة الغلاية هذا بالنسبة لنا. بدون ذلك، لن يتم النظر إلى كائنين يحتويان على “Julie” و”Lerman” على أنهما متساويان مع عامل التشغيل Equals أو ==.
ولكن مع وجود هذا الكود، فإن المساواة تعتمد فقط على قيمتي الخاصيتين ويتم الاعتراف بها. يتمتع JetBrains ‘Rider بميزة مماثلة. لم أر أي شيء لملحقات VS Code.
باستخدام سجلات C#9
هناك خيار آخر وهو ميزة التسجيل C#9 الجديدة. تعرف على المزيد حول السجلات من المؤلف Thomas Claudius Huber على thomasclaudiushuber.com/2020/09/01/c-9-0-records-work-with-immutable-data-classes.
بدلاً من تطبيق IEquatable وإضافة كل هذه التعليمات البرمجية إلى كائن القيمة الخاص بك، يمكنك تغيير النوع من فئة إلى سجل.
لدي نسخة مكررة من الفئة الأصلية (قبل استخدام الإجراء السريع، الذي قمت بتسميته PersonFullNameRecord والفرق الوحيد هو أنه تم الإعلان عنه كسجل، وليس فئة.
السجل العام PersonFullNameRecord
في وقت الترجمة، ستضيف لغة C# جميع العناصر الأساسية القابلة للمساواة ورمز التجزئة إلى جانب بعض الأشياء الجيدة الأخرى.
في الواقع، باستخدام ملحق ILSpy لـ Visual Studio، أستطيع أن أرى (عند تصحيح الأخطاء) أن الفئة المترجمة تحتوي بالفعل على هذا النموذج المضمن.
النسخة المترجمة من الفئة طويلة بعض الشيء ولكني قمت بتضمينها كملف نصي في الحل.
ولكنه يحتوي على كل ما أضفته إلى الإصدار السابق من الفصل وبعض الأساليب الإضافية مثل PrintMembers وToString لتجاوز هذا السجل الذي يجلبه إلى الحفلة.
قد لا تحل سجلات C#9 كل مشكلة في كائن القيمة الخاص بك، لذا تأكد من اختبار حالات الاستخدام الخاصة بك!
المسار الآخر هو الوراثة من فئة أساسية ذكية تنفذ كل المنطق المطلوب مثل ValueObject لجيمي بوجارد (من عام 2007) أو مؤلف Pluralsight، ValueObject لفلاديمير خوريكوف أو تطبيقات أخرى تتوسع في هذا المفهوم.
ماذا عن ثبات البيانات؟
عادةً، يتم تخزين قيم خاصية كائن القيمة في جدول قاعدة البيانات الذي تم تعيين الكيان إليه. في حالتي، سيتضمن جدول المؤلفين أعمدة للقيم المعطاة وقيم اللقب.
تخصصي في استمرارية البيانات هو Entity Framework Core الخاص بـ .NET والذي يمنحنا طريقتين لاستمرار كائنات القيمة.
الطريقة الأولى هي من خلال تعيين الكيان المملوك (أو المجموعة المملوكة) لشركة EF Core (https://docs.microsoft.com/en-us/ef/core/modeling/OWN-entities). تم تصميم هذا للأنواع المعقدة التي يتم استخدامها كخصائص.
إن سمات الثبات والمساواة التي تساعدنا في تحديد كائن القيمة تأتي للتو في الرحلة.
ميزة EF Core الأخرى التي تعمل بشكل جيد للغاية مع كائنات القيمة المستمرة التي لها قيمة واحدة هي تحويل القيمة. تخيل أن لديك كائن قيمة يسمى OneWordName للأشخاص ذوي الأسماء الفردية، مثل Prince.
يحتوي هذا النوع على خاصية سلسلة واحدة تسمى TheName وبعض المنطق المساعد الإضافي ذي الصلة بنطاقك.
باستخدام طريقة تعيين HasConversion الخاصة بـ EF Core، يمكنك تحويل كائن OneWordName إلى سلسلة (سوف تجد EF Core تلك الخاصية الفردية) ومن ثم يمكن لـ EF Core بسهولة تعيين السلسلة إلى مخطط قاعدة بيانات. يحتوي HasConversion أيضًا على منطق لإعادة ترطيب البيانات إلى كائناتك.
وكما قال Sparkjoy مرة أخرى في عام 2012، “إن جمال كل هذه العزلة هو أننا نقوم بإنشاء طبقة نطاق أصلية خالية من التكنولوجيا، ولا نترك سوى منطق عمل موجز،” وهو أحد الأهداف الرئيسية للتصميم المبني على النطاق.
المصدر: pluralsight
شاهد ايضا:
فتح حساب بنك الإمارات دبي الوطني