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
Örnek DDD projesi

Serinin diğer yazıları :
1 - Strategic Domain Driven Design (Stratejik DDD)
3 - Domain-Driven Design and the Hexagonal Architecture (DDD ve Altıgen Mimari)

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?

Anemic Domain Model - Wikipedia - Çevirisi

Anemik domain modeli(anemic domain model), domain nesnelerinin çok az iş mantığı içerdiği (doğrulama, hesaplamalar, iş kuralları vb.) veya içermediği bir yazılım domain modelinin kullanılmasıdır.

Bu pattern ilk olarak uygulamayı bir anti-pattern olarak gören Martin Fowler tarafından tanımlanmıştır. Diyor ki:

Bu anti-paternin temel sorunu, nesne yönelimli tasarımın temel fikrine çok aykırı olmasıdır; bu da verileri birleştirip birlikte işlemektir. Anemik alan modeli sadece prosedürel bir stil tasarımıdır, benim gibi nesne kavramına inananların Smalltalk'taki ilk günlerimizden beri savaştığı şeydir. Daha da kötüsü, birçok insan anemik nesnelerin gerçek nesneler olduğunu düşünür ve bu nedenle nesneye yönelik tasarımın tümüyle ilgili olduğu noktayı tamamen kaçırır.

Anemik bir domain tasarımında, iş mantığı genellikle domain nesnelerinin state'ini değiştiren ayrı sınıflarda uygulanır. Fowler bu tür harici sınıfları transaction scripts olarak adlandırır. Bu model, Java uygulamalarında, EJB'nin Entity Beans'ın ilk sürümleri gibi teknolojiler tarafından teşvik edilen yaygın bir yaklaşımdır. Bu durum "Business Entity kategorisine giren Üç Katmanlı Uygulama mimarisini izleyen .NET uygulamalarında Entitylerde de görülür "(Business Entitylerinin de davranış içerebilmesi mümkün olduğu halde)

Fowler,  transaction script pattern'ini şöyle açıklar:

Çoğu business uygulaması bir dizi işlem olarak düşünülebilir. Bir işlem bazı bilgileri belirli bir şekilde organize edilmiş olarak görebilir, bir diğeri üzerinde değişiklik yapar. İstemci sistemi ile sunucu sistemi arasındaki her etkileşim belirli miktarda mantık içerir. Bazı durumlarda bu, veritabanından bilgi görüntülemek kadar basit olabilir. Diğer bazı durumlarda birçok doğrulama ve hesaplama adımı içerebilir. Bir transaction script dosyası, tüm bu mantığı öncelikli olarak tek bir metod olarak düzenleyerek doğrudan veritabanına veya basit bir veritabanı wrapper aracılığıyla arama yapar. Her transaction kendi tranaction script'e sahip olacaktır, ancak ortak alt görevler alt prosedürlere ayrılabilir.

Fowler, "Patterns of Enterprise Application Architecture(Kurumsal Uygulama Mimarisinin Kalıpları)" adlı kitabında, transaction script deseninin birçok basit iş uygulaması için uygun olduğunu kaydetmiştir ve karmaşık bir OO-veritabanı mapping katmanını ortadan kaldırabileceğimizi söylemiştir.

Anemik domain modelinin olmasının nedenleri

Anemik Domain Model, davranışın dolaşmadığı veya dolaşıma meğilli olmadığı SOA Mimarilerden etkilenen sistemlerde oluşabilir.

  • Mesajlaşma / Pipeline mimarileri
  • SOAP / REST gibi API'lar
Anemik olmanın anti-pattern olmadığı yönündeki eleştiriler
  • Bu yazılım tasarım modelinin bir anti-desen olarak kabul edilip edilmeyeceği konusunda bazı eleştiriler vardır, çünkü birçoğu da bunun faydalarını görür, örneğin:
  • Mantık ve veri arasındaki açık ayrım.
  • Basit uygulamalar için iyi çalışır.
  • Ölçeklendirmeyi kolaylaştıran state'siz mantık ile sonuçlanır.
  • Karmaşık bir OO-Veritabanı mapping katmanı ihtiyacını ortadan kaldırır.
  • Belirli bir kurucu veya sıralı property'ler yerine aptal özellikler bekleyen mapping ve injection frameworkleri ile daha fazla uyumluluk.
Anemik Modelin anti-pattern olmasının sebepleri

  • Mantık gerçekten nesne yönelimli bir şekilde uygulanamaz.
  • Kapsülleme ve bilgi gizleme ilkelerinin ihlali.
  • Bir domain modelinde mantığı içermek için ayrı bir iş katmanına ihtiyaç duyar. Ayrıca, domain modelinin nesnelerinin herhangi bir anda doğruluğunu garanti edemeyeceği anlamına gelir, çünkü doğrulama ve mutasyon mantığı dışarıda bir yere yerleştirilir (büyük olasılıkla birden fazla yerde).
  • Bir nesne modelinin farklı tüketicileri arasında domain mantığını paylaşırken bir hizmet katmanı gerekir.
  • Bir modeli daha az anlamlı hale getirir.
ÖRNEK KODLAR
Anemik:
class Box
{
    public int Height { get; set; }
    public int Width { get; set; }
}
Anemik olmayan:
class Box
{
    public int Height { get; private set; }
    public int Width { get; private set; }

    public Box(int height, int width)
    {
        if (height <= 0) {
            throw new ArgumentOutOfRangeException(nameof(height));
        }
        if (width <= 0) {
            throw new ArgumentOutOfRangeException(nameof(width));
        }
        Height = height;
        Width = width;
    }

    public int Area()
    {
       return Height * Width;
    }
}


Bonus : 



(GRASP) Genel Sorumluluk Atama Yazılım Kalıpları (veya Prensipleri), Wikipedia Çevirisi

Merhaba, bu yazımda sizlere bilgisayar bilimcisi Craig Larman tarafından Applying UML and Patterns kitabında yayınladığı GRASP prensiplerinin Wikipedia'da yayınlanan halinin Türkçe çevirisini yapacağım. Umarım faydalı bir yazı olur.



GRASP(General Responsibility Assignment Software Patterns (or Principles))

