Mert Özen Her Satırda Daha İleri
Yazılım Tasarımı UML ve Modeling — #06

Class Diagram'dan Koda ve Koddan Diyagrama: Domain Model, Design Model ve puml-gen

Mert Özen 7 Kas 2026 14 dk 6 görüntülenme
Class Diagram'dan Koda ve Koddan Diyagrama: Domain Model, Design Model ve puml-gen

Domain model iş dünyasının kavramlarını tipsiz ve operation'sız gösterir, design class diagram ise aynı vakanın C#'a yakın tasarım kararlarını; ilki elle çizilir, ikincisinin dilimleri koda dökülür ve puml-gen gibi araçlarla koddan üretilen diyagramlar yalnız gözlem ve drift kontrolü için kullanılır. Yazıda İK Portalı'nın izin modülü diyagramdan EF Core configuration'ına ve DDL'e, oradan da puml-gen ile tekrar PlantUML'e taşınıyor.

Sürüm notu: Bu yazıdaki diyagramlar UML (Unified Modeling Language) 2.5.1 notasyonuyla çizilmiştir; C# örnekleri .NET 10 ve Entity Framework Core (EF Core) 10, üretilen diyagramlar PlantUmlClassDiagramGenerator 1.4.0 ve PlantUML 1.2026.9 ile denenmiştir.

Önce Tahmin Et: Yeni Gelene Hangi Diyagramı Gönderirsin?

İK (insan kaynakları) Portalı ekibine yeni bir developer katıldı ve ilk gün "izin modülü nasıl çalışıyor, bir diyagram var mı?" diye sordu. Ekipten biri beş dakikada şunu yaptı: solution'ın kök klasöründe class diagram üreten bir aracı çalıştırdı, çıkan dosyayı PNG'ye (Portable Network Graphics) çevirip gönderdi.

dotnet tool install --local PlantUmlClassDiagramGenerator
dotnet puml-gen src docs/all -dir -createAssociation -allInOne
docker run --rm -v "$PWD":/data plantuml/plantuml -tpng /data/docs/all/include.puml

Solution'da dört project var: HrPortal.Leave.Domain, HrPortal.Leave.Application, HrPortal.Leave.Infrastructure ve HrPortal.Api. Üretilen diyagramda kaç kutu olur, resim ne kadar geniş çıkar? Daha önemlisi: yeni gelen arkadaş bu resme bakıp "LeaveRequestService bakiyeyi nereden kontrol ediyor?" sorusunun cevabını bulabilir mi?

Cevap: 56 Kutu, 14.166 Piksel ve Görünmeyen Bağımlılıklar

Gerçek koşunun sonucu: PlantUML'in SVG (Scalable Vector Graphics) çıktısı 14.166 × 788 piksel; içinde 56 kutu ve 54 ok var. PNG ise 4096 piksel genişlikte kesildi, çünkü PlantUML varsayılan olarak bu sınırın ötesini çizmiyor (PLANTUML_LIMIT_SIZE). Yani resmin üçte ikisi hiç yok. Kalan kısımda DateTimeOffset ve DateOnly gibi framework tipleri birer class kutusu olarak duruyor; ilk çalıştırmada obj klasöründeki üretilmiş dosyalar için de 12 boş .puml dosyası oluştu.

Asıl soru ise cevapsız kalıyor. LeaveRequestService kutusundan IBalanceService'e giden tek bir ok yok. Sebep basit: service primary constructor kullanıyor ve araç primary constructor parametrelerini görmüyor. Aynı bağımlılığı klasik constructor ve private readonly field ile yazınca ok çıkıyor:

public sealed class PrimaryCtorService(IBalanceService balances)
{
    public bool Check() => balances is not null;
}

public sealed class FieldService
{
    private readonly IBalanceService balances;

    public FieldService(IBalanceService balances) => this.balances = balances;

    public bool Check() => balances is not null;
}
class PrimaryCtorService <<sealed>> {
    + Check() : bool
}
class FieldService <<sealed>> {
    + FieldService(balances:IBalanceService)
    + Check() : bool
}
FieldService --> "balances" IBalanceService

Bu yazının konusu tam bu: diyagramla kod arasında iki yönde gidip gelmek. Önce aynı vakanın iki farklı seviyede çizildiğini göreceğiz (domain model ve design class diagram), sonra diyagramdan C# ve EF Core koduna (forward engineering), en son da koddan diyagrama (reverse engineering) gideceğiz. Hangi diyagramın elle, hangisinin üretilerek tutulacağı sorusu da bu yolculuğun sonunda kendiliğinden cevaplanacak.

