مواصفات المنشطات

يعد موضوع التجريد وجميع أنواع الأنماط الجميلة أساسًا جيدًا لتطوير holivars والنزاعات الأبدية: من ناحية ، نحن نتبع الاتجاه السائد ، وجميع أنواع الكلمات العصرية والكود النظيف ، من ناحية أخرى ، لدينا ممارسة وواقعية تملي قواعدهما دائمًا.

ماذا تفعل إذا بدأت التجريدات في "التسرب" ، وكيفية استخدام شرائح اللغة وما يمكنك الضغط عليه من نمط "المواصفات" - انظر أسفل المقطع.

لذلك ، دعونا ننكب على العمل. سوف تحتوي المقالة على الأقسام التالية: بالنسبة للمبتدئين ، سوف ندرس ماهية نمط "المواصفات" ولماذا يسبب تطبيقه على عينات من قاعدة البيانات في شكله النقي صعوبات.

بعد ذلك ، ننتقل إلى أشجار التعبير ، والتي هي أداة قوية للغاية ، ونرى كيف يمكنهم مساعدتنا.

في النهاية ، سأثبت تنفيذي لـ "المواصفات" على المنشطات.

لنبدأ بالأشياء الأساسية. أعتقد أن الجميع قد سمعوا عن نمط "المواصفات" ، ولكن بالنسبة لأولئك الذين لم يسمعوا ، إليك تعريفه من ويكيبيديا :

"المواصفات" في البرمجة هي نمط تصميم يمكن من خلاله تحويل تمثيل قواعد منطق الأعمال إلى سلسلة من الكائنات المرتبطة بعمليات منطقية منطقية.

يسلط هذا القالب الضوء على هذه المواصفات (القواعد) في منطق الأعمال المناسبة "للاقتران" مع الآخرين. يرث كائن منطق الأعمال وظيفته من فئة التجميع التجميعية CompositeSpecification ، التي تحتوي على طريقة IsSatisfiedBy واحدة فقط تقوم بإرجاع قيمة منطقية. بعد إنشاء مثيل ، يتم ربط الكائن مع كائنات أخرى. نتيجة لذلك ، دون فقد المرونة في إعداد منطق العمل ، يمكننا بسهولة إضافة قواعد جديدة.

بمعنى آخر ، المواصفات هي كائن يقوم بتنفيذ الواجهة التالية (تجاهل طرق إنشاء سلاسل):

public interface ISpecification { bool IsSatisfiedBy(object candidate); } 

كل شيء بسيط وواضح هنا. ولكن الآن دعونا نلقي نظرة على مثال من العالم الواقعي ، حيث يوجد ، بالإضافة إلى المجال ، بنية أساسية تعتبر أيضًا شخصًا لا يرحم: فلننتقل إلى حالة استخدام ORM و DBMS ومواصفات تصفية البيانات في قاعدة البيانات.

لكي لا تكون بلا أساس ولا أصابع الاتهام ، نأخذ على سبيل المثال مجال الموضوع التالي: لنفترض أننا نقوم بتطوير MMORPGs ، ولدينا مستخدمون ، ولكل مستخدم حرف واحد أو أكثر ، ولكل حرف مجموعة من العناصر ( نحن نفترض أن العناصر فريدة لكل مستخدم) ، ولكل عنصر من العناصر ، بدوره ، يمكن تطبيق رموز التحسين. بشكل إجمالي ، في شكل رسم تخطيطي (سننظر في فئة ReadCharacter لاحقًا عندما نتحدث عن استعلامات متداخلة):

صورة

يرتبط هذا النموذج ارتباطًا وثيقًا بالعالم الحقيقي ، كما أنه يحتوي على حقول تعكس بعض الارتباطات مع ORM المستخدمة ، ولكن هذا سيكون كافياً لإظهار العمل.

لنفترض أننا نريد تصفية جميع الأحرف التي تم إنشاؤها بعد التاريخ المحدد.
للقيام بذلك ، نكتب مواصفات النموذج التالي:

 public class CreatedAfter: ISpecification { private readonly DateTime _target; public CreatedAfter(DateTime target) { _target = target; } bool IsSatisfiedBy(object candidate) { var character = candidate as Character; if(character == null) return false; return character.CreatedAt > target; } } 

حسنًا ، إذن ، لتطبيق هذه المواصفات ، ننفذ ما يلي (فيما يلي سأنظر في التعليمات البرمجية المستندة إلى NHibernate):

 var characters = await session.Query<Character>().ToListAsync(); var filter = new CreatedAfter(new DateTime(2020, 1, 1)); var newCharacters = characters.Where(x => filter.IsSatisfiedBy(x)).ToArray(); 