Genel Sorumluluk Atama Yazılım Kalıpları (veya Prensipleri), kısaltılmış haliyle GRSAP, nesneye yönelik tasarımdaki sınıflara ve nesnelere sorumluluk atamak için kurallardan oluşur. SOLID tasarım prensibi ile ilgili değildir.

GRASP'ta kullanılan farklı pattern ve prensipler controller, creator, indirection, information expert, high cohesion, low coupling, polymorphism, protected variations, ve pure fabrication'dır. Tüm bu modeller bazı yazılım problemlerini cevaplar ve bu problemler hemen hemen her yazılım geliştirme projesinde ortaktır. Bu teknikler yeni çalışma yolları yaratmak için değil, nesne yönelimli tasarımda eski, denenmiş ve test edilmiş programlama ilkelerini daha iyi belgelemek ve standartlaştırmak için icat edilmiştir.

Bilgisayar bilimcisi Craig Larman, "yazılım geliştirme için kritik tasarım aracının tasarım ilkeleri konusunda iyi eğitimli bir zihin olduğunu, UML veya başka bir teknoloji olmadığını" belirtiyor. Böylece, GRASP gerçekten zihinsel bir araç seti, nesne yönelimli yazılımın tasarımında yardımcı olacak bir öğrenme yardımcısıdır.

PATTERNS
(Yazılım Desenleri)

OO tasarımında, yazılım deseni, yeni bağlamlarda uygulanabilecek bir sorunun ve çözümün adlandırılmış bir tanımıdır; ideal olarak bir yazılım deseni, çözümünün değişen koşullarda nasıl uygulanacağı konusunda tavsiyede bulunur ve engelleri ve değişimleri dikkate alır. Belirli bir sorun kategorisi verilen birçok desen, nesnelere sorumlulukların atanmasına rehberlik eder.

Controller
(Denetleyici)

Controller pattern, genel sistemi veya bir kullanım senaryosunu temsil eden UI olmayan bir sınıfa sistem olaylarıyla ilgilenme sorumluluğunu atar. Controller nesnesi, bir sistem olayını almak veya işlemekten sorumlu olan kullanıcısız bir arabirim nesnesidir.

Bir kullanım senaryosunun tüm sistem olaylarıyla ilgilenmek için bir veya birden fazla kullanım senaryosu controller'ı kullanılmalıdır. Örneğin, Kullanıcı Yarat ve Kullanıcıyı Sil gibi durumlarda, iki ayrı kullanım senaryosu denetleyicisi yerine UserController adlı tek bir sınıf bulunabilir.

Controller, bir UI katmanının gerisinde bir sistem olayını alan ve koordine eden ("kontrol eden") ilk nesne olarak tanımlanır. Controller, yapılması gereken işi başka nesnelere devretmelidir; etkinliği koordine eder veya kontrol eder. Çok fazla iş yapmamalıdır.  GRASP Controller'ın , bir bilgi sistemi mantıksal mimarisinde ortak katmanları olan nesne yönelimli bir sistemdeki uygulama / hizmet katmanının (uygulamanın uygulama / hizmet katmanı ve etki alanı katmanı arasında açık bir ayrım yaptığını varsayarak) bir parçası olduğu düşünülebilir.


Creator
(Yaratıcı)

Ayrıca bakınız: Factory Pattern

Nesnelerin oluşturulması, nesne yönelimli bir sistemdeki en yaygın etkinliklerden biridir. Hangi sınıfın nesnelerin yaratılmasından sorumlu olduğu, belirli sınıfların nesneleri arasındaki ilişkinin temel bir özelliğidir.

Genel olarak, B sınıfı aşağıdakilerden biri veya tercihen daha fazlası geçerli olduğunda A sınıfı örnekleri oluşturmaktan sorumlu olmalıdır:

-B'nin nesneleri , A nesnelerini içerir ya da bir araya getirir.
-B'nin nesneleri, A'nın nesnelerini kaydeder
-B'nin nesneleri, A nesnelerini yakından kullanır.
-B nesneleri, A nesneleri için başlangıç bilgisine sahiptir ve onu yaratıma aktarır.


Indirection
(Dolaylı)

Indirection pattern, ara elemanlara arabuluculuk sorumluluğunu atayarak iki eleman arasındaki düşük bağlantı ve yeniden kullanım potansiyelini destekler. Buna bir örnek MVC pattern'ında veriler(model) ile verilerin gösterimin sağlayan(view) arasında aracılık yapan controllerlar'dır. Bu, aralarındaki bağların düşük kalmasını sağlar.


Information Expert
(Bilgi Uzmanı)

Ayrıca bakınız: Information Hiding

Information Expert (aynı zamanda uzman ya da uzman ilkesi), metodlar, hesaplanan field'lar vb. Gibi sorumlulukların nereye delege edileceğini belirlemek için kullanılan bir ilkedir.

Information Expert prensibini kullanarak, sorumlulukların atanmasına yönelik genel bir yaklaşım, verilen bir sorumluluğa bakmak, yerine getirmek için gerekli bilgileri belirlemek ve daha sonra bu bilgilerin nerede saklandığını belirlemektir.

Bu, sorumluluğu yerine getirmek için gereken en fazla bilgi ile ilgili sınıfa yerleştirmeyi sağlayacaktır.

High Cohession
(Yüksek Uyum)

Yüksek uyum, nesneleri uygun şekilde odaklanmış, yönetilebilir ve anlaşılır tutmaya çalışan bir değerlendirme örüntüsüdür. Yüksek kohezyon genellikle düşük bağlantıyı desteklemek için kullanılır. Yüksek uyum, belirli bir öğenin sorumluluklarının güçlü bir şekilde ilişkili ve yüksek derecede odaklanmış olduğu anlamına gelir. Programları sınıflara ve alt sistemlere bölmek, sistemin tutarlı özelliklerini artıran etkinliklere bir örnektir. Alternatif olarak, düşük uyum, belirli bir elementin çok fazla ilgisiz sorumluluğa sahip olduğu bir durumdur. Düşük kohezyona sahip elementler genellikle kavranması, tekrar kullanılması, bakımı ve değiştirilmesi zor olmaktan muzdariptir.

Low coupling

(Düşük Bağlantı)