Aynı Vakanın İki Seviyesi: Domain Model ve Design Class Diagram

Serinin önceki iki yazısında class diagram notasyonunu öğrendik: attribute, operation, visibility, multiplicity ve ilişki türleri. Ama aynı notasyonla iki çok farklı şey çizilebilir. Craig Larman, Applying UML and Patterns (3. baskı, 2004) kitabında bunları ayrı ayrı adlandırır: domain model, problem alanındaki kavramların görsel temsilidir; yazılım class'larını değil, iş dünyasındaki şeyleri gösterir. Design class diagram (DCD) ise yazılımın gerçek class'larını, tipleriyle, operation'larıyla ve kim kimi biliyor bilgisiyle gösterir. Martin Fowler da UML Distilled'da benzer ayrımı perspective olarak anlatır: conceptual perspective ile software perspective. Aynı kutu iki perspective'te farklı şey söyler.

06-domain-vs-design-tr.png

Üstteki domain model'de Employee, LeaveRequest, LeaveType, Department ve Approval var. Attribute'ların tipi yok, operation compartment'ı hiç yok, ok ucu yok. "Bir çalışan sıfır ya da daha fazla izin talebi açar" ve "bir talep en fazla iki onaycı tarafından karara bağlanır" gibi cümleler multiplicity'lerle okunuyor. Approval bir association class: çalışanla talep arasındaki "onaycı" ilişkisinin kendisinin karar, yorum ve tarih taşıdığını söylüyor. Bu diyagramı İK direktörlüğüyle aynı masada konuşabilirsin; içinde onların bilmediği tek kelime yok.

Alttaki design class diagram ise aynı vakanın tasarım kararlarını gösteriyor. LeaveRequestService diye bir class var; iş biriminin dünyasında böyle bir kavram yok, onu sorumlulukları dağıtırken biz icat ettik. İki «interface» var: service'in bağımlı olduğu ama implementation'ını bilmediği ILeaveRequestRepository ve IBalanceService. Dependency'ler kesikli çizgi ve açık ok ucuyla, «use» ve «create» keyword'leriyle çizildi. period artık Period: DateRange; DateRange da «dataType» olarak işaretli, çünkü kimliği olmayan, değeriyle karşılaştırılan bir tip. LeaveRequest ile Approval arasındaki ilişki composition'a dönüştü ve ok ucu navigability'yi gösteriyor: talep onaylarını biliyor, kod tarafında Approvals collection'ı bunun karşılığı.

Domain Model: Neyi Koyarsın, Neyi Koymazsın?

Domain model'in tek görevi kavramları ve aralarındaki ilişkileri doğru yakalamak. Requirement metninden (serinin ikinci yazısındaki R1–R12) isimler aday kavram, fiiller aday association olarak çıkıyordu; domain model bunların ayıklanmış hâli. İçine koyduğun şeyler: kavram class'ları, iş açısından anlamlı attribute'lar, association'lar ve multiplicity'ler, gerekiyorsa generalization ve association class.

Koymadıkların daha öğretici. Operation koymazsın, çünkü "kim onaylar" sorusunun cevabı henüz bir method değil, bir sorumluluk dağıtımı kararı. Visibility koymazsın, çünkü kavramların private'ı yok. Id, EmployeeId gibi foreign key attribute'larını koymazsın; ilişki zaten association ile gösteriliyor, foreign key bir veritabanı kararı. Interface, repository, DbContext, controller koymazsın. Ve navigability oku çizmezsin: "talep çalışanı mı bilir, çalışan talepleri mi?" sorusu analiz sorusu değil, tasarım sorusu.

Bu diyagramı elle çizmek zorundasın, çünkü kodda karşılığı yok. Kodda Employee.FullName var ama "bir talep en fazla iki onaycıdan geçer" bilgisi hiçbir property'de yazmıyor; Approve method'unun içindeki if'e dağılmış durumda. Diagrams-as-code yaklaşımıyla PlantUML'de şöyle tutulabilir:

@startuml leave-domain-model
hide circle
hide empty methods

class Employee {
  fullName
  email
}
class Department {
  name
}
class LeaveRequest {
  period
  workingDays
  status
  reason
}
class LeaveType {
  name
  isPaid
}
class Approval {
  decision
  comment
  decidedAt
}

