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

Class Diagram 2: Association, Aggregation, Composition, Generalization ve Dependency — C# Karşılıklarıyla

Mert Özen 7 Kas 2026 15 dk 4 görüntülenme
Class Diagram 2: Association, Aggregation, Composition, Generalization ve Dependency — C# Karşılıklarıyla

Association kalıcı bir bağdır ve C#'ta navigation property olur; composition'da parça tek sahibe aittir ve onunla silinir, EF Core'da owned/complex type ya da cascade delete'tir; aggregation'ın UML 2.5.1'de kesin bir anlamı yoktur. Altı ilişkinin notasyonu ve C# karşılığı, EF Core'un zorunlu ilişkilerdeki cascade varsayılanı, qualified association ve Approval'ın association class'tan ayrı bir class'a yükselmesi.

Sürüm notu: Bu yazıdaki diyagramlar UML 2.5.1 notasyonuyla çizilmiştir; C# örnekleri .NET 10 ile denenmiştir.

Önce Tahmin Et

Alan Turing, Engineering departmanında bir ekibin yöneticisi; kendi yöneticisi de departmanın direktörü Grace Hopper. Alan 5 iş günlük bir izin istiyor. Kural gereği talep önce yöneticiye, 3 günü aştığı için sonra direktöre gidiyor; ikisi de Grace. Onaylar, Employee ile LeaveRequest arasındaki many-to-many ilişkinin üzerinde veri taşıyan bir join entity olarak modellenmiş. Grace'in ikinci onayı kaydedilirken ne olur?

using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;

using SqliteConnection connection = new("Data Source=:memory:");
connection.Open();
using HrPortalDbContext db = new(new DbContextOptionsBuilder<HrPortalDbContext>().UseSqlite(connection).Options);
db.Database.EnsureCreated();

Employee grace = new() { Id = 7, FullName = "Grace Hopper" };
Employee alan = new() { Id = 420, FullName = "Alan Turing", ManagerId = 7 };
LeaveRequest request = new() { Id = 44, EmployeeId = 420, WorkingDays = 5 };
db.AddRange(grace, alan, request);

// Alan'ın yöneticisi de departman direktörü de Grace
db.Add(new Approval { LeaveRequestId = 44, ApproverId = 7, Step = "Manager", DecidedAt = DateTimeOffset.UtcNow });
try
{
    db.Add(new Approval { LeaveRequestId = 44, ApproverId = 7, Step = "Director", DecidedAt = DateTimeOffset.UtcNow });
    db.SaveChanges();
    Console.WriteLine($"Approvals: {db.Set<Approval>().Count()}");
}
catch (InvalidOperationException ex)
{
    Console.WriteLine(ex.Message);
}

public sealed class Employee
{
    public int Id { get; set; }
    public string FullName { get; set; } = "";
    public int? ManagerId { get; set; }
    public List<LeaveRequest> ApprovedRequests { get; } = [];
}

public sealed class LeaveRequest
{
    public int Id { get; set; }
    public int EmployeeId { get; set; }
    public int WorkingDays { get; set; }
    public List<Employee> Approvers { get; } = [];
}

// Association class: Employee (onaycı) ile LeaveRequest arasındaki link'in kendi verisi
public sealed class Approval
{
    public int LeaveRequestId { get; set; }
    public int ApproverId { get; set; }
    public string Step { get; set; } = "";
    public DateTimeOffset DecidedAt { get; set; }
}

public sealed class HrPortalDbContext(DbContextOptions<HrPortalDbContext> options) : DbContext(options)
{
    protected override void OnModelCreating(ModelBuilder b)
    {
        b.Entity<Employee>();
        b.Entity<LeaveRequest>()
            .HasMany(r => r.Approvers)
            .WithMany(e => e.ApprovedRequests)
            .UsingEntity<Approval>(
                j => j.HasOne<Employee>().WithMany().HasForeignKey(a => a.ApproverId),
                j => j.HasOne<LeaveRequest>().WithMany().HasForeignKey(a => a.LeaveRequestId),
                j => j.HasKey(a => new { a.LeaveRequestId, a.ApproverId }));
    }
}

Model Ne Söylüyorsa Kod Onu Uyguluyor

The instance of entity type 'Approval' cannot be tracked because another instance with the same key value for {'LeaveRequestId', 'ApproverId'} is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the conflicting key values.

Entity Framework Core (EF Core) ikinci Approval'ı takip etmeyi reddetti, çünkü anahtarı (LeaveRequestId, ApproverId) ve bu çift zaten var. Kod hatalı değil; modelin söylediğini harfiyen uyguluyor. Bu model UML'de (Unified Modeling Language) bir association class'tır: Approval, Employee ile LeaveRequest arasındaki association'ın kendisidir ve UML'de bir association, varsayılan olarak aynı iki object arasında yalnızca bir link taşır. "Grace, 44 numaralı talebi onayladı" bir link'tir; aynı link'ten ikinci bir tane olamaz. Yönetici ve direktörün aynı kişi olduğu küçük bir departmanda model çöküyor.

Önceki yazıda class kutusunun içine baktık. Bu yazıda kutuları birbirine bağlayan çizgilere bakıyoruz: altı ilişki türü, her birinin notasyonu, C# karşılığı ve EF Core'da veritabanına nasıl yansıdığı. Sonunda yukarıdaki problemi doğru modelle çözeceğiz.

Altı İlişki Bir Bakışta

Bir class diagram'daki çizgiler iki şeyle ayrışır: çizginin kendisi (düz ya da kesikli) ve ucundaki işaret (açık ok, içi boş üçgen, içi boş ya da dolu elmas). Düz çizgiler yapısal ilişkilerdir: bir object diğerini bir şekilde tutar ya da onun bir türüdür. Kesikli çizgiler daha zayıftır: bir sözleşmeyi yerine getirmek ya da bir şeyi geçici olarak kullanmak.

05-relationship-arrows-tr.png

Her satırdaki C# satırı o ilişkinin en tipik karşılığı; tek karşılığı değil. Şimdi tek tek bakalım.

Association: Navigability, Role Adı ve Multiplicity

Association, iki class'ın instance'ları arasında kalıcı bir bağ olduğunu söyler: bir izin talebi bir çalışana aittir. Çizginin iki ucu association end'tir ve her uca üç bilgi yazılabilir. Multiplicity, karşı taraftan bakınca bu uçta kaç instance olduğunu söyler: bir çalışanın sıfır ya da daha fazla talebi (0..*), bir talebin tam bir sahibi (1) vardır. Role adı, o uçtaki object'in ilişkideki rolüdür: employee, approver, manager. Navigability ise uçtaki açık okla gösterilir ve "bu yönde gidilebilir" demektir: LeaveRequest'ten Employee'ye ok varsa, talep sahibini bilir.

C#'ta navigable bir uç bir navigation property'ye dönüşür, role adı da çoğu zaman property'nin adı olur: public Employee Employee { get; set; }. Okun olmadığı yön için property yazmazsın; Employee class'ında LeaveRequests collection'ı yoksa diyagramda o yönde ok da olmamalı. İki yönlü navigability bedava değildir: iki tarafı senkron tutmak, serialize ederken döngüye girmemek ve gereksiz yere büyük object graph'ları yüklememek senin işin olur. UML bir ucun açıkça navigable olmadığını ucuna küçük bir X koyarak söylemene de izin verir; pratikte çoğu ekip "ok yoksa yok" der.

Bir class kendisiyle de ilişki kurabilir. Çalışan ile yöneticisi arasındaki ilişki bir self-association'dır: bir uçta manager 0..1, diğer uçta reports 0..*. C#'ta int? ManagerId ve Employee? Manager; multiplicity'deki 0, nullable tipin ta kendisi. UML 2.5.1'de bir association end'in karşı class'a ait olduğunu göstermek için ucuna küçük bir dolu nokta da konabilir (ownership); C#'ta bu "property o class'ta tanımlı" demektir. Çoğu diyagramda gösterilmez.

Aggregation ve Composition: İki Elmas

Aggregation ve composition, association'ın bütün ile parça arasındaki özel hâlleridir. Elmas her zaman bütünün tarafına konur.

İçi boş elmas (shared aggregation), en çok tartışılan UML işaretidir. UML 2.5.1 onun anlamını kasıtlı olarak açık bırakır: spesifikasyona göre shared aggregation'ın kesin anlamı uygulama alanına ve modelleyene göre değişir. Martin Fowler "UML Distilled"da bu belirsizliği, James Rumbaugh'un ifadesini aktararak bir "modeling placebo" diye anar. Departman ile çalışanları arasına içi boş elmas çizdiğinde okuyan herkes farklı bir şey anlar: çalışan departman silinince silinir mi, başka bir departmana da ait olabilir mi? Diyagram bunu söylemez. Pratik öneri: aggregation yerine düz bir association çiz ve gerekiyorsa multiplicity ile anlat.

Dolu elmas (composite aggregation, yani composition) ise kesin kurallar taşır. Bir parça aynı anda en fazla bir bütüne ait olabilir ve bütün silindiğinde parçaları da onunla silinir. Parça bütünden önce ayrılabilir, ama başka bir bütünle paylaşılamaz. İzin modülünde onaylar ve rapor dosyası talebin parçalarıdır: talep silinirse onların ayrı bir anlamı kalmaz. Çalışan ise talebin parçası değildir; talep silinince çalışan silinmez, çalışan silinince de talepler sessizce yok olmamalı.

EF Core'da composition üç farklı biçimde karşılık bulabilir. Kimliği olmayan, tek bir değer gibi davranan parçalar (bizim DateRange) için EF Core 10'da önerilen yol ComplexProperty'dir; değer aynı tablonun kolonlarına yazılır. Kimliği önemsiz ama entity gibi takip edilen parçalar için owned type (OwnsOne, OwnsMany) kullanılır; EF Core dokümantasyonu owned entity'lerin sahibinden bağımsız var olamayacağını ve paylaşılamayacağını açıkça yazar. Kendi kimliği olan parçalar (bizim Approval) ayrı bir tablo ve zorunlu bir foreign key ile, OnDelete(DeleteBehavior.Cascade) olarak modellenir.

Burada EF Core'un bir varsayılanı tuzak kurar: foreign key'i nullable olmayan, yani zorunlu her ilişkide varsayılan davranış cascade delete'tir. LeaveRequest.EmployeeId zorunlu olduğu için, ayrıca bir şey söylemezsen EF Core bu association'ı composition gibi davranacak şekilde kurar. Aşağıdaki örnekte OnDelete(DeleteBehavior.Restrict) satırını kaldırıp aynı kodu çalıştırdığımda üretilen foreign key ve sonuç şöyle oldu:

CONSTRAINT "FK_LeaveRequests_Employees_EmployeeId" FOREIGN KEY ("EmployeeId") REFERENCES "Employees" ("Id") ON DELETE CASCADE
Alan deleted; leave requests left: 0

Alan'ı silmek, onun izin geçmişini de hiçbir uyarı vermeden sildi. Diyagramda düz bir association çizip kodda composition davranışına razı olmak, modelin ve kodun iki farklı şey söylediği klasik bir durum. Kural basit: diyagramda dolu elmas yoksa, zorunlu foreign key'in OnDelete davranışını açıkça Restrict ya da NoAction yap.

Generalization, Realization ve Dependency

Generalization, içi boş üçgenli düz çizgidir ve "bir türüdür" (is-a) demektir; üçgen genel tipe bakar. C#'ta class inheritance ya da record inheritance: LeaveRequestApproved, soyut LeaveRequestEvent'in bir türüdür. Bu ilişki, alt tipin üst tipin beklendiği her yerde kullanılabileceği sözünü verir; SOLID (Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, Dependency Inversion) serisinin Liskov Substitution Principle yazısı bu sözün nasıl bozulduğunu anlatıyor. Generalization'ı yalnızca kod tekrarını azaltmak için çizme; ilk yazıdaki "rol mü, tip mi?" tartışması burada da geçerli.

Realization, içi boş üçgenli kesikli çizgidir: bir class, bir interface'in sözleşmesini yerine getirir. WorkingDayCalculator : IWorkingDayCalculator. Component diagram yazısında aynı ilişkinin "lollipop" denen kısa gösterimini göreceğiz.

Dependency, açık oklu kesikli çizgidir ve en zayıf ilişkidir: bir class diğerini kullanır ama onu kalıcı olarak tutmaz. Tipik C# karşılıkları bir method parametresi, bir local değişken ya da bir static çağrıdır. WorkingDayCalculator, PublicHoliday listesini parametre olarak alır, hesaplar ve unutur; PublicHoliday değişirse etkilenir, ama onu bir field'da saklamaz. Dependency'nin türünü bir keyword ile belirtebilirsin: «use», «create» (instance oluşturur) gibi. Constructor injection ile alınıp bir field'da tutulan bir service ise dependency değil association'dır; çünkü bağ object'in ömrü boyunca sürer.

İzin Modülünün İlişkileri

Bu altı ilişkiyle izin modülünün design seviyesindeki class diagram'ını çizebiliriz. Kalabalık olmasın diye yalnızca ad bölmelerini gösterdim; Approval'ın attribute'ları birazdan konuşacağımız karar yüzünden görünür:

05-leave-relations-tr.png

Diyagramda tek yeni notasyon, Employee ile LeaveBalance arasındaki qualified association. Employee'nin kenarına yapışık küçük dikdörtgen bir qualifier'dır: year ve leaveTypeId. Okunuşu şöyle: bir çalışan ve belirli bir yıl ile izin türü verildiğinde, en fazla bir bakiye vardır (0..1). Qualifier olmadan aynı ilişki 0..* olurdu ve "Ada'nın 2026 yıllık izin bakiyesi tek midir?" sorusunun cevabı diyagramda görünmezdi. Kodda qualified association iki yerde karşılık bulur: veritabanında (EmployeeId, Year, LeaveTypeId) üzerinde bir unique index, bellekte Dictionary<(int Year, int LeaveTypeId), LeaveBalance> gibi bir anahtarlı collection.

Diyagramın EF Core karşılığı aşağıda. Bu kez Approval kendi kimliği olan, talebin parçası (composition) ve onaycıya bağlı (association) bir class; Alan'ın talebine Grace'in iki onayını ekliyoruz:

using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;

using SqliteConnection connection = new("Data Source=:memory:");
connection.Open();
using HrPortalDbContext db = new(new DbContextOptionsBuilder<HrPortalDbContext>().UseSqlite(connection).Options);
db.Database.EnsureCreated();

string script = db.Database.GenerateCreateScript();
int start = script.IndexOf("CREATE TABLE \"LeaveRequests\"");
Console.WriteLine(script[start..(script.IndexOf(");", start) + 2)]);
foreach (string line in script.Split('\n').Where(l => l.Contains("FK_Approvals") || l.Contains("FK_Employees")))
    Console.WriteLine(line.Trim());

Employee grace = new() { Id = 7, FullName = "Grace Hopper" };
Employee alan = new() { Id = 420, FullName = "Alan Turing", ManagerId = 7 };
LeaveRequest request = new()
{
    Id = 44, EmployeeId = 420, WorkingDays = 5,
    Period = new DateRange(new DateOnly(2026, 12, 21), new DateOnly(2026, 12, 25)),
    Attachment = new LeaveAttachment { FileName = "report.pdf", ContentType = "application/pdf" }
};
request.Approvals.Add(new Approval { ApproverId = 7, Step = ApprovalStep.Manager, DecidedAt = DateTimeOffset.UtcNow });
request.Approvals.Add(new Approval { ApproverId = 7, Step = ApprovalStep.Director, DecidedAt = DateTimeOffset.UtcNow });
db.AddRange(grace, alan, request);
db.SaveChanges();
Console.WriteLine($"Approvals saved: {db.Approvals.Count()}");

// Composition: talep silinince onaylar da silinir
db.LeaveRequests.Remove(request);
db.SaveChanges();
Console.WriteLine($"After deleting request 44: approvals = {db.Approvals.Count()}");

// Association: talebi olan bir çalışan silinemez
db.LeaveRequests.Add(new LeaveRequest { Id = 45, EmployeeId = 420, WorkingDays = 2,
    Period = new DateRange(new DateOnly(2026, 11, 19), new DateOnly(2026, 11, 20)) });
db.SaveChanges();
db.ChangeTracker.Clear();
try
{
    db.Employees.Remove(new Employee { Id = 420 });
    db.SaveChanges();
}
catch (DbUpdateException ex)
{
    Console.WriteLine(ex.InnerException?.Message);
}

public enum ApprovalStep { Manager, Director }

public readonly record struct DateRange(DateOnly Start, DateOnly End);

public sealed class LeaveAttachment
{
    public string FileName { get; set; } = "";
    public string ContentType { get; set; } = "";
}

public sealed class Employee
{
    public int Id { get; set; }
    public string FullName { get; set; } = "";
    public int? ManagerId { get; set; }
    public Employee? Manager { get; set; }
}

public sealed class LeaveRequest
{
    public int Id { get; set; }
    public int EmployeeId { get; set; }
    public Employee Employee { get; set; } = null!;
    public DateRange Period { get; set; }
    public int WorkingDays { get; set; }
    public LeaveAttachment? Attachment { get; set; }
    public List<Approval> Approvals { get; } = [];
}

public sealed class Approval
{
    public int Id { get; set; }
    public int LeaveRequestId { get; set; }
    public int ApproverId { get; set; }
    public Employee Approver { get; set; } = null!;
    public ApprovalStep Step { get; set; }
    public DateTimeOffset DecidedAt { get; set; }
}

public sealed class HrPortalDbContext(DbContextOptions<HrPortalDbContext> options) : DbContext(options)
{
    public DbSet<Employee> Employees => Set<Employee>();
    public DbSet<LeaveRequest> LeaveRequests => Set<LeaveRequest>();
    public DbSet<Approval> Approvals => Set<Approval>();

    protected override void OnModelCreating(ModelBuilder b)
    {
        // Self-association: manager 0..1, reports 0..*
        b.Entity<Employee>().HasOne(e => e.Manager).WithMany().HasForeignKey(e => e.ManagerId);

        b.Entity<LeaveRequest>(r =>
        {
            // Association: talep bir çalışana ait ama çalışanın parçası değil
            r.HasOne(x => x.Employee).WithMany().HasForeignKey(x => x.EmployeeId).OnDelete(DeleteBehavior.Restrict);
            // «dataType» değer olarak aynı tabloda
            r.ComplexProperty(x => x.Period);
            // Composition [0..1]: ek dosya talebin parçası
            r.OwnsOne(x => x.Attachment);
            // Composition [0..*] {ordered}: onaylar talebin parçası
            r.HasMany(x => x.Approvals).WithOne().HasForeignKey(a => a.LeaveRequestId).OnDelete(DeleteBehavior.Cascade);
        });

        b.Entity<Approval>(a =>
        {
            a.HasOne(x => x.Approver).WithMany().HasForeignKey(x => x.ApproverId).OnDelete(DeleteBehavior.Restrict);
            // Her basamakta tek karar; aynı kişi iki basamakta onaylayabilir
            a.HasIndex(x => new { x.LeaveRequestId, x.Step }).IsUnique();
        });
    }
}
CREATE TABLE "LeaveRequests" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_LeaveRequests" PRIMARY KEY AUTOINCREMENT,
    "EmployeeId" INTEGER NOT NULL,
    "WorkingDays" INTEGER NOT NULL,
    "Attachment_FileName" TEXT NULL,
    "Attachment_ContentType" TEXT NULL,
    "Period_End" TEXT NOT NULL,
    "Period_Start" TEXT NOT NULL,
    CONSTRAINT "FK_LeaveRequests_Employees_EmployeeId" FOREIGN KEY ("EmployeeId") REFERENCES "Employees" ("Id") ON DELETE RESTRICT
);
CONSTRAINT "FK_Employees_Employees_ManagerId" FOREIGN KEY ("ManagerId") REFERENCES "Employees" ("Id")
CONSTRAINT "FK_Approvals_Employees_ApproverId" FOREIGN KEY ("ApproverId") REFERENCES "Employees" ("Id") ON DELETE RESTRICT,
CONSTRAINT "FK_Approvals_LeaveRequests_LeaveRequestId" FOREIGN KEY ("LeaveRequestId") REFERENCES "LeaveRequests" ("Id") ON DELETE CASCADE
Approvals saved: 2
After deleting request 44: approvals = 0
SQLite Error 19: 'FOREIGN KEY constraint failed'.

Çıktıyı yukarıdan aşağı diyagramla eşleyelim. LeaveRequests tablosunda Attachment_FileName ve Period_Start kolonları var: iki composition'ın ikisi de ayrı tablo değil, sahibinin satırına yazılmış parçalar. Talep ile çalışan arasındaki association ON DELETE RESTRICT oldu, onay ile talep arasındaki composition ON DELETE CASCADE. Self-association'ın foreign key'inde hiçbir ON DELETE yok; bu, nullable foreign key'ler için EF Core'un varsayılanı olan ClientSetNull'ın veritabanına yansıması: veritabanı engeller, EF Core takip ettiği çalışanların ManagerId'sini kendisi null'a çeker. Grace'in iki onayı bu kez sorunsuz kaydedildi, talep silinince ikisi de gitti, talebi olan Alan'ı silme denemesi ise veritabanı tarafından reddedildi.

Association Class mı, Ayrı Class mı?

Önce Tahmin Et'teki model ile çalışan model arasındaki fark tek bir figürde:

05-association-class-tr.png

Association class, bir association'ın kendisinin attribute taşıdığı durumlar için güzel bir notasyondur: kesikli çizgiyle association'a bağlanan class, o link'in verisidir. EF Core'da karşılığı tam olarak UsingEntity<Approval> ile kurulan, payload'lı bir many-to-many'dir ve join entity'nin varsayılan primary key'i iki foreign key'in birleşimidir. Model ve kod tutarlı; sorun, modelin requirement'la tutarsız olması. R7 (requirement 7, ikinci yazıdaki listeden) yönetici ve direktör onayını iki ayrı basamak olarak tanımlıyor; basamak, link'in değil onayın kimliğinin parçası.

Çözüm, Approval'ı bir association class olmaktan çıkarıp kendi kimliği olan bir class'a yükseltmek: talebe composition ile bağlı (0..*, {ordered}), onaycıya düz bir association ile. Asıl kural da artık doğru yerde: "bir talepte her basamak için tek karar" kuralı, (leaveRequest, step) üzerinde unique bir kısıt ve kodda HasIndex(...).IsUnique(). Genel ders şu: bir association class'ın attribute'larından biri, aynı iki object arasında birden fazla ilişki olabileceğini ima ediyorsa (tarih, basamak, sıra numarası), o artık bir association class değil, kendi başına bir class'tır.

Aynı Diyagram Mermaid ile

Mermaid'in class diagram'ı ilişki türlerinin hepsini destekliyor. İzin modülünün ilişkilerini ve figürdeki üç örnek ilişkiyi şöyle yazdım ve yerelde Mermaid CLI (Command Line Interface) 12.2.0 ile render ettim:

classDiagram
    direction LR
    Department "1" o-- "0..*" Employee
    Employee "0..*" --> "0..1" Employee : manager
    LeaveRequest "0..*" --> "1" Employee : employee
    LeaveRequest "1" *-- "0..*" Approval : approvals
    Approval "0..*" --> "1" Employee : approver
    LeaveRequest "1" *-- "0..1" LeaveAttachment
    LeaveRequest "0..*" --> "1" LeaveType
    LeaveRequestEvent <|-- LeaveRequestApproved
    IWorkingDayCalculator <|.. WorkingDayCalculator
    WorkingDayCalculator ..> PublicHoliday
    class Approval {
        +step: ApprovalStep
        +decidedAt: DateTimeOffset
        +comment: string [0..1]
    }
    class LeaveRequestEvent {
        <<abstract>>
    }
    class IWorkingDayCalculator {
        <<interface>>
    }

Söz dizimi okları neredeyse çizer gibi yazdırıyor: o-- içi boş elmas, *-- dolu elmas, <|-- generalization, <|.. realization, ..> dependency, tırnak içindeki metinler de iki uçtaki multiplicity'ler. Üç eksik var. Association class ve qualified association için bir söz dizimi yok; bu iki notasyonu Mermaid'de çizemezsin. Abstract class'ı <<abstract>> annotation'ıyla işaretlemek, UML'deki italik ad ya da {abstract} yerine bir stereotype gibi görünüyor. Bir de etiket ilişkiye yazılıyor, uca değil; manager bir role adı olmasına rağmen ilişkinin ortasında duruyor.

Sık Düşülen Tuzaklar

Her ilişkiye elmas koymak. "Talep çalışana ait, o zaman elmas" düşüncesi, sahiplik ile ilişkiyi karıştırır. Composition'ın testi ömürdür: bütün silinince parça da anlamsızlaşıyor mu? Çalışan için cevap hayır.

İçi boş elmasla bir şey anlatmaya çalışmak. Aggregation'ın UML 2.5.1'de kesin bir anlamı yok; okuyan herkes kendi anlamını yükler. Söylemek istediğini multiplicity ile ya da bir notla söyle.

Gereksiz iki yönlü navigability. Her association'ı iki uçtan da oklu çizmek, kodda her iki class'a da navigation property ekletir. EF Core'da bu, serialization döngülerine ve yanlışlıkla yüklenen dev object graph'larına açılan kapıdır. Gerçekten kullanılan yönü çiz.

Diyagram ile OnDelete'in çelişmesi. EF Core'un zorunlu ilişkilerdeki varsayılan cascade delete'i, diyagramda düz çizgi olan bir association'ı veritabanında composition'a çevirir. Composition olmayan her zorunlu foreign key için davranışı açıkça yaz.

Association class'ı her many-to-many'ye uygulamak. Link'in verisi aynı iki object arasında tekrar edebiliyorsa (tarihli onaylar, sıralı adımlar), association class yanlış modeldir. Join entity'yi kendi kimliği olan bir class'a yükselt.

Dependency ile association'ı karıştırmak. Constructor'dan alınıp field'da tutulan her şey association'dır; yalnızca bir method içinde kullanılan şey dependency'dir. Bu fark, bir değişikliğin hangi class'ları etkileyeceğini okurken önemlidir.

Küçük Bir Deneme

★☆☆ İlişkiler figüründeki Department ile Employee arasındaki aggregation'ı kaldır ve yerine düz bir association çiz. Uçlara hangi multiplicity'leri yazdın? CASE metnindeki "departman direktörü" bilgisini de ikinci bir association olarak ekle; role adı ne olmalı?

★★☆ Örnek koddaki LeaveAttachment'ı OwnsOne yerine EF Core 10'un optional complex type desteğiyle ComplexProperty olarak map et. Üretilen tablo değişiyor mu? İki yöntemin farkını EF Core'un "Owned Entity Types" ve "Complex types" sayfalarından okuyup bir paragrafta anlat.

★★★ LeaveBalance'ı ekleyip qualified association'ı gerçekle: (EmployeeId, Year, LeaveTypeId) üzerinde unique index tanımla ve Ada için aynı yıl ve türde ikinci bir bakiye eklemeyi dene. Hatanın metnini diyagramdaki 0..1 ile eşleştir.

Bir sonraki yazıda bu class diagram'ı iki seviyeye ayırıyoruz: operation'sız, iş birimiyle konuşulan domain model ve service'leri, repository'leri içeren design class diagram. Ardından diyagramdan koda ve puml-gen ile koddan diyagrama gidip ikisini senkron tutmanın yollarına bakacağız.