Coupling, bir elemanın diğer elemanlara ne kadar güçlü bir şekilde bağlandığını, bilgisine sahip olduğunu veya bunlara dayandığını gösterir. Düşük bağlantı, aşağıdaki faydalar için sorumlulukların nasıl atanacağını belirleyen bir değerlendirme modelidir:

sınıflar arasında daha düşük bağımlılık,
bir sınıfta diğer sınıflar üzerinde daha az etkisi olan değişim,
daha yüksek yeniden kullanım potansiyeli.

Polymorphism
Ayrıca Bakınız : Nesneye yönelik programlamada polimorfizm

Polimorfizm ilkesine göre, davranışa göre davranış çeşitliliğini tanımlama sorumluluğu bu değişimin gerçekleştiği alt türe verilir. Bu, polimorfik işlemler kullanılarak elde edilir. Türün kullanıcısı, türe dayalı dallanmalar yerine polimorfik işlemleri kullanmalıdır.

Protected variations
(Korumalı Varyasyonlar)

Korumalı varyasyon modeli, kararsızlığın odağını bir arayüzle sararak ve bu arayüzün çeşitli uygulamalarını oluşturmak için polimorfizm kullanarak elemanları diğer elemanlardaki (nesneler, sistemler, alt sistemler) varyasyonlardan korur.

Pure Fabrication

Bir pure fabrication sınıfı, özellikle düşük bağlantı, yüksek uyum ve türetilmiş yeniden kullanım potansiyeline (bilgi uzmanı modeli tarafından sunulan bir çözüm sunmadığında) ulaşmak için özel olarak oluşturulmuş, problem alanında bir kavramı temsil etmeyen bir sınıftır. Bu tür sınıfa, domain driven design'da  "service" adı verilir.




SOLID Prensipleri -The Open-Closed Principle- Uncle Bob Çevirisi

Merhaba, bu seride sizlere Uncele Bob'un (Robert C. Martin) SOILD presnipleri için yazdığı makaleleri Türkçe'ye çevireceğim. Umarım yararlı bir yazı dizisi olur.

Seri'nin diğer yazıları :

The Single Responsibility Principle




2- The Open-Closed Principle


Ivar Jacobson'un dediği gibi: “Tüm sistemler yaşam döngüleri boyunca değişir. İlk sürümden daha uzun sürmesi beklenen sistemler geliştirilirken bu akılda tutulmalıdır. ” Değişim karşısında, kararlı ve ilk sürümden daha uzun sürecek tasarımlar nasıl oluşturabiliriz? Bertrand Meyer, ünlü open-closed prensibini icat ettiği 1988'den beri bize rehberlik etti. Onu yeniden ifade etmek için:

SOFTWARE ENTITIES (CLASSES, MODULES, FUNCTIONS, ETC.) SHOULD BE OPEN FOR EXTENSION, BUT CLOSED FOR MODIFICATION.
(Yazılım varlıkları (SINIFLAR, MODÜLLER, FONKSİYONLAR, VB.) gelişime açık, ancak değişikliğe kapalı olmalıdır.) 

Bir programda yapılan tek bir değişiklik, bağımlı modüllerde kademeli bir değişiklikle sonuçlandığında, bu program “kötü” tasarımla ilişkilendirdiğimiz istenmeyen nitelikleri sergiler. Program kırılgan, katı, öngörülemez ve kullanılamaz hale gelir. Açık kapalı prensibi bunu çok basit bir şekilde engeller. Asla değişmeyen modüller tasarlamanız gerektiğini söylüyen bu prensip, gereksinimler değiştiğinde, bu tür modüllerin davranışlarını, zaten çalışan eski kodu değiştirerek değil, yeni kod ekleyerek genişletmeyi önerir.

TANIM

Açık-kapalı prensibine uyan modüllerin iki temel özelliği vardır.

1.  “Genişlemeye Açık” dır.
Bu, modülün davranışının genişletilebileceği anlamına gelir. Uygulamanın gereksinimleri değiştikçe veya yeni uygulamaların gereksinimlerini karşılamak için modülün yeni ve farklı şekillerde davranmasını sağlayabiliriz.

2. “Değişikliğe Kapalıdır”.
Böyle bir modülün kaynak kodu dokunulmazdır. Hiç kimsenin kaynak kodda değişiklik yapmasına izin verilmez.

Bu iki özelliğin birbiriyle çeliştiği anlaşılıyor. Bir modülün davranışını genişletmenin normal yolu, o modülde değişiklik yapmaktır. Değiştirilemeyen bir modülün normalde sabit bir davranışı olduğu düşünülmektedir. Bu iki karşıt özellik nasıl çözülebilir?


Abstraction is the Key
(Soyutlama burada kilit anahtardır)

C ++ 'da, nesne yönelimli tasarım ilkelerini kullanarak, sabit ve sınırsız olası davranış grubunu temsil eden soyutlamalar oluşturmak mümkündür. Soyutlamalar soyut temel sınıflardır ve sınırsız olası davranış grubu tüm olası alt sınıfları tarafından temsil edilir. Bir modülün bir soyutlamayı manipüle etmesi mümkündür. Böyle bir modül, düzeltilmiş bir soyutlamaya bağlı olduğu için modifikasyon için kapatılabilir. Yine de bu modülün davranışı, soyutlamanın yeni alt sınıfları yaratılarak genişletilebilir.

Şekil 1, açık-kapalı prensibine uymayan basit bir tasarımı göstermektedir. Hem Client hem de Server sınıfları somuttur. Server sınıfının üye metodlarının abstract olduğuna dair bir garanti yoktur. Client sınıfı Server sınıfını kullanır. Bir Client nesnesinin farklı bir sunucu nesnesi kullanmasını istiyorsak, yeni sunucu sınıfını adlandırmak için Client sınıfının değiştirilmesi gerekir.


Şekil 2, açık-kapalı prensibine uyan ilgili tasarımı göstermektedir. Bu durumda, AbstractServer sınıfı saf abstract üye metodlarına sahip soyut bir sınıftır. Client sınıfı bu soyutlamayı kullanır. Ancak Client sınıfının nesneleri, alt sınıf Server sınıfının nesnelerini kullanacaktır. Client nesnelerinin farklı bir sunucu sınıfı kullanmasını istiyorsak, AbstractServer sınıfının yeni bir alt sınıfı oluşturulabilir. Client sınıfı değişmeden kalabilir.



