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

Class Diagram 1: Class, Attribute, Operation, Visibility, Multiplicity ve Stereotype'lar

Mert Özen 7 Kas 2026 13 dk 7 görüntülenme
Class Diagram 1: Class, Attribute, Operation, Visibility, Multiplicity ve Stereotype'lar

Class diagram'daki her işaret kodda bir karardır: [0..1] nullable tip, [0..*] collection, eğik çizgi hesaplanan property, altı çizili satır static üye, {readOnly} hiç değişmeyen property demektir. Class kutusunun üç bölmesi, attribute ve operation söz dizimi, C# ile örtüşmeyen visibility köşeleri, interface, enumeration, data type ve stereotype'lar ile aynı diyagramın Mermaid hâli.

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

Aşağıdaki class'ı bir class diagram'a çevireceksin. Her property'nin önüne hangi visibility işareti gelir, hangilerinin yanına [0..1] yazılır, hangisinin altı çizilir, hangisinin adı / ile başlar? Bir de şunu tahmin et: proje Nullable açıkken derlendiğinde derleyici kaç uyarı verir ve hangi satıra?

LeaveRequest request = new();
Console.WriteLine(request.RequiresDirectorApproval);

public sealed class LeaveRequest
{
    public const int DirectorApprovalThreshold = 3;
    private readonly List<Approval> _approvals = [];

    public int Id { get; private set; }
    public DateRange Period { get; private set; }
    public string? Reason { get; private set; }
    public string RejectionReason { get; private set; }
    public LeaveStatus Status { get; private set; } = LeaveStatus.Draft;
    public int WorkingDays { get; private set; }
    public bool RequiresDirectorApproval => WorkingDays > DirectorApprovalThreshold;
    public IReadOnlyList<Approval> Approvals => _approvals;

    internal void Submit(int workingDays) => WorkingDays = workingDays;
}

public readonly record struct DateRange(DateOnly Start, DateOnly End);
public sealed record Approval(int ApproverId, DateTimeOffset DecidedAt);
public enum LeaveStatus { Draft, PendingManagerApproval, PendingDirectorApproval, Approved, Rejected, Cancelled }

Derleyici [1] ile [0..1] Farkını Biliyor

Tek bir uyarı var:

Program.cs(12,19): warning CS8618: Non-nullable property 'RejectionReason' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

Reason ile RejectionReason aynı tipte iki property gibi duruyor ama C# 8'den beri gelen nullable reference types ile ikisi farklı şeyler söylüyor. string? Reason "açıklama olmayabilir" der; UML'de bu reason: string [0..1] demektir. string RejectionReason ise "her zaman bir değer vardır" der; UML'de rejectionReason: string [1], ya da hiç yazılmamış multiplicity, çünkü bir attribute'un varsayılan multiplicity'si 1'dir. Derleyici bu sözü tutmadığımızı yakaladı: constructor bittiğinde property hâlâ null. Asıl tasarım hatası ise başka yerde: red gerekçesi yalnızca reddedilmiş talepte vardır, yani [0..1] olmalıydı. Ya da daha iyisi, talebin değil reddin, yani Approval'ın bir attribute'u olmalıydı.

Bu yazının konusu tam olarak bu: class diagram'daki her işaret, kodda bir karara karşılık gelir. Tahmin sorusunun geri kalanını cevaplamak için önce class kutusunun anatomisine bakalım.

Class Kutusunun Üç Bölmesi

UML (Unified Modeling Language) 2.5.1'de bir class, üç bölmeli bir dikdörtgendir. En üstte adı, ortada attribute'ları, altta operation'ları. Aşağıdaki figür, seride kullanacağımız LeaveRequest'in design seviyesindeki hâli; numaralı rozetler, sağdaki açıklamalarla eşleşiyor:

04-class-anatomy-tr.png

