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

Component Diagram: Provided/Required Interface, Port ve .NET Modüllerini Çizmek

Mert Özen 7 Kas 2026 11 dk 4 görüntülenme
Component Diagram: Provided/Required Interface, Port ve .NET Modüllerini Çizmek

Component diagram, sistemi değiştirilebilir modüllere ayırıp her modülün hangi interface'i sağladığını (lollipop) ve hangisini istediğini (socket) gösterir; .NET'te bu, public sözleşme project'i, internal implementasyon ve composition root'taki DI kayıtları demektir. Bu yazıda İzin, Bordro ve Bildirim modüllerini port ve delegation connector'larıyla çizip DI registration'ından geri okuyoruz.

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

Önce Tahmin Et: Bordro Ekibinin Kısayolu Neden Derlenmedi?

İK (insan kaynakları) Portalı artık tek bir proje değil. İzin, Bordro ve Bildirim ayrı modüller; her biri kendi project'lerinde, aynı process içinde çalışıyor. Bordro ekibinden bir geliştirici, ücretsiz izin günlerini hesaplamak için İzin modülünün sorgu class'ını doğrudan kullanmak istiyor:

namespace HrPortal.Payroll;

public sealed class UnpaidLeaveShortcut
{
    public object Create() => new HrPortal.Leave.ApprovedLeaveQuery();
}

Bu kod HrPortal.Payroll project'inde derlenir mi? Aynı satırı, bütün modüllere referansı olan HrPortal.Api project'ine taşırsan ne olur? İki durumda da derleyici sana neyi söylüyor olabilir?

Component Nedir? UML 1'den UML 2'ye

Gerçek derleme çıktıları:

Shortcut.cs(5,50): error CS0234: The type or namespace name 'ApprovedLeaveQuery' does not exist in the namespace 'HrPortal.Leave' (are you missing an assembly reference?)
Shortcut.cs(3,57): error CS0122: 'ApprovedLeaveQuery' is inaccessible due to its protection level

İlk hata, Bordro'nun İzin modülünün implementasyon assembly'sini hiç görmediğini söylüyor: yalnız HrPortal.Leave.Contracts'a referansı var. İkincisi, referansı olan HrPortal.Api'de bile class'ın internal olduğu için erişilemediğini. İki hata birlikte bir sınırı anlatıyor: İzin modülü dışarıya yalnız belirli interface'ler sunuyor, içini saklıyor. UML'de bu sınırın adı component.

UML 2.5.1 spesifikasyonunun 11.6 bölümü (Components) component'i, içini kapsülleyen ve dış dünyayla yalnız provided ve required interface'leri üzerinden konuşan, değiştirilebilir modüler bir parça olarak tanımlar. Teknik olarak component, class'ın özelleşmiş bir hali: attribute'u, operation'ı, port'u ve iç yapısı olabilir. UML 1.x'te component daha çok fiziksel bir şeydi; bir DLL (dynamic-link library) ya da executable. UML 2 bu anlamı ikiye ayırdı: mantıksal modül component olarak kaldı, fiziksel dosya ise artifact oldu. HrPortal.Leave.dll bir artifact'tir ve Leave component'ini "manifest" eder; bu ilişkiyi bir sonraki yazıda, deployment diagram'da göreceğiz.

Component diagram şu soruya cevap verir: "Sistem hangi değiştirilebilir parçalardan oluşuyor ve bu parçalar birbirine hangi sözleşmelerle bağlanıyor?" Class diagram'dan farkı ölçek ve odak. Class diagram bir modülün içindeki kavramları gösterir; component diagram modülleri kara kutu olarak ele alır ve yalnız sınırdaki sözleşmeleri çizer.

Notasyon: Component, Provided ve Required Interface

12-components-tr.png

Component bir dikdörtgendir ve «component» keyword'ü ya da sağ üstteki küçük component ikonu (iki çıkıntılı kutu) ile işaretlenir; ikisini birlikte kullanmak da serbest. Provided interface, component'ten çıkan bir çizginin ucundaki dolu daire, yani lollipop (ball) ile gösterilir: "bu sözleşmeyi ben gerçekleştiriyorum". Required interface ise yarım daire, yani socket ile gösterilir: "çalışmak için bu sözleşmeye ihtiyacım var". Bir component'in socket'i diğerinin lollipop'unu kavradığında oluşan bağa assembly connector denir. Figürdeki dört bağın her biri, bir modülün ihtiyacını başka bir modülün karşıladığı yeri gösteriyor.

