Bu yazı Design Patterns/Tasarım Desenleri nedir? başlıklı yazı dizisinin bir parçasıdır.
Bu içerik ağırlıklı olarak refactoring.guru sitesindeki içeriğin tercümesi ve derlenmesinden oluşturulmuştur.
Tüm tasarım desenleri ya da diğer adıyla tasarım kalıplarına yönelik ayrıntılı içeriklere yazının sonundaki bağlantılardan ulaşabilirsiniz.
Visitor Tasarım Deseninin Amacı
Visitor, algoritmaları üzerinde çalıştıkları nesnelerden ayırmanıza olanak sağlayan davranışsal bir tasarım kalıbıdır.
Sorun
Ekibinizin coğrafi bilgileri devasa bir graph yapısında saklayan bir uygulama üzerinde çalıştığını hayal edin. Bu Graph’ın her bir düğümü bir şehir gibi kompleks bir varlığı temsil ediyor olabileceği gibi, sanayi, gezi alanları gibi daha ayrıntı olan şeyleri de temsil edebilsin. Düğümler, temsil ettikleri gerçek nesneler arasında bir yol varsa da birbirine bağlı olsunlar. Arka tarafta her düğüm kendi sınıfıyla temsil ediliyorken, her biri aynı zamanda bir nesnedir.
Günün birinde bu graph yapısını XML formatında dışarıya aktarmanız istendi. Bu iş ilk başta size oldukça basit göründü. Düğümün tüm sınıflarına dışarı aktarmak için bir metot tanımlayıp, daha sonra da tüm düğüm uçlarını dolaşarak bu dışarı aktarma metodunu çağırabileceğinizi düşündünüz. Çözüm basit ve zarif: polimorfizm sayesinde, dışa aktarma metodunu düğümlerin asıl sınıfı içine koymadınız.
Ne yazık ki, sistem mimarı (system architect) mevcut düğüm sınıflarını değiştirmenize izin vermedi. Kodun zaten canlıda (production) olduğunu ve değişikliklerinizdeki olası bir hata nedeniyle kodu bozma riskini almak istemediğini söyledi.
Mimar XML dışa aktarma kodunun düğüm sınıfları içinde olmasının mantıklı olup olmadığını da sorguladı. Bu sınıfların birincil işi coğrafi verilerle çalışmaktı. XML dışa aktarma davranışının onlarla doğrudan ilişkisi yoktu.
Reddedilmenizin başka bir nedeni daha vardı. Bu özellik uygulandıktan sonra, pazarlama departmanından birinin sizden farklı bir formata dışa aktarma özelliği veya başka garip şeyler istemesi de muhtemeldi. Bu, sizi o değerli ve kırılgan sınıfları tekrar değiştirmeye zorlayacaktı.
Çözüm
Visitor Pattern, gereken bu yeni davranışı mevcut sınıflara entegre etmek yerine, visitor adı verilen ayrı bir sınıfa koymanızı önerir. Bu davranışı gerçekleştirmesi gereken asıl nesnemizi, nesne içerisindeki gereken tüm verilere erişebileceğinden de emin olarak, visitor’un metodlarından birine parametre olarak göndeririz.
Peki bu yeni davranış farklı sınıflar üzerinde çalıştırıldığında ne olur? XML olarak dışa aktarma örneğimize göndersek, uygulama muhtemelen her düğüm sınıfı için farklı olacaktır. Bu nedenle visitor sınıfı bir değil, farklı tipleri parametre olarak alabilecek aşağıdaki gibi birden fazla metod içermelidir:
class ExportVisitor implements Visitor is
method doForCity(City c) { ... }
method doForIndustry(Industry f) { ... }
method doForSightSeeing(SightSeeing ss) { ... }
// ...
İyi ama bu metodları, özellikle de graph’ın tamamı düşünüldüğünde nasıl çağıracağız? Her metodun farklı bir kullanım şekli olduğunu dikkate alınca polimorfizm kullanamayız. Verilen nesneyi doğru şekilde işeyebilecek bir metod belirlemek için, nesnenin sınıfını kontrol etmemiz gerekir. Bu size de işkence gibi gelmedi mi?
foreach (Node node in graph)
if (node instanceof City)
exportVisitor.doForCity((City) node)
if (node instanceof Industry)
exportVisitor.doForIndustry((Industry) node)
// ...
}
Neden aşırı yükleme / metod ezme (overloading) yöntemini kullanmıyoruz diye sorabilirsiniz. Bu yöntemde bir metot aynı isimler fakat farklı parametre tipleri alacak şekilde tanımlanır. Maalesef kullandığımız programlama dilinin bunu desteklediğini varsaysak bile (Örneğin Java ve C# desteklerken Golang desteklemiyor) bu yöntemi kullanmamız mümkün değil. Bir düğüm nesnesinin sınıfı önceden bilinemediğinden aşırı yükleme mekanizması çalıştırılacak doğru metodu seçemez. Tüm uçlar için temel düğüm uç sınıfını esas alarak ona ilişkin metodu seçer.
Visitor deseni tam olarak bu sorunu giderir. Koşullu ifade tanımlama külfetine girmeden, “Double Dispatch” adı verilen bir teknikle doğru metodun çalıştırılmasını sağlar. İstemcinin çalıştırılacak doğru metodu seçmesi yerine, bu seçimi visitor’e parametre olarak gönderdiğimiz nesnelere yaptırır. Nesneler kendi sınıflarını bildikleri için, visitordeki doğru metodu seçebilirler. Bir visitor kabul edip, ona hangi metodu çağıracağını söyleyebilirler.
foreach (Node node in graph)
if (node instanceof City)
exportVisitor.doForCity((City) node)
if (node instanceof Industry)
exportVisitor.doForIndustry((Industry) node)
// ...
}
İtiraf ediyorum, sonuçta düğüm uç sınıflarını değiştirmek zorunda kaldık ama yaptığımız değişiklik oldukça basit ve ileride kodu değiştirmeden başka değişiklikler yapmamıza olanak sağlıyor.
Tüm visitor’ler için ortak bir arayüz oluşturursak, mevcut tüm düğüm uçları, vereceğimiz herhangi bir visitor nesnesi ile çalışabilirler. Bu uçlara yeni özellikler eklemek istersek yapmamız gereken tek şey yeni visitor sınıfları tanımlamak olacaktır.
Uygulanabilirlik
Karmaşık bir nesne yapısının (örneğin bir nesne ağacı) tüm öğeleri üzerinde bir işlem gerçekleştirmeniz gerektiğinde Visitor desenini kullanabilirsiniz.
Visitor deseni ile, bir işlemin farklı varyantlar için metodlarını bir visitor nesnesi içinde tanımlayabilir ve bu işlemi farklı sınıflardaki bir dizi nesnede kolayca çalıştırabilirsiniz.
İkinci/yardımcı davranışların çalışma mantığını ana koddan temizlemek için Visitor desenini kullanabilirsiniz.
Bu desen yan işleri ayrı sınıflara alarak ana sınıfların kendi işlerine konsantre olmasına olanak sağlar.
Bir davranış bir hiyerarşideki sadece bazı sınıflar için uygun ama diğerleri için değilse bu deseni kullanabilirsiniz.
Bu davranışı ayrı bir visitor sınıfı içine alabilir ve gereksizleri boş bırakarak sadece ilgili sınıfları kabul eden visitor metodları oluşturabilirsiniz.
Diğer tasarım desenleri/kalıpları ile ilişkisi
- Visitor‘ı Command‘in çok daha güçlü hali olarak görebilirsiniz. Visitor nesneleri, farklı sınıflardan nesneler üzerinde işlem çalıştırmaya olanak sağlar.
- Composite ağacın tamamında bir operasyon çalıştırmak için Visitor kullanabilirsiniz.
- Kompleks bir veri yapısında dolaşmak, sınıfları farklı olsa bile elemanları üzerinde çeşitli işlemler gerçekleştirmek için Visitor ve Iteratorü birlikte kullanabilirsiniz.
Visitor Deseni Kod Örnekleri
Örnek PHP Kodu
<?php
namespace RefactoringGuru\Visitor\Conceptual;
/**
* The Component interface declares an `accept` method that should take the base
* visitor interface as an argument.
*/
interface Component
{
public function accept(Visitor $visitor): void;
}
/**
* Each Concrete Component must implement the `accept` method in such a way that
* it calls the visitor's method corresponding to the component's class.
*/
class ConcreteComponentA implements Component
{
/**
* Note that we're calling `visitConcreteComponentA`, which matches the
* current class name. This way we let the visitor know the class of the
* component it works with.
*/
public function accept(Visitor $visitor): void
{
$visitor->visitConcreteComponentA($this);
}
/**
* Concrete Components may have special methods that don't exist in their
* base class or interface. The Visitor is still able to use these methods
* since it's aware of the component's concrete class.
*/
public function exclusiveMethodOfConcreteComponentA(): string
{
return "A";
}
}
class ConcreteComponentB implements Component
{
/**
* Same here: visitConcreteComponentB => ConcreteComponentB
*/
public function accept(Visitor $visitor): void
{
$visitor->visitConcreteComponentB($this);
}
public function specialMethodOfConcreteComponentB(): string
{
return "B";
}
}
/**
* The Visitor Interface declares a set of visiting methods that correspond to
* component classes. The signature of a visiting method allows the visitor to
* identify the exact class of the component that it's dealing with.
*/
interface Visitor
{
public function visitConcreteComponentA(ConcreteComponentA $element): void;
public function visitConcreteComponentB(ConcreteComponentB $element): void;
}
/**
* Concrete Visitors implement several versions of the same algorithm, which can
* work with all concrete component classes.
*
* You can experience the biggest benefit of the Visitor pattern when using it
* with a complex object structure, such as a Composite tree. In this case, it
* might be helpful to store some intermediate state of the algorithm while
* executing visitor's methods over various objects of the structure.
*/
class ConcreteVisitor1 implements Visitor
{
public function visitConcreteComponentA(ConcreteComponentA $element): void
{
echo $element->exclusiveMethodOfConcreteComponentA() . " + ConcreteVisitor1\n";
}
public function visitConcreteComponentB(ConcreteComponentB $element): void
{
echo $element->specialMethodOfConcreteComponentB() . " + ConcreteVisitor1\n";
}
}
class ConcreteVisitor2 implements Visitor
{
public function visitConcreteComponentA(ConcreteComponentA $element): void
{
echo $element->exclusiveMethodOfConcreteComponentA() . " + ConcreteVisitor2\n";
}
public function visitConcreteComponentB(ConcreteComponentB $element): void
{
echo $element->specialMethodOfConcreteComponentB() . " + ConcreteVisitor2\n";
}
}
/**
* The client code can run visitor operations over any set of elements without
* figuring out their concrete classes. The accept operation directs a call to
* the appropriate operation in the visitor object.
*/
function clientCode(array $components, Visitor $visitor)
{
// ...
foreach ($components as $component) {
$component->accept($visitor);
}
// ...
}
$components = [
new ConcreteComponentA(),
new ConcreteComponentB(),
];
echo "The client code works with all visitors via the base Visitor interface:\n";
$visitor1 = new ConcreteVisitor1();
clientCode($components, $visitor1);
echo "\n";
echo "It allows the same client code to work with different types of visitors:\n";
$visitor2 = new ConcreteVisitor2();
clientCode($components, $visitor2);
Örnek Python Kodu
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import List
class Component(ABC):
"""
The Component interface declares an `accept` method that should take the
base visitor interface as an argument.
"""
@abstractmethod
def accept(self, visitor: Visitor) -> None:
pass
class ConcreteComponentA(Component):
"""
Each Concrete Component must implement the `accept` method in such a way
that it calls the visitor's method corresponding to the component's class.
"""
def accept(self, visitor: Visitor) -> None:
"""
Note that we're calling `visitConcreteComponentA`, which matches the
current class name. This way we let the visitor know the class of the
component it works with.
"""
visitor.visit_concrete_component_a(self)
def exclusive_method_of_concrete_component_a(self) -> str:
"""
Concrete Components may have special methods that don't exist in their
base class or interface. The Visitor is still able to use these methods
since it's aware of the component's concrete class.
"""
return "A"
class ConcreteComponentB(Component):
"""
Same here: visitConcreteComponentB => ConcreteComponentB
"""
def accept(self, visitor: Visitor):
visitor.visit_concrete_component_b(self)
def special_method_of_concrete_component_b(self) -> str:
return "B"
class Visitor(ABC):
"""
The Visitor Interface declares a set of visiting methods that correspond to
component classes. The signature of a visiting method allows the visitor to
identify the exact class of the component that it's dealing with.
"""
@abstractmethod
def visit_concrete_component_a(self, element: ConcreteComponentA) -> None:
pass
@abstractmethod
def visit_concrete_component_b(self, element: ConcreteComponentB) -> None:
pass
"""
Concrete Visitors implement several versions of the same algorithm, which can
work with all concrete component classes.
You can experience the biggest benefit of the Visitor pattern when using it with
a complex object structure, such as a Composite tree. In this case, it might be
helpful to store some intermediate state of the algorithm while executing
visitor's methods over various objects of the structure.
"""
class ConcreteVisitor1(Visitor):
def visit_concrete_component_a(self, element) -> None:
print(f"{element.exclusive_method_of_concrete_component_a()} + ConcreteVisitor1")
def visit_concrete_component_b(self, element) -> None:
print(f"{element.special_method_of_concrete_component_b()} + ConcreteVisitor1")
class ConcreteVisitor2(Visitor):
def visit_concrete_component_a(self, element) -> None:
print(f"{element.exclusive_method_of_concrete_component_a()} + ConcreteVisitor2")
def visit_concrete_component_b(self, element) -> None:
print(f"{element.special_method_of_concrete_component_b()} + ConcreteVisitor2")
def client_code(components: List[Component], visitor: Visitor) -> None:
"""
The client code can run visitor operations over any set of elements without
figuring out their concrete classes. The accept operation directs a call to
the appropriate operation in the visitor object.
"""
# ...
for component in components:
component.accept(visitor)
# ...
if __name__ == "__main__":
components = [ConcreteComponentA(), ConcreteComponentB()]
print("The client code works with all visitors via the base Visitor interface:")
visitor1 = ConcreteVisitor1()
client_code(components, visitor1)
print("It allows the same client code to work with different types of visitors:")
visitor2 = ConcreteVisitor2()
client_code(components, visitor2)
Anlatım için teşekkürler.