Çok Katmanlı Mimari Örnek Proje(3)

Samet Çınar
SabancıDx
Published in
8 min readMar 14, 2021
Çok Katmanlı Mimari

Çok katmanlı mimarı konumuzun ikinci yazısında “Data, Serializer, Mapper, Encryptor, EmailSender” katmanlarından bahsettim.

Bu yazımda sırasıyla “Caching, Validation, Operation” katmanlarından bahsedeceğim.

08.Corex.Sample.Caching

Projemizde kullanacağımız caching katmanı için Corex’den belirlediğimiz arayüzü kullanıyoruz. Bu arayüz tüm caching kütüphanelerinin destekleceği bir yapıda oluşturuldu.

CacheManager Infrastructure

Burada ISingletonDependency olması bizim için değerli. IoC register sırasında lifestyle olarak “Singleton” belirlediğimiz sınıfların uygulamamız içerisinde tek bir örneğinin olması gerekiğinden fazla ön bellek kullanımını engelleyecektir.

CacheManager bizim için değer alıp veren bir yapıda olduğu için uygulama içerisinde tek bir örneğinin(instance) olması doğru bir tercih olacaktır.

Caching katmanında “MemoryCache” kullanacağız. Her katmanda yaptığımız gibi bir infrastructure oluşturacağız.(Corex.Sample.Caching.Inftrastructure)
Bu oluşturduğumuz alt yapıda direkt Corex’den almak yerine kendi ara yüzümüzü koyarak yeri geldiğinde esnekliğimizi sağlayacağız.(ICorexCacheManager.cs)

MemoryCache kullanımı için ilk olarak “Corex.Sample.Caching.Derived.MemoryCaching” class library oluşturuyoruz. İçerisinde arayüzümüz implemente edeceğimiz bir somut sınıf oluşturuyoruz.

Bu sınıf içerisinde “BaseMemoryCacheManager” kullanarak Corex paketi içerisinde bulunan memory caching kodlarına ulaşmış oluyoruz. Bunlar zaten standart herkesin kullandığı kodlar biz sadece yazdıklarımızı tekrarlamak istemediğimiz için bu paketleri kullanıyoruz bunu hep dile getiriyorum.

Kodumuz böyle güzel olmuyor mu ?

public class MemoryCacheManager : BaseMemoryCacheManager, ICorexCacheManager{}

Eminim hepimiz böyle yapılar kullanıyoruz, projemin içerisinde de “BaseMemoryCacheManager” yapıp kullanabilirdim. Peki farklı bir proje oluşturduğumda yine bu işlemi tekrarlamak ister miyim? Şahsen ben istemem.

09.Corex.Sample.Validation

Geçmişten bugüne dahil olduğum projelerin büyük bir bölümde validasyon görevi genellikle ön yüze verilmiştir. Bu bence çok yanlış. Neden?

Tabloya kayıt atacağımız bir senaryoyu düşünürken bu isteğin sadece bizim geliştirdiğimiz ui projesinden geleceğini düşünerek validasyon işlerini orada hallederiz. Kütüphanelerle bunu rahat bir şekilde yaparız ve işimize gelir.Ancak UI dışında farklı platformlardan da aynı tablolara kayıt alabiliriz.

Örn : Yıllardır devam eden bir projenin yeni versiyonu ile beraber data yapsının değiştiğini düşünelim. Geçmiş datalarında bir şekilde yeni data yapısına migration yapılması gerekiyor. Bu yüklü bir iş olduğu için bunu bir console app üzerinden yaptığımızı varsayalım. O zaman ne yapacağız? Gelen datanın tutarlığını, veri tabanına uyumluluğunu validasyon yapamayacağız.

Bu UI tarafında validasyon yapmayalım demek değil kesinlikle, her UI kendi validasyonunu yapmalı. Bizde kendimizi güvence altına alacağımız server-side validasyon yapmalıyız. Bunun için birden fazla kütüphane vardır(FluentValidation v.b) ama ben custom bir şekilde kendim düzenlemeyi tercih ediyorum.

Corex.Validation.Infrastructure içerisinde “ValidationBase” bize min,max, dateformat, string required v.b basit yardımcı methodlar sağlayacak. User tablosunun validasyon sınıflarını örnek alalım.

UserDto Validations

Dikkatini çekecektir, her bir property için ayrı bir class açıyoruz. Bu başta çok maliyetli gibi gözüksede ilerisi çok esnek bir yapıda olmamızı sağlayacak. Aynı zamanda bir sınıfa tek bir görev vererek “single of responsibility” kuralınada uymuş olacağız.

public class UserDtoEmailValidation : ValidationBase<UserDto>
{
public UserDtoEmailValidation(UserDto item) : base(item)
{
}
protected override void Validate()
{
EmailRequiredValidation();
EmailLimitValidation();
EmailFormatValidation();
}
private void EmailRequiredValidation()
{
StringRequiredValidation(nameof(UserDto.Email), Item.Email);
}
private void EmailLimitValidation()
{
StringLimitValidation(nameof(UserDto.Email), Item.Email, 32);
}
private void EmailFormatValidation()
{
//Base'den sadece format methodunu alıp message'a kendim ekleyebilirim.
//Böylece istediğim code ve message değerini set edebilirim.
if (!EmailFormatIsValid(Item.Email))
{
IsValid = false;
Messages.Add(new ValidationMessage()
{
Code = ValidationConstans.NOTVALID_VALUE,
Message = CodeFormat(nameof(UserDto.Email))
});
}
}
}

ValidationBase “Validate()” adında bir methodu var, bu method içerisinde vereceğiniz aksiyonları ValidationOperation’da cağırarak aksiyon alacak. Detayına “ValidationOperation” katmanında bakacağız. Ve neden her birisi için tek tek sınıf oluşturuyoruz, bunun sağladığı faydayıda konuşmuş olacağız.

10.Corex.Sample.Operation

Evet geldik en kapsamlı, en değerli katmana. Bir çok aksiyonumuz artık burada değer kazanacak, benim için en önemli yer burası. Buraya gelene kadar her katman kendi özelinde tek bir görevi vardı. Operation katmanında da bu böyle devam edecek, en son OperationManager’a geldiği zaman tüm kodlar birleşip bir bütün oluşturacak. Bunu unitofwork pattern gibide düşünebiliriz, tam olarak o tarzda kullanmıyorum kafanızda canlanması için örnek vermek istedim.

10.Corex.Sample.Operation

Corex.Sample.Operation.BusinessOperation

BusinessOperation içerisinde bir çok aksiyon yapabiliriz. Mesela elimizde iki tane liste var, biz onlardan geriye bir tane liste elde etmek istediğimiz bir aksiyon burada konumlanabilir.

BusinessOperation içerisinde her hangi bir CRUD işlemi söz konusu değildir. Var olan hazır datalar üzerinden işlem yapmalıdır. En basit bir örnek için User BusinessOperationlara göz atacağız.

User Business Operations

Her yapacağımız aksiyon için arayüz kullanmayı kesinlikle ihmal etmiyoruz. Çünkü her parçanın yeni bir versiyonu gelebilir, çıkartılıp yeni parça takılabilir şeklinde düşünmeliyiz.

UserBusinessOperation için “register” aksiyonu üzerinde konuşalım. Yeni bir kullanıcı kayıtı alırken sayfadan sadece “email, password” bilgilerini aldığımızı düşünelim. Bu yüzden sayfadan alacağımız parametre “userDto” olamaz, userDto içerisinde gereğinden fazla parametre var bu değerlerin sayfadan set edilmesi benim için bir şey ifade etmeyecekse bunun için “UserRegisterInputModel” diye ayrı bir inputModel oluşturmalıyız.

Bu inputModel elimizde peki bizim CRUD operasyonlarımızı DTO’lar üzerinde işlem yapıyor o yüzden ne yapmamız gerekiyor ? Birisinin bu inputModel’i UserDto’ya çevirmesi gerekiyor. Bunu biz sayfadan parametre aldığımızda direkt olarak userDto’dan yeni bir örnek alarak inputModel’den geleni set ederek yapabiliriz öyle değil mi ? Biz bu aksiyonu “IUserRegisterBusinessOperation” içerisinde alacağız.

UserRegisterInputBusinessOperation