Aynı bilgiyi daha resmi bir biçimde de çizebilirsin: interface'i «interface» keyword'lü bir class kutusu olarak çizip provided tarafı realization (içi boş üçgen uçlu kesikli ok), required tarafı «use» etiketli dependency (açık uçlu kesikli ok) ile bağlamak. Bu gösterim interface'in operation'larını da göstermek istediğinde işe yarar. Ball-and-socket ise modüller arası bağımlılığı tek bakışta okutur. Kalabalık bir sistemde bir de kara kutu gösterimi var: component dikdörtgeninin içine «provided interfaces» ve «required interfaces» başlıklı bölmeler açıp interface adlarını liste olarak yazmak. Çizgi çekmeden sözleşmeyi belgelemek istediğinde kullanışlı.

Okuma kuralı basit. Lollipop'tan socket'e doğru sağlayan ve isteyen, socket'ten lollipop'a doğru bağımlılık var. Figürde Leave iki lollipop sunuyor (ILeaveRequestService, IApprovedLeaveQuery) ve iki socket taşıyor (INotificationSender, ICalendarGateway). Api ve Payroll yalnız tüketiyor, Notification ve CalendarIntegration yalnız sağlıyor. Takvim Sistemi'ne giden HTTP (Hypertext Transfer Protocol) çağrısı CalendarIntegration'ın içinde saklı; Leave onu yalnız bir interface olarak görüyor.

Port, Delegation ve Composite Structure

12-leave-whitebox-tr.png

Kara kutu görünümü dışarıdan bakanın ihtiyacını karşılar. İçeriyi açmak istediğinde white-box görünümüne geçersin. Bu görünümün dili, UML'in composite structure diagram'ından gelir. Component'in sınırına oturan küçük kareler port'lardır: component'in dış dünyayla etkileşim noktaları. Her port'un kendi provided ve required interface'leri olabilir. Böylece "API (Application Programming Interface) tarafına açılan yüz" ile "Bordro'ya açılan yüz" ayrı adlandırılır (api, payroll). İhtiyaçlar da ayrı ayrı adlandırılır (notify, calendar).

İçerideki kutular part'lardır ve ad : Tip biçiminde yazılır. service : LeaveRequestService, component çalışırken var olan bir LeaveRequestService instance'ını temsil eder; class diagram'daki bir class değil, o class'tan bir rol. Port'tan part'a çizilen connector delegation connector'dır: dışarıdan api port'una gelen çağrı içeride service'e iletilir. Ters yönde, service'in bildirim ihtiyacı notify port'u üzerinden dışarı çıkar. Birçok araç bu bağı ok ve «delegate» etiketiyle gösterir. İçerideki service ile balance arasındaki ball-and-socket da bir assembly connector'dır, yalnız bu kez component'in içinde.

Port'u ne zaman çizmeli? Bir component farklı tüketicilere farklı yüzler sunuyorsa ya da aynı interface'i iki farklı amaçla istiyorsa (ör. iki ayrı takvim sistemi) port, bu yüzleri adlandırmanın yoludur. Tek tüketicili küçük modüllerde port'suz lollipop/socket yeterlidir. Composite structure diagram'ın kendisi daha genel bir araç: herhangi bir class'ın çalışma zamanındaki iç yapısını, yani part'larını ve aralarındaki connector'ları gösterir. Component'in white-box görünümü onun en sık kullanılan halidir.

.NET Karşılığı: Modular Monolith'te Modül = Component

Diyagramdaki her öğenin kodda bir karşılığı var. İzin modülü iki project'ten oluşuyor: sözleşmeleri taşıyan HrPortal.Leave.Contracts ve implementasyonu taşıyan HrPortal.Leave. Sözleşme project'i yalnız interface'ler içeriyor:

namespace HrPortal.Leave.Contracts;

// Provided interface 1: API'nin kullandığı işlem yüzü
public interface ILeaveRequestService
{
    Task ApproveAsync(int leaveRequestId, int approverId, CancellationToken ct = default);
}

// Provided interface 2: Bordro modülünün okuduğu sorgu yüzü
public interface IApprovedLeaveQuery
{
    Task<int> GetUnpaidLeaveDaysAsync(int employeeId, int year, int month, CancellationToken ct = default);
}

// Required interface: sahibi Leave, implementasyonunu başka bir component verir
public interface ICalendarGateway
{
    Task AddLeaveAsync(int leaveRequestId, DateOnly start, DateOnly end, CancellationToken ct = default);
}