Bölmelerin hepsini göstermek zorunda değilsin. Conceptual bir modelde yalnızca ad bölmesi çizilir; analysis seviyesinde attribute'lar eklenir; operation bölmesi genelde design seviyesinde gelir. Bir bölmeyi gizlemek "bu class'ın operation'ı yok" demek değildir, "bu diyagramda göstermiyorum" demektir. İlk yazıdaki model ve diyagram ayrımı burada da geçerli.

Ad bölmesinde class'ın adı kalın ve ortalı yazılır. Class abstract ise ad italik yazılır ya da adın altına {abstract} eklenir; C#'ta abstract class. Elle çizilen diyagramlarda italik kolayca gözden kaçtığı için {abstract} yazmak daha güvenlidir. Adın üstüne «interface» gibi bir keyword ya da stereotype gelebilir; onlara birazdan döneceğiz.

Attribute Söz Dizimi: Her Parçanın C# Karşılığı

UML 2.5.1'de bir attribute satırının genel biçimi şöyle:

[visibility] [/] name [: type] [[multiplicity]] [= default] [{modifiers}]

Köşeli parantezler isteğe bağlı parçaları gösteriyor; yalnızca ad zorunlu. Figürdeki satırları bu kalıba yerleştirelim.

Visibility, satırın başındaki işarettir: + public, - private, # protected, ~ package. Tip, iki noktadan sonra gelir: id: int. Burada C# alışkanlığına dikkat; int id yazmak UML değildir. Design diyagramlarında tip olarak hedef dilin tiplerini (int, string, DateOnly) kullanmak yaygın ve doğrudur. UML'in kendi primitive'leri (Integer, String, Boolean, Real, UnlimitedNatural) daha çok dilden bağımsız analysis modellerinde görülür.

Multiplicity, tipten sonra köşeli parantezle yazılır ve bu attribute'un kaç değer taşıyabileceğini söyler. [1] tam bir değer, [0..1] boş olabilir, [*] ya da [0..*] sıfır ya da daha fazla, [1..*] en az bir. C# karşılıkları: [1] nullable olmayan tip, [0..1] string? ya da DateTimeOffset?, [0..*] bir collection. [1..*] için C#'ın tip sisteminde hazır bir karşılık yok; "en az bir" kuralını constructor'da ya da bir factory method'da kendin korursun.

Default değer eşittir işaretinden sonra gelir: status: LeaveStatus = Draft. C#'ta property initializer: = LeaveStatus.Draft.

Derived attribute, adının başındaki eğik çizgiyle gösterilir: /requiresDirectorApproval: bool. Değeri başka attribute'lardan hesaplanır ve ayrıca saklanmaz. C#'taki karşılığı expression-bodied bir property: => WorkingDays > DirectorApprovalThreshold. Entity Framework Core (EF Core) de setter'ı olmayan bu property'yi kolon olarak map etmez; diyagramdaki eğik çizgi, veritabanında kolon olmadığını da ima eder.

Static attribute'un altı çizilir. directorApprovalThreshold bir instance'a değil, class'ın kendisine ait; C#'ta const ya da static readonly.

Süslü parantezdeki modifier'lar en çok atlanan kısımdır. {readOnly}, değerin ilk atamadan sonra değiştirilemeyeceğini söyler: C#'ta getter-only property, init ya da readonly field. {ordered}, çok değerli bir attribute'un sırasının anlamlı olduğunu söyler: onaylar zaman sırasıyla tutulur, bu yüzden List<Approval>. {unique} çok değerli attribute'larda varsayılandır ve aynı değerin iki kez bulunamayacağını söyler; {nonunique} tekrarlara izin verir. {id} ise bu attribute'un class'ın kimliğinin parçası olduğunu belirtir; EF Core'daki primary key'in model seviyesindeki karşılığı.

Operation İmzası

Operation'ların söz dizimi attribute'lara benzer:

[visibility] name([direction] param: type [[multiplicity]] [= default], ...) [: returnType [[multiplicity]]] [{properties}]