طالما كانت قاعدتنا صغيرة ، فسيعمل كل شيء بشكل جميل وسريع ، ولكن إذا أصبحت لعبتنا أكثر أو أقل اكتسبت عشرات الآلاف من المستخدمين ، فإن هذا السحر سيأكل الذاكرة والوقت والمال ، ومن الأفضل إطلاق هذا الوحش فورًا ل هو ليس مستأجر. في هذه الملاحظة المحزنة ، سنؤجل المواصفات وننتقل قليلاً إلى ممارستي.

ذات مرة ، في مشروع بعيد جدًا جدًا ، كان لدي فئات في التعليمات البرمجية الخاصة بي تحتوي على منطق لاسترداد البيانات من قاعدة البيانات. نظروا إلى شيء مثل هذا:

 public class ICharacterDal { IEnumerable<Character> GetCharactersCreatedAfter(DateTime date); IEnumerable<Character> GetCharactersCreatedBefore(DateTime date); IEnumerable<Character> GetCharactersCreatedBetween(DateTime from, DateTime to); ... } 

واستخدامها:

 var dal = new CharacterDal(); var createdCharacters = dal.GetCharactersCreatedAfter(new DateTime(2020, 1, 1)); 

داخل الفصول كان المنطق للعمل مع DBMS (في ذلك الوقت كان ADO.NET).

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

تم استبدال هذا النهج بمستودع IQueryable <T> ، والذي سمح بنقل جميع القواعد مباشرةً إلى طبقة المجال.

 public interface IRepository<T> { T Get(object id); IQueryable<T> List(); void Delete(T obj); void Save(T obj); } 

الذي كان يستخدم شيئا مثل هذا:

 var repository = new Repository(); var targetDate = new DateTime(2020, 1, 1); var createdUsers = await repository.List().Where(x => x.CreatedAd > targetDate).ToListAsync(); 

أجمل قليلاً ، لكن المشكلة تكمن في أن القواعد تتسلل على طول الكود ، ويمكن أن يحدث نفس الاختيار في مئات الأماكن ، ومن السهل تخيل ما يمكن أن يؤدي إلى تغيير المتطلبات.

يخفي هذا الأسلوب مشكلة أخرى - إذا لم تقم بتجسيد الاستعلام ، فهذه فرصة لتحقيق استعلامات متعددة لقاعدة البيانات ، بدلاً من استبيان واحد ، مما يؤثر بالطبع سلبًا على أداء النظام.

وهنا في أحد المشاريع ، اقترح أحد الزملاء استخدام مكتبة تقترح تنفيذ نمط "المواصفات" على أساس أشجار التعبير.

باختصار ، على أساس هذه المكتبة ، قمنا بتصوير المواصفات التي سمحت لنا بإنشاء عوامل تصفية للكيانات وبناء عوامل تصفية أكثر تعقيدًا استنادًا إلى سلسلة من القواعد البسيطة. على سبيل المثال ، لدينا مواصفات للأحرف التي تم إنشاؤها بعد السنة الجديدة وهناك مواصفات لاختيار الأحرف مع عنصر معين - ثم من خلال الجمع بين هذه القواعد ، يمكننا إنشاء طلب للحصول على قائمة من الأحرف التي تم إنشاؤها بعد السنة الجديدة والحصول على العنصر المحدد. وإذا قمنا في المستقبل بتغيير قاعدة تحديد الأحرف الجديدة (على سبيل المثال ، سوف نستخدم تاريخ السنة الصينية الجديدة) ، فسوف نقوم بتصحيحها فقط في المواصفات نفسها ولن تكون هناك حاجة للبحث عن جميع استخدامات هذا المنطق عن طريق الكود!

تم الانتهاء من هذا المشروع بنجاح ، وكانت تجربة استخدام هذا النهج ناجحة للغاية. لكنني لا أريد الوقوف صامتًا ، وكانت هناك بعض المشاكل في التنفيذ ، وهي:

  • عامل الإلتصاق OR لا يعمل ؛
  • يعمل الاتحاد فقط للاستعلامات التي تحتوي على عوامل تصفية من النوع أين ، لكنني أردت قواعد أكثر ثراء (استعلامات متداخلة ، تخطي / أخذ ، الحصول على إسقاطات) ؛
  • رمز المواصفات يعتمد على ORM المحدد ؛
  • لم يكن من الممكن استخدام ميزات ORM ، مثل أدى ذلك إلى تضمين التبعيات عليه في طبقة منطق الأعمال (على سبيل المثال ، كان من المستحيل القيام بجلب).