The Shape Abstraction
(Şekil Soyutlaması)


Aşağıdaki örneği ele alalım. Standart bir GUI üzerine daireler ve kareler çizebilmemiz gereken bir uygulamamız var. Daireler ve kareler belirli bir sırada çizilmelidir. Dairelerin ve karelerin bir listesi uygun sırayla oluşturulacaktır ve program listeyi bu sırayla yürütmeli ve her daireyi veya kareyi çizmelidir.

C'de, açık-kapalı prensibine uymayan prosedürel teknikler kullanarak, bu problemi aşağıdai kodda gösterildiği gibi çözebiliriz. Burada aynı ilk öğeye sahip olan ancak bunun ötesinde farklı bir veri yapıları kümesi görüyoruz. Her birinin ilk öğesi, veri yapısını daire veya kare olarak tanımlayan bir tür kodudur. DrawAllShapes metodu, bu veri yapılarına bir dizi işaretçi yürütür, tür kodunu inceler ve sonra uygun fonksiyonu çağırır (DrawCircle veya DrawSquare).
Procedural Solution to the Square/Circle Problem
enum ShapeType {circle, square};
struct Shape
{
 ShapeType itsType;
};
struct Circle
{
 ShapeType itsType;
 double itsRadius;
 Point itsCenter;
};
struct Square
{
 ShapeType itsType;
 double itsSide;
 Point itsTopLeft;
};
//
// These functions are implemented elsewhere
//
void DrawSquare(struct Square*)
void DrawCircle(struct Circle*);
typedef struct Shape *ShapePointer;
void DrawAllShapes(ShapePointer list[], int n)
{
 int i;
 for (i=0; iitsType)
 {
 case square:
 DrawSquare((struct Square*)s);
 break;
 case circle:
 DrawCircle((struct Circle*)s);
 break;
 }
 }
}

DrawAllShapes metodu, yeni şekil türlerine karşı kapatılamadığından açık-kapalı prensibine uymaz. Üçgenleri içeren şekiller listesi çizebilmek için bu fonksiyonu genişletmek isteseydim, fonksiyonu değiştirmek zorunda kalırdım. Aslında, çizmem gereken herhangi bir yeni şekil için fonksiyonu değiştirmem gerekirdi.

Elbette bu program sadece basit bir örnektir. Gerçek hayatta DrawAllShapes fonksiyonundaki switch deyimi, uygulamanın her yerindeki çeşitli fonksiyonlarda tekrar tekrar tekrarlanır; her biri biraz farklı bir şey yapar. Böyle bir uygulamaya yeni bir şekil eklemek, bu tür anahtar ifadelerin (veya if / else zincirlerinin) bulunduğu her yer için her birine yeni şekil ekleme anlamına gelir. Ayrıca, tüm switch deyimlerinin ve if / else zincirlerinin DrawAllShapes'deki kadar güzel yapılandırılmış olması pek olası değildir.

İf ifadelerinin tahminlerinin mantıksal işleçlerle birleştirilecek ya da
switch statementlarının case'leri yerel karar almayı “basitleştirmek” için birleştirilecektir. Bu nedenle, yeni şeklin eklenmesi gereken tüm yerleri bulma ve anlama problemi önemsiz olabilir.

Aşağıdaki kod bloğu, açık-kapalı prensibine uyan kare / daire problemine bir çözüm kodunu gösterir. Bu durumda soyut bir Shape sınıfı oluşturulur. Bu soyut sınıf, Draw adında tek bir saf sanal metoda sahiptir. Daire ve Kare, Shape sınıfının alt sınıflarıdır.


Square/Circle problemine OOD Çözümü.
class Shape
{
 public:
 virtual void Draw() const = 0;
};
class Square : public Shape
{
 public:
 virtual void Draw() const;
};
class Circle : public Shape
{
 public:
 virtual void Draw() const;
};
void DrawAllShapes(Set& list)
{
 for (Iteratori(list); i; i++)
 (*i)->Draw();
}

Yeni bir şekil çizmek için yukarıdaki kodda DrawAllShapes metodunun davranışını genişletmek istiyorsak, tek yapmamız gereken Shape sınıfının yeni bir alt sınıfı eklemektir. DrawAllShapes metodunun değiştirilmesi gerekmez. Böylece DrawAllShapes açık kapalı prensibine uygundur. Davranışı değiştirilmeden genişletilebilir.

Gerçek dünyada Shape sınıfının çok daha fazla methodu olacaktır. Yine de uygulamaya yeni bir şekil eklemek hala oldukça basittir, çünkü gerekli olan tek şey yeni alt sınıfı oluşturmak ve tüm metodlarını uygulamaktır. Değişiklik gerektiren yerleri arayan tüm uygulamada arayarak avlanmaya gerek yoktur.

Açık-kapalı prensibine uyan programlar, mevcut kodu değiştirmek yerine yeni kod ekleyerek değiştirildiğinden, bu presibe uymayan programlar tarafından sergilenen değişikliklerin sıkıntısını yaşamazlar.

STRATEJİK KAPATMA

Hiçbir programın% 100 kapatılamayacağı açık olmalıdır. Örneğin, tüm Circle'ların herhangi bir Square'den önce çizilmesi gerektiğine karar verirsek, yukarıdaki koddaki DrawAllShapes metoduna ne olacağını düşünün. DrawAllShapes metodu böyle bir değişikliğe karşı kapalı değildir. Genel olarak, bir modül ne kadar “kapalı” olursa olsun, her zaman kapalı olmadığı bir tür değişiklik olacaktır.

Kapanış tamamlanamayacağı için stratejik olmalı. Yani, tasarımcı tasarımını kapatmak için hangi tür değişiklikleri yapması gerektiğini seçmelidir. Bu, deneyimden elde edilen belli bir miktar önseziyi gerektirir. Deneyimli tasarımcı, kullanıcıları ve sektörü farklı türdeki değişikliklerin olasılığını değerlendirecek kadar iyi tanır. Daha sonra açık-kapalı prensibinin en olası değişiklikler için çağrıldığından emin olur.

Using Abstraction to Gain Explicit Closure.
(Açık Kapatma Kazanmak için Soyutlama Kullanma.)