Figürdeki + submit(workingDays: int, now: DateTimeOffset) satırının C# karşılığı public void Submit(int workingDays, DateTimeOffset now). Dönüş tipi yoksa iki nokta ve tip hiç yazılmaz; UML'de void diye bir tip yok. Parametrelerin önüne bir yön yazılabilir: in (varsayılan), out, inout ve return. C#'taki out parametresi UML'de out, ref parametresi inout ile gösterilir. C#'taki in modifier'ı ise "salt okunur referansla geç" demektir ve UML'in varsayılan in'iyle aynı şey değildir; o bir performans ayrıntısı, modelde gösterilmez.

Operation'ın sonundaki {query}, operation'ın sistemin durumunu değiştirmediğini söyler. DateRange.overlaps() böyle bir operation: iki aralığa bakar, hiçbir şeyi değiştirmez. C#'ta bunu derleyiciye söyletebileceğin en yakın araç, struct'larda readonly member'dır. Abstract operation'lar italik yazılır; interface'lerin operation'ları da bu yüzden çoğu araçta italik görünür.

Visibility'nin C# ile Örtüşmeyen Köşeleri

Dört UML işareti ile C#'ın altı erişim belirleyicisi birebir eşleşmez. + public ve - private sorunsuz. # protected da öyle. ~ package ise UML'de "aynı package içindekiler görebilir" demektir; C#'taki en yakın karşılığı internal, ama internal namespace'e değil assembly'ye göre çalışır. protected internal ve private protected'ın UML'de bir işareti yok; gerekiyorsa bir notla ya da kendi profilindeki bir stereotype'la belirtirsin.

Daha sık karşılaşacağın boşluk şu: C#'ta en yaygın property biçimi olan { get; private set; }'in UML'de tek bir karşılığı yok. Okuma public, yazma private. Üç yol var. Birincisi, attribute'u + ile çizip yazma kuralını operation'larla anlatmak; bu seri bu yolu kullanıyor, çünkü status'un yalnızca submit() ve cancel() ile değiştiği operation bölmesinden zaten okunuyor. İkincisi, attribute'u - ile çizip bir + getStatus() operation'ı eklemek; doğru ama gürültülü. Üçüncüsü ve yanlış olanı, {readOnly} yazmak: status değişiyor, yalnızca dışarıdan değiştirilemiyor. {readOnly}'i yalnızca gerçekten bir kez atanan id gibi property'ler için kullan.

Önce Tahmin Et'teki internal void Submit da bir tuzaktı. ~ submit(workingDays: int) olarak çizilir, ama okuyan kişi bunu "aynı package'taki class'lar çağırabilir" diye anlar. Kodda kastedilen "aynı assembly'deki uygulama katmanı çağırabilir" ise, bunu diyagramın yanına bir cümleyle yaz.

Interface, Enumeration, Data Type ve Stereotype'lar

Class'ın yanında, aynı dikdörtgen notasyonunu kullanan ama farklı anlam taşıyan üç tip daha var. Ad bölmesinin üstündeki guillemet (« ») içindeki kelime hangisi olduğunu söyler:

04-type-kinds-tr.png

«interface» davranış sözleşmesidir; attribute bölmesi genellikle boştur ve operation'lar abstract'tır. C#'ta interface ILeaveRequestRepository. Interface'lerin "lollipop" denen kısa gösterimini component diagram yazısında göreceğiz. «enumeration» sabit bir değer listesidir; literal'ler ayrı bir bölmede, tipsiz yazılır. C#'ta enum LeaveStatus. «dataType», instance'ları kimlikle değil yalnızca değerle ayırt edilen tiptir: iki DateRange'in başlangıç ve bitişi aynıysa ikisi aynıdır. C#'ta bunun en dürüst karşılığı readonly record struct; aşağıdaki çıktının son satırı bu eşitliği gösteriyor. «primitive» da bir data type'tır ama iç yapısı yoktur (int, DateOnly gibi).

