State Machine Diagram: State, Transition, Guard, Entry/Exit ve İzin Talebinin Yaşam Döngüsünü Koda Dökmek
State machine diagram, bir object'in hangi durumlarda bulunabileceğini ve hangi event'in, hangi guard'la onu nereye taşıdığını gösterir; her ok bir switch kolu, bir Stateless PermitIf'i ve bir unit test olur. Bu yazıda İzin Talebi'nin yaşam döngüsünü composite state, choice ve entry/effect ayrımıyla çizip C#'a ve testlere dönüştürüyoruz.
Sürüm notu: Bu yazıdaki diyagramlar UML (Unified Modeling Language) 2.5.1 notasyonuyla çizilmiştir; C# örnekleri .NET 10 ve Stateless 5.20.1 ile denenmiştir.
Önce Tahmin Et: Reddedilmiş Talep Nasıl İptal Edildi?
İK (insan kaynakları) Portalı'nın ilk sürümünde iptal kuralı, R11 (iptal kuralları) maddesi okunarak tek bir method'a yazılmış. Kod kısa ve masum görünüyor:
public void Cancel(DateOnly today)
{
if (Status == LeaveStatus.Approved && today >= Period.Start)
{
throw new InvalidOperationException("Leave already started.");
}
Status = LeaveStatus.Cancelled;
}
Ada'nın 21–25 Aralık yıllık izin talebi (#42) altı farklı durumdayken 1 Aralık'ta Cancel çağrılırsa ne olur? Hangi durumlarda exception beklersin, hangilerinde talep sessizce Cancelled olur? Ve İK uzmanı Margaret izin başladıktan sonra iptal etmek isterse bu kod ona izin verir mi?
State Machine Diagram Neyi Cevaplar?
Gerçek koşunun çıktısı:
Draft -> Cancelled
PendingManagerApproval -> Cancelled
PendingDirectorApproval -> Cancelled
Approved -> Cancelled
Rejected -> Cancelled
Cancelled -> Cancelled
Hiçbir durumda exception yok. Yönetici Alan'ın gerekçesiyle reddettiği bir talep artık "iptal edildi" görünüyor; ret kaydı raporlardan kayboldu. Zaten iptal edilmiş talep yeniden iptal ediliyor ve bir iade daha tetikleniyorsa bakiye bozuluyor. Margaret ise izin başladıktan sonra iptal edemiyor, çünkü kod rolü hiç bilmiyor. Hata tek bir if'te değil; kod "hangi durumdan hangi duruma, hangi koşulla geçilebilir?" sorusunu hiç sormamış.
Bu yazıda tam o soruyu cevaplayan diyagramı öğreniyoruz. Use case diagram sistemi dışarıdan, sequence diagram bir senaryodaki mesajları, activity diagram iş akışını gösteriyordu. State machine diagram ise tek bir object'in ömrünü anlatır: hangi durumlarda bulunabilir, hangi event onu bir durumdan diğerine taşır ve geçiş sırasında ne olur. UML 2.5.1 spesifikasyonunun 14. bölümü (StateMachines) iki türünü tanımlar: davranışı modelleyen behavior state machine ve bir class'ın operation'larının hangi sırayla çağrılabileceğini anlatan protocol state machine ({protocol} etiketiyle). Bu yazıdaki diyagram birincisi.
Ne zaman çizilir? Bir entity'nin Status property'si varsa, bu property'yi birden fazla aksiyon değiştiriyorsa ve en az bir kural "şu durumdaysa şunu yapamaz" diyorsa. LeaveRequest bu üçünü de karşılıyor. Diyagramı her class için çizmek ise gereksiz: iki durumlu bir IsActive field'ının state machine'i olmaz.
Notasyon: State, Transition ve Pseudostate'ler

