19 Mayıs 2020 Salı

2- Tactical Domain-Driven Design (Java Kod Örnekleri İle) - vaadin.com - Petter Holmström - Çevirsi


"Bu makale dizisinde, Domain Driven Desgin (etki alnına dayalı tasarım)'ın ne olduğunu ve projenize - veya bir kısmının - projelerinize nasıl uygulanacağını öğreneceksiniz." diyor Petter Holmström. Ben de elimden geldiğince bu yazı dizisini Türkçe'ye çevirmeye çalışacağım. Umarım İngilizce okumada zorluk çeken arkadaşlar için yararlı olur.


Yazı Dizisinin Orjinali



Serinin diğer yazıları : 

1 - Strategic Domain Driven Design (Stratejik DDD) 


2 - Tactical Domain-Driven Design

(Taktiksel DDD)

Bu yazıda taktiksel DDD tasarım hakkında bilgi edineceğiz. Taktiksel DDD, DDD sistemleri tasarlamak için kullanabileceğiniz bir dizi tasarım deseni ve yapı taşıdır. DDD olmayan projeler için bile, bazı taktiksel DDD kalıplarını kullanmaktan faydalanabilirsiniz.

Stratejik DDD tasarıma kıyasla, taktiksel DDD çok daha uygulamalı ve gerçek koda daha yakındır. Stratejik tasarım soyut varlıklarla ilginelirken, taktiksel tasarım sınıflar ve modüller ile ilgilidir. Taktiksel tasarımın amacı, DDD modelini çalışma koduna dönüştürülebilecek bir aşamaya geçirmektir.

Tasarım yinelemeli bir süreçtir ve bu nedenle stratejik ve taktiksel tasarımı birleştirmek mantıklıdır. Stratejik tasarım ile başlayan süreç ardından taktiksel tasarım ile devam eder. En büyük DDD modeli atılımları muhtemelen taktiksel tasarım sırasında gerçekleşecek ve bu da stratejik tasarımı etkileyelerek süreci tekrarlayabilme ihtimali oluşacaktır.

Yine, içerik büyük ölçüde Eric Evans'ın Domain-Driven Design: Tackling Complexity in the Heart of Software(Yazılımın Kalbinde Karmaşıklıkla Mücadele)  ve Vaughn Vernon tarafından yazılan Implementing Domain-Driven Design kitaplarına dayanıyor ve her ikisini de okumanızı şiddetle tavsiye ediyorum. Önceki makalede olduğu gibi, kendi sözlerimle mümkün olduğunca açıklamayı seçtim, uygun olduğunda kendi fikirlerimi, düşüncelerimi ve deneyimlerimi enjekte ettim.

Bu kısa tanıtım ile, taktiksel DDD araç kutusunu ortaya çıkarmanın ve içinde ne olduğuna bir göz atmanın zamanı geldi.

Value Objects
(Değer Nesneleri)

Taktiksel DDD'deki en önemli kavramlardan biri değer nesnesidir. Bu aynı zamanda DDD olmayan projelerde en çok kullandığım DDD yapı taşıdır ve umarım bunu okuduktan sonra siz de kullanırsınız.

Değer nesnesi, değeri önem taşıyan bir nesnedir. Bu, tam olarak aynı değere sahip iki değer nesnesinin aynı değer nesnesi olarak kabul edilebileceği ve dolayısıyla birbirinin yerine geçebileceği anlamına gelir. Bu nedenle, değer nesneleri her zaman değişmez (immutable) kılınmalıdır. Değer nesnesinin durumunu değiştirmek yerine, yeni bir örnekle değiştirirsiniz. Karmaşık değerli nesneler için builder veya essence tasarım kalıplarını kullanmayı düşünün.

Değer nesneleri yalnızca veri kapları değildir, aynı zamanda business mantığı da içerebilir. Değer nesnelerinin aynı zamanda değişmez (immutable) olması, business işlemlerini hem thread-safe hem de yan etkilere kapalı yapar. Bu, değer nesnelerini çok sevmemin nedenlerinden biri ve neden domain kavramlarınızı (domain concepts) değer nesneleri olarak mümkün olduğunca çok modellemeye çalışmanız gerektiğidir. Ayrıca, değer nesnelerini mümkün olduğunca küçük ve tutarlı hale getirmeye çalışın - bu onların bakımını ve yeniden kullanımını kolaylaştırır.

Değer nesneleri yapmak için iyi bir başlangıç noktası, bir business anlamı olan tüm tek değerli özellikleri almak ve bunları değer nesneleri olarak sarmaktır. Örneğin:

  • Parasal değerler için bir BigDecimal kullanmak yerine, BigDecimal'ı saran bir Money değeri nesnesi kullanın. Birden fazla para birimi ile uğraşıyorsanız, bir Currency değer nesnesi de oluşturarak, Money nesnenizin bir BigDecimal-Currency çiftini sarmasını isteyebilirsiniz.
  • Telefon numaraları ve e-posta adresleri için String kullanmak yerine, Stringleri saran PhoneNumber ve EmailAddress değer nesnelerini kullanın.

Bunun gibi değer nesneleri kullanmanın birçok avantajı vardır. Her şeyden önce, değere bağlam (context) getirir. Belirli bir String'in telefon numarası, e-posta adresi, ad veya posta kodu içerip içermediğini veya BigDecimal'in parasal bir değer, yüzde veya tamamen farklı bir şey olup olmadığını bilmeniz gerekmez. Tipin kendisi hemen neyle uğraştığınızı söyler.

İkinci olarak, belirli bir tipdeki değerler üzerinde gerçekleştirilebilecek tüm business işlemlerini değer nesnesinin kendisine ekleyebilirsiniz. Örneğin, bir Money nesnesi, temeldeki BigDecimal'in hassasiyetinin her zaman doğru olmasını ve işleme dahil olan tüm Money nesnelerinin aynı para birimine sahip olmasını sağlarken, para ekleme ve çıkarma veya yüzde hesaplama işlemleri içerebilir.

Üçüncü olarak, değer nesnesinin her zaman geçerli bir değer içerdiğinden emin olabilirsiniz. Örneğin, EmailAddress değer nesnenizin yapıcısında e-posta adresi String değerini validate edebilirsiniz.


KOD ÖRNEKLERİ

Java'daki bir Money değer nesnesi böyle bir şeye benzeyebilir (kod test edilmemiştir ve netlik için bazı yöntem uygulamaları atlanmıştır):

Money.java

public class Money implements Serializable, Comparable&ltMoney&gt {
    private final BigDecimal amount;
    private final Currency currency; // Currency is an enum or another value object

    public Money(BigDecimal amount, Currency currency) {
        this.currency = Objects.requireNonNull(currency);
        this.amount = Objects.requireNonNull(amount).setScale(currency.getScale(), currency.getRoundingMode());
    }

    public Money add(Money other) {
        assertSameCurrency(other);
        return new Money(amount.add(other.amount), currency);
    }

    public Money subtract(Money other) {
        assertSameCurrency(other);
        return new Money(amount.subtract(other.amount), currency);
    }

    private void assertSameCurrency(Money other) {
        if (!other.currency.equals(this.currency)) {
            throw new IllegalArgumentException("Money objects must have the same currency");
        }
    }

    public boolean equals(Object o) {
        // Check that the currency and amount are the same
    }

    public int hashCode() {
        // Calculate hash code based on currency and amount
    }

    public int compareTo(Money other) {
        // Compare based on currency and amount
    }
}

Java'da bir StreetAddress değer nesnesi ve karşılık gelen builder şöyle görünebilir (kod test edilmemiştir ve netlik için bazı yöntem uygulamaları atlanmıştır):

StreetAddress.java
public class StreetAddress implements Serializable, Comparable&ltStreetAddress&gt {
    private final String streetAddress;
    private final PostalCode postalCode; // PostalCode is another value object
    private final String city;
    private final Country country; // Country is an enum

    public StreetAddress(String streetAddress, PostalCode postalCode, String city, Country country) {
        // Verify that required parameters are not null
        // Assign the parameter values to their corresponding fields
    }

    // Getters and possible business logic methods omitted

    public boolean equals(Object o) {
        // Check that the fields are equal
    }

    public int hashCode() {
        // Calculate hash code based on all fields
    }

    public int compareTo(StreetAddress other) {
        // Compare however you want
    }

    public static class Builder {

        private String streetAddress;
        private PostalCode postalCode;
        private String city;
        private Country country;

        public Builder() { // For creating new StreetAddresses
        }

        public Builder(StreetAddress original) { // For "modifying" existing StreetAddresses
            streetAddress = original.streetAddress;
            postalCode = original.postalCode;
            city = original.city;
            country = original.country;
        }

        public Builder withStreetAddress(String streetAddress) {
            this.streetAddress = streetAddress;
            return this;
        }

        // The rest of the 'with...' methods omitted

        public StreetAddress build() {
            return new StreetAddress(streetAddress, postalCode, city, country);
        }
    }
}


Entities
(Varlıklar)

Taktiksel DDD'de ikinci önemli kavram ve değer nesnelerine(value objects) kardeş varlık'tır(Entity). Varlık, kimliği önem taşıyan bir nesnedir. Bir varlığın kimliğini belirleyebilmek için, her varlığın, varlık yaratıldığında atanan ve varlığın ömrü boyunca değişmeden kalan benzersiz bir ID'si vardır.