Bu kelimelerin bir kısmı keyword, bir kısmı stereotype. Keyword'ler UML'in kendi notasyonunun parçasıdır: «interface», «enumeration», «dataType», önceki yazıdaki «include» ve «extend». Stereotype'lar ise bir profile ile gelir. Profile, UML'i bir alana göre genişletme mekanizmasıdır ve kendi diyagram türü vardır (profile diagram). UML 2.5.1'in kendi Standard Profile'ı da birkaç stereotype tanımlar: örneğin «Utility», bütün üyeleri static olan ve instance'ı olmayan bir class'ı işaretler; C#'taki static class'ın tam karşılığıdır.

Figürdeki «entity» ise başka bir hikâye. Standard Profile'da «Entity» adlı bir stereotype var, ama class'lar için değil, kalıcı bir iş kavramını temsil eden component'ler için tanımlı. Bizim kullandığımız «entity» ve «value object», DDD (Domain-Driven Design) dünyasının kavramları; kendi küçük profilimizden geliyor. Bunda bir sakınca yok, ama bir şartı var: diyagramda kendi stereotype'ını kullanıyorsan anlamını bir legend'da ya da notta yaz. Okuyan kişi «entity»'yi görünce EF Core entity'si mi, DDD entity'si mi, yoksa Jacobson'ın analysis class'larındaki entity mi kastedildiğini bilemez.

Diyagramdan Koda: Figürün Tam Karşılığı

Figürlerdeki dört tipin ve LeaveRequest'in derlenen karşılığı aşağıda. Yorum satırları, her property'nin hangi diyagram satırından geldiğini gösteriyor:

DateRange period = new(new DateOnly(2026, 12, 21), new DateOnly(2026, 12, 25));
LeaveRequest request = new(42, employeeId: 1001, leaveTypeId: 1, period, reason: null);
request.Submit(workingDays: 5, now: new DateTimeOffset(2026, 12, 1, 9, 0, 0, TimeSpan.FromHours(3)));

LeaveBalance balance = new(employeeId: 1001, leaveTypeId: 1, year: 2026, entitledDays: 14);
balance.Deduct(6);

Console.WriteLine($"{request.Status}, needs director: {request.RequiresDirectorApproval}");
Console.WriteLine($"Overlaps 24-28 Dec: {period.Overlaps(new(new DateOnly(2026, 12, 24), new DateOnly(2026, 12, 28)))}");
Console.WriteLine($"Remaining: {balance.RemainingDays}");
Console.WriteLine(period == new DateRange(new DateOnly(2026, 12, 21), new DateOnly(2026, 12, 25)));

// «enumeration»
public enum LeaveStatus { Draft, PendingManagerApproval, PendingDirectorApproval, Approved, Rejected, Cancelled }

// «dataType»: kimliği yok, iki değeri de eşitse eşittir
public readonly record struct DateRange(DateOnly Start, DateOnly End)
{
    public bool Overlaps(DateRange other) => Start <= other.End && other.Start <= End;
}

// «interface»: operation'lar abstract
public interface ILeaveRequestRepository
{
    Task<LeaveRequest?> FindAsync(int id, CancellationToken ct);
    void Add(LeaveRequest request);
}

public sealed record Approval(int ApproverId, DateTimeOffset DecidedAt);

public sealed class LeaveRequest(int id, int employeeId, int leaveTypeId, DateRange period, string? reason)
{
    // + directorApprovalThreshold: int = 3 {readOnly}, altı çizili (static)
    public const int DirectorApprovalThreshold = 3;

    // - approvals: Approval [0..*] {ordered}
    private readonly List<Approval> _approvals = [];