Bu aksiyon aslında ne kadarda basit zaten UserDto cons içerisinde password ve email alarak yeni bir örneğini oluşturuyor. Biz neden bunun için bir daha sınıf açıyoruz diye düşünebiliriz. Neden açıyoruz?

  • Kayıt işlemi sırasında bir çok aksiyon alınabilir, hata olması durumunda nerede hata alındığının tespitini rahatlıkla yapabilmemizi sağlar.
  • BusinessOperation’da bir hata aldıysam “BusinessOperationException” patlatıp exceptionHandling yapmamızı sağlar.
  • User tablosuna kayıt olunurken eklenmesi için “nickName” alanı eklendi, bizde bu alanı eklemek için tek bir yerden aksiyon alarak değiştirebiliriz.
  • “Single of responsibility” uygunluk için tercih etmeliyiz.

Corex.Sample.Operation.DataOperation

Adından da anlaşıldığı gibi data operasyonlarımızı bu class library üzerinde yapıyoruz. Biz bir kullanıcı kaydı talebi yaptığımızde direkt olarak “UserRepository” ile konuşmayacağız, bu repo için “Insert” yetkimiz belkide yok. Ya da insert işlemi yaparken connection timeout aldım bu hatayı birisinin data datası olduğunu handle etmesi gerekiyor. Bunun için “DataOperation” katmanı kullanıyoruz.

BaseEntityRepository gibi “BaseDataOperation” göreceksiniz, bunun içerisinde temel CRUD operasyonlarımızın hepsi bulunuyor. İçerisinde bizden bir repository vermemizi isteyecek ve onun kontrollerini ona devretmiş olacağız. Corex içerisinde bulunan “BaseDataOperation”dan biraz bahsetmek istiyorum. ISelectableRepository,IDeletableRepository v.b table bozunda repolarda yetki verme konusundan bahsetmiştik, onun kontrolünü data operation şu şekilde yapıyor.

IDeletableRepository Permission Control

BaseDataOperation işlem sırasında ilgili repository’nin bu işlemi yapma yetkisi var mı diye kontrol ediyor yetkisi yok ise hata patlatıyor. Ve yaptığı işlem sırasında yine bir hata varsa “DatabaseOperationException” patlatıyor. Böylece yine bir katman kendi özelinde bir hata olduğunda o katmana ait hatayı patlatarak bize hata kaynağı tespitinde yardımcı oluyor.

Özetle DataOperation içerisinde repository kullanılır. Kontrollerini yapar, aksiyonu alır olumlu, olumsuz durumları operationManager’a bildirir.

Corex.Operation.Derived.ValidationOperation

09.Corex.Sample.Validation içerisinde hatırlarsanız her bir dto’nun bir özelliği için validasyon sınıfları oluşturmuştuk. Bunlar kendi içerisinde validasyon aksiyonları alıyor ancak bunu parçaları birisinin bir araya getirip operationManager’a iletmesi gerekiyor. Bunlara “ValidationOperation” diyoruz.

UserDtoValidationOperation

UserDtoValidationOperation’ı örnek alalım. Parçalarımızı bir liste getirdiğimiz “BaseValidationOperation”dan kendi projemizde “CorexValidationOperation” olarakda türettiğimiz sınıfı kullandığımızı görüyoruz. Bu bizden “GetValidations”() methodu ile validasyon listesini istiyor. Bizde yaptığımız küçük parça validasyonları burada birleştirerek bu görevi de ayrı bir katmana vermiş oluyoruz. Bu bize ne avantaj sağladı ?

  • Single of responsibility :)
  • Kayıt işlemi sırasında “Surname” alanı artık zorunlu değil bunu kaldırabiliriz denildiğinde burada surname bölümünü kaldırıp ya da yorum satırı yapabiliriz. Genellikle yöneticilerimizden “Surname alanı geri zorunlu olsun” tarzında hızlı geri dönüşler aldığımız çok olmuştur:)
  • Yeni bir aksiyon eklendiğinde bunu tam olarak nereden ekleceğimiz ve çıkartacağımız konusuna hakimiyet
  • Hata olması durumunda “ValidationOperationException” patlatarak hatanın kaynağını tespit edebilmek

Corex.Sample.Operation.MailOperation

Bir projenin ayağa kalma sürecinde operation katmanlarından olmazsa olmaz katmanlar ; business, data, validation ve manager’dır. Bu “MailOperation” projesinde özelinde mail göndermek istediğimiz için açıldı. Yani bu size şöyle bir fikir verebilir. Projemizde örneğin her Dto‘yu ilgilendiren genel bir geliştirme yapılacaksa o zaman bu görevi bir operation katmanına verip daha sonra operationManager ile konuşmasını sağlamalıyız.