ICalendarGateway'in burada durması bilinçli bir karar. Bu bir required interface ama sahibi Leave: ihtiyacı Leave tanımlıyor, takvim entegrasyonu ona uyuyor. SOLID (Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, Dependency Inversion) serisinin Dependency Inversion Principle yazısında konuştuğumuz bağımlılığı çevirmenin component seviyesindeki hali bu. INotificationSender ise Bildirim modülünün kendi sözleşme project'inde duruyor, çünkü onu birçok modül ortak kullanıyor. Implementasyon tarafında her şey internal:

internal sealed class LeaveRequestService(
    IBalanceService balance,
    INotificationSender notifications,
    ICalendarGateway calendar) : ILeaveRequestService
{
    public async Task ApproveAsync(int leaveRequestId, int approverId, CancellationToken ct = default)
    {
        if (!balance.HasEnoughBalance(employeeId: 1001, workingDays: 5))
        {
            throw new InvalidOperationException("Insufficient balance.");
        }
        await calendar.AddLeaveAsync(leaveRequestId, new DateOnly(2026, 12, 21), new DateOnly(2026, 12, 25), ct);
        await notifications.SendAsync("ada@hrportal.example", $"Leave request #{leaveRequestId} approved", ct);
    }
}

public static class LeaveModule
{
    // Port'lar: dışarıya yalnız iki provided interface açılıyor, implementasyonlar internal
    public static IServiceCollection AddLeaveModule(this IServiceCollection services)
    {
        services.AddScoped<ILeaveRequestService, LeaveRequestService>();
        services.AddScoped<IApprovedLeaveQuery, ApprovedLeaveQuery>();
        services.AddScoped<IBalanceService, BalanceService>();
        return services;
    }
}

Okuma kılavuzu olarak eşleme şöyle:

UML öğesi.NET karşılığı
ComponentModül: bir sözleşme project'i ve bir implementasyon project'i
Provided interfaceContracts içindeki public interface ve onu gerçekleştiren internal class
Required interfaceConstructor parametresi olarak istenen, başka bir modülün sözleşmesindeki interface
Required'ın derleme iziSağlayıcının Contracts project'ine ProjectReference, implementasyona değil
Delegation connectorAddScoped<ILeaveRequestService, LeaveRequestService>(): dış sözleşmeden iç class'a
Assembly connectorComposition root'ta iki modülün kaydının birbirini karşılaması
PortTüketiciye göre ayrılmış sözleşme yüzü (ayrı interface ya da facade)

HrPortal.Leave.csproj'daki referanslar da diyagramdan okunabiliyor. Leave'in iki socket'i, iki sözleşme project'ine referans demek, implementasyonlara değil:

<ItemGroup>
  <ProjectReference Include="..\HrPortal.Leave.Contracts\HrPortal.Leave.Contracts.csproj" />
  <ProjectReference Include="..\HrPortal.Notification.Contracts\HrPortal.Notification.Contracts.csproj" />
</ItemGroup>

Project'ler arası bu bağımlılık grafiğini package diagram ile çizmeyi serinin Object Diagram ve Package Diagram yazısında yapmıştık; project'leri neye göre bölmek gerektiği ise SOLID serisinin Component Prensipleri: .NET Solution'ını Project'lere Bölmek yazısının konusu. Component diagram bu iki görünümün üstünde duruyor: project'leri değil, sözleşmeleri gösteriyor.

DI Registration'ı Assembly Connector Olarak Okumak

Diyagramdaki dört assembly connector kodda tek bir yerde, composition root'ta kuruluyor. DI (dependency injection) container'ına yapılan her kayıt bir bağın bir ucu:

// Composition root: assembly connector'lar burada kuruluyor
ServiceCollection services = new();
services.AddLeaveModule();
if (mode != "missing")
{
    services.AddNotificationModule();
}
services.AddCalendarIntegration();
services.AddPayrollModule();

Örnek veride Ada'nın Kasım 2026'da 3 iş günlük ücretsiz izni var; Bordro brüt maaşı ayda 22 iş günü varsayımıyla buna göre düşüyor. Çalıştırınca İzin modülü onayı işliyor, takvim ve bildirim modülleri kendi sözleşmeleri üzerinden çağrılıyor:

Approve #42 by Grace:
  [Calendar] leave #42 2026-12-21..2026-12-25 added
  [Notification] e-mail to ada@hrportal.example: Leave request #42 approved
Payroll: Ada, 2026-11 gross = 57000