    public int Id { get; } = id;                                      // + id: int {readOnly}
    public int EmployeeId { get; } = employeeId;
    public int LeaveTypeId { get; } = leaveTypeId;
    public DateRange Period { get; private set; } = period;           // + period: DateRange
    public string? Reason { get; private set; } = reason;             // + reason: string [0..1]
    public int WorkingDays { get; private set; }                      // + workingDays: int
    public LeaveStatus Status { get; private set; } = LeaveStatus.Draft;  // + status: LeaveStatus = Draft
    public DateTimeOffset? SubmittedAt { get; private set; }          // + submittedAt: DateTimeOffset [0..1]
    public bool RequiresDirectorApproval => WorkingDays > DirectorApprovalThreshold;  // + /requiresDirectorApproval: bool
    public IReadOnlyList<Approval> Approvals => _approvals;

    // + submit(workingDays: int, now: DateTimeOffset)
    public void Submit(int workingDays, DateTimeOffset now)
    {
        if (Status != LeaveStatus.Draft)
            throw new InvalidOperationException($"Request {Id} is already {Status}.");
        WorkingDays = workingDays;
        SubmittedAt = now;
        Status = LeaveStatus.PendingManagerApproval;
    }

    // + cancel(today: DateOnly)
    public void Cancel(DateOnly today)
    {
        bool cancellable = Status is LeaveStatus.PendingManagerApproval or LeaveStatus.PendingDirectorApproval
            || (Status == LeaveStatus.Approved && today < Period.Start);
        if (!cancellable)
            throw new InvalidOperationException($"Request {Id} cannot be cancelled in status {Status}.");
        Status = LeaveStatus.Cancelled;
    }
}

public sealed class LeaveBalance(int employeeId, int leaveTypeId, int year, int entitledDays)
{
    public int EmployeeId { get; } = employeeId;
    public int LeaveTypeId { get; } = leaveTypeId;
    public int Year { get; } = year;
    public int EntitledDays { get; } = entitledDays;
    public int UsedDays { get; private set; }
    public int RemainingDays => EntitledDays - UsedDays;              // + /remainingDays: int

    public void Deduct(int days)
    {
        if (days > RemainingDays)
            throw new InvalidOperationException($"Only {RemainingDays} days left.");
        UsedDays += days;
    }
}
PendingManagerApproval, needs director: True
Overlaps 24-28 Dec: True
Remaining: 8
True

Diyagramla kod arasındaki ad farkı bilinçli: UML geleneğinde attribute ve operation adları küçük harfle başlar (workingDays, submit()), C#'ta property ve method'lar büyük harfle (WorkingDays, Submit()). Bu seride diyagramlar UML geleneğini, kod C# geleneğini izliyor; eşleme mekanik olduğu için kafa karıştırmıyor. Bir şey daha: Approval'ı burada geçici olarak bir record bıraktım. Onu Employee ve LeaveRequest arasında bir ilişki olarak ele almak bir sonraki yazının konusu.

Aynı Diyagram Mermaid ile

Class diagram, Mermaid'in en olgun diyagram türlerinden biri ve GitHub Markdown'ında doğrudan render ediliyor. Aynı tipleri metin olarak şöyle yazdım ve yerelde Mermaid CLI (Command Line Interface) 12.2.0 ile render ederek kontrol ettim:

classDiagram
    class LeaveRequest {
        +id: int
        +period: DateRange
        +reason: string [0..1]
        +workingDays: int
        +status: LeaveStatus = Draft
        +directorApprovalThreshold: int = 3$
        +/requiresDirectorApproval: bool
        -approvals: List~Approval~
        +submit(workingDays: int, now: DateTimeOffset) void
        +cancel(today: DateOnly) void
    }
    class LeaveStatus {
        <<enumeration>>
        Draft
        PendingManagerApproval
        PendingDirectorApproval
        Approved
        Rejected
        Cancelled
    }
    class DateRange {
        <<dataType>>
        +start: DateOnly
        +end: DateOnly
        +overlaps(other: DateRange) bool
    }
    class ILeaveRequestRepository {
        <<interface>>
        +findAsync(id: int, ct: CancellationToken)* Task~LeaveRequest~
        +add(request: LeaveRequest)*
    }