كانت نتيجة حل هذه المشكلات هي إطار Singularis.Secification المصغر ، والذي يتكون من عدة مجموعات:

  • Singularis.Specification.Definition - يحدد كائن المواصفات ، ويحتوي أيضًا على واجهة IQuery التي تم تشكيل القاعدة بها.
  • ينفذ Singularis.Specification.Executor. * - مستودع وكائن لتنفيذ مواصفات ORMs المحددة (المدعومة حاليًا من قبل ef.core و NHibernate ، كجزء من التجارب التي قمت بها أيضًا بتنفيذ mongodb ، لكن هذا الرمز لم يدخل حيز الإنتاج).

دعونا نلقي نظرة فاحصة على التنفيذ.

تعرّف واجهة المواصفات الخاصية العامة التي تحتويها قاعدة المواصفات:

 public interface ISpecification { IQuery Query { get; } Type ResultType { get; } } public interface ISpefication<T>: ISpecification { } 

بالإضافة إلى ذلك ، تحتوي الواجهة على خاصية ResultType ، والتي تُرجع نوع الكيان الذي تم الحصول عليه نتيجة الاستعلام.

تطبيقه موجود في فئة المواصفات <T> ، والتي تنفذ خاصية ResultType ، وتحسبها بناءً على القاعدة المخزنة في الاستعلام ، وكذلك طريقتين: المصدر () والمصدر <مصدر _ (المصدر) () . تعمل هذه الطرق على تكوين مصدر القاعدة. ينشئ المصدر () قاعدة بنوع يطابق وسيطة فئة المواصفات ، ويسمح لك المصدر <TSource> () بإنشاء قاعدة لفئة اعتباطية (تُستخدم عند إنشاء استعلامات متداخلة).

بالإضافة إلى ذلك ، هناك أيضًا فئة SpecificationExtension ، والتي تحتوي على طرق ملحق لطلبات التسلسل.

هناك نوعان من الانضمام مدعومان: السَلسَلة (يمكن اعتبارها صلة بشروط "AND") والانضمام بواسطة شرط "OR".

دعنا نعود إلى مثالنا وتنفيذ قاعدتنا:

 public class CreatedAfter: Specification<Character> { public CreatedAfter(DateTime target) { Query = Source().Where(x => x.CreatedAt > target); } } public class CreatedBefore: Specification<Character> { public CreatedBefore(DateTime target) { Query = Source().Where(x => x.CreatedAt < target); } } 