Çizim sırasındaki değişikliklere karşı DrawAllShapes metodunu nasıl kapatabiliriz? Kapamanın soyutlamaya dayandığını unutmayın. Bu nedenle, DrawAllShapes'i sıralamaya karşı kapatmak için bir çeşit “sıralama soyutlaması” na ihtiyacımız var. Yukarıdaki özel sıralama durumu, diğer şekil türlerinden önce belirli şekil türlerinin çizilmesiyle ilgilidir.

Bir sıralama politikası, herhangi iki nesne göz önüne alındığında, hangisinin önce çizilmesi gerektiğini keşfetmenin mümkün olduğunu gösterir. Böylece, başka bir Şekli bağımsız değişken olarak alan ve bir bool sonucu döndüren Precedes adlı bir Shape yöntemi tanımlayabiliriz. İletiyi alan Shape nesnesinin, Shape nesnesi bağımsız değişken olarak geçmeden önce sıralanması gerekmesi sonucunu doğrudur.

C ++ 'da bu metod aşırı yüklenmiş bir operatör <fonksiyonu ile temsil edilebilir.
Liste 3, yerinde sıralama yöntemleriyle Shape sınıfının nasıl görünebileceğini göstermektedir. Artık iki Shape nesnesinin göreceli sırasını belirlemenin bir yoluna sahip olduğumuza göre, bunları sıralayabilir ve sonra sırayla çizebiliriz. Liste 4, bunu yapan C ++ kodunu gösterir.

Bu bize Shape nesnelerini sıralamak ve bunları uygun sırada çizmek için bir araç sağlar. Ama hala iyi bir sıralama soyutlamamız yok. Haliyle, her bir Shape nesnesinin sıralamasını belirtmek için Precedes yöntemini geçersiz kılması gerekecektir. Bu nasıl olurdu? Karelerin Liste 5'ten önce çizildiğinden emin olmak için, Circle :: Preedes'de ne tür bir kod yazardık?


Listing 3
Shape with ordering methods.
class Shape
{
 public:
 virtual void Draw() const = 0;
 virtual bool Precedes(const Shape&) const = 0;
 bool operator<(const Shape& s) {return Precedes(s);}
};
Listing 4
DrawAllShapes with Ordering
void DrawAllShapes(Set& list)
{
 // copy elements into OrderedSet and then sort.
 OrderedSet orderedList = list;
 orderedList.Sort();
 for (Iterator i(orderedList); i; i++)
 (*i)->Draw();
}

}
Listing 5
Ordering a Circle
bool Circle::Precedes(const Shape& s) const
{
 if (dynamic_cast(s))
 return true;
 else
 return false;
}
<
Bu metodun açık-kapalı prensibine uymadığı çok açık olmalıdır. Yeni Shape alt sınıflarına karşı kapatmanın bir yolu yoktur. Her yeni bir Shape alt sınıfı oluşturulduğunda, bu metodun değiştirilmesi gerekir.


Using a “Data Driven” Approach to Achieve Closure.
(Kapanışı Gerçekleştirmek için “Veriye Dayalı” Bir Yaklaşım Kullanmak.)

Shape alt sınıflarının kapatılması, türetilmiş her sınıftaki değişiklikleri zorlamayan tablo güdümlü bir yaklaşım kullanılarak elde edilebilir. Liste 6'da bir olasılık gösterilmektedir.
Bu yaklaşımı kullanarak, genel olarak sıralama sorunlarına karşı DrawAllShapes metodunu ve yeni Shape alt sınıflarının oluşturulmasına veya Shape nesnelerini türlerine göre yeniden sıralayan politikadaki bir değişikliğe karşı Shape alt sınıflarının her birini başarıyla kapattık. (örneğin, önce Kareler çizilecek şekilde sıralamayı değiştirme.)

Listing 6
Tabloya dayalı sıralama mekanizması
#include 
#include 
enum {false, true};
typedef int bool;
class Shape
{
 public:
 virtual void Draw() const = 0;
 virtual bool Precedes(const Shape&) const;
 bool operator<(const Shape& s) const
 {return Precedes(s);}
 private:
 static char* typeOrderTable[];
};
char* Shape::typeOrderTable[] =
{
 “Circle”,
 “Square”,
 0
};
// Bu fonksiyon sınıf isimlerine göre tabloda arama yapar
// Tablo hangi şeklin çizileceğikonusunda sıralama bilgisi verir
// Bulunamayan şekiller
// her zaman bulunan şekillerden önce gelir.

bool Shape::Precedes(const Shape& s) const
{
 const char* thisType = typeid(*this).name();
 const char* argType = typeid(s).name();
 bool done = false;
 int thisOrd = -1;
 int argOrd = -1;
 for (int i=0; !done; i++)
 {
 const char* tableEntry = typeOrderTable[i];
 if (tableEntry != 0)
 {
 if (strcmp(tableEntry, thisType) == 0)
 thisOrd = i;
 if (strcmp(tableEntry, argType) == 0)
 argOrd = i;
 if ((argOrd > 0) && (thisOrd > 0))
 done = true;
 }
 else // table entry == 0
 done = true;
 }
 return thisOrd < argOrd;
}

Çeşitli Şekillerin sırasına karşı kapalı olmayan tek öğe tablonun kendisidir. Ve bu tablo, diğer modüllerden ayrı olarak kendi modülüne yerleştirilebilir, böylece tabloda yapılan değişiklikler diğer modüllerin hiçbirini etkilemez.


Heuristics and Conventions
(Buluşsal Yöntemler ve Sözleşmeler)

Bu makalenin başında belirtildiği gibi, açık-kapalı prensibi, yıllar boyunca OOD ile ilgili olarak yayınlanan birçok buluşsal yöntem ve sözleşmenin arkasındaki temel motivasyondur. İşte bunların en önemlilerinden bazıları.

Make all Member Variables Private.
(Tüm Üye Değişkenlerini Private Yap.)