Aynı türden ve aynı ID'ye sahip iki varlık, diğer tüm özellikler farklı olsa bile aynı varlık olarak kabul edilir. Aynı şekilde, aynı tipte ve aynı özelliklere sahip ancak farklı ID'lere sahip iki varlık, tıpkı aynı ada sahip iki kişi aynı kabul edilmez.

Değer nesnelerin aksine, varlıklar değiştirilebilir. Ancak bu, her özellik için setter metodlar oluşturmanız gerektiği anlamına gelmez. Tüm durum değiştirme işlemlerini, business işlemlere karşılık gelen fiiller olarak modellemeye çalışın. Bir setter size yalnızca hangi özelliği değiştirdiğinizi söyler, ancak nedenini söylemez. Örneğin: bir EmploymentContract varlığınız olduğunu ve bunun bir endDate özelliği olduğunu varsayalım. EmployeeContracts'da kontratlar sadece, geçici olmaları, başlangıçta bir şirket şubesinden diğerine iç transfer nedeniyle, çalışan istifa etmesi veya işverenin çalışanı işten çıkarması nedeniyle sona erebilir. Tüm bu durumlarda, endDate değiştirilir, ancak çok farklı nedenlerle. Ayrıca, sözleşmenin neden sona erdiğine bağlı olarak yapılması gereken başka önlemler de olabilir. Bir terminateContract (reason, finalDay) metodu zaten bir setEndDate (finalDay) metodundan çok daha fazlasını söyler.

Bununla birlikte, setter'lerin DDD'de yeri hala var. Yukarıdaki örnekte, bitiş tarihini ayarlamadan önce başlangıç tarihinden sonra olduğundan emin olan özel bir setEndDate (..) metodu olabilir. Bu belirleyici diğer varlık metodları tarafından kullanılır, ancak dış dünyaya açılmaz. Ana ve referans verileri ile bir varlığın business state'ini değiştirmeden tanımlayan özellikler için, setter'leri kullanmak metodları fiillere dönüştürmeye çalışmaktan daha mantıklıdır. SetDescription (..) adı verilen bir metod, (..) metodundan daha okunaklı bir şekilde okunabilir.

Bunu başka bir örnekle açıklayacağım. Diyelim ki bir kişiyi temsil eden bir Person varlığınız var. Kişinin bir firstName ve bir lastName özelliği vardır. Şimdi, bu sadece basit bir adres defteri olsaydı, kullanıcının bu bilgileri gerektiği gibi değiştirmesine izin verirsiniz ve setFirstName (..) ve setLastName (..) setter'lerini kullanabilirsiniz. Bununla birlikte, resmi bir devlet kayıt defteri oluşturuyorsanız, bir adı değiştirmek daha çok şey işin içine girer. ChangeName (firstName, lastName, reason, activeAsOfDate) gibi bir şeyle sonuçlanabilir. Yine, bağlam(context) her şeydir.

Getter'ler üzerine bir not:

Getter metodları Java'ya JavaBean specification'un bir parçası olarak eklendi . Bu belirtim Java'nın ilk sürümünde yoktu, bu nedenle standart Java API'sinde (örneğin String.getLength () yerine String.length ()) gibi spesifikasyona uymayan bazı metodlar bulabilirsiniz.

Şahsen benim için, Java'daki gerçek propertyler için şu açıdan destek görmek istiyorum. Sahne arkasında getter'lar ve setter'lar kullanıyor olsalar da, bir özellik değerine sadece sıradan bir alan gibi aynı şekilde erişmek istiyorum: mycontact.phoneNumber. Bunu henüz Java'da yapamıyoruz, ancak getter'lardan get ilk ekini bırakarak oldukça yaklaşabiliriz. Benim düşünceme göre, bu, özellikle bir şey almak için bir nesne hiyerarşisine daha derine inmeniz gerekiyorsa, kodu daha akıcı hale getirir: mycontact.address (). StreetNumber ().

Bununla birlikte, getter'lardan kurtulmanın bir dezavantajı da var ve bu da araç desteği. Tüm Java IDE'leri ve birçok libary JavaBean standardına dayanır, bu da sizin için otomatik olarak oluşturulmuş kodu elle yazabileceğiniz ve kurallara bağlı kalarak önlenebilecek ek açıklamalar ekleyebileceğiniz anlamına gelir.

Entity or Value Object?
(Varlık veta Değer Nesnesi)


Bir şeyin bir değer nesnesi mi yoksa bir varlık olarak mı modelleneceğini bilmek her zaman kolay değildir. Aynı gerçek dünya kavramı, bir bağlamda bir varlık olarak ve bir başka bağlamda bir değer nesnesi olarak modellenebilir. Örnek olarak street address'ini ele alalım.

Bir fatura sistemi oluşturuyorsanız, street address'i yalnızca faturada yazdırdığınız bir şeydir. Faturadaki metin doğru olduğu sürece hangi nesne örneğinin kullanıldığı önemli değildir. Bu durumda, street address'i bir değer nesnesidir.

Bir kamu hizmeti için bir sistem kuruyorsanız, belirli bir daireye hangi gaz hattının veya hangi elektrik hattının gittiğini tam olarak bilmeniz gerekir. Bu durumda, street address'i bir varlıktır ve hatta bina veya apartman gibi daha küçük varlıklara ayrılabilir.

Değişken olmamaları(immutable) ve küçük oldukları için değer nesneleriyle çalışmak daha kolaydır. Bu nedenle, az sayıda varlığa ve çok fazla değer nesnelere sahip bir tasarım hedeflemelisiniz.

KOD ÖRNEKLERİ

Java'daki bir Person varlığı böyle bir şeye benzeyebilir (kod test edilmemiştir ve netlik için bazı yöntem uygulamaları atlanmıştır):

Person.java

public class Person {

    private final PersonId personId;
    private final EventLog changeLog;

    private PersonName name;
    private LocalDate birthDate;
    private StreetAddress address;
    private EmailAddress email;
    private PhoneNumber phoneNumber;

    public Person(PersonId personId, PersonName name) {
        this.personId = Objects.requireNonNull(personId);
        this.changeLog = new EventLog();
        changeName(name, "initial name");
    }

    public void changeName(PersonName name, String reason) {
        Objects.requireNonNull(name);
        this.name = name;
        this.changeLog.register(new NameChangeEvent(name), reason);
    }

    public Stream getNameHistory() {
        return this.changeLog.eventsOfType(NameChangeEvent.class).map(NameChangeEvent::getNewName);
    }

    // Other getters omitted

    public boolean equals(Object o) {
        if (o == this) {
            return true;
        }
        if (o == null || o.getClass() != getClass()) {
            return false;
        }
        return personId.equals(((Person) o).personId);
    }

    public int hashCode() {
        return personId.hashCode();
    }
}

