Behavioral — Davranışsal Desenler
Visitor
Ziyaretçi Deseni
Amaç: Bir nesne yapısındaki sınıfları değiştirmeden, bu sınıflar üzerinde çalışacak yeni işlemler eklemek.
Basit Anlatım
Teknik terim yok — günlük hayattan bir benzetme
Bir şehirde farklı türde binalar var: evler, fabrikalar, okullar. Şehre farklı uzmanlar gelir: vergi memuru, itfaiye denetçisi, sigorta danışmanı.
Vergi memuru eve gittiğinde emlak vergisini, fabrikaya gittiğinde ticari vergiyi hesaplar. İtfaiye denetçisi evde duman dedektörüne, fabrikada yangın merdivenine bakar. Her uzman, hangi bina türüne geldiğine göre farklı bir iş yapar.
Önemli olan şu: yeni bir uzman (ör. deprem denetçisi) şehre geldiğinde binaları yıkıp yeniden yapmanıza gerek yok. Binalar sadece kapıyı açar ve "buyurun" der. İşin nasıl yapılacağını ziyaretçi bilir.
Teknik Anlamı
Bir yazılımcının bakış açısıyla
Visitor, bir nesne yapısı üzerinde yapılacak işlemi ayrı bir ziyaretçi sınıfına taşır. Her eleman bir Accept(visitor) metodu sunar ve içinde visitor.Visit(this) çağırır. Bu sayede doğru Visit overload'u elemanın gerçek tipine göre seçilir.
Double Dispatch
C# gibi dillerde hangi metodun çağrılacağı normalde tek bir nesnenin tipine göre belirlenir (single dispatch). Visitor, iki çağrı ile hem elemanın hem de ziyaretçinin tipine göre doğru kodun çalışmasını sağlar:
element.Accept(visitor)→ elemanın gerçek tipi belirlenir.visitor.Visit(this)→thisartık somut tiptedir, doğru overload seçilir.
Katılımcılar
- Visitor: Her eleman tipi için bir
Visitmetodu bildirir. - ConcreteVisitor: Bir işlemin tüm eleman tipleri için uygulaması.
- Element:
Accept(IVisitor)metodunu tanımlar. - ConcreteElement:
Acceptiçindevisitor.Visit(this)çağırır.
Ne zaman kullanılır?
- Eleman sınıfları sabit, ama üzerlerinde yapılacak işlemler sık sık ekleniyorsa (derleyicilerdeki AST üzerinde tip kontrolü, optimizasyon, kod üretimi).
- .NET'teki
ExpressionVisitorve Roslyn'inCSharpSyntaxVisitorsınıfları bu desenin gerçek dünyadaki örnekleridir.
Dezavantajlar
- Yeni bir eleman tipi eklemek tüm ziyaretçileri değiştirmeyi gerektirir.
- Ziyaretçi, elemanların iç verisine erişmek için onların public alanlarına ihtiyaç duyar; bu kapsüllemeyi zayıflatabilir.
switch ifadeleri (pattern matching) basit durumlarda Visitor'a hafif bir alternatiftir; ancak Visitor, unutulan bir tipi derleme zamanında yakalama avantajını korur.C# ile Uygulama
Adım adım, çalışan kod örneği
Senaryo: Bir sigorta şirketinin portföyünde konut, araç ve sağlık poliçeleri var. Poliçe sınıflarına dokunmadan "prim hesaplama" ve "rapor çıkarma" işlemleri ekleyeceğiz.
Adım 1 Element ve Visitor arayüzleri
public interface IPolicyVisitor
{
void Visit(HomePolicy policy);
void Visit(CarPolicy policy);
void Visit(HealthPolicy policy);
}
public interface IPolicy
{
void Accept(IPolicyVisitor visitor);
}Adım 2 Somut elemanlar: poliçeler
Poliçe sınıfları yalnızca veri taşır ve Accept sunar. Prim hesaplama veya raporlama mantığı burada yok.
public class HomePolicy : IPolicy
{
public decimal HouseValue { get; init; }
public bool EarthquakeZone { get; init; }
public void Accept(IPolicyVisitor v) => v.Visit(this); // double dispatch
}
public class CarPolicy : IPolicy
{
public int CarAge { get; init; }
public int DriverAge { get; init; }
public void Accept(IPolicyVisitor v) => v.Visit(this);
}
public class HealthPolicy : IPolicy
{
public int Age { get; init; }
public bool Smoker { get; init; }
public void Accept(IPolicyVisitor v) => v.Visit(this);
}Adım 3 Somut ziyaretçiler: yeni işlemler
Her ziyaretçi bir işlemin tüm poliçe türleri için nasıl yapılacağını tek bir sınıfta toplar.
public class PremiumCalculator : IPolicyVisitor
{
public decimal Total { get; private set; }
public void Visit(HomePolicy p) =>
Add("Konut", p.HouseValue * 0.002m * (p.EarthquakeZone ? 1.5m : 1m));
public void Visit(CarPolicy p) =>
Add("Araç", 8_000m + p.CarAge * 400m + (p.DriverAge < 25 ? 5_000m : 0m));
public void Visit(HealthPolicy p) =>
Add("Sağlık", 3_000m + p.Age * 60m + (p.Smoker ? 2_500m : 0m));
private void Add(string type, decimal premium)
{
Total += premium;
Console.WriteLine($" {type,-7} primi: {premium,10:N2} TL");
}
}
public class RiskReport : IPolicyVisitor
{
public void Visit(HomePolicy p) => Print("Konut", p.EarthquakeZone, "deprem bölgesi");
public void Visit(CarPolicy p) => Print("Araç", p.DriverAge < 25, "genç sürücü");
public void Visit(HealthPolicy p) => Print("Sağlık", p.Smoker, "sigara");
private static void Print(string type, bool risky, string reason) =>
Console.WriteLine($" {type,-7}: {(risky ? $"YÜKSEK risk ({reason})" : "normal")}");
}Adım 4 Kullanım
Tüm parçaları bir araya getiren istemci (client) kodu:
var portfolio = new List<IPolicy>
{
new HomePolicy { HouseValue = 4_000_000m, EarthquakeZone = true },
new CarPolicy { CarAge = 5, DriverAge = 22 },
new HealthPolicy { Age = 40, Smoker = false },
};
Console.WriteLine("Prim hesaplama:");
var calculator = new PremiumCalculator();
portfolio.ForEach(p => p.Accept(calculator));
Console.WriteLine($" TOPLAM : {calculator.Total,10:N2} TL");
Console.WriteLine("\nRisk raporu:");
var report = new RiskReport();
portfolio.ForEach(p => p.Accept(report));Konsol çıktısı
Prim hesaplama: Konut primi: 12.000,00 TL Araç primi: 15.000,00 TL Sağlık primi: 5.400,00 TL TOPLAM : 32.400,00 TL Risk raporu: Konut : YÜKSEK risk (deprem bölgesi) Araç : YÜKSEK risk (genç sürücü) Sağlık : normal
Kodda dikkat edilmesi gerekenler
- İki farklı işlem ekledik ama poliçe sınıflarının hiçbirine dokunmadık.
- "Yıllık yenileme e-postası" gibi yeni bir işlem için yeni bir ziyaretçi yazmak yeterli.
- Bunun bedeli:
PetPolicygibi yeni bir poliçe türü eklenirseIPolicyVisitor'a yeni bir metot eklenir ve tüm ziyaretçiler güncellenmek zorunda kalır. Derleyici eksik olanları size gösterir.