Employee "0..*" -- "1" Department : works in
Employee "1" -- "0..*" LeaveRequest : requests
Employee "0..2 approver" -- "0..*" LeaveRequest
(Employee, LeaveRequest) .. Approval
LeaveRequest "0..*" -- "1" LeaveType : type
@enduml

(Employee, LeaveRequest) .. Approval satırı PlantUML'in association class söz dizimi; render edildiğinde Approval kutusu, iki class arasındaki association çizgisine kesikli bir çizgiyle bağlanıyor. Bu dosya PlantUML 1.2026.9 ile hatasız render oldu. PlantUML'in ayrıntıları serinin ilerideki PlantUML ile Diagrams-as-Code yazısında.

Design Class Diagram: Kararlar Nereden Geliyor?

Domain model'den design model'e geçerken her eklenen öğe bir kararın kaydı. Hangi kararları verdiğimizi listelemek, diyagramın neden o şekilde olduğunu anlamanın en kısa yolu:

  • Sorumluluk: "talep oluştur ve gönder" akışını kim yönetecek? Bir application service (LeaveRequestService). İş kuralları ise LeaveRequest'in kendi method'larında (Submit, Approve, Cancel).
  • Tip: period iki ayrı DateOnly değil, Overlaps davranışı olan bir DateRange; status bir enum; LeaveType ise enum değil, İK'nın tanımlayabildiği bir entity (R12).
  • Navigability: LeaveRequest onaylarını biliyor, Approval da hangi talebe ait olduğunu. Employee ise taleplerini bilmiyor; çalışanın tüm taleplerini sorgu ile buluyoruz.
  • Dependency yönü: service, altyapıya değil interface'lere bağlı. Implementation'lar HrPortal.Leave.Infrastructure'da.
  • Derived attribute: /RequiresDirectorApproval saklanmıyor, WorkingDays > 3'ten hesaplanıyor (R7).

Design class diagram'ın bütün sistemi göstermesi gerekmiyor; göstermemesi daha iyi. Şekildeki alt kart bir dilim: "İzin Talep Et" use case'ini gerçekleştiren class'lar. Larman da DCD'yi use case'lerin tasarımıyla (interaction diagram'larla) birlikte, parça parça büyüyen bir şey olarak anlatır. Sıradaki sequence diagram yazısında bu dilimin içindeki mesajları çizeceğiz.

Forward Engineering: Diyagramdan C# ve EF Core'a

Design class diagram'daki her öğenin C# ve EF Core'da bir karşılığı var. Önce LeaveRequest:

public class LeaveRequest
{
    public const int DirectorApprovalThreshold = 3;

    private readonly List<Approval> approvals = [];

    private LeaveRequest() { } // EF Core için

    public LeaveRequest(Employee employee, LeaveType leaveType, DateRange period, string? reason, DateTimeOffset now)
    {
        Employee = employee;
        EmployeeId = employee.Id;
        LeaveType = leaveType;
        LeaveTypeId = leaveType.Id;
        Period = period;
        Reason = reason;
        Status = LeaveStatus.Draft;
        CreatedAt = now;
    }

    public int Id { get; private set; }
    public int EmployeeId { get; private set; }
    public Employee Employee { get; private set; } = null!;
    public int LeaveTypeId { get; private set; }
    public LeaveType LeaveType { get; private set; } = null!;
    public DateRange Period { get; private set; }
    public int WorkingDays { get; private set; }
    public string? Reason { get; private set; }
    public LeaveStatus Status { get; private set; }
    public DateTimeOffset CreatedAt { get; private set; }
    public LeaveAttachment? Attachment { get; private set; }
    public IReadOnlyList<Approval> Approvals => approvals;

    public bool RequiresDirectorApproval => WorkingDays > DirectorApprovalThreshold;

    public void Submit(int workingDays, DateTimeOffset now) { /* ... */ }
    public void Approve(Employee approver, string? comment, DateTimeOffset now) { /* ... */ }
}

public readonly record struct DateRange
{
    public DateRange(DateOnly start, DateOnly end)
    {
        if (end < start)
            throw new ArgumentException("End date cannot be before start date.", nameof(end));
        Start = start;
        End = end;
    }

    public DateOnly Start { get; }
    public DateOnly End { get; }

    public bool Overlaps(DateRange other) => Start <= other.End && other.Start <= End;
}

Kod diyagramda olmayan şeyler de içeriyor: EmployeeId ve LeaveTypeId foreign key'leri, EF Core'un ihtiyaç duyduğu private constructor, = null! ile susturulan nullable uyarıları. Bunlar design kararı değil, implementation detayı; design class diagram'a koymadık ve koymamalıyız. Notasyon ile EF Core arasındaki eşleme ise configuration'da:

internal sealed class LeaveRequestConfiguration : IEntityTypeConfiguration<LeaveRequest>
{
    public void Configure(EntityTypeBuilder<LeaveRequest> b)
    {
        // «dataType» DateRange: kimliği yok, tabloda iki kolon olur
        b.ComplexProperty(r => r.Period, p =>
        {
            p.Property(x => x.Start).HasColumnName("StartDate");
            p.Property(x => x.End).HasColumnName("EndDate");
        });
        b.Property(r => r.Status).HasConversion<string>().HasMaxLength(32);
        b.Property(r => r.Reason).HasMaxLength(500);
        // composition [0..1]: owned type, aynı tabloda
        b.OwnsOne(r => r.Attachment, a =>
        {
            a.Property(x => x.FileName).HasMaxLength(260);
            a.Property(x => x.ContentType).HasMaxLength(100);
            a.Property(x => x.StoragePath).HasMaxLength(400);
        });
        // composition [0..*]: parent silinince onaylar da silinir
        b.HasMany(r => r.Approvals).WithOne(a => a.LeaveRequest)
            .HasForeignKey(a => a.LeaveRequestId).OnDelete(DeleteBehavior.Cascade);
        b.Navigation(r => r.Approvals).HasField("approvals");
        // derived attribute: veritabanında kolon yok
        b.Ignore(r => r.RequiresDirectorApproval);
    }
}

Diyagramdan koda giden eşlemeyi bir tabloda toplayınca karar listesi netleşiyor:

DiyagramdaC#EF Core ve veritabanı
Class, attributeclass, propertyentity, kolon
«dataType» (DateRange)readonly record structComplexProperty, aynı tabloda kolonlar
Composition [0..1] (Attachment)nullable propertyOwnsOne, aynı tabloda nullable kolonlar
Composition [0..*] (Approvals)private List + IReadOnlyListforeign key + ON DELETE CASCADE
Association [1] (LeaveType)navigation property + foreign key propertyforeign key, NO ACTION
Derived /RequiresDirectorApprovalexpression-bodied propertyIgnore, kolon yok
Enumeration (LeaveStatus)enumHasConversion<string>()

Son adım, EF Core'un bu modelden ürettiği DDL (Data Definition Language), yani tabloları kuran SQL (Structured Query Language) script'i. Database.GenerateCreateScript() SQL Server için şunu üretti (kısaltılmış):

CREATE TABLE [LeaveRequests] (
    [Id] int NOT NULL IDENTITY,
    [EmployeeId] int NOT NULL,
    [LeaveTypeId] int NOT NULL,
    [WorkingDays] int NOT NULL,
    [Reason] nvarchar(500) NULL,
    [Status] nvarchar(32) NOT NULL,
    [CreatedAt] datetimeoffset NOT NULL,
    [SubmittedAt] datetimeoffset NULL,
    [Attachment_FileName] nvarchar(260) NULL,
    [Attachment_ContentType] nvarchar(100) NULL,
    [Attachment_StoragePath] nvarchar(400) NULL,
    [EndDate] date NOT NULL,
    [StartDate] date NOT NULL,
    CONSTRAINT [PK_LeaveRequests] PRIMARY KEY ([Id]),
    CONSTRAINT [FK_LeaveRequests_Employees_EmployeeId] FOREIGN KEY ([EmployeeId]) REFERENCES [Employees] ([Id]) ON DELETE NO ACTION,
    CONSTRAINT [FK_LeaveRequests_LeaveTypes_LeaveTypeId] FOREIGN KEY ([LeaveTypeId]) REFERENCES [LeaveTypes] ([Id]) ON DELETE NO ACTION
);

ALTER TABLE [Approvals] ADD CONSTRAINT [FK_Approvals_LeaveRequests_LeaveRequestId] FOREIGN KEY ([LeaveRequestId]) REFERENCES [LeaveRequests] ([Id]) ON DELETE CASCADE;

Composition'ın dolu elması iki farklı yere düştü: Attachment aynı satırın kolonları oldu (parçanın ayrı bir varlığı yok), Approvals ayrı bir tablo ama ON DELETE CASCADE ile (parça bütünle birlikte ölüyor). Normal association'lar ise NO ACTION: bir izin türünü silmek talepleri silmemeli. Owned type ve complex type'ın ayrıntıları EF Core serisindeki EF Core'da Owned Type ve Value Converter ile EF Core 8 Yenilikleri: Complex Type yazılarında.

Reverse Engineering: puml-gen ile Koddan PlantUML

06-round-trip-tr.png

Ters yönde, mevcut koddan diyagram üretmek için .NET dünyasındaki en pratik araç Pierre3'ün PlantUmlClassDiagramGenerator'ı (komutu puml-gen). Roslyn ile kaynak kodu parse edip her dosya için bir .puml üretiyor; derleme gerektirmiyor. Visual Studio'nun eski Class Designer'ı (.cd dosyaları) Microsoft Learn'e göre yalnız .NET Framework project'lerini destekliyor, .NET Core ve sonrasını desteklemiyor; bu yüzden modern .NET'te bu tür araçlara yöneliyoruz. Global tool yerine repo'ya bağlı local tool olarak kurmak, herkesin ve CI (Continuous Integration) ortamının aynı sürümü kullanmasını sağlıyor:

dotnet new tool-manifest
dotnet tool install --local PlantUmlClassDiagramGenerator
dotnet puml-gen src/HrPortal.Leave.Domain docs/diagrams/generated/domain \
  -dir -public -createAssociation -excludePaths "**/obj,**/bin" -allInOne

-dir klasör modunu açıyor, -public yalnız public üyeleri alıyor, -createAssociation field ve property tiplerinden ok çiziyor, -excludePaths üretilmiş dosyaları dışarıda bırakıyor, -allInOne her şeyi tek include.puml'de topluyor. Domain project'i için çıktının bir kısmı:

class LeaveRequest {
    + <<const>> DirectorApprovalThreshold : int = 3
    + Id : int <<get>>
    + WorkingDays : int <<get>>
    + RequiresDirectorApproval : bool <<get>>
    + Submit(workingDays:int, now:DateTimeOffset) : void
    + Approve(approver:Employee, comment:string?, now:DateTimeOffset) : void
}
LeaveRequest o-> "Employee" Employee
LeaveRequest o-> "LeaveType" LeaveType
LeaveRequest --> "Period" DateRange
LeaveRequest --> "CreatedAt" DateTimeOffset
LeaveRequest --> "Approvals<Approval>" "IReadOnlyList`1"
Approval o-> "LeaveRequest" LeaveRequest

Bu çıktı PlantUML ile sorunsuz render oluyor ama elle çizdiğimiz design diagram'la karşılaştırınca dört fark göze çarpıyor. Birincisi, Employee ve LeaveType okları içi boş elmaslı (o->), yani aggregation. Biz böyle bir karar vermedik; araç, initializer'ı olan property'leri (burada EF Core için yazdığımız = null!) aggregation olarak çiziyor. İkincisi, DateTimeOffset ve DateOnly birer class gibi kutu ve ok oluyor; bunlar attribute tipi olarak kalmalıydı. Üçüncüsü, Approvals collection'ı Approval'a giden 0..*'lı bir ok yerine IReadOnlyList`1 adlı garip bir generic class'a gidiyor ve composition bilgisi kayboluyor. Dördüncüsü, Application project'inde ILeaveRequestService <|-- LeaveRequestService satırı üretiliyor; bu düz çizgi generalization demek. UML'de bir class'ın interface'i implement etmesi realization'dır ve kesikli çizgiyle (<|..) çizilir.

Bunlar aracın kusuru olmaktan çok doğası: kod, tasarım niyetinin yalnız bir kısmını taşıyor. "Bu bir composition" bilgisi kodda bir cascade configuration'ında, "bu bir dependency" bilgisi bir constructor parametresinde saklı. Araç gördüğünü çiziyor, niyeti çizemiyor.

Diyagramı Kodla Senkron Tutmak: Elle mi, Üretilerek mi?

Pratik kural şu: niyet ve karar anlatan diyagramlar elle, gözlem yapan diyagramlar üretilerek tutulur. Domain model, use case'ler, bir use case'in design dilimi, sequence ve state machine diagram'ları elle çizilir ve docs/diagrams/ altında kodla birlikte versiyonlanır. Üretilen diyagramların görevi ise elle çizileni denetlemek: "kodda diyagramda olmayan bir şey var mı?" sorusuna cevap vermek.

Üretilen dosyaları repo'ya commit'leyip CI'da yeniden üretmek, kod değişip diyagram değişmediğinde PR'ı (pull request) uyarır hâle getiriyor:

#!/usr/bin/env bash
set -euo pipefail
dotnet tool restore > /dev/null
dotnet puml-gen src/HrPortal.Leave.Domain docs/diagrams/generated/domain \
  -dir -public -createAssociation -excludePaths "**/obj,**/bin" -allInOne > /dev/null
git diff --exit-code --stat -- docs/diagrams/generated

Biri LeaveRequest'e CancelledAt property'si ekleyip script'i çalıştırınca:

 docs/diagrams/generated/domain/LeaveRequest.puml | 1 +
 docs/diagrams/generated/domain/include.puml      | 1 +
 2 files changed, 2 insertions(+)
exit=1

Bu kontrol elle çizilen diyagramı otomatik güncellemiyor; review eden kişiye "domain'de bir şey değişti, elle çizilen diyagrama da bak" diyor. Git tarafındaki diff ve PR akışı Git ve GitHub serisinde, diyagramların CI'da render edilmesi bu serinin ilerideki PlantUML yazısında.

Sık Düşülen Tuzaklar

Tüm solution'ın tek dev class diagram'ı. Açılıştaki 56 kutulu, 14.166 piksellik resim kimseye bir şey anlatmıyor. Diyagram bir soruya cevap vermeli; "her şey" bir soru değil. Üretiyorsan modül modül üret, elle çiziyorsan bir use case'in dilimini çiz.

Domain model'e teknik class koymak. LeaveRequestRepository, HrPortalDbContext, LeaveRequestDto domain model'e girdiği anda iş birimiyle konuşamaz hâle gelir ve analiz ile tasarım karışır.

Foreign key'leri attribute olarak çizip bir de association çizmek. EmployeeId: int ve Employee'ye giden ok aynı bilgiyi iki kez söylüyor; design diagram'da ilişkiyi association ile göster, foreign key'i kodda bırak.

Üretilen diyagrama tasarım belgesi gibi güvenmek. Yukarıda gördüğümüz gibi araç primary constructor bağımlılıklarını göstermiyor, = null! yüzünden aggregation uyduruyor, realization'ı generalization olarak çiziyor. Üretilen diyagram "kodda ne var"ı gösterir, "neden böyle"yi göstermez.

Diyagramı sadece başta çizip bırakmak. Elle çizilen diyagram kodla birlikte PR'a girmiyorsa üç sprint sonra yanlış bilgi veren bir belgeye dönüşür. Ya kodla birlikte güncelle ya da sil; bayat diyagram, hiç diyagram olmamasından daha zararlı.

Ne Zaman Kullanma

Basit CRUD (Create, Read, Update, Delete) ekranları için design class diagram çizme; EF Core entity'leri zaten kendini anlatıyor. Domain model'i ise kavramların tartışmalı olduğu yerde çiz: "onay" bir durum mu, bir kayıt mı, ikisi birden mi? Bu soruyu iş birimiyle konuşurken bir domain model, kodun kendisinden çok daha hızlı yakınsar. puml-gen'i sürekli bir dokümantasyon kaynağı olarak değil, mevcut bir kod tabanını ilk kez okurken keşif aracı ve CI'da drift kontrolü olarak kullan.

Küçük Bir Deneme

★☆☆ Domain model'e R12'deki BalanceAdjustment kavramını ekle: hangi class'larla association'ı var, multiplicity'leri ne? Operation ya da foreign key eklemeden çiz ve PlantUML ile render et.

★★☆ HrPortal.Leave.Application project'ine puml-gen'i -createAssociation ile çalıştır. LeaveRequestService'in bağımlılıklarını gösteren ok çıkıyor mu? Service'i klasik constructor ve private readonly field'larla yazıp tekrar dene ve iki çıktıyı karşılaştır. Hangisini tercih ederdin ve neden diyagram için kodu değiştirmek doğru bir motivasyon değil?

★★★ tools/check-diagrams.sh script'ini GitHub Actions'ta çalışacak bir job'a dönüştür. Script fail ettiğinde PR'a "domain diyagramı güncellenmeli" yorumu düşmesini sağla ve üretilen include.puml'u PNG'ye çevirip artifact olarak yükle.

Bir sonraki yazıda class diagram'ın iki kardeşine geçiyoruz: object diagram ile req42: LeaveRequest gibi somut instance'ları çizip multiplicity hatalarını yakalayacak, package diagram ile de bu yazıdaki dört project'in bağımlılık haritasını çizip katman ihlallerini görünür hâle getireceğiz.