Bu, OOD'nin tüm sözleşmelerinde en yaygın olarak tutulanlardan biridir. Sınıfların üye değişkenleri, yalnızca sınıfın tanımlayan yöntemleri tarafından bilinmelidir. Üye değişkenler, türetilmiş sınıflar da dahil olmak üzere hiçbir zaman başka bir sınıf tarafından bilinmemelidir. Bu nedenle, public ya da protected yerine private ilan edilmelidir. Açık kapalı prensibi ışığında, bu sözleşmenin nedeni açık olmalıdır. Bir sınıfın üye değişkenleri değiştiğinde, bu değişkenlere bağlı her metod değiştirilmelidir. Böylece, bir değişkene bağlı hiçbir fonksiyon, bu değişkene göre kapatılamaz.

OOD'de, bir sınıfın metodlarının, o sınıfın üye değişkenlerindeki değişikliklere kapalı olmamasını bekliyoruz. Ancak, alt sınıflar da dahil olmak üzere diğer sınıfların bu değişkenlerdeki değişikliklere karşı kapalı olmasını bekliyoruz. Bu beklenti için bir ismimiz var, buna encapsulation (kapsülleme) diyoruz. Peki ya asla değişmeyeceğini bildiğiniz bir üye değişkeniniz olsaydı? Özel yapmak için herhangi bir neden var mı? Örneğin, Liste 7'de bool durum değişkeni olan bir sınıf Device gösterilmektedir. Bu değişken son işlemin durumunu içerir. Bu işlem başarılı olursa, durum true, aksi halde false olur.

Listing 7
non-const public variable
class Device
{
 public:
 bool status;
};

Bu değişkenin türü veya anlamının asla değişmeyeceğini biliyoruz. Öyleyse neden herkese açık hale getirmiyor ve istemci kodunun içeriğini incelemesine izin vermiyoruz? Bu değişken gerçekten hiç değişmezse ve diğer tüm istemciler kurallara uyarsa ve yalnızca durumun içeriğini sorgularsa, değişkenin herkese açık olması hiçbir zarar vermez. Bununla birlikte, bir istemci bile status değişkenin yazılabilir doğasından yararlanırsa ve değerini değiştirirse ne olacağını düşünün. Aniden, bu bir istemci diğer tüm istemcileri etkileyebilir. Bu, herhangi bir Device istemcisini bu hatalı davranış modülündeki değişikliklere karşı kapatmanın imkansız olduğu anlamına gelir. Bu muhtemelen alınamayacak kadar büyük bir risktir.


Öte yandan, Liste 8'de gösterildiği gibi Time sınıfına sahip olduğumuzu varsayalım. Bu sınıftaki public üye değişkenlerin verdiği zarar nedir? Elbette değişmeleri pek olası değildir. Ayrıca, istemci modüllerinden herhangi birinin değişkenlerde değişiklik yapması önemli değildir, değişkenlerin istemciler tarafından değiştirilmesi gerekir. Ayrıca, türetilmiş bir sınıfın belirli bir üye değişkenin ayarını değiştirmek istemesi pek olası değildir. Peki bu durumun herhangi bir zarar var mı?


Listing 8
class Time
{
 public:
 int hours, minutes, seconds;
 Time& operator-=(int seconds);
 Time& operator+=(int seconds);
 bool operator< (const Time&);
 bool operator> (const Time&);
 bool operator==(const Time&);
 bool operator!=(const Time&);
};

Liste 8 ile ilgili yapabileceğim bir şikayet, zamanın değiştirilmesinin atomik olmamasıdır. Yani, istemci hours değişkenini hours değişkenini değiştirmeden değiştirebilir. Bu bir Time nesnesi için tutarsız değerlere neden olabilir. Üç argüman alıp zamanı atomik olarak ayarlayan tek bir fonksiyonu tercih ederdim. Fakat bu çok zayıf bir argüman.


Bu değişkenlerin public doğasının bazı sorunlara neden olduğu diğer koşulları düşünmek zor olmayacaktır. Bununla birlikte, uzun vadede, bu değişkenleri private yapmak için baskın bir neden yoktur. Hala public hale getirmenin kötü bir tarzı olduğunu düşünüyorum, ancak muhtemelen kötü bir tasarım değil. Kötü tarz olarak görüyorum çünkü uygun satır içi üye moetdları oluşturmak çok ucuz ve ucuz maliyet kesinlikle kapama sorunlarının ortaya çıkması riskine karşı korumaya değer.

Bu nedenle, açık-kapalı prensibinin ihlal edilmediği nadir durumlarda, public ve protected değişkenlerin yasaklanması, sağlamlılıktan ziyade stile bağlıdır.

No Global Variables -- Ever.
(Hiçbir zaman için Global Değişken kullanılmamalı)

Global değişkenlere karşı argüman, public değişkenlere karşı olan argümana benzer. Global değişkene bağlı hiçbir modül, bu değişkene yazabilecek diğer modüllere karşı kapatılamaz. Değişkeni diğer modüllerin beklemediği şekilde kullanan herhangi bir modül, diğer modülleri kıracaktır. Birçok modülün kötü davranmış bir modülün kaprisine maruz kalması çok risklidir.

Diğer taraftan, global bir değişkenin çok az bağımlısı olduğu veya tutarsız bir şekilde kullanılamadığı durumlarda, diğer moduller çok az zarar verirler. Tasarımcı, bir global için ne kadar kapatmanın feda edildiğini değerlendirmeli ve global değişken tarafından sunulan kolaylığın maliyete değip değmediğini belirlemelidir.

Yine, ortaya çıkan stil sorunları var. Globalleri kullanmanın alternatifleri genellikle çok ucuzdur. Bu gibi durumlarda, böyle bir risk taşımayan yöntemlere karşı çok az miktarda kapanma riski taşıyan bir teknik kullanmak kötü bir stildir. Bununla birlikte, global bir rahatlığın önemli olduğu durumlar vardır. Global değişkenler için cout ve cin yaygın örneklerdir. Bu gibi durumlarda, açık-kapalı prensibi ihlal edilmezse, kolaylık stil ihlaline değebilir.


RTTI is Dangerous.
(RTTI tehlikelidir)

Bir diğer çok yaygın yasak, dynamic_cast'e karşıdır. Genellikle dynamic_cast veya herhangi bir run time türü tanımlamasının (RTTI) kendiliğinden tehlikeli olduğu ve bundan kaçınılması gerektiği iddia edilir. Genellikle atıfta bulunulan durum, açık-kapalı prensibini açıkça ihlal eden Liste 9'a benzer. Ancak Liste 10, dynamic_cast kullanan ancak açık-kapalı ilkesini ihlal etmeyen benzer bir programı gösterir.

Bu ikisi arasındaki fark, yeni bir Shape türü elde edildiğinde  Liste 9'un değiştirilmesi gerektiğidir. (Sadece düpedüz saçma olduğunu söylememe gerek yok). Ancak, yeni bir Shape alt sınıfı oluşturulduğunda Liste 10'da hiçbir şey değişmez. Bu nedenle, Liste 10 açık-kapalı prensibini ihlal etmez. Genel bir kural olarak, RTTI kullanımı açık-kapalı prensibini ihlal etmiyorsa, güvenlidir.

Listing 9
RTTI violating the open-closed principle.
class Shape {};
class Square : public Shape
{
 private:
 Point itsTopLeft;
 double itsSide;
 friend DrawSquare(Square*);
};
class Circle : public Shape
{
 private:
 Point itsCenter;
 double itsRadius;
 friend DrawCircle(Circle*);
};
void DrawAllShapes(Set& ss)
{
 for (Iteratori(ss); i; i++)
 {
 Circle* c = dynamic_cast(*i);
 Square* s = dynamic_cast(*i);
 if (c)
 DrawCircle(c);
 else if (s)
 DrawSquare(s);
 }
}
Listing 10
RTTI that does not violate the open-closed Principle.
class Shape
{
 public:
 virtual void Draw() cont = 0;
};
class Square : public Shape
{
 // as expected.
};
void DrawSquaresOnly(Set& ss)
{
 for (Iteratori(ss); i; i++)
 {
 Square* s = dynamic_cast(*i);
 if (s)
 s->Draw();
 }
}

SONUÇ

Açık-kapalı prensibi hakkında söylenebilecek çok şey var. Birçok yönden bu ilke nesne yönelimli tasarımın merkezindedir. Bu prensibe uygunluk, nesne yönelimli teknoloji için talep edilen en büyük faydaları mevcuttur; ör: tekrar kullanılabilirlik ve sürdürülebilirlik. Ancak bu ilkeye uyum basitçe nesne yönelimli bir programlama dili kullanılarak elde edilemez. Aksine, tasarımcının, değişikliğe tabi olacağını düşündüğü programlara soyutlama uygulaması için bir özveri gerekmektedir.








Bonus :




SOLID Prensipleri -The Single Responsibility Principle- Uncle Bob Çevirisi

Merhaba, bu seride sizlere Uncele Bob'un (Robert C. Martin) SOILD presnipleri için yazdığı makaleleri Türkçe'ye çevireceğim. Umarım yararlı bir yazı dizisi olur.

Seri'nin diğer yazıları :

Open Closed Principle

1 - SRP: The Single Responsibility Principle




"None but Buddha himself must take the responsibility of giving out occult secrets..." — E. Cobham Brewer 1810–1897. Dictionary of Phrase and Fable. 1898.

"Buda'nın kendisi dışında hiçbiri gizli sırlar verme sorumluluğunu almamalıdır ..."

Bu ilke Tom DeMarco ve Meilir Page-Jones çalışmalarında tanımlanmıştır. Bu çalışmalarada bu prensibe cohession denmiştir. Cohession ise bir modülün ilişkili elemanları arasında fonksiyonel ilişkinliği ifade etmektedir.

Bu bölümde, bu anlamı biraz değiştireceğiz ve uyumu bir modülün veya bir sınıfın değişmesine neden olan sebeplerle ilişkilendireceğiz.

A CLASS SHOULD HAVE ONLY ONE REASON TO CHANGE.
(Bir sınıf sadece bir sebepten dolayı değişmelidir.)

Bir Game sınıfının olduğunu ve bu sınıfın mevcut kareyi takip etmek ve skoru hesaplamak gibi iki ayrı sorumluluğu olduğunu düşünelim. Dikkat ederseniz sınıfın birbirinden farklı iki sorumluluğu mevcut. Bu durumada, bu iki sorumluluk iki sınıfa ayrılmalıdır. Game, kareleri takip etme sorumluluğunu sürdürürken, yeni oluşturulan Scorer sınıfı skoru hesaplama sorumluluğunu alır.

Bu iki sorumluluğu iki ayrı sınıfa ayırma nedenimiz nedir ? Çünkü her sorumluluk bir değişim eksenidir. Gereksinimler değiştiğinde, bu değişiklik sınıflar arasında sorumluluk değişikliği ile ortaya çıkacaktır. Bir sınıf birden fazla sorumluluk üstlenirse, değişmesi için birden fazla sebep olacaktır.

Bir sınıfın birden fazla sorumluluğu varsa, sorumluluklar coupled(çift hale gelmiş) olmuş demektir.
Bir sorumlulukta yapılan değişiklikler, sınıfın diğer sorumluklarını kullanma yeteneğini bozabilir veya engelleyebilir. Sorumluluklar arasında bu tür bağlantı, değişiklikte beklenmedik şekillerde kırılgan tasarımlara yol açar.

Örneğin, aşağıdaki tasarımı düşünün. Rectangle sınıfının gösterilen iki yöntemi vardır. Biri dikdörtgeni ekrana çizer, diğeri dikdörtgenin alanını hesaplar.


İki farklı uygulama Rectangle sınıfını kullanır. Bir uygulama hesaplamalı geometri yapar. Geometrik şekillerin matematiğinde yardımcı olması için Rectangle kullanır.Asla dikdörtgeni ekrana çizmez.Diğer uygulama doğası gereği grafikseldir. Ayrıca bazı hesaplama geometrisi yapabilir, ancak kesinlikle dikdörtgeni ekrana çizer.

Bu tasarım SRP'yi ihlal etemektedir. Rectangle sınıfının iki sorumluluğu vardır. İlk sorumluluk, bir dikdörtgenin geometrisinin matematiksel bir modelini sağlamaktır. İkinci sorumluluk, dikdörtgeni grafik kullanıcı arayüzünde oluşturmaktır.

SRP'nin ihlali birkaç kötü soruna neden olur. İlk olarak, GUI'yi hesaplamalı geometri uygulamasına dahil etmeliyiz. .NET'te GUI bağımlılığının derlemesinin hesaplamalı geometri uygulamasıyla oluşturulması ve dağıtılması gerekir.

