التحقق من صحة السياق في تصميم يحركه المجال
نشرت: 2022-03-11التصميم القائم على المجال (باختصار DDD) ليس تقنية أو منهجية. يوفر DDD هيكلًا من الممارسات والمصطلحات لاتخاذ قرارات التصميم التي تركز على مشروعات البرامج التي تتعامل مع المجالات المعقدة وتسريعها. كما وصفه إريك إيفانز ومارتن فاولر ، فإن كائنات المجال هي مكان لوضع قواعد التحقق من الصحة ومنطق العمل.
إريك إيفانز:
طبقة المجال (أو طبقة النموذج): مسؤولة عن تمثيل مفاهيم الأعمال ومعلومات حول وضع العمل وقواعد العمل. يتم التحكم في الحالة التي تعكس حالة العمل واستخدامها هنا ، على الرغم من تفويض التفاصيل الفنية لتخزينها إلى البنية التحتية. هذه الطبقة هي قلب برامج الأعمال.
مارتن فاولر:
المنطق الذي يجب أن يكون في كائن المجال هو منطق المجال - عمليات التحقق من الصحة والحسابات وقواعد العمل - أيًا كان ما تريد تسميته.
يؤدي وضع جميع عمليات التحقق في كائنات المجال إلى كائنات مجال ضخمة ومعقدة للعمل معها. أنا شخصياً أفضل فكرة فصل عمليات التحقق من صحة المجال إلى مكونات مدقق منفصلة يمكن إعادة استخدامها في أي وقت وستستند إلى السياق وإجراء المستخدم.
كما كتب مارتن فاولر في مقال رائع بعنوان: ContextualValidation.
أحد الأشياء الشائعة التي أرى أن الأشخاص يفعلونها هو تطوير إجراءات التحقق من صحة الكائنات. تأتي هذه الإجراءات بطرق مختلفة ، فقد تكون في الكائن أو خارجية ، وقد تعيد قيمة منطقية أو تطرح استثناءًا للإشارة إلى الفشل. الشيء الوحيد الذي أعتقد أنه يزعج الناس باستمرار هو عندما يفكرون في صلاحية الكائن في سياق مستقل ، مثل طريقة isValid. [...] أعتقد أنه من المفيد جدًا التفكير في التحقق من الصحة على أنه شيء مرتبط بالسياق ، وعادةً ما يكون الإجراء الذي تريد القيام به. مثل السؤال عما إذا كان هذا الطلب صالحًا ليتم تنفيذه ، أو هل هذا العميل صالح لتسجيل الوصول إلى الفندق. لذا فبدلاً من استخدام طرق مثل isValid ، استخدم طرقًا مثل isValidForCheckIn.
اقتراح التحقق من صحة الإجراء
في هذه المقالة سنقوم بتطبيق واجهة ItemValidator البسيطة التي تحتاج إلى تنفيذ طريقة التحقق من الصحة مع نوع الإرجاع ValidationResult . ValidationResult هو كائن يحتوي على العنصر الذي تم التحقق من صحته وكذلك كائن الرسائل . يحتوي الأخير على تراكم للأخطاء والتحذيرات وحالات التحقق من صحة المعلومات (الرسائل) التي تعتمد على سياق التنفيذ.
المدققات عبارة عن مكونات منفصلة يمكن إعادة استخدامها بسهولة في أي مكان يحتاجون إليه. باستخدام هذا النهج ، يمكن إدخال جميع التبعيات اللازمة لعمليات التحقق من الصحة بسهولة. على سبيل المثال ، للتحقق في قاعدة البيانات إذا كان هناك مستخدم لديه بريد إلكتروني معين فقط يتم استخدام UserDomainService.
سيتم فصل المصادقات حسب السياق (الإجراء). لذلك إذا كان إجراء UserCreate وإجراء UserUpdate يحتويان على مكونات منفصلة أو أي إجراء آخر (UserActivate و UserDelete و AdCampaignLaunch وما إلى ذلك) ، يمكن أن تنمو عملية التحقق بسرعة.
يجب أن يكون لكل مدقق إجراء نموذج إجراء مطابق والذي سيحتوي فقط على حقول الإجراء المسموح بها. لإنشاء المستخدمين ، الحقول التالية مطلوبة:
UserCreateModel:
{ "firstName": "John", "lastName": "Doe", "email": "[email protected]", "password": "MTIzNDU=" }
ولتحديث المستخدم ، يُسمح بما يلي: ExternalId و firstName و lastName . يتم استخدام المعرف الخارجي لتعريف المستخدم ويسمح فقط بتغيير الاسم الأول والاسم الأخير .
UserUpdateModel:
{ "externalId": "a55ccd60-9d82-11e5-9f52-0002a5d5c51b", "firstName": "John Updated", "lastName": "Doe Updated" }
يمكن مشاركة عمليات التحقق من سلامة الحقول ، ويكون الحد الأقصى لطول الاسم الأول 255 حرفًا دائمًا.
أثناء التحقق من الصحة ، من المستحسن ليس فقط الحصول على الخطأ الأول الذي يحدث ، ولكن قائمة بجميع المشكلات التي تمت مواجهتها. على سبيل المثال ، قد تحدث المشكلات الثلاثة التالية في نفس الوقت ، ويمكن الإبلاغ عنها وفقًا لذلك أثناء التنفيذ:
- تنسيق عنوان غير صالح [ERROR]
- يجب أن يكون البريد الإلكتروني فريدًا بين المستخدمين [ERROR]
- كلمة المرور أقصر مما يجب [ERROR]
لتحقيق هذا النوع من التحقق ، هناك حاجة إلى شيء مثل منشئ حالة التحقق ، ولهذا الغرض يتم تقديم الرسائل . الرسائل هي مفهوم سمعته من معلمي العظيم منذ سنوات عندما قدمه لدعم التحقق وأيضًا للعديد من الأشياء الأخرى التي يمكن القيام بها معها ، لأن الرسائل ليست فقط للتحقق من الصحة.
لاحظ أنه في الأقسام التالية سنستخدم Scala لتوضيح التنفيذ. فقط في حال لم تكن خبيرًا في سكالا ، فلا تخف لأنه من السهل متابعتها رغم ذلك.
الرسائل في التحقق من السياق
الرسائل هي كائن يمثل منشئ حالة التحقق من الصحة. يوفر طريقة سهلة لجمع رسائل الأخطاء والتحذيرات والمعلومات أثناء التحقق من الصحة. يحتوي كل كائن من عناصر الرسائل على مجموعة داخلية من كائنات الرسائل ويمكن أن يحتوي أيضًا على مرجع لكائن الرسائل الرئيسي .
كائن الرسالة هو كائن يمكن أن يكون له نوع ، messageText ، مفتاح (وهو اختياري ويستخدم لدعم التحقق من صحة مدخلات معينة تم تحديدها بواسطة المعرف) ، وأخيرًا childMessages الذي يثبت طريقة رائعة لبناء أشجار رسائل قابلة للتكوين.
يمكن أن تكون الرسالة من أحد الأنواع التالية:
- معلومة
- تحذير
- خطأ
تسمح لنا الرسائل التي تم تنظيمها بهذا الشكل ببنائها بشكل متكرر وتسمح أيضًا باتخاذ قرارات بشأن الإجراءات التالية بناءً على حالة الرسائل السابقة. على سبيل المثال ، إجراء التحقق أثناء إنشاء المستخدم:
@Component class UserCreateValidator @Autowired (private val entityDomainService: UserDomainService) extends ItemValidator[UserCreateEntity] { Asserts.argumentIsNotNull(entityDomainService) private val MAX_ALLOWED_LENGTH = 80 private val MAX_ALLOWED_CHARACTER_ERROR = s"must be less than or equal to $MAX_ALLOWED_LENGTH character" override def validate(item: UserCreateEntity): ValidationResult[UserCreateEntity] = { Asserts.argumentIsNotNull(item) val validationMessages = Messages.of validateFirstName (item, validationMessages) validateLastName (item, validationMessages) validateEmail (item, validationMessages) validateUserName (item, validationMessages) validatePassword (item, validationMessages) ValidationResult( validatedItem = item, messages = validationMessages ) } private def validateFirstName(item: UserCreateEntity, validationMessages: Messages) { val localMessages = Messages.of(validationMessages) val fieldValue = item.firstName ValidateUtils.validateLengthIsLessThanOrEqual( fieldValue, MAX_ALLOWED_LENGTH, localMessages, UserCreateEntity.FIRST_NAME_FORM_ID.value, MAX_ALLOWED_CHARACTER_ERROR ) } private def validateLastName(item: UserCreateEntity, validationMessages: Messages) { val localMessages = Messages.of(validationMessages) val fieldValue = item.lastName ValidateUtils.validateLengthIsLessThanOrEqual( fieldValue, MAX_ALLOWED_LENGTH, localMessages, UserCreateEntity.LAST_NAME_FORM_ID.value, MAX_ALLOWED_CHARACTER_ERROR ) } private def validateEmail(item: UserCreateEntity, validationMessages: Messages) { val localMessages = Messages.of(validationMessages) val fieldValue = item.email ValidateUtils.validateEmail( fieldValue, UserCreateEntity.EMAIL_FORM_ID, localMessages ) ValidateUtils.validateLengthIsLessThanOrEqual( fieldValue, MAX_ALLOWED_LENGTH, localMessages, UserCreateEntity.EMAIL_FORM_ID.value, MAX_ALLOWED_CHARACTER_ERROR ) if(!localMessages.hasErrors()) { val doesExistWithEmail = this.entityDomainService.doesExistByByEmail(fieldValue) ValidateUtils.isFalse( doesExistWithEmail, localMessages, UserCreateEntity.EMAIL_FORM_ID.value, "User already exists with this email" ) } } private def validateUserName(item: UserCreateEntity, validationMessages: Messages) { val localMessages = Messages.of(validationMessages) val fieldValue = item.username ValidateUtils.validateLengthIsLessThanOrEqual( fieldValue, MAX_ALLOWED_LENGTH, localMessages, UserCreateEntity.USERNAME_FORM_ID.value, MAX_ALLOWED_CHARACTER_ERROR ) if(!localMessages.hasErrors()) { val doesExistWithUsername = this.entityDomainService.doesExistByUsername(fieldValue) ValidateUtils.isFalse( doesExistWithUsername, localMessages, UserCreateEntity.USERNAME_FORM_ID.value, "User already exists with this username" ) } } private def validatePassword(item: UserCreateEntity, validationMessages: Messages) { val localMessages = Messages.of(validationMessages) val fieldValue = item.password ValidateUtils.validateLengthIsLessThanOrEqual( fieldValue, MAX_ALLOWED_LENGTH, localMessages, UserCreateEntity.PASSWORD_FORM_ID.value, MAX_ALLOWED_CHARACTER_ERROR ) } }
بالنظر إلى هذا الرمز ، يمكنك مشاهدة استخدام ValidateUtils. تُستخدم وظائف الأداة المساعدة هذه لملء كائن الرسائل في الحالات المحددة مسبقًا. يمكنك التحقق من تنفيذ ValidateUtils على كود Github.
أثناء التحقق من صحة البريد الإلكتروني ، يتم التحقق أولاً مما إذا كان البريد الإلكتروني صالحًا عن طريق الاتصال ValidateUtils.validateEmail (… ، كما يتم التحقق مما إذا كان البريد الإلكتروني له طول صالح عن طريق استدعاء ValidateUtils.validateLengthIsLessThanOrEqual (… . بمجرد الانتهاء من هاتين العمليتين ، تحقق مما إذا كان البريد الإلكتروني تم تعيينه بالفعل لبعض المستخدمين ، فقط إذا مرت شروط التحقق من صحة البريد الإلكتروني السابقة ويتم ذلك مع if (! localMessages.hasErrors ()) {… . بهذه الطريقة يمكن تجنب استدعاءات قاعدة البيانات باهظة الثمن. هذا جزء فقط من UserCreateValidator يمكن العثور على كود المصدر الكامل هنا.
لاحظ أن إحدى معلمات التحقق تبرز: UserCreateEntity.EMAIL_FORM_ID . تقوم هذه المعلمة بتوصيل حالة التحقق من الصحة بمعرف إدخال محدد.
في الأمثلة السابقة ، يتم تحديد الإجراء التالي بناءً على حقيقة ما إذا كان كائن الرسائل به أخطاء (باستخدام طريقة hasErrors). يمكن للمرء بسهولة التحقق مما إذا كانت هناك أية رسائل "تحذير" وإعادة المحاولة إذا لزم الأمر.

الشيء الوحيد الذي يمكن ملاحظته هو طريقة استخدام localMessages . الرسائل المحلية هي الرسائل التي تم إنشاؤها مثل أي رسالة ، ولكن مع رسائل الأصل. مع ذلك ، فإن الهدف هو الحصول على مرجع فقط لحالة التحقق الحالية (في هذا المثال emailValidation) ، لذلك يمكن استدعاء localMessages.hasErors ، حيث يتم فحصها فقط إذا كان سياق التحقق من صحة البريد الإلكتروني به أخطاء. أيضًا عند إضافة رسالة إلى localMessages ، تتم إضافتها أيضًا إلى الرسائل الرئيسية وبالتالي توجد جميع رسائل التحقق من الصحة في سياق أعلى لـ UserCreateValidation.
الآن بعد أن رأينا الرسائل قيد التنفيذ ، سنركز في الفصل التالي على ItemValidator.
ItemValidator - مكون التحقق القابل لإعادة الاستخدام
ItemValidator هي سمة بسيطة (واجهة) تجبر المطورين على تنفيذ طريقة التحقق من الصحة ، والتي تحتاج إلى إرجاع ValidationResult.
ItemValidator:
trait ItemValidator[T] { def validate(item:T) : ValidationResult[T] }
النتيجة:
case class ValidationResult[T: Writes]( validatedItem : T, messages : Messages ) { Asserts.argumentIsNotNull(validatedItem) Asserts.argumentIsNotNull(messages) def isValid :Boolean = { !messages.hasErrors } def errorsRestResponse = { Asserts.argumentIsTrue(!this.isValid) ResponseTools.of( data = Some(this.validatedItem), messages = Some(messages) ) } }
عندما يتم تنفيذ ItemValidators مثل UserCreateValidator لتكون مكونات حقن تبعية ، يمكن عندئذٍ حقن كائنات ItemValidator وإعادة استخدامها في أي كائن يحتاج إلى التحقق من صحة إجراء UserCreate.
بعد تنفيذ التحقق ، يتم التحقق من نجاح التحقق. إذا كان الأمر كذلك ، فسيتم الاحتفاظ ببيانات المستخدم في قاعدة البيانات ، ولكن إذا لم يتم إرجاع استجابة API التي تحتوي على أخطاء التحقق من الصحة.
في القسم التالي ، سنرى كيف يمكننا تقديم أخطاء التحقق من الصحة في استجابة RESTful API وأيضًا كيفية التواصل مع مستهلكي واجهة برمجة التطبيقات حول حالات إجراء التنفيذ.
استجابة API الموحدة - تفاعل بسيط مع المستخدم
بعد التحقق من صحة إجراء المستخدم بنجاح ، في حالتنا الخاصة بإنشاء المستخدم ، يجب عرض نتائج إجراءات التحقق على مستهلك RESTful API. أفضل طريقة هي الحصول على استجابة API موحدة حيث سيتم تبديل السياق فقط (من حيث JSON ، قيمة "البيانات"). من خلال الاستجابات الموحدة ، يمكن تقديم الأخطاء بسهولة إلى عملاء RESTful API.
هيكل الاستجابة الموحد:
{ "messages" : { "global" : { "info": [], "warnings": [], "errors": [] }, "local" : [] }, "data":{} }
تم تصميم الاستجابة الموحدة بحيث تحتوي على مستويين من الرسائل ، العالمية والمحلية. الرسائل المحلية هي رسائل مقترنة بمدخلات محددة. مثل "اسم المستخدم طويل جدًا ، يُسمح بـ 80 حرفًا على الأكثر" _. الرسائل العالمية هي الرسائل التي تعكس حالة البيانات الكاملة على الصفحة ، مثل "لن يكون المستخدم نشطًا حتى تتم الموافقة عليه". تحتوي الرسائل المحلية والعالمية على ثلاثة مستويات - الخطأ والتحذير والمعلومات. قيمة "البيانات" خاصة بالسياق. عند إنشاء المستخدمين ، سيحتوي حقل البيانات على بيانات المستخدم ، ولكن عند الحصول على قائمة المستخدمين ، سيكون حقل البيانات عبارة عن مجموعة من المستخدمين.
من خلال هذا النوع من الاستجابة المنظمة ، يمكن إنشاء معالج واجهة مستخدم العميل بسهولة ، والذي سيكون مسؤولاً عن عرض الأخطاء والتحذيرات ورسائل المعلومات. سيتم عرض الرسائل العالمية في الجزء العلوي من الصفحة ، لأنها مرتبطة بحالة إجراء واجهة برمجة التطبيقات العالمية ، ويمكن عرض الرسائل المحلية بالقرب من الإدخال المحدد (حقل) ، لأنها مرتبطة مباشرة بقيمة الحقل. يمكن عرض رسائل الخطأ باللون الأحمر ورسائل التحذير باللون الأصفر والمعلومات باللون الأزرق.
على سبيل المثال ، في تطبيق عميل يستند إلى AngularJS ، يمكن أن يكون لدينا توجيهان مسؤولان عن التعامل مع رسائل الاستجابة المحلية والعالمية ، بحيث يمكن لهذين المعالجين فقط التعامل مع جميع الردود بطريقة متسقة.
يجب تطبيق التوجيه الخاص بالرسالة المحلية على أحد العناصر الأصل على العنصر الفعلي الذي يحتفظ بجميع الرسائل.
localmessages.direcitive.js :
(function() { 'use strict'; angular .module('reactiveClient') .directive('localMessagesValidationDirective', localMessagesValidationDirective); /** @ngInject */ function localMessagesValidationDirective(_) { return { restrict: 'AE', transclude: true, scope: { binder: '=' }, template: '<div ng-transclude></div>', link: function (scope, element) { var messagesWatchCleanUp = scope.$watch('binder', messagesBinderWatchCallback); scope.$on('$destroy', function() { messagesWatchCleanUp(); }); function messagesBinderWatchCallback (messagesResponse) { if (messagesResponse != undefined && messagesResponse.messages != undefined) { if (messagesResponse.messages.local.length > 0) { element.find('.alert').remove(); _.forEach(messagesResponse.messages.local, function (localMsg) { var selector = element.find('[]').parent(); _.forEach(localMsg.info, function (msg) { var infoMsg = '<div class="form-control validation-alert alert alert-info alert-dismissable"><button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>' + msg + '</div>'; selector.after(infoMsg); }); _.forEach(localMsg.warnings, function (msg) { var warningMsg = '<div class="form-control validation-alert alert alert-warning alert-dismissable"><button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>' + msg + '</div>'; selector.after(warningMsg); }); _.forEach(localMsg.errors, function (msg) { var errorMsg = '<div class="form-control validation-alert alert alert-danger alert-dismissable"><button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>' + msg + '</div>'; selector.after(errorMsg); }); }); } } } } } } })();
سيتم تضمين التوجيه الخاص بالرسائل العالمية في مستند تخطيط الجذر (index.html) وسيتم تسجيله في حدث لمعالجة جميع الرسائل العامة.
globalmessages.directive.js :
(function() { 'use strict'; angular .module('reactiveClient') .directive('globalMessagesValidationDirective', globalMessagesValidationDirective); /** @ngInject */ function globalMessagesValidationDirective(_, toastr, $rootScope, $log) { return { restrict: 'AE', link: function (scope) { var cleanUpListener = $rootScope.$on('globalMessages', globalMessagesWatchCallback); scope.$on('$destroy', function() { cleanUpListener(); }); function globalMessagesWatchCallback (event, messagesResponse) { $log.log('received rootScope event: ' + event); if (messagesResponse != undefined && messagesResponse.messages != undefined) { if (messagesResponse.messages.global != undefined) { _.forEach(messagesResponse.messages.global.info, function (msg) { toastr.info(msg); }); _.forEach(messagesResponse.messages.global.warnings, function (msg) { toastr.warning(msg); }); _.forEach(messagesResponse.messages.global.errors, function (msg) { toastr.error(msg); }); } } } } } } })();
للحصول على مثال أكثر اكتمالاً ، دعنا نفكر في الرد التالي الذي يحتوي على رسالة محلية:
{ "messages" : { "global" : { "info": [], "warnings": [], "errors": [] }, "local" : [ { "inputId" : "email", "errors" : ["User already exists with this email"], "warnings" : [], "info" : [] } ] }, "data":{ "firstName": "John", "lastName": "Doe", "email": "[email protected]", "password": "MTIzNDU=" } }
يمكن أن تؤدي الاستجابة أعلاه إلى شيء على النحو التالي:
من ناحية أخرى ، مع استجابة رسالة عالمية:
{ "messages" : { "global" : { "info": ["User successfully created."], "warnings": ["User will not be available for login until is activated"], "errors": [] }, "local" : [] }, "data":{ "externalId": "a55ccd60-9d82-11e5-9f52-0002a5d5c51b", "firstName": "John", "lastName": "Doe", "email": "[email protected]" } }
يمكن لتطبيق العميل الآن عرض الرسالة بشكل أكثر بروزًا:
في الأمثلة أعلاه ، يمكن رؤية كيف يمكن معالجة بنية الاستجابة الموحدة لأي طلب باستخدام نفس المعالج.
خاتمة
يمكن أن يصبح تطبيق التحقق من الصحة على المشاريع الكبيرة أمرًا محيرًا ، ويمكن العثور على قواعد التحقق في كل مكان في رمز المشروع. الحفاظ على اتساق التحقق وتنظيمه جيدًا يجعل الأمور أسهل وقابلة لإعادة الاستخدام.
يمكنك العثور على هذه الأفكار مطبقة في نسختين مختلفتين من النماذج المعيارية أدناه:
- Standard Play 2.3 و Scala 2.11.1 و Slick 2.1 و Postgres 9.1 و Spring Dependency Injection
- اللعب التفاعلي غير المعوق 2.4 ، Scala 2.11.7 ، Slick 3.0 ، Postgres 9.4 ، حقن تبعية Guice
في هذه المقالة قدمت اقتراحاتي حول كيفية دعم التحقق من صحة السياق العميق والقابل للإنشاء والذي يمكن تقديمه بسهولة للمستخدم. آمل أن يساعدك هذا في حل تحديات التحقق السليم ومعالجة الأخطاء بشكل نهائي. لا تتردد في ترك تعليقاتك ومشاركة أفكارك أدناه.