Mermaid satırın sonundaki $ işaretini static (altı çizili), * işaretini abstract (italik) olarak çiziyor; generic tipleri ~ ile yazıyorsun ve çıktıda List<Approval> olarak görünüyor. Sınırları da var: attribute satırını yorumlamadan olduğu gibi yazdığı için [0..1], / ve = Draft metin olarak görünür ama Mermaid onları anlamaz, yanlış yazsan da hata vermez. {readOnly} ve {ordered} gibi modifier'lar için bir söz dizimi yok, operation dönüş tipini de UML'deki iki nokta yerine boşlukla ayırıyorsun. Yani Mermaid notasyonu taşıyor ama doğrulamıyor; notasyonun doğruluğu yine sende.

Sık Düşülen Tuzaklar

C# söz dizimiyle UML yazmak. int id, void Submit(int workingDays) ya da List<Approval> approvals UML değildir. UML'de ad önce, tip iki noktadan sonra gelir, collection'lar multiplicity ile gösterilir: approvals: Approval [0..*].

private set için {readOnly} yazmak. {readOnly} "hiç değişmez" demektir; "dışarıdan değiştirilemez" değil. Status'ü {readOnly} çizen bir diyagram, state machine diagram'ıyla çelişir.

Multiplicity'yi atlamak ve varsayılanı unutmak. Multiplicity yazılmamış bir attribute [1] demektir. Nullable bir property'yi multiplicity'siz çizersen diyagram "her zaman vardır" der, kod "olmayabilir" der; Önce Tahmin Et'teki CS8618 uyarısı bu çelişkinin derleyici tarafından yakalanmış hâliydi.

Her şeyi her seviyede çizmek. İş birimiyle konuşulan bir conceptual diyagramda visibility işaretleri, DateTimeOffset gibi tipler ve Id property'leri gürültüdür. Detayı hedef kitleye göre seç; bir bölmeyi gizlemek meşrudur.

Açıklamasız stereotype. «entity», «service», «aggregate» gibi stereotype'lar ancak okuyan herkes aynı şeyi anladığında işe yarar. Kendi profilini kullanıyorsan diyagramın yanına bir legend koy.

Aynı bilgiyi iki kez çizmek. approvals: Approval [0..*] bir attribute olarak yazılabilir, ama aynı bilgi LeaveRequest ile Approval arasında bir association olarak da çizilebilir. İkisini birden çizmek, okuyana iki ayrı ilişki varmış gibi görünür. Hangisinin ne zaman seçileceği bir sonraki yazının konusu.

Küçük Bir Deneme

★☆☆ Önce Tahmin Et'teki class'ın UML karşılığını kâğıda çiz: her satırın visibility'si, tipi ve multiplicity'si ile. RejectionReason'ı hangi multiplicity ile çizdin? CS8618 uyarısını gideren iki farklı C# değişikliğini yaz ve her birinin diyagramda neyi değiştirdiğini not et.

★★☆ Employee class'ını CASE metnindeki attribute'larla design seviyesinde çiz: employeeNumber için {id} mi kullanırsın, id için mi? managerId'nin multiplicity'si ne olmalı? Sonra C# karşılığını yaz ve derleyicinin nullable uyarılarıyla diyagramını karşılaştır.

★★★ WorkingDayCalculator adında, yalnızca static üyeleri olan bir class yaz (C#'ta static class). Diyagramda «Utility» stereotype'ıyla çiz ve static operation'larının altını çiz. Aynı class'ı Mermaid ile yaz, $ işaretinin çıktıda nasıl göründüğünü kontrol et.

Bir sonraki yazıda kutuların arasındaki çizgilere geçiyoruz: association, aggregation, composition, generalization, realization ve dependency. Her okun C# ve EF Core karşılığını yan yana koyacağız ve Approval'ın neden bir association class olarak başlayıp ayrı bir class'a dönüştüğünü göreceğiz.