Daha ilginci, bağları koddan geri okumak. Her kayıtlı class'ın constructor parametreleri onun required interface'leridir; o parametreyi karşılayan kayıt da assembly connector'ın diğer ucu. Bunu birkaç satır reflection ile yazdırabiliriz:

static void PrintWiring(IServiceCollection services)
{
    // Her constructor parametresi bir required interface; onu karşılayan kayıt bir assembly connector
    foreach (ServiceDescriptor d in services.Where(d => d.ImplementationType is not null))
    {
        Type impl = d.ImplementationType!;
        ConstructorInfo ctor = impl.GetConstructors(BindingFlags.Public | BindingFlags.Instance).Single();
        foreach (ParameterInfo p in ctor.GetParameters())
        {
            ServiceDescriptor? provider = services.LastOrDefault(s => s.ServiceType == p.ParameterType);
            string by = provider?.ImplementationType is { } t ? $"{t.Assembly.GetName().Name}.{t.Name}" : "MISSING";
            Console.WriteLine($"{impl.Assembly.GetName().Name,-22} {impl.Name,-20} requires {p.ParameterType.Name,-20} <- {by}");
        }
    }
}
HrPortal.Leave         LeaveRequestService  requires IBalanceService      <- HrPortal.Leave.BalanceService
HrPortal.Leave         LeaveRequestService  requires INotificationSender  <- HrPortal.Notification.EmailNotificationSender
HrPortal.Leave         LeaveRequestService  requires ICalendarGateway     <- HrPortal.CalendarIntegration.CorporateCalendarGateway
HrPortal.Payroll       PayrollCalculator    requires IApprovedLeaveQuery  <- HrPortal.Leave.ApprovedLeaveQuery

Dört satırdan üçü figürdeki modüller arası bağlar, biri (IBalanceService) white-box görünümdeki iç bağ. Assembly adı iki ucun aynı mı farklı mı modülde olduğunu söylüyor. Bu çıktıyı diyagramla karşılaştırmak, "diyagram hâlâ doğru mu?" sorusunun ucuz bir cevabı.

Peki bir socket boşta kalırsa? Bildirim modülünü kaydetmeden, ValidateOnBuild açıkken container'ı kurmaya çalışınca gerçek hata:

Error while validating the service descriptor 'ServiceType: HrPortal.Leave.Contracts.ILeaveRequestService Lifetime: Scoped ImplementationType: HrPortal.Leave.LeaveRequestService': Unable to resolve service for type 'HrPortal.Notification.Contracts.INotificationSender' while attempting to activate 'HrPortal.Leave.LeaveRequestService'.

Diyagram diliyle: Leave'in INotificationSender socket'ine takılan bir lollipop yok. ValidateOnBuild olmasa bu hata ilk onay isteğinde, yani production'da ortaya çıkardı. ASP.NET Core'da Development ortamında bu doğrulama varsayılan olarak açıktır. Lifetime'lar ve doğrulama ayrıntısı ASP.NET Core Web API serisinin ASP.NET Core'da Dependency Injection: Lifetime'lar ve Captive Dependency Tuzağı yazısında.

Diagrams-as-Code: PlantUML ile Component Diagram