Bu örnekte dikkat edilmesi gereken bazı noktalar:


  • Varlık ID'si için bir değer nesnesi - PersonId - kullanılır. Bir UUID, bir String veya bir Long kullanabilirdik, ancak bir değer nesnesi hemen bunun belirli bir Person'u tanımlayan bir ID olduğunu söyler.
  • Varlık kimliğine ek olarak, bu varlık birçok başka değer nesnesi de kullanır: PersonName, LocalDate (evet, bu standart Java API'sinin bir parçası olmasına rağmen bir değer nesnesidir), StreetAddress, EmailAddress ve PhoneNumber.
  • Adı değiştirmek için bir setter kullanmak yerine, adın değiştirilme nedeninin yanı sıra, değişikliği bir olay günlüğünde de saklayan bir business metodu kullanırız.
  • İsim değişikliklerinin geçmişini almak için bir getter var.
  • equals ve hashCode yalnızca varlık ID'sini kontrol eder.



Domain-Driven Design and CRUD
(DDD ve CRUD İşlemleri)

Şimdi DDD ve CRUD ile ilgili soruyu yanıtlamanın uygun olduğu bir noktaya geldik. CRUD, Oluşturma(Create), Okuma(retrieve), Güncelleme(Update) ve Silme(Delete) anlamına gelir ve aynı zamanda kurumsal uygulamalarda ortak bir kullanıcı arayüzü modelidir:


  • Ana görünüm, belki de filtreleme ve sıralama ile varlıkları arayabileceğiniz (alma(retrieve)) bir grid'den oluşur.
  • Ana görünümde, yeni varlıklar oluşturmak için bir button vardır. Buttona tıklamak boş bir form getirir ve form gönderildiğinde, yeni varlık gridde görünür (oluştur (create)).
  • Ana görünümde, seçilen objeyi düzenlemek için bir button bulunur. Düğmeye tıklandığında varlık verilerini içeren bir form açılır. Form gönderildiğinde, varlık yeni bilgilerle güncellenir (güncelleme(update)).
  • Ana görünümde, seçilen varlığı silmek için bir button bulunur. Buttona tıklamak varlığı gridden siler (sil(delete)).

Bu modelin kesinlikle yazılım dünyasında yeri vardır, ancak DDD bir uygulamada norm yerine istisna olmalıdır. Nedeni şudur: CRUD uygulaması yalnızca verileri yapılandırmak, görüntülemek ve düzenlemekle ilgilidir. Normalde altta yatan business sürecini desteklemez. Bir kullanıcı sisteme bir şey girdiğinde, bir şeyi değiştirdiğinde veya bir şeyi kaldırdığında, bu kararın arkasında bir business nedeni vardır. Belki değişim daha büyük bir business sürecinin bir parçası olarak gerçekleşiyordur? Bir CRUD sisteminde, değişikliğin nedeni kaybolur ve business süreci kullanıcının sadece zihnindedir.

Gerçek bir DDD kullanıcı arabirimi, kendilerini ortak dilin (ve böylece DDD'nin) parçası olan eylemlere dayandırır ve iş süreçleri, kullanıcıların zihninin aksine sisteme yerleştirilir. Bu da, saf bir CRUD uygulamasından daha sağlam, ancak tartışmasız daha az esnek bir sisteme yol açar. Bu farkı bir karikatürsel bir örnekle açıklayacağım:

Şirket A, çalışanları yönetmek için DDD bir sisteme sahipken, Şirket B, CRUD güdümlü bir yaklaşıma sahiptir. Her iki şirkette de çalışan ayrılır. Bu durumda aşağıdakiler olur:


Şirket A:
  • Yönetici, çalışanın sistemdeki kaydını arar.
  • Yönetici 'İş Sözleşmesini Sonlandır' eylemini seçer.
  • Sistem sonlandırma tarihini ve nedenini sorar.
  • Yönetici gerekli bilgileri girer ve 'Sözleşmeyi Sonlandır' seçeneğini tıklatır.
  • Sistem, çalışan kayıtlarını otomatik olarak günceller, çalışanın kullanıcı bilgilerini ve elektronik ofis anahtarını iptal eder ve bordro sistemine bir bildirim gönderir.

Şirket B:
  • Yönetici, çalışanın sistemdeki kaydını arar.
  • Yönetici, 'Sözleşme feshedildi' onay kutusuna bir onay işareti koyar ve fesih tarihini girer, ardından 'Kaydet'i tıklatır.
  • Yönetici, kullanıcı yönetim sistemine giriş yapar, kullanıcının hesabına bakar, 'Devre dışı' onay kutusuna bir onay işareti ekler ve 'Kaydet'i tıklar.
  • Yönetici, ofis anahtarı yönetim sisteminde oturum açar, kullanıcının anahtarını arar, 'Devre dışı' onay kutusuna bir onay işareti koyar ve 'Kaydet'i tıklar.
  • Yönetici bordro departmanına çalışanın işten ayrıldığını bildiren bir e-posta gönderir.

Önemli çıkarımlar şunlardır: Tüm uygulamalar DDD tasarım için uygun değildir ve DDD bir uygulama yalnızca DDD bir backend'e değil aynı zamanda domain güdümlü bir kullanıcı arayüzüne sahiptir.

Aggregates
(Agregalar(Nesne Grupları))

Şimdi varlıkların ve değer nesnelerinin ne olduğunu bildiğimizde, bir sonraki önemli konsepte bakacağız: gruplar(agregalar). Agregalar, belirli özelliklere sahip bir varlık ve değer nesneleri grubudur:

  • Agrega bir bütün olarak oluşturulur, alınır ve saklanır.
  • Agrega daima tutarlı bir durumdadır.
  • Agrega, ID'si agreganın kendisini tanımlamak için kullanılan agrega kökü(aggregate root) adı verilen bir varlığa(entity) aittir.



Ayrıca, agregalarle ilgili iki önemli kısıtlama vardır:
  • Bir agregaya dışarıdan sadece köküne referans verilebilir. Agrega dışındaki nesneler, agrega içindeki herhangi bir başka varlığı referans gösteremez.
  • Agrega kökü, agrega içinde business değişmezlerin(invariants) uygulanmasından sorumludur ve agreganın her zaman tutarlı bir durumda olmasını sağlar.




Bu, bir varlık tasarladığınızda, ne tür bir varlık yapacağınıza karar vermeniz gerektiği anlamına gelir. Varlık, bir aggregate kök gibi mi davranacak, aggregate kökün denetimi altında yaşayan local varlık(local entity) olarak adlandırdığım şey mi olacak? Local varlıklara toplamın dışından referans verilemediğinden, ID'lerinin aggregate içinde benzersiz olması (local bir kimliğe sahip olmaları) yeterlidir, oysa aggregate köklerin global olarak benzersiz ID'leri (global bir kimliğe sahip olmaları) gerekir. Ancak, bu semantik farkın önemi, aggregate'i nasıl saklayacağımıza bağlı olarak değişir. İlişkisel bir veritabanında, tüm varlıklar için aynı birincil anahtar oluşturma mekanizmasını kullanmak en mantıklıdır. Öte yandan, aggregate'in tamamı bir dokuman veritabanında tek bir belge olarak kaydedilirse, yerel varlıklar için gerçek local ID'lerinin kullanılması daha mantıklıdır.


Öyleyse bir varlığın aggregate kök olup olmadığını nasıl anlarsınız? Her şeyden önce, iki varlık arasında bir parent-child (veya master-detail) ilişkisi olması, parent'i otomatik olarak bir aggregate köke ve child'ı local bir varlığa dönüştürmez. Bu kararın verilebilmesi için daha fazla bilgiye ihtiyaç vardır. İşte bu noktada bu ayrımı nasıl yapmalıyız:
  • Varlığa uygulamadan nasıl erişelecek?
  • Varlık ID ile veya bir tür arama yoluyla aranacaksa, büyük olasılıkla bir aggregate köküdür.
  • Diğer aggregate'ler varlığı referans alması gerekecek mi?
  • Eğer varlığa diğer aggregatelerden referans verilecekse, kesinlikle bir aggregate köküdür.
  • Uygulamada varlık nasıl değiştirilecek(modifiye edilecek)?
  • Bağımsız olarak modifiye edilebilirse, muhtemelen bir aggregate köküdür.
  • Başka bir varlıkta değişiklik yapılmadan değiştirilemezse, büyük olasılıkla local bir varlıktır.

Bir aggregate kök oluşturduğunuzu bildikten sonra, business değişmezlerini(invariants) zorla nasıl uygularsınız ve bu ne anlama geliyor? Bir business değişmezi, toplamda ne olursa olsun her zaman geçerli olması gereken bir kuraldır. Basit bir business değişkeni, bir faturada, toplam tutarın, öğelerin eklenmesine, düzenlenmesine veya kaldırılmasına bakılmaksızın her zaman satır öğelerinin tutarlarının toplamı olması olabilir. Değişmezler ortak dilin ve domain modelinin bir parçası olmalıdır.


Teknik olarak bir aggregate kök, business değişmezlerini farklı şekillerde zorlayabilir:
  • Tüm durum değiştirme işlemleri aggregate kök üzerinden gerçekleştirilir.
  • Local varlıklar üzerinde durum değiştirme işlemlerine izin verilir, ancak her değiştiğinde aggregate köke bildirir.
Bazı durumlarda, örneğin fatura toplamına sahip örnekte, değişmez, aggregate kök'e her istek yapıldığında toplamı dinamik olarak hesaplaması zorlanabilir.

Aggregatelerimi şahsen kendim tasarlarım, böylece değişmezler hemen ve her zaman uygulanır. Muhtemelen, aggregate kaydedilmeden önce yapılan sıkı veri doğrulaması yapılarak aynı sonucu elde edebilirsiniz (Java EE yolu). Günün sonunda, bu kişisel bir zevk meselesi.


Aggregate Design Guidelines
(Aggregate Tasarım Yönergeleri)

Agregalar tasarlarken, takip edilmesi gereken belirli yönergeler vardır. Onları kurallardan ziyade yönergeler olarak adlandırmayı seçiyorum çünkü onları kırmanın mantıklı olduğu durumlar var.

YÖNERGE 1: AGREGALARINIZI KÜÇÜK TUTUN

Agregalar her zaman bir bütün olarak alınır ve saklanır. Ne kadar az veri okumak ve yazmak zorunda kalırsanız, sisteminiz o kadar iyi performans gösterir. Aynı nedenle, zamanla büyüyebileceğinden, sınırsız one-to-many ilişkilerden (koleksiyonlar) kaçınmalısınız.

Agregadanızdaki local varlıklar (mutable) yerine değer nesnelerini (immutable) kullanmayı tercih eteseniz bilei küçük bir agregaya sahip olmak, agrega kökünün business değişmezlerinin kullanılmasını kolaylaştırır.

YÖNERGE 2: DİĞER AGREGALARA ID İLE REFERANS GÖSTERİN

Doğrudan başka bir aggreagete referans göstermek yerine, agrega kökü ID'sini saran bir değer nesnesi oluşturun ve bunu referans olarak kullanın. Bu, bir agregate'in durumunu yanlışlıkla bir diğerinin içinden değiştiremeyeceğiniz için toplam tutarlılık sınırlarının korunmasını kolaylaştırır. Ayrıca, toplu bir nesne alındığında derin nesne ağaçlarının veri deposundan alınmasını önler.



Diğer agreganın verilerine gerçekten erişmeniz gerekiyorsa ve sorunu çözmenin daha iyi bir yolu yoksa, bu yönergeyi kırmanız gerekebilir. Persistance frameworke'ün lazy loading yeteneklerine güvenebilirsiniz, fakat deneyimlerime göre, çözdüklerinden daha fazla soruna neden olma eğilimindedirler. Daha fazla kodlama gerektiren ancak daha açık bir yaklaşım, repository'i bir metod parametresi olarak geçmektir:
public class Invoice extends AggregateRoot {

    private CustomerId customerId;

    // All the other methods and fields omitted

    public void copyCustomerInformationToInvoice(CustomerRepository repository) {
        Customer customer = repository.findById(customerId);
        setCustomerName(customer.getName());
        setCustomerAddress(customer.getAddress());
        // etc.
    }
}
Her durumda, agregalar arasında çift yönlü ilişkilerden kaçınmalısınız.


YÖNERGE 3: TRNASACTION BAŞINA BİR AGREGA DEĞİŞTİRİN

İşlemlerinizi, tek bir transaction içinde yalnızca bir agregada değişiklik yapacak şekilde tasarlamaya çalışın. Birden çok agrega içeren işlemler için domain eventlerini ve nihai tutarlılığı kullanın (bunun hakkında daha fazla konuşacağız). Bu, istenmeyen yan etkileri önler ve gerekirse sistemin gelecekte distribute edilmesini kolaylaştırır. Ayrıca, document veritabanlarının transaction desteği olmadan kullanılmasını da kolaylaştırır.


Bununla birlikte, bu ek bir karmaşıklık maliyeti ile birlikte gelir.Domain eventlerini güvenilir bir şekilde işlemek için bir altyapı kurmanız gerekir. Özellikle aynı thread ve transaction içinde eşzamanlı olarak domain eventleri gönderebileceğiniz monolitik bir uygulamada, eklenen karmaşıklık nadiren motive olur. Bence iyi bir uzlaşma, diğer agregalarda değişiklik yapmak için domain eventlerine güvenmektir, ancak aynı transaction içinde yapmaktır:




Her durumda, bir agreganın durumunu doğrudan başka bir agreganın içinden değiştirmekten kaçınmalısınız.

Domain eventlerini ele aldığımızda bununla ilgili daha fazla tartışacağız.


YÖNERGE 4: OPTIMISTIC LOCKING KULLANIN

Agregaların temel özelliği, business değişmezlerini zorlamak ve veri tutarlılığını her zaman sağlamaktır. Agrega, çakışan veri depolama güncellemeleri nedeniyle bozulursa, bu durum boşuna olur. Bu nedenle, agregaları kaydederken veri kaybını önlemek için optimistic locking kullanmalısınız.

Optimistic locking'in pestimistic locking'e tercih edilmesinin nedeni, eğer persistence framework optimistic locking'i desteklemiyorsa bunu kendinizin gerçekleştirebilmesi ve  distribute edilmesinin ve ölçeklendirmenin kolay olmasıdır.

Küçük agregalar (ve dolayısıyla küçük transactionlar) de çatışma riskini azalttığı için ilk yönergeye bağlı kalmak da bu konuda yardımcı olacaktır.


Agregalar, Değişmezler, Kullanıcı Arayüzü Bağlama ve Doğrulama

Bazılarınız büyük olasılıkla şimdi agregaların ve zorlayıcı iş değişmezlerinin kullanıcı arayüzleriyle birlikte nasıl çalıştıklarını ve daha spesifik olarak bağlayıcı oluşturduklarını merak ediyorsunuz. Değişmezler her zaman uygulanacaksa ve bir agreganın her zaman tutarlı bir durumda olması gerekiyorsa, kullanıcı formları doldururken ne yaparsınız? Ayrıca, setterlar yoksa, form alanlarını agregalara nasıl bağlarsınız?

Bununla baş etmenin birçok yolu vardır. En basit çözüm, agrega kaydedilene kadar değişmez zorlamayı ertelemek, tüm özellikler için setter'lar eklemek ve entityleri doğrudan forma bağlamaktır. Ben şahsen bu yaklaşımı sevmiyorum çünkü bu DDD olmaktan çok veri güdümlü olduğuna inanıyorum. Entitylerin, business logic'i bir hizmet katmanında (veya daha kötüsü, kullanıcı arayüzünde) sonuçlanacak şekilde anemik veri sahiplerine ayrılma riski yüksektir.

bkz : anemic domain model

Bunun yerine, diğer iki yaklaşımı tercih ediyorum. Birincisi, formları ve içeriklerini kendi domain modeli kavramlarına modellemektir. Gerçek dünyada, bir şey için başvurursanız, genellikle bir başvuru formu doldurmanız ve göndermeniz gerekir. Uygulama daha sonra işlenir ve gerekli tüm bilgiler sağlandıktan ve kurallara uyduğunuzda, uygulama çalışır ve başvurduğunuz her şeyi alırsınız. Domain modelinde bu işlemi taklit edebilirsiniz. Örneğin, Üyelik agrega kökü varsa, Üyelik oluşturmak için gereken tüm bilgileri toplamak için kullanılan bir MembershipApplication agrega köküne de sahip olabilirsiniz. Uygulama nesnesi daha sonra üyelik nesnesi oluşturulurken girdi olarak kullanılabilir.

İkinci yaklaşım birincinin bir varyantıdır ve essence pattern'idir. Düzenlemeniz gereken her varlık veya değer nesnesi için aynı bilgileri içeren değiştirilebilir(mutable) bir essence nesne oluşturun. Bu essence nesne daha sonra forma bağlanır. Essence nesnesi gerekli tüm bilgileri içerdikten sonra, gerçek varlıklar veya değer nesneleri oluşturmak için kullanılabilir. İlk yaklaşımın farkı, essence nesnelerin domain modelinin bir parçası olmaması, sadece gerçek domain nesneleriyle etkileşimi kolaylaştırmak için var olan teknik yapılardır. Pratikte, essence pattern böyle bir şeye benzeyebilir:

public class Person extends AggregateRoot {

    private final DateOfBirth dateOfBirth;
    // Rest of the fields omitted

    public Person(String firstName, String lastName, LocalDate dateOfBirth) {
        setDateOfBirth(dateOfBirth);
        // Populate the rest of the fields
    }

    public Person(Person.Essence essence) {
        setDateOfBirth(essence.getDateOfBirth());
        // Populate the rest of the fields
    }

    private void setDateOfBirth(LocalDate dateOfBirth) {
        this.dateOfBirth = Objects.requireNonNull(dateOfBirth, "dateOfBirth must not be null");
    }

    @Data // Lombok annotation to automatically generate getters and setters
    public static class Essence {
        private String firstName;
        private String lastName;
        private LocalDate dateOfBirth;
        private String streetAddress;
        private String postalCode;
        private String city;
        private Country country;

        public Person createPerson() {
            validate();
            return new Person(this);
        }

        private void validate() {
            // Make sure all necessary information has been entered, throw an exception if not
        }
    }
}

İsterseniz, bu kalıba daha aşina iseniz essence'i bir builder ile değiştirebilirsiniz. Sonuç aynı olurdu.

KOD ÖRNEKLERİ

Aşağıda, local ID'ye sahip bir agreaga kök (Order) ve bir local varlık (OrderItem) örneği verilmiştir (kod test edilmemiştir ve netlik için bazı metod uygulamaları atlanmıştır):


Order.java

public class Order extends AggregateRoot { // ID type passed in as generic parameter

    private CustomerId customer;
    private String shippingName;
    private PostalAddress shippingAddress;
    private String billingName;
    private PostalAddress billingAddress;
    private Money total;
    private Long nextFreeItemId;
    private List items = new ArrayList<>();

    public Order(Customer customer) {
        super(OrderId.createRandomUnique());
        Objects.requireNonNull(customer);

        // These setters are private and make sure the passed in parameters are valid:
        setCustomer(customer.getId());
        setShippingName(customer.getName());
        setShippingAddress(customer.getAddress());
        setBillingName(customer.getName());
        setBillingAddress(customer.getAddress());

        nextFreeItemId = 1L;
        recalculateTotals();
    }

    public void changeShippingAddress(String name, PostalAddress address) {
        setShippingName(name);
        setShippingAddress(address);
    }

    public void changeBillingAddress(String name, PostalAddress address) {
        setBillingName(name);
        setBillingAddress(address);
    }

    private Long getNextFreeItemId() {
        return nextFreeItemId++;
    }

    void recalculateTotals() { // Package visibility to make the method accessible from OrderItem
        this.total = items.stream().map(OrderItem::getSubTotal).reduce(Money.ZERO, Money::add);
    }

    public OrderItem addItem(Product product) {
        OrderItem item = new OrderItem(getNextFreeItemId(), this);
        item.setProductId(product.getId());
        item.setDescription(product.getName());
        this.items.add(item);
        return item;
    }

    // Getters, private setters and other methods omitted
}
OrderItem.java

public class OrderItem extends LocalEntity { // ID type passed in as generic parameter

    private Order order;
    private ProductId product;
    private String description;
    private int quantity;
    private Money price;
    private Money subTotal;

    OrderItem(Long id, Order order) {
        super(id);
        this.order = Objects.requireNonNull(order);
        this.quantity = 0;
        this.price = Money.ZERO;
        recalculateSubTotal();
    }

    private void recalculateSubTotal() {
        Money oldSubTotal = this.subTotal;
        this.subTotal = price.multiply(quantity);
        if (oldSubTotal != null && !oldSubTotal.equals(this.subTotal)) {
            this.order.recalculateTotals(); // Invoke aggregate root to enforce invariants
        }
    }

    public void setQuantity(int quantity) {
        if (quantity < 0) {
            throw new IllegalArgumentException("Quantity cannot be negative");
        }
        this.quantity = quantity;
        recalculateSubTotal();
    }

    public void setPrice(Money price) {
        Objects.requireNonNull(price, "price must not be null");
        this.price = price;
        recalculateSubTotal();
    }

    // Getters and other setters omitted
}

Domain Events
(Domain Olayları)

Şimdiye kadar yalnızca domain modelindeki "şeylere" baktık. Bununla birlikte, bunlar sadece modelin herhangi bir anda bulunduğu statik durumu tanımlamak için kullanılabilir. Birçok iş modelinde, gerçekleşen şeyleri tanımlamanız ve modelin durumunu değiştirebilmeniz gerekir. Bunun için domain eventlerini kullanabilirsiniz.

Domain eventleri Evans'ın DDD hakkındaki kitabına dahil edilmedi. Araç kutusuna daha sonra eklendi ve Vernon’un kitabına dahil edildi.

Bir domain eventi, domain modelinde, sistemin diğer bölümlerini ilgilendirebilecek herhangi bir şeydir. Domain eventleri, kaba taneli(coarse-grained) (ör. Belirli bir agrega kök oluşturulur veya bir işlem başlatılır) veya ince taneli(fine-grained) (ör. Belirli bir agrega kökün belirli bir özniteliği değiştirilir) olabilir.

  • Domain eventleri genellikle aşağıdaki özelliklere sahiptir:
  • Değişmezler(Immutable) (sonuçta geçmişi değiştiremezsiniz).
  • Söz konusu olay gerçekleştiğinde zaman damgası(timestamp) vardır.
  • Bir etkinliği diğerinden ayırt etmeye yardımcı olan benzersiz bir kimliğe(ID) sahip olabilirler.Bu, olayın türüne ve olayın nasıl dağıtıldığına bağlıdır.
  • Agrega kökler veya domain servisleri(daha sonra hakkında daha fazla bilgi vereceğiz) tarafından yayınlanır. 

Bir domain eventi yayınlandıktan sonra, bir veya daha fazla domain eventi dinleyicisi tarafından alınabilir ve bu da ek işleme ve yeni domain eventlerini tetikleyebilir. Yayıncı, eventin ne olduğunun farkında değildir ve dinleyici de yayıncıyı etkileyemez (diğer bir deyişle, domain eventlerini yayınlamak yayıncının bakış açısından yan etki içermemelidir). Bu nedenle, domain event dinleyicilerinin eventi yayımlayan ile aynı işlem içinde çalıştırılmaması önerilir.

Tasarım açısından bakıldığında, domain eventlerinin en büyük avantajı sistemi genişletilebilir hale getirmeleridir. Mevcut kodu değiştirmek zorunda kalmadan yeni iş mantığını tetiklemek için ihtiyaç duyduğunuz sayıda domain event dinleyicisi ekleyebilirsiniz. Bu doğal olarak doğru eventlerin ilk etapta yayınlandığını varsayar. Bazı eventlerin önceden farkında olabilirsiniz, ancak diğerleri kendilerini yolda daha fazla gösterecektir. Elbette, ne tür eventlere ihtiyaç duyulacağını tahmin etmeye ve bunları modelinize eklemeye çalışabilirsiniz, ancak daha sonra hiçbir yerde kullanılmayan domain eventleriyle sistemi tıkama riski de vardır. Daha iyi bir yaklaşım, domain eventlerini yayınlamayı mümkün olduğunca kolaylaştırmak ve daha sonra bunlara ihtiyacınız olduğunu fark ettiğinizde eksik eventleri eklemektir.

Event Kaynaklarına İlişkin Not

Event kaynağı, bir sistemin durumunun düzenli bir event günlüğü olarak devam ettiği bir tasarım modelidir. Her biri sistemin durumunu değiştirir ve mevcut durum, event günlüğünü baştan sona tekrar oynatarak herhangi bir zamanda hesaplanabilir. Bu örüntü, özellikle tarihin mevcut durum kadar önemli (hatta daha önemli) olduğu finansal defterler veya tıbbi kayıtlar gibi uygulamalarda kullanışlıdır.

Deneyimlerime göre, tipik bir iş sisteminin çoğu bölümü event kaynağı gerektirmez, ancak bazı bölümleri gerektirir. Tüm sistemi event kaynağı oluşturmayı bir kalıcılık modeli olarak kullanmaya zorlamak, bence, aşırıya kaçmak olacaktır. Ancak, domain eventlerinin gerektiğinde event kaynağı oluşturmak için kullanılabileceğini buldum. Uygulamada, bu, modelin durumunu değiştiren her işlemin, bazı event günlüğünde depolanan bir domain eventi de yayınlayacağı anlamına gelir. Bunun teknik olarak nasıl yapılacağı bu yazının kapsamı dışındadır.

Domain Eventlerini Dağıtma

Domain eventleri yalnızca bunları dinleyicilere dağıtmanın güvenilir bir yoluna sahipseniz kullanılabilir. Bir monolitin içinde, standart observer desenini kullanarak bellek içi dağılımı işleyebilirsiniz. Ancak, bu durumda bile, event yayıncılarını ayrı transactionlarda çalıştırmanın en iyi yöntemlerini izlerseniz daha karmaşık bir şeye ihtiyacınız olabilir. Event dinleyicilerinden biri başarısız olursa ve event yeniden gönderilirmesi gerekirse ne olur?

Vernon, hem uzaktan hem de local olarak çalışan eventleri dağıtmanın iki farklı yolunu sunar. Detaylar için kitabını okumanızı tavsiye ederim ama buradaki seçeneklerin kısa bir özetini vereceğim.

MESAJ KUYRUĞU İLE DAĞITIM

Bu çözüm, AMQP veya JMS gibi harici bir mesajlaşma çözümü (MQ) gerektirir. Çözüm, publish-subscribe  modelini ve garantili teslimatı desteklemelidir. Bir domain eventi yayınlandığında, publisher eventi MQ'ya gönderir. Domain event dinleyicileri MQ'ya abone olur ve hemen bilgilendirilir.



Bu modelin avantajları, hızlı, uygulanması oldukça kolay ve mevcut denenmiş ve gerçek mesajlaşma çözümlerine güvenmesidir. Dezavantajları, MQ çözümünü kurmanız ve sürdürmeniz gerektiğidir ve yeni bir tüketici abone olursa geçmiş olayları almanın bir yolu yoktur.

EVENT LOG İLE DAĞILIMI

Bu çözüm ek bileşen gerektirmez, ancak kodlama gerektirir. Bir domain eventi yayınlandığında, log'a eklenir. Domain event dinleyicileri, yeni eventleri kontrol etmek için bu logu düzenli olarak yoklar. Ayrıca, her seferinde tüm logu gözden geçirmek zorunda kalmamak için zaten hangi eventleri işlediklerini takip ederler.



Bu modelin avantajları, herhangi bir ek bileşen gerektirmemesi ve yeni event dinleyicileri için tekrarlanabilen eksiksiz bir olay geçmişi içermesidir. Dezavantajı, uygulanması için biraz çalışma gerektirmesi ve bir dinleyici tarafından yayınlanan ve alınan bir event arasındaki gecikmenin en çok yoklama aralığında olmasıdır.

Nihai Tutarlılık Üzerine Bir Not

Veri tutarlılığı, dağıtılmış sistemlerde veya aynı mantıksal transactionda birden fazla veri deposunun yer aldığı bir sorundur. Gelişmiş uygulama sunucuları, bu sorunu çözmek için kullanılabilen dağıtılmış işlemleri destekler, ancak özel yazılım gerektirirler ve yapılandırılması ve bakımı karmaşık işler olabilir. Güçlü tutarlılık mutlak bir gereklilikse, dağıtılmış transactionları kullanmaktan başka seçeneğiniz yoktur, ancak birçok durumda güçlü tutarlılığın aslında bir iş açısından o kadar da önemli olmadığı ortaya çıkabilir. Biz sadece tek bir ACID transactionunda tek bir veritabanı ile konuşan tek bir uygulamamız olduğu zamanlardan beri güçlü tutarlılık açısından düşünmeye alışkınız.

Güçlü tutarlılığın alternatifi nihai tutarlılıktır. Bu, uygulamadaki verilerin nihayetinde tutarlı hale geleceği anlamına gelir, ancak sistemin tüm bölümlerinin birbiriyle senkronize olmadığı ve mükemmel derecede iyi olduğu zamanlar olacaktır. Nihai tutarlılık için bir uygulama tasarlamak farklı bir düşünme yöntemi gerektirir, ancak bunun karşılığında, yalnızca güçlü tutarlılık gerektiren bir sistemden daha esnek ve ölçeklenebilir bir sistem elde edilebilir.

DDD bir sistemde domain eventleri, nihai tutarlılığa ulaşmanın mükemmel bir yoludur. Başka bir modülde veya sistemde bir şey olduğunda kendini güncellemesi gereken herhangi bir sistem veya modül, o sistemden gelen domain eventlerine abone olabilir:


Yukarıdaki örnekte, Sistem A'da yapılan değişiklikler sonundadomain eventleri aracılığıyla B, C ve D sistemlerine yayılacaktır. Her sistem veri deposunu güncellemek için kendi yerel transactionlarını kullanacaktır. Olay dağıtım mekanizmasına ve sistemlerin yüküne bağlı olarak, yayılma süresi bir saniyeden az olabilir (tüm sistemler aynı ağda çalışır ve olaylar derhal abonelere gönderilir) ila birkaç saat hatta gün (bazılarının sistemler çevrimdışıdır ve yalnızca son check-in işleminden bu yana gerçekleşen tüm domain eventlerini indirmek için ağa zaman zaman bağlanır).

Nihai tutarlılığı başarıyla uygulamak için, bir event ilk kez yayınlandığında abonelerin bazıları şu anda çevrimiçi olmasa bile çalışan domain eventlerini dağıtmak için güvenilir bir sisteme sahip olmanız gerekir. Ayrıca, herhangi bir veri parçasının bir süre için out of date olabileceği varsayımı çerçevesinde hem iş mantığınızı hem de kullanıcı arayüzünüzü tasarlamanız gerekir. Ayrıca, verilerin ne kadar tutarsız olabileceğine dair kısıtlamalar oluşturmanız gerekir. Bazı veri parçalarının günlerce tutarsız kalabildiğini görmek sizi şaşırtabilirken, diğer veri parçalarının saniyeler içinde veya daha kısa bir sürede güncellenmesi gerekir.

KOD ÖRNEKLERİ

Aşağıda, sipariş gönderildiğinde bir domain eventi (OrderShipped) yayınlayan bir agrega kök (Order) örneği verilmiştir. Bir domain dinleyicisi (InvoiceCreator) eventi alır ve ayrı bir işlemde yeni bir fatura oluşturur. Agrega kök kaydedildiğinde kayıtlı tüm eventleri yayınlayan bir mekanizma olduğu varsayılmaktadır (kod test edilmemiştir ve netlik için bazı yöntem uygulamaları atlanmıştır):

OrderShipped.java

public class OrderShipped implements DomainEvent {
    private final OrderId order;
    private final Instant occurredOn;

    public OrderShipped(OrderId order, Instant occurredOn) {
        this.order = order;
        this.occurredOn = occurredOn;
    }

    // Getters omitted
}
Order.java

public class Order extends AggregateRoot {

    // Other methods omitted

    public void ship() {
        // Do some business logic
        registerEvent(new OrderShipped(this.getId(), Instant.now()));
    }
}
InvoiceCreator.java

public class InvoiceCreator {

    final OrderRepository orderRepository;
    final InvoiceRepository invoiceRepository;

    // Constructor omitted

    @DomainEventListener
    @Transactional
    public void onOrderShipped(OrderShipped event) {
        var order = orderRepository.find(event.getOrderId());
        var invoice = invoiceFactory.createInvoiceFor(order);
        invoiceRepository.save(invoice);
    }
}


Hareketli ve Statik Nesneler

Devam etmeden önce, sizi hareketli ve statik nesnelere tanıtmak istiyorum. Bunlar gerçek DDD terimleri değil, domain modelinin farklı bölümlerini düşündüğümde kendim kullandığım bir şey. Benim dünyamda, taşınabilir bir nesne, birden fazla örneği olabilen ve uygulamanın farklı bölümleri arasında geçirilebilen herhangi bir nesnedir. Değer nesneleri, varlıklar ve domain eventlerinin tümü taşınabilir nesnelerdir.

Öte yandan, statik bir nesne, her zaman tek bir yerde duran ve uygulamanın diğer bölümleri tarafından çağrılan ancak nadiren (diğer statik nesnelere enjekte edilenler hariç) geçirilen singleton (veya pooled bir kaynak) 'tır. Repositoryler, domain servisleri ve factorylerin tümü statik nesnelerdir.

Bu fark önemlidir çünkü nesneler arasında ne tür ilişkileriniz olabileceğini belirler. Statik nesneler, diğer statik nesnelere ve taşınabilir nesnelere referans tutabilir.

Taşınabilir nesneler, diğer hareketli nesnelerin referansını tutabilir. Ancak, hareketli bir nesne hiçbir zaman statik bir nesneye başvuru yapamaz. Taşınabilir bir nesnenin statik bir nesneyle etkileşime girmesi gerekiyorsa, statik nesnenin kendisiyle etkileşime girecek metoda bir metod parametresi olarak iletilmesi gerekir. Bu, taşınabilir nesneleri daha taşınabilir ve bağımsız bir hale getirir, çünkü her seferinde deserialize ettiğimizde statik nesnelere herhangi bir referansa bakmanız ve enjekte etmeniz gerekmez.

Diğer Domain Nesneleri

DDD kodla çalışırken, bir sınıfın değer nesnesine, varlık veya domain event kalıbına gerçekten uymadığı durumlarda karşılaşacağınız zamanlar olacaktır. Deneyimlerime göre, bu genellikle aşağıdaki durumlarda olur:

  • Harici bir sistemden herhangi bir bilgi (= başka bir sınırlı bağlam(bounded context)). Bilgiler sizin bakış açınızdan değiştirilemez, ancak benzersiz bir şekilde tanımlamak için kullanılan bir global ID'ye sahiptir.
  • Diğer varlıkları tanımlamak için kullanılan verileri yazın (Vaughn Vernon, bu nesnelere standart tipler olarak adlandırır). Bu nesneler global ID'lere sahiptir ve bir dereceye kadar değişebilir olabilir, ancak uygulamanın kendisinin tüm pratik amaçları için değişmezdirler(immutable).
  • Denetim girişlerini veya domain eventlerini veritabanında depolamak için kullanılan altyapı altyapı - framework düzeyindeki varlıklar. Global ID'leri olabilir veya olmayabilir ve kullanım durumuna bağlı olarak değişebilir(mutable) veya değişmeyebilir(immutable).
Bu durumları ele almanın yolu, DomainObject adı verilen bir şeyle başlayan temel sınıflar ve arabirimler hiyerarşisini kullanmaktır. Domain nesnesi, bir şekilde domain modeliyle ilgili olan herhangi bir hareketli nesnedir. Bir nesne tamamen bir değer nesnesiyse veya tamamen bir varlık değilse, bunu bir domain nesnesi olarak tanımlayabilirim ve JavaDocs'ta ne yaptığını ve nedenini açıklayabilir ve devam edebilirim.


Arabirimleri hiyerarşinin en üstünde kullanmayı seviyorum çünkü bunları istediğiniz şekilde birleştirebilir ve hatta enum'ların bunları uygulamasını sağlayabilirsiniz. Arabirimlerin bazıları, yalnızca uygulayıcı sınıfın domain modelinde hangi rolü oynadığını göstermek için kullanılan herhangi bir metod içermeyen işaretleyici arabirimleridir. Yukarıdaki şemada, sınıflar ve arayüzler aşağıdaki gibidir:

DomainObject - tüm domain nesneleri için en üst düzey işaretleyici arabirimi.

DomainEvent - tüm domain event'leri için arabirim. Bu genellikle event hakkında eventin tarihi ve saati gibi bazı meta veriler içerir, ancak bir işaretleyici arayüzü de olabilir.

ValueObject - tüm değer nesneleri için işaretleyici arabirimi. Bu arayüzün uygulamalarının değişmez olması ve equals () ve hashCode () yöntemlerinin uygulanması gerekir. Ne yazık ki, güzel olsa da, bunu arayüz seviyesinden zorlamanın bir yolu yoktur.

IdentifiableDomainObject - bir bağlamda(context) benzersiz olarak tanımlanabilen tüm domain nesneleri için arabirim. Genellikle bunu genel bir parametre olarak ID türü ile genel bir arayüz olarak tasarlarım.

StandardType - standart tipler için işaretleyici arabirimi.

Entity - varlıklar için soyut temel sınıf. Ben genellikle ID için bir alan eklerim ve buna göre equals () ve hashCode () uygularım. Persistence framework'e bağlı olarak bu sınıfa optimistic locking bilgisi de ekleyebilirim.

LocalEntity - yerel varlıklar için soyut temel sınıf. Yerel varlıklar için yerel kimlik kullanırsam, bu sınıf bunu yönetmek için kod içerecektir. Aksi takdirde, sadece boş bir işaretleyici sınıfı olabilir.

AggregateRoot - agrega kökleri için soyut temel sınıf. Local varlıklar için local kimlik kullanırsam, bu sınıf yeni local kimlikler oluşturmak için kod içerecektir. Sınıf ayrıca domain eventlerini göndermek için kod içerecektir. Varlık sınıfına optimistic locking bilgileri dahil edilmemişse, kesinlikle buraya dahil edilir. Denetim bilgileri (yaratılan, son güncellenen vb.), uygulamanın gereksinimlerine bağlı olarak bu sınıfa da eklenebilir.

KOD ÖRNEKLERİ

Bu kod örneğinde, iki sınırlı bağlamımız(bounded context) var: kimlik yönetimi ve çalışan yönetimi:


Çalışan yönetimi bağlamının kimlik yönetim bağlamından kullanıcılar hakkında hepsi olmasa da bazı bilgilere ihtiyacı vardır. Bunun için bir REST endpoint vardır ve veriler JSON'a serialize edilir.

Kimlik yönetimi bağlamında bir Kullanıcı şu şekilde temsil edilir:

User.java (identity management)



User.java (identity management)
public class User extends AggregateRoot {
    private String userName;
    private String firstName;
    private String lastName;
    private Instant validFrom;
    private Instant validTo;
    private boolean disabled;
    private Instant nextPasswordChange;
    private List passwordHistory;

    // Getters, setters and business logic omitted
}

Çalışan yönetimi bağlamında, yalnızca kullanıcı ID'sne ve adına ihtiyacımız var. Kullanıcı ID ile benzersiz bir şekilde tanımlanır, ancak ad kullanıcı arayüzünde gösterilir. Açıkçası herhangi bir kullanıcı bilgisini değiştiremeyiz, böylece kullanıcı bilgileri değiştirilemez. Kod şöyle görünür:

User.java (employee management)
public class User implements IdentifiableDomainObject {
    private final UserId userId;
    private final String firstName;
    private final String lastName;

    @JsonCreator // We can deserialize the incoming JSON directly into an instance of this class.
    public User(String userId, String firstName, String lastName) {
        // Populate fields, convert incoming userId string parameter into a UserId value object instance.
    }

    public String getFullName() {
        return String.format("%s %s", firstName, lastName);
    }

    // Other getters omitted.

    public boolean equals(Object o) {
        // Check userId only
    }

    public int hashCode() {
        // Calculate based on userId only
    }
}

Repositories

Artık domain modelinin tüm hareketli nesnelerini ele aldık ve artık statik olanlara geçme zamanı. İlk statik nesne repository'dir. Bir repository, agregaların peristence konteynırıdır. Bir repository'e kaydedilen tüm agregalar, sistem yeniden başlatıldıktan sonra bile daha sonra oradan alınabilir.

Bir repository'nin en azından aşağıdaki yetenekleri olmalıdır:


  • Bir tür veri depolamada bir agregayı tamamen kaydetme yeteneği.
  • Bir agregayı, ID'sine göre bütünüyle alabilme.
  • Bir agregayo ID'sine göre tamamen silme yeteneği.
Çoğu durumda, gerçekten kullanılabilir olmak için, bir repository'nin daha gelişmiş query yöntemlerine de ihtiyacı vardır.

Pratikte, bir repository, ilişkisel veritabanı, NoSQL veritabanı, dizin hizmeti veya hatta dosya sistemi gibi harici bir veri depolama alanına domainin farkında olan bir arabirimdir. Gerçek depolama repository'nin arkasında gizlenmiş olsa da, depolama semantiği tipik olarak sızıntı yapacak ve repository'nin neye benzemesi gerektiği konusunda sınırlar getirecektir. Bu nedenle, repository'ler tipik olarak collection-oriented veya persistence-oriented'dır.

Collection oriented bir repository, bellek içi bir nesne koleksiyonunu taklit etmeyi amaçlamaktadır. Bir agrega koleksiyona eklendikten sonra, agrega repository'den kaldırılana kadar üzerinde yapılan değişiklikler otomatik olarak devam eder. Başka bir deyişle, koleksiyon odaklı bir repository add () ve remove () gibi yöntemlere sahip olacak, ancak kaydetmek için herhangi bir metod bulunmayacaktır.

Persistence oriented bir repository ise bir koleksiyonu taklit etmeye çalışmaz. Bunun yerine, harici bir kalıcı çözüm için bir facede görevi görür ve insert (), update () ve delete () gibi metodlar içerir. Toplamda yapılan herhangi bir değişikliğin, bir update () metoduna yapılan çağrı yoluyla açıkça depoya kaydedilmesi gerekir.

Anlamsal olarak oldukça farklı oldukları için repository türünün projenin başında doğru olması önemlidir. Genellikle, kalıcılığa yönelik bir repository'nin uygulanması daha kolaydır ve mevcut persistence frameworklerin çoğuyla çalışır. Koleksiyona dayalı bir repository'nin altında yatan persistence framework onu ortaya çıkarmadıkça uygulanması daha zordur.

KOD ÖRNEKLERİ

Bu örnek, collection oriented ve persistence oriented repository'ler arasındaki farkları gösterir.


Collection-oriented repository

public interface OrderRepository {

    Optional get(OrderId id);

    boolean contains(OrderID id);

    void add(Order order);

    void remove(Order order);

    Page search(OrderSpecification specification, int offset, int size);
}


// Would be used like this:

public void doSomethingWithOrder(OrderId id) {
    orderRepository.get(id).ifPresent(order -> order.doSomething());
    // Changes will be automatically persisted.
}
Persistence oriented repository

public interface OrderRepository {

    Optional findById(OrderId id);

    boolean exists(OrderId id);

    Order save(Order order);

    void delete(Order order);

    Page findAll(OrderSpecification specification, int offset, int size);
}


// Would be used like this:

public void doSomethingWithOrder(OrderId id) {
    orderRepository.findById(id).ifPresent(order -> {
        order.doSomething();
        orderRepository.save(order);
    });
}

CQRS Hakkında Bir Not

Repository'ler  her zaman agregaları kaydeder ve alır. Bu, nasıl uygulandıklarına ve her bir agrega için oluşturulması gereken nesne grafiklerinin boyutuna bağlı olarak oldukça yavaş olabileceği anlamına gelir. Bu, UX açısından sorunlu olabilir ve özellikle iki kullanım durumu akla gelir. Birincisi, yalnızca bir veya iki özellik kullanarak agrega listesini göstermek istediğiniz küçük bir listedir. Yalnızca birkaç öznitelik değerine ihtiyacınız olduğunda eksiksiz bir nesne grafiği oluşturmak zaman kaybı ve bilgi işlem kaynaklarıdır ve genellikle yavaş bir kullanıcı deneyimine yol açar. Başka bir durum, bir listede tek bir öğeyi göstermek için birden fazla agregadaki verileri birleştirmeniz gerektiğidir. Bu, daha kötü performansa neden olabilir.

Veri setleri ve agregalar küçük olduğu sürece, performans kaybı kabul edilebilir, ancak performansın kabul edilebilir olmadığı zaman gelirse bir çözüm vardır: Komut Sorgu Sorumluluk Ayrımı (Command Query Responsibility Segregation)(CQRS).


CQRS, yazma (komutlar) ve okuma (sorgular) işlemlerini birbirinden tamamen ayırdığınız bir modeldir. Detaylara girmek bu yazının kapsamı dışındadır, ancak DDD açısından, deseni şu şekilde uygularsınız:

  • Sistemin durumunu değiştiren tüm kullanıcı işlemleri repository'lerden normal şekilde geçer.
  • Tüm sorgular repository'leri atlar ve doğrudan temel alınan veritabanına gider ve yalnızca gerekli verileri alır ve başka bir şey almaz.
  • Gerekirse, kullanıcı arayüzündeki her görünüm için ayrı sorgu nesneleri bile tasarlayabilirsiniz
  • Sorgu nesneleri tarafından döndürülen Veri Aktarım Nesneleri (DTO), agrega ID'lerini içermelidir, böylece değişiklik yapma zamanı geldiğinde doğru agrega repository'den alınabilir.
Birçok projede, bazı görünümlerde CQRS'yi ve diğerlerinde doğrudan repository sorgularını kullanabilirsiniz.

Domain Services(Domain Servisleri)

Hem değer nesnelerinin hem de varlıkların iş mantığı içerebileceğini (ve içermesi gerektiğini) daha önce belirtmiştik. Bununla birlikte, bir mantığın belirli bir değer nesnesine veya belirli bir varlığa uymadığı senaryolar vardır. İş mantığını yanlış yere koymak kötü bir fikirdir, bu yüzden başka bir çözüme ihtiyacımız var. İkinci statik nesnesimizi: domain servisi.

Domain servisleri aşağıdaki özelliklere sahiptir:
  • Durum bilgisi tutmazlar(stateless)
  • Son derece cohesive'lerdir (yani sadece bir şey yapmakta uzmanlaşmışlardır)
  • Başka bir yerde doğal olarak uymayan iş mantığı içerirler.
  • Diğer domain servisleriyle ve bir ölçüde repository'lerle etkileşime girebilirler.
  • Domain event'lerini yayınlayabilirler.
En basit haliyle, bir domain servisi, içinde statik bir metod bulunan bir utility sınıfı olabilir. Daha gelişmiş domain servisleri diğer domain serrvislerinin ve repositorylerin bunlara enjekte edildiği singleton öğeler olarak uygulanabilir.

Bir domain servis bir uygulama servisiyle karıştırılmamalıdır. Bu serinin bir sonraki makalesinde uygulama servislerine daha yakından bakacağız, ancak kısacası, bir uygulama servisi, yalıtılmış domain modeli ile dünyanın geri kalanı arasındaki orta insan gibi davranır. Uygulama servisi, transactionların gerçekleştirilmesinden, sistem güvenliğinin sağlanmasından, uygun agregaların aranmasından, bunlara metod çağrılmasından ve değişiklikleri veritabanına geri kaydedilmesinden sorumludur. Uygulama servisleri herhangi bir iş mantığı içermez.

Uygulama ve domain servisleri arasındaki farkı şu şekilde özetleyebilirsiniz: bir domain servisi yalnızca iş kararları vermekten sorumluyken, bir uygulama servisi yalnızca orkestrasyondan sorumludur (doğru nesneleri bulmak ve doğru metodları doğru sırayla çağırmak). Bu nedenle, bir domain servisi genellikle veritabanının durumunu değiştiren herhangi bir repository'i çağırmamalıdır - bu, uygulama servisinin sorumluluğundadır.

KOD ÖRNEKLERİ


Bu ilk örnekte, belirli bir parasal işlemin devam etmesine izin verilip verilmediğini kontrol eden bir domain servisi oluşturacağız. Uygulama büyük ölçüde basitleştirilmiştir, ancak önceden tanımlanmış bazı iş kurallarına dayanarak açıkça bir iş kararı vermektedir.
Bu durumda, iş mantığı çok basit olduğu için, bunu doğrudan Account sınıfına ekleyebilirsiniz. Bununla birlikte, daha gelişmiş iş kuralları yürürlüğe girer girmez, karar vermeyi kendi sınıfına taşımak mantıklıdır (özellikle kurallar zaman içinde değişir veya bazı dış yapılandırmalara bağlıysa). Bu mantığın domain servisibe ait olabileceğine dair bir başka işaret, birden fazla toplama (iki hesap) içermesidir.

TransactionValidator.java

public class TransactionValidator {

    public boolean isValid(Money amount, Account from, Account to) {
        if (!from.getCurrency().equals(amount.getCurrency())) {
            return false;
        }
        if (!to.getCurrency().equals(amount.getCurrency())) {
            return false;
        }
        if (from.getBalance().isLessThan(amount)) {
            return false;
        }
        if (amount.isGreaterThan(someThreshold)) {
            return false;
        }
        return true;
    }
}

İkinci örnekte, özel bir özelliğe sahip bir domain servisibe bakacağız: arabirimi domain modelinin bir parçasıdır, ancak uygulaması değildir. Bu, domain modelinizde bir iş kararı vermek için dış dünyadan bilgiye ihtiyaç duyduğunuzda ortaya çıkabilecek bir durumdur, ancak bu bilgilerin nereden geldiğiyle ilgilenmezsiniz.

CurrencyExchangeService.java

public interface CurrencyExchangeService {

    Money convertToCurrency(Money currentAmount, Currency desiredCurrency);
}

Domain modeli, örneğin bir dependency injection framework kullanılarak bağlandığında, bu arabirimin doğru uygulamasını enjekte edebilirsiniz. Local cache çağıran bir servvise, veya uzak bir web servisi çağıran bir servise sahip olabilirsiniz; bir diğeri yalnızca test amacıyla kullanılıabilir vb.

Factories

Görüneceğimiz son statik nesne factory. Adından da anlaşılacağı gibi, factoryler yeni agregalar oluşturmaktan sorumludur. Ancak bu, her bir agrega için yeni bir fabrika oluşturmanız gerektiği anlamına gelmez. Çoğu durumda, agrega kökün yapıcısı, agaregayı tutarlı bir durumda olacak şekilde ayarlamak için yeterli olacaktır. Aşağıdaki durumlarda genellikle ayrı bir factory'e ihtiyacınız olacaktır:

  • İş mantığı agreganın oluşturulmasında rol oynar
  • Agregatın yapısı ve içeriği giriş verilerine bağlı olarak çok farklı olabilir
  • Giriş verileri o kadar geniştir ki, oluşturucu modeli (veya benzer bir şey) gereklidir
  • Factory, sınırlı bir bağlamdan(bounded context) diğerine çeviri yapar.
Factory, agrega kök sınıfında statik bir factory metodu veya ayrı bir factory sınıfı olabilir. Factory diğer factorylerle, repositorylerle ve domain servisleriyle etkileşime girebilir ancak veritabanının durumunu asla değiştirmemelidir (bu nedenle kaydetme veya silme işlemi yapılmaz).

KOD ÖRNEKLERİ

Bu örnekte, iki sınırlı bağlam(bounded context) arasında çeviri yapan bir fabrikaya bakacağız. Sevkiyat bağlamında müşteriye artık müşteri olarak değil, gönderi alıcısı olarak atıfta bulunulur. Gerekirse iki kavramı daha sonra ilişkilendirebilmemiz için müşteri ID'si hala saklanabilir.

ShipmentRecipientFactory.java

public class ShipmentRecipientFactory {
    private final PostOfficeRepository postOfficeRepository;
    private final StreetAddressRepository streetAddressRepository;

    // Initializing constructor omitted

    ShipmentRecipient createShipmentRecipient(Customer customer) {
        var postOffice = postOfficeRepository.findByPostalCode(customer.postalCode());
        var streetAddress = streetAddressRepository.findByPostOfficeAndName(postOffice, customer.streetAddress());
        var recipient = new ShipmentRecipient(customer.fullName(), streetAddress);
        recipient.associateWithCustomer(customer.id());
        return recipient;
    }
}

Modules
(Modüller)

Bir sonraki makaleye geçme zamanı geldi, ancak taktiksel DDD tasarımdan ayrılmadan önce, bakmamız gereken bir kavram daha var ve bu da modüller.

DDD'deki modüller Java'daki paketlere ve C # 'daki namespace'lere karşılık gelir. Bir modül sınırlı bir bağlama(bounded context) karşılık gelebilir, ancak tipik olarak sınırlı bir bağlamda birden fazla modül bulunur.

Birbirine ait sınıflar aynı modüle gruplanmalıdır. Ancak, sınıf türüne göre değil, sınıfların iş perspektifinden domain modeline nasıl uyduğuna bağlı olarak modüller oluşturmalısınız. Yani, tüm repositoryleri bir modüle, tüm varlıkları diğerine vb. koymamalısınız. Bunun yerine, belirli bir agrega veya belirli bir iş süreci ile ilgili tüm sınıfları aynı modüle koymalısınız. Bu, birlikte ait ve birlikte çalışan sınıflar da birlikte yaşadığından kodunuzda gezinmeyi kolaylaştırır.

Modül Örneği

Bu, sınıfları türe göre gruplayan bir modül yapısına örnektir. Bunu yapmayın:

  • foo.bar.domain.model.services
  • AuthenticationService
  • PasswordEncoder
  • foo.bar.domain.model.repositories
  • UserRepository
  • RoleRepository
  • foo.bar.domain.model.entities
  • User
  • Role
  • foo.bar.domain.model.valueobjects
  • UserId
  • RoleId
  • UserName
Daha iyi bir yol, sınıfları sürece ve agregaya göre gruplandırmaktır. Yukardekinin yerine bunu yapın:

  • foo.bar.domain.model.authentication
  • AuthenticationService
  • foo.bar.domain.model.user
  • User
  • UserRepository
  • UserId
  • UserName
  • PasswordEncoder
  • foo.bar.domain.model.role
  • Role
  • RoleRepository
  • RoleId
Taktiksel DDD Neden Önemlidir?

Bu serinin ilk makalesinin girişinde bahsettiğim gibi, öncelikle ciddi veri tutarsızlığı sorunlarından muzdarip bir projeyi kurtarırken alan güdümlü tasarıma girdim. Herhangi bir domain modeli veyaortak dil olmadan, varolan veri modelini agregalara ve veri erişim nesnelerini repository'lere dönüştürmeye başladık. Bunların yazılıma getirdiği kısıtlamalar sayesinde tutarsızlık sorunlarından kurtulmayı başardık ve sonunda yazılımı production ortamına deoploy edebilir hale geldik.

Taktiksel DDD tasarım ile bu ilk karşılaşma bana projenin tüm diğer yönleri DDD olmasa bile bundan yararlanabileceğimizi kanıtladı. Katıldığım tüm projelerde kullanmak istediğim en sevdiğim DDD yapı taşı değer nesnesidir. Girişleri kolaydır ve özelliklerinize bağlam getirdiği için kodu hemen okumayı ve anlamayı kolaylaştırır. Değişmezlik(Immutability) aynı zamanda karmaşık şeyleri basitleştirme eğilimindedir.

Ayrıca, veri modeli başka şekilde tamamen anemik olsa bile (sadece herhangi bir iş mantığı olmayan getter ve setterlar) veri modellerini agregalar ve repositorylerde gruplandırmaya çalışırım. Bu, verilerin tutarlı tutulmasına yardımcı olur ve aynı varlık farklı mekanizmalar aracılığıyla güncellenirken garip yan etkilerden ve optimistic locking istisnalarından kaçınır.

Domain eventleri kodunuzu ayırmak için yararlıdır, ancak bu iki ucu keskin bir kılıçtır. Eventşere çok fazla güvenirseniz, kodunuzun anlaşılması ve hatalarının ayıklanması zorlaşacaktır, çünkü belirli bir eventin hangi diğer işlemleri tetikleyeceğini veya hangi eventşerin belirli bir işlemin tetiklenmesine neden olduğunu hemen netleştirmez.

Diğer yazılım tasarım desenleri gibi, taktiksel DDD tasarım da, özellikle kurumsal yazılımlar oluştururken karşılaştığınız bir dizi soruna çözümler sunar. Ne kadar elinizin altında olursa, bir yazılım geliştiricisi olarak kariyerinizde kaçınılmaz olarak karşılaştığınız sorunları çözmeniz o kadar kolay olacaktır.

Çeviri için izin veren  Petter Holmström'e teşekkürler.


Bonus :

Barış Velioğlu
Domain Driven Design Kimdir?

0 yorum: