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

Object Diagram ve Package Diagram: Anlık Görüntü ve .NET Solution'ının Bağımlılık Haritası

Mert Özen 7 Kas 2026 12 dk 4 görüntülenme
Object Diagram ve Package Diagram: Anlık Görüntü ve .NET Solution'ının Bağımlılık Haritası

Object diagram belli bir andaki somut instance'ları ve link'leri çizerek class diagram'daki multiplicity hatalarını yakalar, package diagram ise project'ler arasındaki bağımlılıkları göstererek katman ihlallerini görünür kılar. Yazıda #42 talebinin snapshot'ı yanlış bir 0..1'i ortaya çıkarıyor, solution'ın package diagram'ı da csproj dosyalarından üretilip Domain'e sızan EF Core bağımlılığı CI'da yakalanı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 ile, PlantUML çıktıları PlantUML 1.2026.9 ile denenmiştir.

Önce Tahmin Et: Yöneticinin Onayı Nereye Gitti?

İK (insan kaynakları) Portalı'nın ilk tasarım toplantısında beyaz tahtaya şu ilişki çizilmiş: LeaveRequest "1" *-- "0..1" Approval. Mantıklı görünüyor; bir talep onaylanır ya da onaylanmaz. Kod da diyagramı sadakatle izlemiş:

public class LeaveRequest
{
    public int Id { get; set; }
    public string Status { get; set; } = "PendingManagerApproval";
    public Approval? Approval { get; set; }
}

public class Approval
{
    public int Id { get; set; }
    public int LeaveRequestId { get; set; }
    public required string Approver { get; set; }
    public required string Step { get; set; }
}

// OnModelCreating içinde
b.Entity<LeaveRequest>().HasOne(r => r.Approval).WithOne()
    .HasForeignKey<Approval>(a => a.LeaveRequestId).OnDelete(DeleteBehavior.Cascade);

Ada'nın 21–25 Aralık yıllık izni (#42) beş iş günü tutuyor, yani R7'ye göre önce yöneticisi Alan, sonra direktörü Grace onaylamalı. Kod iki onayı sırayla kaydediyor:

LeaveRequest req = new() { Approval = new() { Approver = "Alan Turing", Step = "Manager" } };
db.LeaveRequests.Add(req);
await db.SaveChangesAsync();

req.Approval = new() { Approver = "Grace Hopper", Step = "Director" };
req.Status = "Approved";
await db.SaveChangesAsync();

İkinci SaveChangesAsync'ten sonra Approvals tablosunda kaç satır olur? Exception bekler misin? Ve altı ay sonra denetçi "bu izni yönetici onayladı mı?" diye sorduğunda cevabı nereden bulursun?

Cevap: Sessiz Bir DELETE

EF Core'un veritabanına gönderdiği SQL (Structured Query Language) komutu ve tablonun son hâli:

SET NOCOUNT ON;
DELETE FROM [Approvals]
OUTPUT 1
WHERE [Id] = @p0;
UPDATE [LeaveRequests] SET [Status] = @p1
OUTPUT 1
WHERE [Id] = @p2;
INSERT INTO [Approvals] ([Approver], [LeaveRequestId], [Step])
OUTPUT INSERTED.[Id]
VALUES (@p3, @p4, @p5);

Approval 2: Director by Grace Hopper

Exception yok. EF Core, one-to-one ilişkide yeni dependent gelince eskisini orphan sayıp sildi; Alan'ın onayı tarihten silindi. Kod doğru çalıştı, sorun modelde: multiplicity 0..1 değil, en az 0..* (pratikte 0..2) olmalıydı. Class diagram bu hatayı göstermedi, çünkü class diagram genel kuralı anlatır; "bir talep" deyince aklımızdaki tipik talep tek onaylıydı. Hatayı yakalayan şey somut bir örnek olurdu: #42'nin onaydan sonraki hâlini çizmek. Bu yazının ilk konusu tam olarak bu çizim, yani object diagram. İkinci konu, aynı somutluğu bu kez kodun fiziksel yapısına uygulayan package diagram.

UML 2.5.1'de object diagram ayrı bir metamodel getirmez; class diagram'ın notasyonunu instance'lar için kullanır. Spesifikasyonun Classification bölümündeki InstanceSpecification kavramı, belli bir anda var olan bir object'i (ya da bir link'i) temsil eder. Notasyonun parçaları şunlar:

  • Instance: kutunun başlığında req42 : LeaveRequest, yani instance adı, iki nokta ve class adı, tamamı altı çizili. Adı önemli değilse anonim yazılır: : Approval.
  • Slot: attribute'un o andaki değeri, status = Approved biçiminde. Bütün attribute'ları yazmak zorunda değilsin; anlatmak istediğin değerler yeter.
  • Link: iki instance arasındaki düz çizgi, bir association'ın instance'ı. Uçlarına multiplicity yazılmaz, çünkü link her zaman tam iki object'i bağlar; çizgi sayısı multiplicity'nin kendisidir. Role adı yazılabilir.

07-object-snapshot-tr.png

Şekil #42'nin direktör onayından sonraki anlık görüntüsü. req42'den Approval instance'larına iki link çıkıyor: a1 (Manager, Alan) ve a2 (Director, Grace). Class diagram'daki 0..1 bu resimle aynı anda doğru olamaz. Aynı resim başka bir multiplicity'yi de doğruluyor: alan'dan grace'e "manager" link'i var ama grace'ten yukarı giden yok. Demek ki Employee.Manager ilişkisi 1 değil 0..1; kodda int? ManagerId yazmamızın sebebi bu.

Object Diagram ile Class Diagram'ı Doğrulamak

Object diagram'ın en değerli kullanımı, soyut bir class diagram'ı somut senaryolarla sınamak. Yöntem basit: requirement'lardan birkaç "zor" senaryo seç, her biri için bir anlık görüntü çiz ve her link sayısını class diagram'daki multiplicity ile karşılaştır. İzin modülü için iyi adaylar şunlar: beş günlük izin (iki onay, R7), reddedilip yeniden gönderilen talep (aynı tarihler için iki ayrı LeaveRequest, R4), yıl dönümünde iki yıla yayılan izin (hangi yılın LeaveBalance'ı?), İK'nın düzelttiği bakiye (BalanceAdjustment, R12).

Kodda object diagram'ın en yakın karşılığı bir test fixture'ı ya da seed verisi: belli bir andaki object graph'ını kuran kod. Bu yüzden diyagramı elle çizmek yerine gerçek object'lerden üretmek de mümkün. Aşağıdaki küçük sınıf, reflection ile public property'leri dolaşıp PlantUML object diagram'ı yazıyor; isim verdiğin object'ler arasındaki referansları link'e, kalan değerleri slot'a çeviriyor:

public sealed class ObjectDiagram
{
    private readonly Dictionary<object, string> names = new(ReferenceEqualityComparer.Instance);
    private readonly StringBuilder objects = new();
    private readonly StringBuilder links = new();
    private readonly HashSet<(string, string)> linked = [];

    public ObjectDiagram Add(object instance, string name)
    {
        names[instance] = name;
        return this;
    }

    public string Render()
    {
        foreach ((object instance, string name) in names)
        {
            objects.AppendLine($"object \"{name} : {instance.GetType().Name}\" as {name} {{");
            foreach (PropertyInfo p in instance.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance))
            {
                object? value = p.GetValue(instance);
                if (value is null || names.ContainsKey(value))
                {
                    if (value is not null) Link(name, names[value], p.Name);
                    continue;
                }
                if (value is IEnumerable items and not string)
                {
                    foreach (object item in items)
                        if (names.TryGetValue(item, out string? target)) Link(name, target, p.Name);
                    continue;
                }
                if (p.Name.EndsWith("Id")) continue;
                objects.AppendLine($"  {p.Name} = {Format(value)}");
            }
            objects.AppendLine("}");
        }
        return $"@startuml\n{objects}{links}@enduml";
    }

    private void Link(string from, string to, string role)
    {
        // Çift yönlü association'ı tek link olarak çiz
        if (linked.Contains((to, from))) return;
        linked.Add((from, to));
        links.AppendLine($"{from} -- {to} : {role}");
    }

    private static string Format(object value) => value switch
    {
        DateRange r => $"{r.Start:yyyy-MM-dd}..{r.End:yyyy-MM-dd}",
        DateTimeOffset d => d.ToString("yyyy-MM-dd"),
        DateOnly d => d.ToString("yyyy-MM-dd"),
        string s => $"\"{s}\"",
        _ => value.ToString() ?? ""
    };
}

Gerçek domain class'larıyla #42 senaryosunu kurup (Submit, sonra Alan ve Grace'in Approve çağrıları) çalıştırınca çıktının bir kısmı:

object "req42 : LeaveRequest" as req42 {
  Period = 2026-12-21..2026-12-25
  WorkingDays = 5
  Reason = "Year-end trip"
  Status = Approved
  CreatedAt = 2026-11-02
  SubmittedAt = 2026-11-02
  RequiresDirectorApproval = True
}
object "a2 : Approval" as a2 {
  Step = Director
  Decision = Approved
  Comment = "Enjoy"
  DecidedAt = 2026-11-04
}
req42 -- ada : Employee
req42 -- annual : LeaveType
req42 -- a1 : Approvals
req42 -- a2 : Approvals
ada -- alan : Manager
alan -- grace : Manager
a1 -- alan : Approver
a2 -- grace : Approver

Bu çıktı PlantUML ile hatasız render oldu. İki not: PlantUML object adlarının altını çizmiyor, yani UML'in "altı çizili ad" kuralını araç uygulamıyor; elle çizdiğinde sen uygula. Ayrıca RequiresDirectorApproval gibi derived bir değer de slot olarak geldi; diyagramda tutup tutmamak senin kararın. Bu tür bir dump'ı bir unit test'in "beklenen durum"unu belgelemek için kullanabilirsin; diyagram kodla aynı anda değişir.

Package Diagram Notasyonu

Object diagram tek bir anın yakın çekimiydi; package diagram ise ters yönde, en uzak çekim. Package, UML'de model elemanlarını gruplayan ve onlara bir isim alanı veren yapı (spesifikasyonun Packages bölümü). Gösterimi sekmeli bir dosya klasörü: içeriği gösteriliyorsa ad sekmeye, gösterilmiyorsa gövdeye yazılır. Notasyonun pratikte kullandığın parçaları:

  • Nesting: package başka bir package'ın içinde çizilir. Tam ad HrPortal::Leave::Domain biçimindedir; UML'de ayraç ::, C#'ta nokta.
  • Dependency: kesikli çizgi ve açık ok ucu. "Bu package'taki bir eleman, oradaki bir elemana ihtiyaç duyuyor; o değişirse bu etkilenebilir" demektir.
  • «import» ve «access»: ikisi de bir package'ın elemanlarını, ad önüne package yazmadan kullanmayı sağlar. Fark görünürlükte: «import» public'tir, içe alınan adlar seni import edenlere de görünür; «access» private'tır, adlar sende kalır.
  • Visibility: package içindeki elemanların önüne + (public) ve - (private) konabilir. C#'taki public ve internal ayrımının package seviyesindeki karşılığı.

.NET Karşılığı: Namespace mi, Project mi?

UML package mantıksal bir gruplamadır; .NET'te iki aday var. Namespace isim alanı açısından birebir karşılık: HrPortal.Leave.Domain namespace'i HrPortal::Leave::Domain package'ı. Ama namespace bir bağımlılık sınırı değildir; aynı project'teki iki namespace birbirini serbestçe kullanabilir. Bağımlılığı derleyiciye zorlatan sınır project'tir (assembly): ProjectReference yoksa kullanamazsın. Bu yüzden bağımlılık haritası çizerken package = project almak pratikte en çok işe yarayan eşleme. Project'in fiziksel tarafı, yani deploy edilen .dll, UML'de artifact ve component konusudur; onları serinin Component ve Deployment Diagram yazılarında çizeceğiz.

07-package-map-tr.png

Şekilde HrPortal ve Leave package'ları iç içe; dört project onların içinde. Oklar dışarıdan içeriye akıyor: Api → Infrastructure → Application → Domain. Infrastructure'ın Application'a bağımlı olması dikkat çekici: interface'ler (ILeaveRequestRepository, IBalanceService) Application'da tanımlı, implementation'ları Infrastructure'da. Bu, dependency inversion'ın package diagram'daki imzası; SOLID (Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, Dependency Inversion) serisinin Dependency Inversion Principle yazısında anlattığımız şeyin kuş bakışı görüntüsü.

.NET'in bir ayrıntısı UML'deki «import» farkına çok benziyor. SDK (Software Development Kit) stilindeki project'lerde ProjectReference geçişlidir: HrPortal.Api yalnız Infrastructure'a referans verdiği hâlde Domain'deki LeaveRequest'i ve Application'daki ILeaveRequestService'i doğrudan kullanabiliyor. Yani project referansı public import gibi davranıyor. Geçişliliği kapatınca derleyici hemen itiraz ediyor:

<PropertyGroup>
    <DisableTransitiveProjectReferences>true</DisableTransitiveProjectReferences>
</PropertyGroup>
Program.cs(1,22): error CS0234: The type or namespace name 'Application' does not exist in the namespace 'HrPortal.Leave' (are you missing an assembly reference?)
Program.cs(2,22): error CS0234: The type or namespace name 'Domain' does not exist in the namespace 'HrPortal.Leave' (are you missing an assembly reference?)

Hangisini seçersen seç, diyagramda neyi çizdiğini bil: geçişli bağımlılıkları da göstermek istiyorsan Api'den Domain'e açık bir ok çiz, yoksa yalnız doğrudan referansları çiz ve bunu diyagramın notunda söyle.

Koddan Package Diagram ve Katman İhlalini Yakalamak

Package diagram'ı elle çizmek yerine .csproj dosyalarından üretmek kolay ve güvenilir, çünkü bağımlılık bilgisi zaten orada. ProjectReference ve PackageReference elemanlarını okuyup PlantUML yazan küçük bir fonksiyon:

static string PackageDiagram(string srcDir)
{
    List<string> lines = ["@startuml", "set separator none", "skinparam packageStyle folder"];
    List<string> deps = [];
    SortedSet<string> nugets = [];
    foreach (string csproj in Directory.GetFiles(srcDir, "*.csproj", SearchOption.AllDirectories).Order())
    {
        string name = Path.GetFileNameWithoutExtension(csproj);
        lines.Add($"package \"{name}\" as {Alias(name)} {{\n}}");
        XDocument doc = XDocument.Load(csproj);
        foreach (XElement r in doc.Descendants("ProjectReference"))
        {
            string target = Path.GetFileNameWithoutExtension(r.Attribute("Include")!.Value.Replace('\\', '/'));
            deps.Add($"{Alias(name)} ..> {Alias(target)}");
        }
        foreach (XElement p in doc.Descendants("PackageReference"))
        {
            string package = p.Attribute("Include")!.Value;
            nugets.Add(package);
            deps.Add($"{Alias(name)} ..> {Alias(package)}");
        }
    }
    lines.AddRange(nugets.Select(n => $"package \"{n}\" as {Alias(n)} <<NuGet>> {{\n}}"));
    return string.Join('\n', [.. lines, .. deps, "@enduml"]);
}

static string Alias(string name) => name.Replace("HrPortal.", "").Replace("Microsoft.", "").Replace(".", "");

İki pratik ayrıntı, ikisi de denerken çıktı. .csproj içindeki yol Windows ayracıyla (..\HrPortal.Leave.Domain\...) yazılı; macOS ve Linux'ta Path.GetFileNameWithoutExtension ters bölüyü ayraç saymadığı için önce /'a çevirmek gerekiyor. PlantUML tarafında ise noktalı adları tırnak ve alias ile vermezsen Microsoft.EntityFrameworkCore.SqlServer iç içe üç package ve bir class olarak çiziliyor; stereotype'lı package'ı da tek satırda {} ile yazınca PlantUML 1.2026.9 sonraki satırda hata verdi, gövdeyi iki satıra bölmek gerekti.

Şimdi bir katman ihlali yapalım. Biri Employee'deki unique index'i configuration yerine attribute ile tanımlamak istiyor ve Domain'e EF Core paketini ekliyor:

using Microsoft.EntityFrameworkCore;

namespace HrPortal.Leave.Domain;

[Index(nameof(EmployeeNumber), IsUnique = true)]
public class Employee
{
    public int Id { get; set; }
    // ...
}

Kod derleniyor, testler geçiyor. Üretilen package diagram'da ise yeni bir satır beliriyor:

Api ..> LeaveInfrastructure
LeaveApplication ..> LeaveDomain
LeaveDomain ..> EntityFrameworkCore
LeaveInfrastructure ..> LeaveApplication
LeaveInfrastructure ..> EntityFrameworkCoreSqlServer
LeaveInfrastructure ..> ExtensionsHostingAbstractions

Diyagram ihlali gösteriyor ama birinin bakması gerekiyor. Kuralı CI (Continuous Integration) içinde çalışan bir kontrole çevirmek için derlenmiş assembly'lerin referanslarına bakabilirsin:

static int Check(string binDir)
{
    Dictionary<string, string[]> allowed = new()
    {
        ["HrPortal.Leave.Domain"] = ["System."],
        ["HrPortal.Leave.Application"] = ["System.", "HrPortal.Leave.Domain"],
    };
    int violations = 0;
    foreach ((string assembly, string[] prefixes) in allowed)
    {
        Assembly asm = Assembly.LoadFrom(Path.Combine(binDir, assembly + ".dll"));
        foreach (AssemblyName dep in asm.GetReferencedAssemblies())
        {
            bool ok = prefixes.Any(p => dep.Name!.StartsWith(p, StringComparison.Ordinal));
            Console.WriteLine($"{(ok ? "OK  " : "FAIL")} {assembly} -> {dep.Name}");
            if (!ok) violations++;
        }
    }
    Console.WriteLine($"{violations} violation(s)");
    return violations == 0 ? 0 : 1;
}
OK   HrPortal.Leave.Domain -> System.Runtime
OK   HrPortal.Leave.Domain -> System.Collections
FAIL HrPortal.Leave.Domain -> Microsoft.EntityFrameworkCore.Abstractions
OK   HrPortal.Leave.Application -> System.Runtime
OK   HrPortal.Leave.Application -> HrPortal.Leave.Domain
OK   HrPortal.Leave.Application -> System.Collections
1 violation(s)

İlginç bir ayrıntı: ihlal paket adıyla değil Microsoft.EntityFrameworkCore.Abstractions olarak görünüyor, çünkü [Index] attribute'u o assembly'de tanımlı. Derleyici assembly'ye yalnız gerçekten kullanılan referansları yazar; paketi ekleyip hiçbir tipini kullanmasaydık bu kontrol bir şey bulamazdı, .csproj'dan üretilen diyagram ise yine gösterirdi. İki bakış birbirini tamamlıyor. Gerçek projelerde bu kuralları ArchUnitNET ya da NetArchTest gibi kütüphanelerle unit test olarak yazmak daha okunaklı; kural mantığı aynı. Project'lerin nasıl bölüneceğinin ilkeleri, örneğin ADP (Acyclic Dependencies Principle) ve SDP (Stable Dependencies Principle), SOLID serisinin Component Prensipleri yazısında.

Sık Düşülen Tuzaklar

Object diagram'a multiplicity yazmak. Link iki object'i bağlar; "kaç tane" sorusunun cevabı çizgi sayısıdır. Uçlara 0..* yazarsan class diagram çizmiş olursun.

Object diagram'da instance adının altını çizmemek ya da class adını unutmak. req42 tek başına bir class gibi okunur; req42 : LeaveRequest altı çizili olmalı.

Yalnız "mutlu yol" snapshot'ı çizmek. Bir günlük, tek onaylı izin her class diagram'ı doğrular. Doğrulama gücü sınır senaryolarından gelir: iki onay, iki yıla yayılan izin, reddedilip tekrar gönderilen talep.

Package'ı namespace ile karıştırıp bağımlılık sınırı sanmak. Aynı project içindeki Domain ve Infrastructure klasörleri birbirini serbestçe kullanır; diyagramda ayrı çizmen derleyiciyi durdurmaz. Sınırı zorlamak istiyorsan ayrı project ya da otomatik bir kontrol gerekir.

Package diagram'ı elle çizip bırakmak. Bağımlılıklar .csproj'da duruyor; elle çizilmiş bir harita ilk dotnet add reference'ta eskir. Üret, commit'le, CI'da farkı kontrol et.

Ne Zaman Kullanma

Her class diagram için object diagram çizme; ilişkiler basit ve tartışmasızsa gereksiz. Multiplicity'nin, composition'ın ya da recursive bir ilişkinin (yönetici zinciri gibi) tartışıldığı yerde ise beş dakikalık bir snapshot saatlerce süren bir yanlış anlamayı önler. Tek project'lik küçük bir uygulama için package diagram çizme; harita, ancak birden fazla project ya da modül olduğunda bir şey anlatır.

Küçük Bir Deneme

★☆☆ Ada'nın 2026'daki bakiye durumunu (EntitledDays 14, UsedDays 11) ve İK uzmanı Margaret'ın yaptığı +2 günlük bir BalanceAdjustment'ı object diagram olarak çiz. LeaveBalance ile BalanceAdjustment arasındaki multiplicity ne olmalı?

★★☆ 30 Aralık 2026 – 5 Ocak 2027 arası bir yıllık izin talebinin object diagram'ını çiz. Talep hangi yılın LeaveBalance'ına bağlanıyor? Snapshot sana class diagram'da ya da Submit akışında neyi değiştirmen gerektiğini söylüyor?

★★★ Package diagram üreticisini, Domain'den herhangi bir PackageReference'a giden oku kırmızı çizecek (PlantUML'de ..> X #line:red) şekilde genişlet ve Check fonksiyonunu HrPortal.Api'nin HrPortalDbContext'i doğrudan kullanmasını da yakalayacak hâle getir. Bu ikinci kural için assembly referansı yeterli mi, yoksa tip seviyesinde bakman mı gerekir?

Bir sonraki yazıda yapıdan davranışa geçiyoruz: sequence diagram ile Angular formundan çıkan POST /leave-requests isteğini endpoint, service, repository ve EF Core üzerinden SQL Server'a kadar takip edeceğiz ve her lifeline'ın kodda neye karşılık geldiğini göreceğiz.