Etki Alanına Dayalı Tasarımda Bağlam Doğrulama
Yayınlanan: 2022-03-11Etki alanına dayalı tasarım (kısaca DDD) bir teknoloji veya metodoloji değildir. DDD, karmaşık etki alanlarıyla ilgilenen yazılım projelerine odaklanan ve hızlandıran tasarım kararları almak için bir uygulama yapısı ve terminoloji sağlar. Eric Evans ve Martin Fowler tarafından tanımlandığı gibi, Etki Alanı nesneleri, doğrulama kuralları ve iş mantığı koymak için bir yerdir.
Eric Evans:
Etki Alanı Katmanı (veya Model Katmanı): İş kavramlarını, iş durumu hakkındaki bilgileri ve iş kurallarını temsil etmekten sorumludur. İş durumunu yansıtan durum, depolamanın teknik detayları altyapıya devredilmesine rağmen burada kontrol edilir ve kullanılır. Bu katman, iş yazılımının kalbidir.
Martin Fowler:
Bir etki alanı nesnesinde olması gereken mantık, etki alanı mantığıdır - doğrulamalar, hesaplamalar, iş kuralları - buna ne derseniz deyin.
Tüm doğrulamayı Etki Alanı nesnelerine koymak, çalışmak için çok büyük ve karmaşık etki alanı nesneleri ile sonuçlanır. Şahsen, etki alanı doğrulamalarını, her zaman yeniden kullanılabilen ve bağlama ve kullanıcı eylemine dayalı olacak ayrı doğrulayıcı bileşenlerine ayırma fikrini daha çok tercih ederim.
Martin Fowler'ın harika bir makalede yazdığı gibi: ContextualValidation.
İnsanların yaptığını gördüğüm yaygın bir şey, nesneler için doğrulama rutinleri geliştirmek. Bu rutinler çeşitli şekillerde gelirler, nesnede veya harici olabilirler, bir boole döndürebilir veya başarısızlığı belirtmek için bir istisna atabilirler. İnsanları sürekli olarak tetiklediğini düşündüğüm bir şey, nesne geçerliliğini isValid yönteminin ima ettiği gibi bağlamdan bağımsız bir şekilde düşündüklerinde. […] Doğrulamayı bir bağlama bağlı, tipik olarak yapmak istediğiniz bir eylem olarak düşünmenin çok daha yararlı olduğunu düşünüyorum. Bu siparişin doldurulması için geçerli olup olmadığını veya bu müşterinin otele giriş yapmak için geçerli olup olmadığını sormak gibi. Yani isValid gibi yöntemlere sahip olmak yerine isValidForCheckIn gibi yöntemlere sahip olun.
Eylem Doğrulama Önerisi
Bu makalede, ValidationResult dönüş türüyle bir validate yöntemi uygulamanız gereken basit bir ItemValidator arabirimi uygulayacağız. ValidationResult, doğrulanmış öğeyi ve ayrıca Mesajlar nesnesini içeren bir nesnedir. İkincisi, yürütme bağlamına bağlı olarak bir dizi hata, uyarı ve bilgi doğrulama durumu (mesaj) içerir.
Doğrulayıcılar, ihtiyaç duyulan her yerde kolayca yeniden kullanılabilen, ayrıştırılmış bileşenlerdir. Bu yaklaşımla doğrulama kontrolleri için gerekli olan tüm bağımlılıklar kolayca enjekte edilebilir. Örneğin, veritabanında e-posta adresi verilen bir kullanıcı olup olmadığını kontrol etmek için yalnızca UserDomainService kullanılır.
Doğrulayıcıların ayrıştırılması bağlam (eylem) başına olacaktır. Bu nedenle, UserCreate eylemi ve UserUpdate eyleminin ayrıştırılmış bileşenleri veya başka herhangi bir eylemi (UserActivate, UserDelete, AdCampaignLaunch, vb.) olacaksa, doğrulama hızla büyüyebilir.
Her eylem doğrulayıcı, yalnızca izin verilen eylem alanlarına sahip olacak ilgili bir eylem modeline sahip olmalıdır. Kullanıcı oluşturmak için aşağıdaki alanlar gereklidir:
UserCreateModel:
{ "firstName": "John", "lastName": "Doe", "email": "[email protected]", "password": "MTIzNDU=" }Ve kullanıcıyı güncellemek için, externalId , firstName ve lastName 'a izin verilir. externalId , kullanıcı tanımlaması için kullanılır ve yalnızca ad ve soyadının değiştirilmesine izin verilir.
Kullanıcı Güncelleme Modeli:
{ "externalId": "a55ccd60-9d82-11e5-9f52-0002a5d5c51b", "firstName": "John Updated", "lastName": "Doe Updated" }Alan bütünlüğü doğrulamaları paylaşılabilir, ad maksimum uzunluğu her zaman 255 karakterdir.
Doğrulama sırasında, yalnızca oluşan ilk hatayı değil, karşılaşılan tüm sorunların bir listesini almak da istenir. Örneğin, aşağıdaki 3 sorun aynı anda olabilir ve yürütme sırasında buna göre raporlanabilir:
- geçersiz adres biçimi [ERROR]
- e-posta, kullanıcılar arasında benzersiz olmalıdır [ERROR]
- şifre çok kısa [ERROR]
Bu tür bir doğrulamayı başarmak için doğrulama durumu oluşturucu gibi bir şeye ihtiyaç vardır ve bu amaçla Mesajlar tanıtılır. Mesajlar , yıllar önce büyük akıl hocamdan doğrulamayı desteklemek ve bununla yapılabilecek diğer çeşitli şeyler için tanıttığında duyduğum bir kavramdır, çünkü Mesajlar sadece doğrulama için değildir.
Aşağıdaki bölümlerde uygulamayı göstermek için Scala kullanacağımızı unutmayın. Scala uzmanı değilseniz, yine de takip etmesi kolay olacağından korkmayın.
Bağlam Doğrulamadaki Mesajlar
Mesajlar , doğrulama durumu oluşturucusunu temsil eden bir nesnedir. Doğrulama sırasında hataları, uyarıları ve bilgi mesajlarını toplamak için kolay bir yol sağlar. Her Messages nesnesinin, Message nesnelerinin bir iç koleksiyonu vardır ve ayrıca parentMessages nesnesine bir referansı olabilir.
Bir Message nesnesi type , messageText , key (isteğe bağlıdır ve tanımlayıcı tarafından tanımlanan belirli girdilerin doğrulanmasını desteklemek için kullanılır) ve son olarak, şekillendirilebilir mesaj ağaçları oluşturmak için harika bir yol sağlayan childMessages'a sahip olabilen bir nesnedir.
Bir mesaj aşağıdaki türlerden biri olabilir:
- Bilgi
- Uyarı
- Hata
Bu şekilde yapılandırılmış mesajlar, onları yinelemeli olarak oluşturmamıza ve ayrıca önceki mesajların durumuna dayalı olarak sonraki eylemler hakkında karar verilmesine olanak tanır. Örneğin, kullanıcı oluşturma sırasında doğrulama gerçekleştirme:
@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 ) } }Bu koda bakarak ValidateUtils kullanımını görebilirsiniz. Bu yardımcı işlevler, önceden tanımlanmış durumlarda Messages nesnesini doldurmak için kullanılır. ValidateUtils'in Github kodundaki uygulamasını kontrol edebilirsiniz.
E-posta doğrulaması yapılırken önce ValidateUtils.validateEmail( … ) çağrılarak e-postanın geçerli olup olmadığı, ayrıca ValidateUtils.validateLengthIsLessThanOrEqual(… . zaten bazı Kullanıcılara atanmışsa, yalnızca önceki e-posta doğrulama koşulları geçiyorsa ve if(!localMessages.hasErrors()) { … ile yapılırsa gerçekleştirilir. Bu şekilde pahalı veritabanı çağrılarından kaçınılabilir.Bu yalnızca UserCreateValidator'ın bir parçasıdır. Tam kaynak kodu burada bulunabilir.
Doğrulama parametrelerinden birinin öne çıktığına dikkat edin: UserCreateEntity.EMAIL_FORM_ID . Bu parametre, doğrulama durumunu belirli bir giriş kimliğine bağlar.
Önceki örneklerde, bir sonraki eyleme, Messages nesnesinin hataları olup olmadığına göre karar verilir (hasErrors yöntemi kullanılarak). Herhangi bir “UYARI” mesajı olup olmadığı kolayca kontrol edilebilir ve gerekirse yeniden denenebilir.

Fark edilebilecek bir şey, localMessages'ın kullanılma şeklidir. Yerel mesajlar, herhangi bir mesajla aynı şekilde, ancak parentMessages ile oluşturulmuş mesajlardır. Bununla birlikte, amaç yalnızca geçerli doğrulama durumuna bir referansa sahip olmaktır (bu örnekte emailValidation), bu nedenle localMessages.hasErrors çağrılabilir, burada yalnızca emailValidation bağlamında hasErrors varsa kontrol edilir. Ayrıca localMessages'a bir mesaj eklendiğinde, parentMessages'a da eklenir ve böylece tüm doğrulama mesajları UserCreateValidation'ın daha yüksek bağlamında bulunur.
Artık Mesajları çalışırken gördüğümüze göre, bir sonraki bölümde ItemValidator'a odaklanacağız.
ItemValidator - Yeniden Kullanılabilir Doğrulama Bileşeni
ItemValidator, geliştiricileri ValidationResult döndürmesi gereken validate yöntemini uygulamaya zorlayan basit bir özelliktir (arayüz).
Öğe Doğrulayıcı:
trait ItemValidator[T] { def validate(item:T) : ValidationResult[T] }DoğrulamaSonucu:
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) ) } }UserCreateValidator gibi ItemValidator'lar bağımlılık enjeksiyon bileşenleri olarak uygulandığında, ItemValidator nesneleri UserCreate eylem doğrulaması gerektiren herhangi bir nesneye enjekte edilebilir ve yeniden kullanılabilir.
Doğrulama yapıldıktan sonra doğrulamanın başarılı olup olmadığı kontrol edilir. Bu durumda, kullanıcı verileri veritabanına kalıcı hale getirilir, ancak değilse, doğrulama hatalarını içeren API yanıtı döndürülür.
Bir sonraki bölümde, RESTful API yanıtında doğrulama hatalarını nasıl sunabileceğimizi ve ayrıca API tüketicileri ile yürütme eylemi durumları hakkında nasıl iletişim kurabileceğimizi göreceğiz.
Birleşik API Yanıtı - Basit Kullanıcı Etkileşimi
Kullanıcı eylemi başarıyla doğrulandıktan sonra, bizim durumumuzda kullanıcı oluşturma, doğrulama eylemi sonuçlarının RESTful API tüketicisine görüntülenmesi gerekir. En iyi yol, yalnızca bağlamın değiştirileceği birleşik bir API yanıtına sahip olmaktır (JSON açısından, "veri"nin değeri). Birleştirilmiş yanıtlarla, hatalar RESTful API tüketicilerine kolayca sunulabilir.
Birleşik yanıt yapısı:
{ "messages" : { "global" : { "info": [], "warnings": [], "errors": [] }, "local" : [] }, "data":{} }Birleşik yanıt, küresel ve yerel olmak üzere iki mesaj katmanına sahip olacak şekilde yapılandırılmıştır. Yerel mesajlar, belirli girişlere bağlanan mesajlardır. "Kullanıcı adı çok uzun, en fazla 80 karaktere izin verilir" gibi. Global mesajlar, "kullanıcı onaylanana kadar aktif olmayacaktır" gibi sayfadaki tüm verilerin durumunu yansıtan mesajlardır. Yerel ve global mesajların üç seviyesi vardır - hata, uyarı ve bilgi. “Veri”nin değeri bağlama özeldir. Kullanıcılar oluşturulurken veri alanı kullanıcı verilerini içerecektir, ancak bir kullanıcı listesi alınırken veri alanı bir dizi kullanıcı olacaktır.
Bu tür yapılandırılmış yanıtla, hataları, uyarıları ve bilgi mesajlarını görüntülemekten sorumlu olacak istemci UI işleyicisi kolayca oluşturulabilir. Global mesajlar, global API eylem durumuyla ilgili oldukları için sayfanın en üstünde görüntülenecektir ve yerel mesajlar, doğrudan alanın değeriyle ilgili olduklarından, belirtilen girişin (alan) yakınında görüntülenebilir. Hata mesajları kırmızı, uyarı mesajları sarı ve bilgiler mavi olarak gösterilebilir.
Örneğin, AngularJS tabanlı bir istemci uygulamasında yerel ve global yanıt mesajlarını işlemekten sorumlu iki yönergeye sahip olabiliriz, böylece yalnızca bu iki işleyici tüm yanıtları tutarlı bir şekilde ele alabilir.
Yerel mesaj yönergesinin, tüm mesajları tutan asıl öğeye bir üst öğeye uygulanması gerekecektir.
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); }); }); } } } } } } })();Global mesajlara yönelik direktif, kök yerleşim belgesine (index.html) dahil edilecek ve tüm global mesajları işlemek için bir olaya kaydedilecektir.
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); }); } } } } } } })();Daha eksiksiz bir örnek için, yerel bir mesaj içeren aşağıdaki yanıtı ele alalım:
{ "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=" } }Yukarıdaki yanıt aşağıdaki gibi bir şeye yol açabilir:
Öte yandan, yanıt olarak küresel bir mesajla:
{ "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]" } }İstemci uygulaması artık mesajı daha belirgin bir şekilde gösterebilir:
Yukarıdaki örneklerde, aynı işleyici ile herhangi bir istek için birleşik bir yanıt yapısının nasıl ele alınabileceği görülebilir.
Çözüm
Doğrulamayı büyük projelere uygulamak kafa karıştırıcı olabilir ve doğrulama kuralları proje kodunun her yerinde bulunabilir. Doğrulamayı tutarlı ve iyi yapılandırılmış tutmak, işleri daha kolay ve yeniden kullanılabilir hale getirir.
Aşağıda, iki farklı kazan plakası versiyonunda uygulanan bu fikirleri bulabilirsiniz:
- Standard Play 2.3, Scala 2.11.1, Slick 2.1, Postgres 9.1, Spring Dependency Injection
- Reaktif blokajsız Play 2.4, Scala 2.11.7, Slick 3.0, Postgres 9.4, Guice Dependency Injection
Bu makalede, bir kullanıcıya kolayca sunulabilen derin, şekillendirilebilir bağlam doğrulamanın nasıl destekleneceğine dair önerilerimi sundum. Umarım bu, doğru doğrulama ve hata işleme zorluklarını bir kez ve herkes için çözmenize yardımcı olur. Lütfen yorumlarınızı bırakmaktan ve düşüncelerinizi aşağıda paylaşmaktan çekinmeyin.