İkinci olarak, GraphicalApplication üzerinde yapılan bir değişiklik Rectangle'ın bir nedenle değişmesine neden olursa, bu değişiklik bizi ComputationalGeometryApplication uygulamasını yeniden oluşturmaya, yeniden test etmeye ve yeniden konuşlandırmaya zorlayabilir. Bunu yapmayı unutursak, bu uygulama öngörülemeyen şekillerde bozulabilir.

Daha iyi bir tasarım, iki sorumluluğu aşağıda gösterildiği gibi tamamen farklı iki sınıfa ayırmaktır. Bu tasarım, Dikdörtgenin hesaplama bölümlerini GeometricRectangle sınıfına taşır. Şimdi dikdörtgenlerin oluşturulma biçiminde yapılan değişiklikler ComputationalGeometryApplication'ı etkileyemez.



What is a Responsibility?
(Sorumluluk nedir?)

Tek Sorumluluk İlkesi (SRP) bağlamında bir sorumluluğu “değişim nedeni” olarak tanımlıyoruz. Bir sınıfı değiştirmek için birden fazla nedeni düşünebiliyorsanız, o sınıfın birden fazla sorumluluğu vardır. Bunu görmek bazen zor olabilir. Sorumlulukları gruplar halinde düşünmeye alışkınız. Örneğin, aşağıdaki Modem arayüzünü düşünün. Çoğumuz bu arayüzün son derece makul göründüğünü kabul edeceğiz. Bildirdiği dört işlev kesinlikle modeme ait işlevlerdir.



Modem.cs -- SRP Violation

public interface Modem
{
   public void Dial(string pno);
   public void Hangup();
   public void Send(char c);
   public char Recv();
}


Ancak, burada gösterilen iki sorumluluk vardır. İlk sorumluluk bağlantı yönetimidir. İkincisi veri iletişimidir. Dial ve Hangup işlevleri modem bağlantısını yönetirken, Send ve Recv işlevleri veri iletir.

Bu iki sorumluluk birbirinden ayrılmalı mı? Bu uygulamanın nasıl değiştiğine bağlıdır. Uygulama, bağlantı işlevlerinin imzasını etkileyecek şekilde değişirse, tasarım Rigidity(Sertlik) kokusu alacaktır, çünkü gönderme ve okuma çağrısı yapan sınıfların bizim istediğimizden daha sık derlenmesi ve yeniden konuşlandırılması gerekecektir. Bu durumda iki sorumluluk aşağıda gösterildiği gibi ayrılmalıdır. Bu, istemci uygulamalarının iki sorumluluğu birleştirmesini önler.



Öte yandan, uygulama iki sorumluluğun farklı zamanlarda değişmesine neden olacak şekilde değişmiyorsa, bunları ayırmaya gerek yoktur. Gerçekten de onları ayırmak Needless Complexity(Gereksiz Karmaşıklık) kokusu alacaktı.

Burada bir korozyon var. Bir değişim ekseni, yalnızca değişiklikler gerçekten meydana gelirse bir değişim eksenidir. Herhangi bir belirti yoksa, SRP'yi veya bu konuda başka bir ilkeyi uygulamak akıllıca değildir.

Separating coupled responsibilities.
(Birleştirilmiş sorumlulukların ayrılması.)

Şekil 8-3'te ModemImplementation sınıfında her iki sorumluluğu da yerine getirdiğime dikkat edin. Bu arzu edilmez, ancak gerekli olabilir. Donanım veya işletim sisteminin ayrıntılarıyla ilgili olmak zorunda olduğumuz ve bizi çift olmayı tercih etmeyeceğimiz şeyleri birleştirmeye zorlayan çoğu zaman sebepler vardır. Bununla birlikte, arayüzlerini ayırarak, uygulamanın geri kalanıyla ilgili olarak kavramları birbirinden ayırdık.

ModemImplementation sınıfını bir çamur veya siğil olarak görebiliriz; ancak, tüm bağımlılıkların bundan uzaklaştığına dikkat edin. Kimsenin bu sınıfa ihtiyacı yoktur. Ana ihtiyaçlar dışında hiç kimse bunun var olduğunu bilmeye ihtiyaç duymaz. Böylece, çirkin parçayı bir çitin arkasına koyduk. Çirkinliğin dışarı sızmasına ve uygulamanın geri kalanını kirletmesine gerek yoktur. (Sınıf yerine Interface bağımlılığından bahsediliyor)

SONUÇ
SRP, ilkelerin en basitlerinden ve doğru yapılması en zorlarından biridir. Birbirine karşı sorumluluklar, doğal olarak yaptığımız bir şeydir. Bu sorumlulukları bulmak ve birbirinden ayırmak, yazılım tasarımının gerçekte ne olduğudur. Gerçekten de tartışacağımız ilkelerin geri kalanı bu ilkeye şu ya da bu şekilde geri dönüyor.


Bibliography [DeMarco79]: Structured Analysis and System Specification, Tom DeMarco, Yourdon Press Computing Series, 1979

[PageJones88]: The Practical Guide to Structured Systems Design, 2d. ed., Meilir PageJones, Yourdon Press Computing Series, 1988

Yazının Orjinali

Bonus :




Bonus :







Cloud Computing Modelleri

Cloud Computing Modelleri


Türkiye’de İnternet’in Tarih Öncesi Üzerine Prof. Dr. Levent Toker ile Röportaj

Türkiye’de İnternet’in Tarih Öncesi Üzerine Prof. Dr. Levent Toker ile Röportaj : "Gece yarısıydı, gece saatlerinden itibaren hazır oldu her şey. Bağlantı gerçekleşir gerçekleşmez, çok enteresan bir şey,
biz kimi bulsak da mesajlaşsak, sohbet denemeleri yapsak derken ekranımıza özellikle Amerika’dan çok sayıda Türk öğrenciden,
“Tebrik ederiz”, “Tebrik ederiz”, “Çok sevindik”, “Yaşasın Türkiye” gibi ilginç mesajlar dökülmeye başladı..."





Rastgele İçerik

DonanımHaber

© tüm hakları saklıdır
made with by templateszoo