Bu katman içerisinde proje özelinde her aksiyona bağlı bir mail model oluşturup mail atan bir yapı var. “BaseMailOperation” göreceksiniz, bu da bu sefer Corex’den değil, Corex.Sample projesi içinde değerlendirdim. Sizce bu da Corex’de mi olmalıydı ? Belki görüşlerinizi belirtiniz ve ortak bir karar veririz. :)

Corex.Sample.Operation.Manager

OperationManager seviyesine geldiğimize göre artık birazda konuyu hikayeleştirerek anlatmak istiyorum. OperationManager için müdür diyelim. Diğer operasyonlara ise takım liderleri diyelim.

BusinessOperation : DTO, inputModel v.b sayfada yapılan aksiyonları ilgilendiren nesnelerde hesaplama, ekleme, çıkartma v.b işlemleri sağlayan takımın lideridir.

DataOperation : Entity, DTO, Repository nesnelerine erişim sağlayan CRUD işlemleri sırasında yetkinliği, tutarlığı sağlayan takımın lideridir.

MailOperation : DTO nesnelerine erişim sağlayıp gerekli mailModel nesnelerine dönüştüren EmailSender yardımı ile mail gönderimini sağlayan takımın lideridir.

ValidationOperation : DTO nesnelerine erişim sağlayarak CRUD işlemler öncesinde datanın tutarlığını, veri tabanına olan uyumluluğunu kontrol eden takımın lideridir.

Takım liderleri süreçlerinde üst katmanlardan yardım alabilir.(Data,Mapper, Serializer v.b) OperationManager bu süreçlerle ilgilenmez. İstediği işin yapılmasını ister, süreç ile ilgilenmez sonuca bakar.

OperationManager’a gelene kadar her bir katmanın TEK bir görevi olmasına özen gösteriyoruz. Bunu bazen %100 sağlayamayabiliriz. Ama bu konuya çok dikkat etmeliyiz. OperationManager “Domain Driven Design” konusunda “ApplicationLayer” olarak değerlendirebiliriz. Sunum katmanı ile use-case ilişkisinde bulunan tek katmandır. Sunum katmanı sadece OperationManager’ları tanır, arkada ne aksiyonlar döndüğü ile ilgilenmez.

Tüm CRUD operasyonlarınız manager üzerinde direkt size geldiğini göreceksiniz. Standart dışında bir şeyler eklemek istediğinizde lütfen bana ulaşın etkileşimde kalmayı çok isterim.

Burada en çok dikkat edilmesi ve bize büyük fayda sağlayacak yer ise “BaseOperationManager” içerisinde bulunan “SetCacheSettings()” methodu burada set ettiğimiz cachemanager bizim operationManager içerisinde bulunan tüm standart methodlarda(Get, GetList, Insert, Update, Delete v.s) senkron bir şekilde ekleyip çıkartacak şekilde düzenlendi. Siz kendiniz bir key belirleyip cachelemek istersenizde manager içerisinde “CacheManager”a direkt olarak ulaşıp ekleme çıkartma yapabiliyorsak olacaksınız. Çok iyi değil mi? Bence çok iyi.

Manager seviyesinde “OrderOperationManager” MemoryCache kullansın, “UserOperationManager” ise RedisCache kullansın diyebileceğimiz bir yapı bize sunuyor. UserDto içerisinde bir bölümü memory, bir bölümü redis üzerinde tutmamız gereken durumlar olursa eğer bu iş bir takım kurmalıyız. Yani UserCacheOperation olmalı. Ben bu örneğimde bunu tercih etmedim.

UserOperationManager içerisinde bulunan “Register” methodunu incelemenizi özellikle tavsiye ederim. Tüm takımlarla konuşan bir aksiyon bu. Böylece “Çok Katmanlı Mimari” serisinin yazılarını tamamlamış oluyorum. İncelemenizi bana ulaşıp sorular sormanızı çok isterim. Github üzerinde corex, corex.sample değerlendirmelerinizi dört gözle bekliyorum.

Corex paketleri online olarak nuget.org’da projeyide incelemek isterseniz ;

Anlattığım tüm süreçlerin örnek kodlarını incelemek isterseniz ;

Corex.Sample.Presentation.ConsoleApp üzerinde denemeler yaptığımı göreceksiniz. Belki de ileride buraya bir API, Blazor App, MVC projeside yapabiliriz. Bunun için ilginiz çok değerli.

Görüşmek üzere..

--

--