Figürdeki kara kutu görünümünün PlantUML karşılığı aşağıda; PlantUML 1.2026.9 ile render edildi. -( socket'i, düz çizgi lollipop'u çiziyor:

@startuml hr-portal-components
title HR Portal modules (UML component diagram)
skinparam componentStyle uml2

component "HrPortal.Api" as Api
component "Leave" as Leave
component "Payroll" as Payroll
component "Notification" as Notification
component "CalendarIntegration" as Calendar

interface ILeaveRequestService
interface IApprovedLeaveQuery
interface INotificationSender
interface ICalendarGateway

Leave -up- ILeaveRequestService
Leave -down- IApprovedLeaveQuery
Notification -left- INotificationSender
Calendar -left- ICalendarGateway

Api -down-( ILeaveRequestService
Payroll -up-( IApprovedLeaveQuery
Leave -right-( INotificationSender
Leave -right-( ICalendarGateway
@enduml

Otomatik yerleşim elle çizilen figür kadar düzenli değil ama bilgi aynı ve dosya repo'da kodla birlikte değişebiliyor. Mermaid'in bu yazının yazıldığı 12.3.0 sürümünde UML component diagram türü yok; bu yüzden component ve deployment diyagramlarında PlantUML daha uygun. Araç karşılaştırmasını serinin Mermaid ve Structurizr DSL (domain-specific language) yazısında yapacağız.

C4 Component Diagram ile Farkı

İsim benzerliği kafa karıştırıyor. C4 (Context, Containers, Components, Code: Simon Brown'ın dört seviyeli mimari diyagram yaklaşımı) model'i de "component" der, ama anlamı daha dar: bir container'ın, yani tek başına çalışan bir uygulama ya da veri deposunun içinde, iyi tanımlı bir interface'in arkasında toplanmış ilgili işlevsellik. C4'e göre component ayrı deploy edilmez ve container'ın process'inde çalışır. Bizim İzin, Bordro ve Bildirim modüllerimiz tam bu tanıma uyuyor.

Fark notasyonda ve amaçta. C4 component diagram'ında port, lollipop ya da socket yoktur; kutular ve üzerine "ne için, hangi teknolojiyle" yazılmış tek yönlü oklar vardır. Hedef kitle mimari resmi hızlıca kavramak isteyen geliştiriciler ve mimarlardır. UML component diagram ise sözleşmeyi kesinleştirir: hangi interface'i kim sağlıyor, kim istiyor, hangi port'tan. Pratik bir kural: mimariyi anlatırken C4, iki ekibin bir modül sınırında anlaşması gerektiğinde UML. İkisi birbirini dışlamaz. C4'te bir container'ın component'lerini çizer, kritik bir sınırı UML component diagram'ıyla detaylandırırsın. C4'ün tamamını ve notation kurallarını iki yazı sonra, C4 Model yazısında işliyoruz.

Sık Düşülen Tuzaklar

Her class'ı component yapmak. BalanceService bir component değil, Leave'in iç part'ı. Component, bağımsız değiştirilebilir ve sözleşmeyle bağlanan bir birimdir. Diyagramda kırk kutu varsa büyük ihtimalle class diagram'ı component notasyonuyla çiziyorsun.

Okları implementasyona çekmek. Payroll'dan Leave'in içindeki ApprovedLeaveQuery'ye çizilen bir ok, açılıştaki kısayolun diyagramdaki karşılığı. Bağımlılık her zaman bir interface'e gider. Kodda bunu internal ve ayrı sözleşme project'i zorluyor; diyagramda ise yalnız disiplin.

Socket'i boşta bırakmak. Required interface'i olan ama hiçbir şeye bağlanmayan bir component, ya eksik bir modülü ya da çalışma zamanında patlayacak bir DI kaydını işaret eder. Diyagramı çizerken her socket için "bunu kim sağlıyor?" diye sor. Kodda ValidateOnBuild aynı soruyu senin yerine sorar.

Component ile deployment'ı karıştırmak. Component diagram'da "API sunucusu", "veritabanı makinesi" gibi kutular varsa çizilen şey artık mantıksal yapı değil, fiziksel topoloji. Onun yeri deployment diagram.

Diyagramla kodu bağlamamak. Modüller arası bağlar yukarıdaki gibi bir çıktıyla ya da bir architecture test'iyle (ör. NetArchTest kuralları) doğrulanmıyorsa, ilk refactoring'de ikisi ayrışır.

Küçük Bir Deneme

★☆☆ HrPortal.Leave içinde IWorkingDayCalculator adlı yeni bir internal servis ekle ve LeaveRequestService'e constructor parametresi olarak ver. wiring çıktısında kaç satır oluyor? White-box figürüne hangi part ve hangi connector eklenmeli?

★★☆ Bordro modülü IApprovedLeaveQuery ile sorgulamak yerine İzin modülünün yayınladığı LeaveRequestApproved integration event'ini dinlesin. Component diagram nasıl değişir: lollipop/socket yön değiştirir mi, araya bir event bus component'i mi girer? İki tasarımın coupling'ini karşılaştır.

★★★ Bir architecture test yaz: HrPortal.Payroll assembly'sinin yalnız HrPortal.Leave.Contracts'a bağımlı olduğunu, HrPortal.Leave'e bağımlı olmadığını doğrulasın. Sonra bu kuralı component diagram'daki bir socket ile eşleştiren kısa bir açıklamayı test adına yaz.

Component diagram neyin neye bağlı olduğunu mantıksal düzeyde söyledi. Bir sonraki yazıda bu modüllerin nerede çalıştığına bakıyoruz: deployment diagram ile node, execution environment, artifact ve «manifest» ilişkisini, İK Portalı'nın docker compose ile kurulan geliştirme ortamı ve Azure'daki production topolojisi üzerinden çizeceğiz.