والعثور على جميع المستخدمين الذين يستوفون كل القواعد:

 var specification = new CreatedAfter(new DateTime(2019, 1, 1).Combine(new CreatedBefore(new DateTime(2020, 1, 1)); var users = repository.List(specification); 

الجمع مع طريقة الجمع يدعم القواعد التعسفية. الشيء الرئيسي هو أن النوع الناتج من الجانب الأيسر يتزامن مع نوع الإدخال من الجانب الأيمن. وبالتالي ، يمكنك إنشاء قواعد تحتوي على إسقاطات ، وتخطي / أخذ ترقيم الصفحات ، وقواعد الفرز ، وجلب ، إلخ.

تعتبر قاعدة Or أكثر تقييدًا - فهي تدعم فقط السلاسل التي تحتوي على شروط تصفية المكان. فكر في استخدام مثال: نجد جميع الشخصيات التي تم إنشاؤها قبل عام 2000 أو بعد عام 2020:

 var specification = new CreatedAfter(new DateTime(2020, 1, 1).Or(new CreatedBefore(new DateTime(2000, 1, 1)); var users = repository.List(specification ); 

تكرر واجهة IQuery إلى حد كبير واجهة IQueryable ، لذا لا ينبغي أن تكون هناك أي أسئلة خاصة. دعونا نتحدث فقط عن طرق محددة:

Fetch / ThenFetch - يسمح لك بتضمين البيانات ذات الصلة في الاستعلام الذي تم إنشاؤه لأغراض التحسين. بطبيعة الحال ، هذا أمر ملتوي بعض الشيء عندما يكون لدينا ميزات لتنفيذ البنية التحتية التي تؤثر على قواعد العمل ، ولكن ، كما قلت ، فإن الواقع هو تجريد قاسي ونقي - وهذا شيء نظري إلى حد ما.

حيث - IQuery تعلن عن تحميلين زائدين لهذه الطريقة ، يأخذ أحدهما مجرد تعبير lambda للترشيح في شكل Expression <Func <T ، bool >> ، والثاني يأخذ أيضًا في معلمات إضافية IQueryContext ، والتي تتيح لك تنفيذ استعلامات فرعية متداخلة. لنلقِ نظرة على مثال.

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

 public class CharactersForUserWithEmailDomain: Specification<ReadCharacter> { public CharactersForUserWithEmailDomain(string domain) { var usersQuery = Source<User>(x => x.Email.Contains(domain)).Projection(x => x.Id); Query = Source().Where((x, ctx) => ctx.GetQueryResult<int>(usersQuery).Contains(x.Id)); } } 

نتيجة للتنفيذ ، سيتم إنشاء استعلام sql التالي:

 select readcharac0_.id as id1_3_, readcharac0_.UserId as userid2_3_, readcharac0_.Name as name3_3_ from ReadCharacters readcharac0_ where readcharac0_.UserId in ( select user1_.Id from Users user1_ where user1_.Email like ('%'+@p0+'%') ); @p0 = '@inmagna.ca' [Type: String (4000:0:0)] 

للوفاء بجميع هذه القواعد الرائعة ، يتم تعريف واجهة IRepository ، والتي تتيح لك استلام العناصر حسب المعرف ، وتلقي واحدة (الأولى مناسبة) أو قائمة من الكائنات وفقًا للمواصفات ، وكذلك حفظ العناصر وحذفها من المستودع.
مع تعريف الاستعلامات ، اكتشفنا ، والآن يبقى لتعليم ORM لدينا لفهم هذا.
للقيام بذلك ، سنقوم بتحليل تجميع Singularis.Infrastructure.NHibernate (على سبيل المثال ، يبدو كل شيء كما هو ، فقط مع تفاصيل ef.core).

نقطة الوصول إلى البيانات هي كائن السجل ، الذي ينفذ واجهة IRepository . في حالة تلقي كائن حسب المعرف ، وكذلك لتعديل التخزين (الحفظ / الحذف) ، فإن هذه الفئة تختتم جلسة وتخفي تنفيذًا محددًا من طبقة العمل. في حالة العمل مع المواصفات ، فإنه يشكل كائن IQueryable يعكس استعلامنا من حيث IQuery ، ثم ينفّذه على كائن الجلسة.

يكمن السحر الرئيسي والرمز الأكثر بشاعة في الفئة المسؤولة عن تحويل IQuery إلى IQueryable - SpecificationExecutor. تحتوي هذه الفئة على الكثير من الانعكاس ، والذي يستدعي أساليب Queryable أو طرق التمديد الخاصة بـ ORM (EagerFetchingExtensionsMethods لـ NHiberante).

يتم استخدام هذه المكتبة بنشاط في مشاريعنا (لنكون صادقين ، يتم استخدام مكتبة تم تحديثها بالفعل لمشاريعنا ، ولكن تدريجياً ستوضع كل هذه التغييرات في المجال العام) تخضع باستمرار للتغييرات. منذ أسبوعين فقط ، تم إصدار الإصدار التالي ، والذي تحول إلى طرق غير متزامنة ، تم إصلاح الخلل في المنفذ من أجل ef.core ، وتمت إضافة الاختبارات والعينات. من المحتمل أن تحتوي المكتبة على أخطاء ومائة مكان للتحسين - لقد ولدت كمشروع جانبي في إطار العمل في المشاريع الرئيسية ، لذلك سأكون سعيدًا باقتراح اقتراحات للتحسين. بالإضافة إلى ذلك ، يجب ألا تتسرع في استخدامه - من المحتمل أن يكون هذا غير ضروري أو غير قابل للتطبيق في حالتك الخاصة.

متى يستحق استخدام الحل الموصوف؟ من الأسهل أن تبدأ من السؤال "متى لا ينبغي":

  • highload - إذا كنت بحاجة إلى أداء عالي ، فإن استخدام ORM نفسه يثير سؤالًا. على الرغم من ذلك ، بالطبع ، لا يمنع أحد تنفيذ منفذي يترجم الاستعلامات إلى SQL وينفذها ...
  • المشاريع الصغيرة جدًا - هذا أمر شخصي للغاية ، ولكن يجب عليك الاعتراف بأن سحب ORM وكل حديقة الحيوانات المصاحبة له في مشروع "قائمة المهام" يشبه إطلاق العصفور من مدفع.

في أي حال ، الذين أتقن القراءة حتى النهاية - شكرا على وقتك. آمل أن ردود الفعل للتنمية في المستقبل!

لقد نسيت تقريبا - رمز المشروع متاح على GitHub'e - https://github.com/SingularisLab/singularis.specification

Source: https://habr.com/ru/post/ar485328/


All Articles