State ve İç Davranışlar
State köşeleri yuvarlatılmış bir dikdörtgendir. Üst bölmede adı, isteğe bağlı alt bölmede iç davranışları yazar. entry / deductBalance state'e her girişte bir kez çalışır; exit / ... her çıkışta çalışır; do / ... entry bittikten sonra başlayan ve state'ten çıkılırsa yarıda kesilen uzun süreli bir aktivitedir. Aynı bölmeye trigger [guard] / effect biçiminde internal transition da yazılabilir: event gelir, effect çalışır ama state'ten çıkılmadığı için ne exit ne entry tekrar çalışır. Bunun karşıtı self-transition'dır, yani state'ten çıkıp kendine dönen ok: o, exit ve entry'yi yeniden çalıştırır. "Hatırlatma e-postası her gönderildiğinde yöneticiye yeniden 'yeni talep' bildirimi gitti" türü hatalar genelde internal transition yerine self-transition çizmekten çıkar.
Transition Etiketinin Grameri
Ok etiketi üç parçalıdır ve hepsi isteğe bağlıdır: trigger [guard] / effect. Trigger, geçişi başlatan event'tir. UML dört temel event türü tanır: call event (bir operation çağrısı, ör. Approve), signal event (asenkron bir mesaj), time event (after(2 working days) ya da at(...)) ve change event (when(today >= start): bir koşul false'tan true'ya döndüğünde). Guard köşeli parantez içindeki boolean ifadedir; event geldiğinde değerlendirilir, false ise ok yokmuş gibi davranılır. Effect, geçiş sırasında çalışan davranıştır. Trigger'ı olmayan ok completion transition'dır: state'in iç işi bitince kendiliğinden ateşlenir.
Bir geçişte çalışma sırası sabittir: önce kaynak state'in exit'i (iç içe state'lerde en içten dışa), sonra transition'ın effect'i, sonra hedef state'in entry'si (dıştan içe). Bir de run-to-completion kuralı var: state machine bir event'i bütünüyle işlemeden sıradakine geçmez. Kodda bunun karşılığı, aynı talep üzerinde iki aksiyonun aynı anda çalışmamasıdır; tek process içinde lock, birden fazla instance'ta veritabanı concurrency kontrolü.
Pseudostate'ler
Pseudostate'ler object'in "içinde bulunduğu" durumlar değildir; okları birleştiren ya da yönlendiren düğümlerdir. Initial pseudostate (dolu daire) her region'da en fazla bir tanedir ve giden okunda trigger ya da guard olmaz. Choice (eşkenar dörtgen) dinamik dallanmadır: guard'lar oka gelindiği anda, önceki adımın effect'leri çalıştıktan sonra değerlendirilir. Hiçbir guard true değilse model hatalıdır, bu yüzden bir dala [else] konur. Junction (küçük dolu daire) ise statik dallanmadır: birden fazla oku tek bir compound transition olarak birleştirir ve bütün guard'lar geçiş başlamadan önce değerlendirilir. History pseudostate, composite state'e geri dönüldüğünde son aktif alt state'i hatırlar: H (shallow history) yalnız bir seviyeyi, H* (deep history) tüm iç içe konfigürasyonu.
Final State ve Terminate
İçi dolu daireyi çevreleyen halka final state'tir. Dikkat: final state bir pseudostate değil, gerçek bir state'tir; içinde bulunduğu region'ın bittiğini söyler. En dış region'da ulaşılırsa state machine tamamlanır. Çarpı işareti (X) terminate pseudostate'tir: makine hiçbir exit çalıştırmadan anında durur. İş uygulamalarında ikisine de nadiren ihtiyaç olur; reddedilmiş bir talep veritabanında yaşamaya devam ettiği için Rejected giden oku olmayan sıradan bir state olarak kalır.
İzin Talebinin State Machine Diagram'ı

Diyagram, vaka metnindeki R1–R11 maddelerinden çıkıyor ve isimler LeaveStatus enum'ıyla birebir aynı. Talep Draft olarak doğar. Submit yalnız bakiye yetiyorsa geçer (R3). Bakiye düşmeyen izin türlerinde (mazeret, hastalık, ücretsiz) guard her zaman true. Gönderilen talep PendingApproval composite state'ine, onun içindeki PendingManagerApproval'a girer ve entry'si yöneticiye bildirim gönderir (R10). Yönetici onaylayınca bakiye yeniden kontrol edilir (R8) ve choice pseudostate devreye girer: 3 iş gününü aşan talep direktöre gider (R7), aşmayan doğrudan Approved olur.
Composite state burada iki işe yarıyor. Birincisi, Reject ve Cancel okları composite'in sınırından çıkıyor; yani iki alt state için de geçerli ve diyagramda iki kez çizilmiyor. İkincisi, "onay bekleyen talepler" ekranının sorgusu doğrudan bu kutu: Status ya yönetici ya direktör onayında. after(2 working days) / remindApprover satırı R10'daki hatırlatmanın bir internal transition olarak çizimi. Her alt state'te ayrı durduğu için süre, talebin o onaycıya düştüğü andan itibaren sayılıyor.
İki ayrıntı daha diyagramda bilinçli olarak ayrılmış. Approved'a girerken bakiye düşülüyor ve takvime yazılıyor; bu bir entry. İade ise Approved'ın exit'i değil, Cancel okunun effect'i. Yarın "izin bitti, talep arşivlendi" gibi yeni bir ok eklendiğinde exit'e yazılmış bir iade, arşivlenen her talep için bakiyeyi geri verirdi. Kural: davranış belirli bir oka aitse effect'e, state'e her giriş ya da çıkışta gerekiyorsa entry/exit'e yaz.
Diyagramı çizerken requirement metninde bir boşluk da göründü: Draft'tan Cancel oku yok, çünkü R11 yalnız onay bekleyen ve onaylanmış talepten söz ediyor. Taslak silinebilir mi, iptal mi edilir, yoksa sonsuza kadar mı kalır? Bunu tahmin edip kodlamak yerine iş birimine sormak gerekiyor. State machine diagram'ın en değerli çıktısı çoğu zaman bu tür sorulardır.
Geçersiz Geçişleri Diyagramdan Okumak
Diyagramı (state × trigger) tablosuna çevirmek, eksik ve fazla okları bir bakışta gösterir. Altı state ve dört trigger 24 hücre eder; oklar bunların yalnız sekizini dolduruyor:
| State | Submit | Approve | Reject | Cancel |
|---|---|---|---|---|
Draft | PendingManagerApproval | — | — | — (requirement'ta yok) |
PendingManagerApproval | — | choice: direktör ya da Approved | Rejected | Cancelled |
PendingDirectorApproval | — | Approved | Rejected | Cancelled |
Approved | — | — | — | Cancelled [başlamadıysa ya da İK] |
Rejected | — | — | — | — |
Cancelled | — | — | — | — |
Açılıştaki naif Cancel bu tablonun son sütununda altı hücreyi de "geçer" sayıyordu; diyagram ise üçünü kapalı tutuyor. Boş hücreler için ne olacağı da bir tasarım kararı: exception mı fırlatılır, sessizce yok mu sayılır? UML'de bir state'in tanımadığı event varsayılan olarak tüketilip atılır. Biz iş kuralı ihlali olduğu için API (Application Programming Interface) tarafında 409 dönecek şekilde reddetmeyi seçiyoruz.
Koda Dönüş: Transition Table'dan switch Expression'a
Bağımlılık eklemeden ilk adım, tabloyu bir tuple switch expression'ına çevirmek. Her ok bir kol, her guard bir when, composite state bir or pattern'i, choice ise kolun içindeki koşul:
public static class LeaveTransitions
{
// Diyagramdaki her ok bir satır; satırı olmayan (durum, trigger) çifti geçersiz
public static LeaveStatus? Next(LeaveStatus from, LeaveTrigger trigger, TransitionContext c) => (from, trigger) switch
{
(LeaveStatus.Draft, LeaveTrigger.Submit) when c.HasEnoughBalance
=> LeaveStatus.PendingManagerApproval,
// Choice pseudostate: [RequiresDirectorApproval] / [else]
(LeaveStatus.PendingManagerApproval, LeaveTrigger.Approve) when c.HasEnoughBalance
=> c.RequiresDirectorApproval ? LeaveStatus.PendingDirectorApproval : LeaveStatus.Approved,
(LeaveStatus.PendingDirectorApproval, LeaveTrigger.Approve) when c.HasEnoughBalance
=> LeaveStatus.Approved,
// Composite state'in sınırından çıkan oklar: iki alt durum için de geçerli
(LeaveStatus.PendingManagerApproval or LeaveStatus.PendingDirectorApproval, LeaveTrigger.Reject) when c.HasComment
=> LeaveStatus.Rejected,
(LeaveStatus.PendingManagerApproval or LeaveStatus.PendingDirectorApproval, LeaveTrigger.Cancel)
=> LeaveStatus.Cancelled,
(LeaveStatus.Approved, LeaveTrigger.Cancel) when !c.LeaveStarted || c.ByHrSpecialist
=> LeaveStatus.Cancelled,
_ => null
};
}
TransitionContext, guard'ların okuduğu değerleri taşıyan bir record: HasEnoughBalance, RequiresDirectorApproval, HasComment, LeaveStarted, ByHrSpecialist. Function saf: veritabanına, saate ya da kullanıcıya dokunmuyor. Bu yüzden test etmesi kolay. LeaveRequest.Fire bu function'ı çağırıp null gelirse InvalidOperationException fırlatıyor. #42 (5 iş günü) ve #43 (2 iş günü) için gerçek çıktı:
#42 (5 days): Draft -> PendingManagerApproval -> PendingDirectorApproval -> Approved
#43 (2 days): Draft -> PendingManagerApproval -> Approved
#42 again: 'Approve' is not allowed when the request is Approved.
#42 cancel by Ada after start: 'Cancel' is not allowed when the request is Approved.
#42 cancel by Margaret after start: Cancelled
Choice'un iki dalı da görünüyor: #42 direktöre uğruyor, #43 uğramıyor. Bu yaklaşımın sınırı da belli: entry, exit ve effect'ler switch'te yok. Onları Fire'ın içinde, hedef state'e bakan ayrı bir blokta yazman gerekir. Durum sayısı ve davranış arttıkça bu blok, diyagramla eşleşmesi zorlaşan ikinci bir switch'e dönüşür.
Koda Dönüş: Stateless ile Composite State, Entry ve Effect
Stateless, .NET'te en yaygın state machine library'si ve UML kavramlarına yakın bir API sunuyor: SubstateOf composite state'i, OnEntry/OnExit iç davranışları, PermitIf guard'lı oku karşılıyor. Bir pürüz var: superstate için de bir değere ihtiyaç duyuyor, oysa LeaveStatus'ta PendingApproval yok ve olmamalı, çünkü veritabanına hiçbir zaman composite state yazılmaz. Çözüm olarak makine için ayrı bir enum tanımlıyor, durumu entity'nin kendi property'sinde tutuyoruz:
// Superstate için bir değer gerekiyor; ilk altı değer LeaveStatus ile aynı, PendingApproval veritabanına hiç yazılmaz
public enum LeaveState { Draft, PendingManagerApproval, PendingDirectorApproval, Approved, Rejected, Cancelled, PendingApproval }
public static StateMachine<LeaveState, LeaveTrigger> Create(LeaveRecord r, TransitionContext c, List<string> log)
{
StateMachine<LeaveState, LeaveTrigger> m = new(() => (LeaveState)r.Status, s => r.Status = (LeaveStatus)s);
m.Configure(LeaveState.Draft)
.PermitIf(LeaveTrigger.Submit, LeaveState.PendingManagerApproval, () => c.HasEnoughBalance, "HasEnoughBalance");
m.Configure(LeaveState.PendingApproval)
.PermitIf(LeaveTrigger.Reject, LeaveState.Rejected, () => c.HasComment, "HasComment")
.Permit(LeaveTrigger.Cancel, LeaveState.Cancelled);
m.Configure(LeaveState.PendingManagerApproval)
.SubstateOf(LeaveState.PendingApproval)
.OnEntry(() => log.Add("entry: notify manager"))
// Choice pseudostate: iki guard birbirini dışlamalı
.PermitIf(LeaveTrigger.Approve, LeaveState.PendingDirectorApproval,
() => c.HasEnoughBalance && r.RequiresDirectorApproval, "RequiresDirectorApproval")
.PermitIf(LeaveTrigger.Approve, LeaveState.Approved,
() => c.HasEnoughBalance && !r.RequiresDirectorApproval, "else");
m.Configure(LeaveState.PendingDirectorApproval)
.SubstateOf(LeaveState.PendingApproval)
.OnEntry(() => log.Add("entry: notify director"))
.PermitIf(LeaveTrigger.Approve, LeaveState.Approved, () => c.HasEnoughBalance, "HasEnoughBalance");
m.Configure(LeaveState.Approved)
.OnEntry(() => log.Add("entry: deduct balance, add calendar entry"))
.PermitIf(LeaveTrigger.Cancel, LeaveState.Cancelled, () => !c.LeaveStarted || c.ByHrSpecialist, "NotStartedOrHr");
m.Configure(LeaveState.Cancelled)
.OnEntry(t =>
{
// Transition effect: iade yalnız Approved'dan gelindiyse
if (t.Source == LeaveState.Approved)
{
log.Add("effect: refund balance, remove calendar entry");
}
});
m.OnTransitioned(t => log.Add($"{t.Source} --{t.Trigger}--> {t.Destination}"));
return m;
}
Örneği kısa tutmak için entity yerine yalnız Status ve WorkingDays taşıyan bir LeaveRecord kullandık; gerçek LeaveRequest'te iki lambda aynı property'yi okur ve yazar, Entity Framework Core (EF Core) da onu kalıcı hale getirir. #42'yi gönderip iki kez onaylayınca ve sonra iptal edince gerçek çıktı:
IsInState(PendingApproval): True
Permitted: Approve, Cancel
Draft --Submit--> PendingManagerApproval
entry: notify manager
PendingManagerApproval --Approve--> PendingDirectorApproval
entry: notify director
PendingDirectorApproval --Approve--> Approved
entry: deduct balance, add calendar entry
Approved --Cancel--> Cancelled
effect: refund balance, remove calendar entry
Stored status: Cancelled
No valid leaving transitions are permitted from state 'Cancelled' for trigger 'Approve'. Consider ignoring the trigger.
Birkaç şey diyagramla birebir örtüşüyor. IsInState(PendingApproval) talep direktör onayındayken true dönüyor; composite state'e "içinde olmak" alt state'lerden birinde olmak demek. Permitted listesinde Reject yok, çünkü HasComment guard'ı false; Cancel ise composite'ten miras geliyor. Sıralama da UML'deki gibi: önce geçiş (OnTransitioned), sonra hedefin entry'si. GetPermittedTriggersAsync ile dönen liste, Angular ekranındaki butonları çizmek için de kullanılabilir. Stateless'ta history pseudostate karşılığı yok; gerekirse son alt state'i bir field'da tutup geri dönüş okunu ona göre kurarsın.
Koddan Diyagram: MermaidGraph
Stateless, yapılandırmayı DOT (Graphviz'in graf dili) ve Mermaid olarak dışa aktarabiliyor. MermaidGraph.Format(machine.GetInfo()) çağrısının gerçek çıktısı:
stateDiagram-v2
state PendingApproval {
PendingManagerApproval
PendingDirectorApproval
}
Draft --> PendingManagerApproval : Submit [HasEnoughBalance]
PendingApproval --> Rejected : Reject [HasComment]
PendingApproval --> Cancelled : Cancel
PendingManagerApproval --> PendingDirectorApproval : Approve [RequiresDirectorApproval]
PendingManagerApproval --> Approved : Approve [else]
PendingDirectorApproval --> Approved : Approve [HasEnoughBalance]
Approved --> Cancelled : Cancel [NotStartedOrHr]
[*] --> Draft
Bu metin Mermaid 12.3.0 ile hatasız render ediliyor. Guard'lar, PermitIf'e verdiğimiz açıklama metinleriyle etiketleniyor; o yüzden açıklama vermek yalnız log için değil, diyagram için de önemli. Eksikleri de görünür: entry davranışları ve effect'ler çıktıda yok, choice pseudostate iki ayrı oka dönüşmüş. Koddan üretilen diyagram "kod gerçekte ne yapıyor" sorusuna, elle çizilen diyagram "ne yapmalı" sorusuna cevap verir. İkisini yan yana koyup farkı code review'da konuşmak, bu serinin sonunda kuracağımız düzenin özü.
Elle çizilen diyagramı da repo'da metin olarak tutabilirsin. Figürdeki modelin PlantUML kaynağı aşağıda; PlantUML 1.2026.9 ile render edildi. Composite state, choice, entry satırları ve time event'li internal transition'lar dahil:
@startuml leave-request-states
title LeaveRequest state machine
hide empty description
[*] --> Draft
Draft --> PendingManagerApproval : Submit [hasEnoughBalance]
state "Pending Approval" as PendingApproval {
state PendingManagerApproval : entry / notifyManager
state PendingManagerApproval : after(2 working days) / remindApprover
state PendingDirectorApproval : entry / notifyDirector
state PendingDirectorApproval : after(2 working days) / remindApprover
state afterManager <<choice>>
PendingManagerApproval --> afterManager : Approve [hasEnoughBalance]
afterManager --> PendingDirectorApproval : [requiresDirectorApproval]
}
afterManager --> Approved : [else]
PendingDirectorApproval --> Approved : Approve [hasEnoughBalance]
PendingApproval --> Rejected : Reject [hasComment] / recordApproval
PendingApproval --> Cancelled : Cancel
state Approved : entry / deductBalance; addCalendarEntry
Approved --> Cancelled : Cancel [!leaveStarted || byHrSpecialist] / refundBalance; removeCalendarEntry
@enduml
PlantUML syntax'ını ve CI (continuous integration) pipeline'ında render etmeyi serinin PlantUML ile Diagrams-as-Code yazısında ayrıntılı işleyeceğiz.
State Pattern ve Status History ile İlişkisi
Aynı diyagramın üçüncü bir kod karşılığı daha var: GoF (Gang of Four: Gamma, Helm, Johnson ve Vlissides'in 1994 tarihli Design Patterns kitabı) içindeki State pattern. Orada her state bir class olur (DraftState, PendingManagerApprovalState…) ve Approve() gibi her trigger, o class'ın method'udur. State'e özgü davranış büyüdükçe, yani her durumda hesaplama, doğrulama ve ekran mantığı farklılaştıkça bu yapı kazançlı olur. Bizim talebimizde davranışın çoğu geçişlerde olduğu için switch ya da Stateless daha az kodla aynı diyagramı karşılıyor. Pattern'in kendisini Design Patterns serisindeki State Pattern: Sipariş Durumlarını switch Yerine State Object'leriyle Yönetmek yazısında kuruyoruz.
Diyagram bir de veritabanına uzanıyor. Her ok bir satır üretmeli: kim, ne zaman, hangi state'ten hangisine, hangi gerekçeyle. Reject okunun recordApproval effect'i bunun ilk adımı (Approval kaydı). Geçişleri bir transition table'da tutmak, durum geçmişini aynı transaction'da yazmak ve iki onaycının aynı anda karar vermesini rowversion ile engellemek, Çözüm Desenleri serisinin Status Field'ı Her Yerden Güncelleniyorsa: Transition Table, State Machine ve Status History yazısının konusu. O yazıdaki satın alma talebinin yaşam döngüsü, bu yazıdaki notasyonla çizilebilecek kardeş bir diyagram.
Diyagramdan Unit Test Türetmek
State machine diagram'ın en somut faydası, test listesini neredeyse mekanik olarak vermesi. Her ok en az bir pozitif test, her guard bir "guard kapalıyken geçmez" testi, tablodaki her boş hücre bir negatif test olur:
// Diyagramdaki her ok bir test satırı
public static TheoryData<LeaveStatus, LeaveTrigger, TransitionContext, LeaveStatus> Arrows => new()
{
{ LeaveStatus.Draft, LeaveTrigger.Submit, new(), LeaveStatus.PendingManagerApproval },
{ LeaveStatus.PendingManagerApproval, LeaveTrigger.Approve, new(RequiresDirectorApproval: true), LeaveStatus.PendingDirectorApproval },
{ LeaveStatus.PendingManagerApproval, LeaveTrigger.Approve, new(RequiresDirectorApproval: false), LeaveStatus.Approved },
{ LeaveStatus.PendingDirectorApproval, LeaveTrigger.Approve, new(), LeaveStatus.Approved },
{ LeaveStatus.PendingManagerApproval, LeaveTrigger.Reject, new(HasComment: true), LeaveStatus.Rejected },
{ LeaveStatus.PendingDirectorApproval, LeaveTrigger.Reject, new(HasComment: true), LeaveStatus.Rejected },
{ LeaveStatus.PendingManagerApproval, LeaveTrigger.Cancel, new(), LeaveStatus.Cancelled },
{ LeaveStatus.PendingDirectorApproval, LeaveTrigger.Cancel, new(), LeaveStatus.Cancelled },
{ LeaveStatus.Approved, LeaveTrigger.Cancel, new(LeaveStarted: false), LeaveStatus.Cancelled },
{ LeaveStatus.Approved, LeaveTrigger.Cancel, new(LeaveStarted: true, ByHrSpecialist: true), LeaveStatus.Cancelled },
};
// Diyagramda oku olmayan her (durum, trigger) çifti: 6 x 4 = 24 hücreden okların kapsamadığı 16 hücre
public static TheoryData<LeaveStatus, LeaveTrigger> MissingArrows()
{
TheoryData<LeaveStatus, LeaveTrigger> data = [];
TransitionContext open = new(HasComment: true, ByHrSpecialist: true);
foreach (LeaveStatus s in Enum.GetValues<LeaveStatus>())
{
foreach (LeaveTrigger t in Enum.GetValues<LeaveTrigger>())
{
if (LeaveTransitions.Next(s, t, open) is null)
{
data.Add(s, t);
}
}
}
return data;
}
[Theory]
[MemberData(nameof(Arrows))]
public void Arrow_leads_to_target(LeaveStatus from, LeaveTrigger trigger, TransitionContext context, LeaveStatus expected) =>
Assert.Equal(expected, LeaveTransitions.Next(from, trigger, context));
[Theory]
[MemberData(nameof(MissingArrows))]
public void Missing_arrow_is_rejected(LeaveStatus from, LeaveTrigger trigger) =>
Assert.Null(LeaveTransitions.Next(from, trigger, new(HasComment: true, ByHrSpecialist: true)));
Test class'ında bunlara ek olarak beş guard için birer "kapalı guard" testi ve açık hücre sayısının tam sekiz olduğunu doğrulayan bir Fact var. Sonuncusu önemli: biri switch'e diyagramda olmayan bir kol eklerse, örneğin Rejected'dan Cancel'a, test kırılır ve "önce diyagramı güncelle" konuşması code review'da başlar. xUnit 2.9.3 ile gerçek koşu:
Passed! - Failed: 0, Passed: 32, Skipped: 0, Total: 32, Duration: 20 ms - HrPortal.Leave.Tests.dll (net10.0)
32 test: 10 ok, 5 kapalı guard, 16 boş hücre ve 1 sayım testi. Açılıştaki naif Cancel, Rejected ve Cancelled satırlarında bu testlerden üçünü kırardı.
Sık Düşülen Tuzaklar
State ile aktiviteyi karıştırmak. "Bakiye kontrol ediliyor" bir state değil, bir geçişin guard'ı ya da effect'idir. State, object'in bir event bekleyerek durduğu yerdir; isimleri de buna göre sıfat ya da "bekliyor" biçiminde olur. Diyagramda adı fiille biten kutular varsa, büyük ihtimalle activity diagram çiziyorsundur.
Choice'ta [else] unutmak ve örtüşen guard'lar yazmak. UML'de hiçbir guard'ı true olmayan choice modeli bozar; iki guard birden true ise davranış belirsizleşir. Stateless bunu çalışma zamanında yakalıyor; guard'ları WorkingDays > 3 ve WorkingDays >= 3 yazınca gerçek mesaj şu:
InvalidOperationException: Multiple permitted exit transitions are configured from state 'PendingManagerApproval' for trigger 'Approve'. Guard clauses must be mutually exclusive.
Effect'i entry'ye ya da exit'e yazmak. Yukarıda gördüğümüz iade örneği: davranış tek bir oka aitse o okun effect'idir. Benzer şekilde, state'e geri dönen bir hatırlatma için self-transition çizmek entry'deki bildirimi her seferinde yeniden tetikler.
Composite state'i veritabanına yazmak. PendingApproval bir sorgu filtresi ve bir okuma kolaylığıdır; kolonda yalnız yaprak state'ler durur. "Onay bekliyor" diye ayrı bir değer eklemek, aynı talebin hem PendingApproval hem PendingManagerApproval olarak raporlanmasına yol açar.
Her şeyi tek diyagrama sıkıştırmak. İK uzmanının bakiye düzeltmesi, bildirimin e-posta kuyruğundaki durumu ve takvim senkronizasyonu ayrı yaşam döngüleridir. Tek diyagramda toplanınca kimse okumaz. Bir state machine bir object'in ömrünü anlatır.
Diyagramı çizip testle bağlamamak. Testsiz bir state machine diagram, ilk sprint sonunda kodun gerisinde kalır. Ok sayısını doğrulayan tek bir test bile ikisini birbirine bağlı tutar.
Küçük Bir Deneme
★☆☆ İş birimi taslağın da iptal edilebileceğini söyledi. Diyagrama ve tabloya Draft --Cancel--> Cancelled okunu ekle. Hangi test önce kırılıyor? Sayım testindeki sekiz kaça çıkmalı?
★★☆ Approved state'ini Upcoming ve InProgress alt state'lerine böl. Aradaki geçişi when(today >= Period.Start) change event'iyle çiz ve R11'deki "izin başladıktan sonra yalnız İK iptal eder" kuralını guard yerine state yapısıyla ifade et. Kodda change event'i ne tetikleyecek: bir background job mı, her okumada hesaplanan bir property mi?
★★★ Bordro kapanışı sırasında İK, onay bekleyen bütün talepleri geçici olarak OnHold durumuna alıyor ve kapanış bitince her talep kaldığı onay adımına geri dönmeli. Bunu shallow history pseudostate ile çiz, sonra Stateless'ta history olmadığı için son alt state'i bir field'da tutarak uygula ve diyagramdan türettiğin testleri yaz.
Şimdiye kadar tek bir object'in içine ve davranışına baktık. Bir sonraki yazıda bir seviye yukarı çıkıyoruz: İzin, Bordro ve Bildirim modüllerini component diagram ile çizecek, provided ve required interface'lerin, port'ların ve assembly connector'ların .NET'te DI (dependency injection) registration'ına nasıl karşılık geldiğini göreceğiz.