Ziyaretçi deseninde kabul etme () yönteminin amacı nedir?


88

Algoritmaları sınıflardan ayırma konusunda çok fazla konuşma var. Ancak, açıklanmayan bir şey bir kenara bırakılır.

Ziyaretçiyi böyle kullanıyorlar

abstract class Expr {
  public <T> T accept(Visitor<T> visitor) {visitor.visit(this);}
}

class ExprVisitor extends Visitor{
  public Integer visit(Num num) {
    return num.value;
  }

  public Integer visit(Sum sum) {
    return sum.getLeft().accept(this) + sum.getRight().accept(this);
  }

  public Integer visit(Prod prod) {
    return prod.getLeft().accept(this) * prod.getRight().accept(this);
  }

Ziyaret (öğe) 'yi doğrudan çağırmak yerine, Ziyaretçi öğeden ziyaret yöntemini çağırmasını ister. Ziyaretçiler hakkında beyan edilen sınıfın farkında olmama fikriyle çelişiyor.

PS1 Lütfen kendi kelimelerinizle açıklayın veya tam açıklamayı işaret edin. Çünkü aldığım iki yanıt genel ve belirsiz bir şeye atıfta bulundu.

PS2 Benim tahminim: yana getLeft()temel getiri Expression, çağıran visit(getLeft())yol açacağı visit(Expression), oysa getLeft()çağıran visit(this)başka neden olacaktır, daha uygun, ziyaret çağırmayı. Yani, accept()tür dönüştürmeyi (aka döküm) gerçekleştirir.

PS3 Scala's Pattern Matching = Steroid üzerindeki Ziyaretçi Modeli , kabul yöntemi olmadan Ziyaretçi modelinin ne kadar basit olduğunu gösterir. Wikipedia bu ifadeye ekliyor : accept()"Düşünme mevcut olduğunda yöntemlerin gereksiz olduğunu gösteren bir makaleyi bağlayarak ; teknik için 'Gezinme' terimini tanıtıyor."



"Ziyaretçi aramaları kabul ettiğinde, arayan uç, aranan ucun türüne göre gönderilir. Daha sonra aranan uç, ziyaretçinin türüne özgü ziyaret yöntemini geri arar ve bu arama, ziyaretçinin gerçek türüne göre gönderilir." Başka bir deyişle, kafamı karıştıran şeyi ifade ediyor. Bu nedenle biraz daha açık konuşur musunuz?
Val

Yanıtlar:


155

Ziyaretçi kalıbı visit/ acceptyapıları, C benzeri dillerin (C #, Java, vb.) Semantiği nedeniyle gerekli bir kötülüktür. Ziyaretçi modelinin amacı, aramanızı kodu okumaktan bekleyeceğiniz şekilde yönlendirmek için çift gönderimi kullanmaktır.

Normalde ziyaretçi modeli kullanıldığında, tüm düğümlerin Nodebundan sonra olarak anılan bir temel türden türetildiği bir nesne hiyerarşisi dahil edilir Node. İçgüdüsel olarak şöyle yazardık:

Node root = GetTreeRoot();
new MyVisitor().visit(root);

Sorun burada yatıyor. Bizim ise MyVisitorsınıf aşağıdaki gibi tanımlanmıştır:

class MyVisitor implements IVisitor {
  void visit(CarNode node);
  void visit(TrainNode node);
  void visit(PlaneNode node);
  void visit(Node node);
}

Çalışma zamanında, gerçek türden bağımsız olarak, rootçağrımız aşırı yüke girecektir visit(Node node). Bu, türden bildirilen tüm değişkenler için geçerli olacaktır Node. Bu neden? Çünkü Java ve diğer C benzeri diller , hangi aşırı yüklemenin çağrılacağına karar verirken parametrenin yalnızca statik türünü veya değişkenin bildirildiği türü dikkate alır. Java, her yöntem çağrısı için çalışma zamanında "Tamam, dinamik türü rootnedir? Ah, anlıyorum. Bu bir TrainNode. Bakalım MyVisitorbir tür parametresini kabul eden herhangi bir yöntem var mı?TrainNode... ". Derleyici, derleme zamanında, hangi yöntemin çağrılacağını belirler. (Java, argümanların dinamik türlerini gerçekten incelemiş olsaydı, performans oldukça kötü olurdu.)

Java, bir yöntem çağrıldığında bir nesnenin çalışma zamanı (yani dinamik) türünü hesaba katmak için bize bir araç sağlar - sanal yöntem gönderimi . Sanal bir yöntemi çağırdığımızda, çağrı aslında bellekte işlev işaretçilerinden oluşan bir tabloya gider . Her türün bir tablosu vardır. Belirli bir yöntem bir sınıf tarafından geçersiz kılınırsa, bu sınıfın işlev tablosu girişi, geçersiz kılınan işlevin adresini içerecektir. Sınıf bir yöntemi geçersiz kılmazsa, temel sınıfın uygulanmasına bir işaretçi içerecektir. Bu yine de bir performans ek yüküne neden olur (her yöntem çağrısı temelde iki işaretleyicinin referansını kaldırır: biri türün işlev tablosunu, diğeri işlevin kendisini gösterir), ancak yine de parametre türlerini incelemekten daha hızlıdır.

Ziyaretçi modelinin amacı, çift ​​gönderimi gerçekleştirmektir - yalnızca dikkate alınan çağrı hedefinin türü değil ( MyVisitorsanal yöntemler aracılığıyla), aynı zamanda parametrenin türü (ne tür Nodearıyoruz)? Ziyaretçi kalıbı, bunu visit/ acceptkombinasyonu ile yapmamızı sağlar .

Hattımızı buna değiştirerek:

root.accept(new MyVisitor());

İstediğimizi elde edebiliriz: sanal yöntem gönderimi yoluyla, alt sınıf tarafından uygulandığı şekliyle doğru accept () çağrısını giriyoruz - örneğimizde aşağıdakilerin uygulamasını TrainElementgireceğiz :TrainElementaccept()

class TrainNode extends Node implements IVisitable {
  void accept(IVisitor v) {
    v.visit(this);
  }
}

Derleyici bu noktada, TrainNode'ın kapsamı içinde ne biliyor accept? Statik türünün thisa olduğunu bilirTrainNode . Bu, derleyicinin arayanın kapsamı içinde farkında olmadığı önemli bir ek bilgi parçacığıdır: Orada, tek bildiği rootbir Node. Artık derleyici this( root) 'nin sadece a olmadığını Node, aslında bir olduğunu biliyor TrainNode. Sonuç olarak, içinde bulunan tek satır accept(): v.visit(this)tamamen başka bir şey anlamına gelir. Derleyici şimdi visit()bir TrainNode. Bir tane bulamazsa, çağrıyı birNode. Hiçbiri yoksa, bir derleme hatası alırsınız (süren bir aşırı yüklemeniz yoksa object). Yürütme, böylece başından beri niyet ettiğimiz şeye girecektir: MyVisitor'ın uygulanmasına visit(TrainNode e). Hiçbir alçıya gerek yoktu ve en önemlisi, herhangi bir yansımaya gerek yoktu. Bu nedenle, bu mekanizmanın ek yükü oldukça düşüktür: yalnızca işaretçi referanslarından oluşur ve başka hiçbir şey yoktur.

Sorunuzda haklısınız - bir alçı kullanabilir ve doğru davranışı alabiliriz. Ancak, çoğu zaman, Düğümün ne tür olduğunu bile bilmiyoruz. Aşağıdaki hiyerarşinin durumunu ele alalım:

abstract class Node { ... }
abstract class BinaryNode extends Node { Node left, right; }
abstract class AdditionNode extends BinaryNode { }
abstract class MultiplicationNode extends BinaryNode { }
abstract class LiteralNode { int value; }

Ve bir kaynak dosyayı ayrıştıran ve yukarıdaki spesifikasyona uyan bir nesne hiyerarşisi üreten basit bir derleyici yazıyorduk. Ziyaretçi olarak uygulanan hiyerarşi için bir tercüman yazıyor olsaydık:

class Interpreter implements IVisitor<int> {
  int visit(AdditionNode n) {
    int left = n.left.accept(this);
    int right = n.right.accept(this); 
    return left + right;
  }
  int visit(MultiplicationNode n) {
    int left = n.left.accept(this);
    int right = n.right.accept(this);
    return left * right;
  }
  int visit(LiteralNode n) {
    return n.value;
  }
}

Biz türlerini bilmiyoruz çünkü çok uzak bize almak değildir Döküm leftveya rightiçinde visit()yöntemlerle. Ayrıştırıcımız büyük olasılıkla Nodehiyerarşinin köküne işaret eden bir tür nesnesi de döndürecektir , bu yüzden onu da güvenli bir şekilde çeviremeyiz. Böylece basit tercümanımız şöyle görünebilir:

Node program = parse(args[0]);
int result = program.accept(new Interpreter());
System.out.println("Output: " + result);

Ziyaretçi kalıbı, çok güçlü bir şey yapmamızı sağlar: bir nesne hiyerarşisi verildiğinde, kodu hiyerarşinin sınıfına koymaya gerek kalmadan hiyerarşi üzerinde çalışan modüler işlemler oluşturmamıza izin verir. Ziyaretçi kalıbı, örneğin derleyici yapımında yaygın olarak kullanılır. Belirli bir programın sözdizimi ağacı göz önüne alındığında, o ağaç üzerinde çalışan pek çok ziyaretçi yazılır: tür kontrolü, optimizasyonlar, makine kodu yayımı genellikle farklı ziyaretçiler olarak uygulanır. Optimizasyon ziyaretçisi durumunda, giriş ağacına göre yeni bir sözdizimi ağacı bile çıkarabilir.

Elbette dezavantajları var: hiyerarşiye yeni bir tür eklersek visit(), IVisitorarayüze bu yeni tür için bir yöntem de eklememiz ve tüm ziyaretçilerimizde saplama (veya tam) uygulamalar oluşturmamız gerekir. accept()Yukarıda açıklanan nedenlerden dolayı yöntemi de eklememiz gerekiyor . Performans sizin için bu kadar önemli değilse, ziyaretçilere ihtiyaç duymadan yazmak için çözümler vardır accept(), ancak bunlar normalde yansıma içerir ve bu nedenle oldukça büyük bir ek yüke neden olabilirler.


5
Etkili Java Öğesi # 41 şu uyarıyı içerir: " aynı parametre setinin, yayınların eklenmesiyle farklı aşırı yüklemelere geçirilebildiği durumlardan kaçının. " accept()Bu uyarı Ziyaretçi'de ihlal edildiğinde yöntem gerekli hale gelir.
jaco0646

" Normalde ziyaretçi kalıbı kullanıldığında, tüm düğümlerin bir temel Düğüm türünden türetildiği bir nesne hiyerarşisi dahil edilir " bu, C ++ 'da kesinlikle gerekli değildir. Bkz Boost.Variant, Eggs.Variant
Jean-Michaël Celerier

Bana öyle geliyor ki,
java'da

1
Vay canına, bu harika bir açıklamaydı. Modelin tüm gölgelerinin derleyici sınırlamalarından kaynaklandığını görmek aydınlatıcı ve şimdi sizin sayenizde net bir şekilde ortaya çıkıyor.
Alfonso Nishikawa

@GiladBaruchian derleyici en spesifik tipi metodu bir çağrı oluşturur derleyici belirleyebilir.
mmw

15

Elbette , Accept'in uygulanmasının tek yolu bu olsaydı aptalca olurdu .

Ama öyle değil.

Örneğin, hiyerarşilerle uğraşırken ziyaretçiler gerçekten çok kullanışlıdır , bu durumda terminal olmayan bir düğümün uygulanması buna benzer bir şey olabilir.

interface IAcceptVisitor<T> {
  void Accept(IVisit<T> visitor);
}
class HierarchyNode : IAcceptVisitor<HierarchyNode> {
  public void Accept(IVisit<T> visitor) {
    visitor.visit(this);
    foreach(var n in this.children)
      n.Accept(visitor);
  }

  private IEnumerable<HierarchyNode> children;
  ....
}

Anlıyorsun? Aptal olarak tanımladığınız şey , hiyerarşileri aşmak için bir çözümdür.

İşte ziyaretçiyi anlamamı sağlayan çok daha uzun ve derinlemesine bir makale .

Düzenleme: Açıklığa kavuşturmak için: Ziyaretçinin Visityöntemi bir düğüme uygulanacak mantığı içerir. Düğümün Acceptyöntemi, bitişik düğümlere nasıl gidileceğine dair mantığı içerir. Eğer durum sadece çift sevk hiçbir basitçe gitmek için komşu düğümler vardır özel bir durumudur.


7
Açıklamanız, çocukları yinelemenin neden Ziyaretçi'nin uygun visit () yöntemi yerine Düğümün sorumluluğu olması gerektiğini açıklamıyor mu? Farklı ziyaretçiler için aynı ziyaret şablonlarına ihtiyacımız olduğunda ana fikrin hiyerarşi geçiş kodunu paylaşmak olduğunu mu söylüyorsunuz? Önerilen kağıttan herhangi bir ipucu göremiyorum.
Val

1
Kabul etmenin rutin geçişler için iyi olduğunu söylemek makul ve genel nüfus için değerlidir. Ancak, örneğimi başka birinin " andymaleh.blogspot.com/2008/04/… okuyana kadar Ziyaretçi kalıbını anlayamadım" dan aldım . Ne bu örnek, ne Wikipedia ne de diğer cevaplar navigasyon avantajından bahsetmiyor. Yine de, hepsi bu aptalca kabul etmeyi talep ediyorlar (). Bu yüzden sorumu soruyorum: Neden?
Val

1
@Val - ne demek istiyorsun? Ne istediğinden emin değilim. Diğer makaleler için konuşamam çünkü bu insanlar bu konuda farklı görüşlere sahipler, ancak anlaşmazlık içinde olduğumuzdan şüpheliyim. Genel olarak hesaplamada birçok sorun ağlarla eşleştirilebilir, bu nedenle kullanım yüzeydeki grafiklerle ilgisi olmayabilir, ancak aslında çok benzer bir sorundur.
George Mauer

1
Bazı yöntemin nerede yararlı olabileceğine dair bir örnek vermek, yöntemin neden zorunlu olduğu sorusuna cevap vermez. Gezinme her zaman gerekli olmadığından, accept () yöntemi her zaman ziyaret için iyi değildir. Bu nedenle, hedeflerimizi onsuz gerçekleştirebilmeliyiz. Yine de zorunludur. Bu, her ziyaretçi pıtırtı içine "bazen yararlıdır" dan daha güçlü bir kabul () nedeninin olduğu anlamına gelir. Sorumda net olmayan nedir? Wikipedia'nın neden kabul etmekten kurtulmanın yollarını aradığını anlamaya çalışmazsanız, sorumu anlamakla ilgilenmezsiniz.
Val

1
@Val "Ziyaretçi Modelinin Özü" ne bağladıkları kağıt, benim verdiğim özetinde aynı gezinme ve işlem ayrımını not ediyor. Basitçe, GOF uygulamasının (sorduğunuz şey budur) yansıma kullanımıyla giderilebilecek bazı sınırlamaları ve sıkıntıları olduğunu söylüyorlar - bu nedenle Gezinme modelini ortaya koyuyorlar. Bu kesinlikle yararlıdır ve ziyaretçinin yapabildiği şeylerin çoğunu yapabilir, ancak oldukça karmaşık bir koddur ve (üstünkörü bir okumada) tür güvenliğinin bazı faydalarını kaybeder. Araç kutusu için bir araç ama ziyaretçiden daha ağır bir araç
George Mauer

0

Ziyaretçi modelinin amacı, nesnelerin ziyaretçinin onlarla ne zaman bitirdiğini ve ayrıldığını bilmesini sağlamaktır, böylece sınıflar daha sonra gerekli herhangi bir temizliği gerçekleştirebilir. Ayrıca, sınıfların içlerini "geçici" olarak "ref" parametreleri olarak göstermelerine ve ziyaretçi gittikten sonra dahili öğelerin artık açığa çıkmayacağını bilmelerine izin verir. Temizlemenin gerekli olmadığı durumlarda, ziyaretçi düzeni çok kullanışlı değildir. Bunlardan hiçbirini yapmayan sınıflar, ziyaretçi deseninden yararlanamayabilir, ancak ziyaretçi desenini kullanmak için yazılan kod, erişimden sonra temizlik gerektirebilecek gelecekteki sınıflarla kullanılabilir olacaktır.

Örneğin, birinin atomik olarak güncellenmesi gereken birçok dizeyi tutan bir veri yapısına sahip olduğunu, ancak veri yapısını tutan sınıfın tam olarak hangi tür atomik güncellemelerin gerçekleştirilmesi gerektiğini bilmediğini varsayalım (örneğin, bir iş parçacığının " X ", başka bir iş parçacığı herhangi bir basamak dizisini sayısal olarak bir üst sırayla değiştirmek isterken, her iki iş parçacığının işlemi de başarılı olmalıdır; her iş parçacığı bir dizeyi okursa, güncellemelerini gerçekleştirir ve onu geri yazarsa, ikinci iş parçacığı dizesini geri yazmak için ilkinin üzerine yazılır). Bunu başarmanın bir yolu, her ipliğin bir kilit alması, işlemini gerçekleştirmesi ve kilidi serbest bırakmasıdır. Maalesef, kilitler bu şekilde açığa çıkarsa,

Ziyaretçi modeli, bu sorunu önlemek için (en az) üç yaklaşım sunar:

  1. Bir kaydı kilitleyebilir, sağlanan işlevi çağırabilir ve ardından kaydın kilidini açabilir; sağlanan işlev sonsuz bir döngüye girerse kayıt sonsuza kadar kilitlenebilir, ancak sağlanan işlev bir istisna döndürür veya atarsa, kaydın kilidi açılır (işlev bir istisna atarsa ​​kaydı geçersiz olarak işaretlemek mantıklı olabilir; kilitli olması muhtemelen iyi bir fikir değildir). Çağrılan işlev başka kilitler almaya çalışırsa kilitlenmenin ortaya çıkabileceğinin önemli olduğunu unutmayın.
  2. Bazı platformlarda, dizeyi 'ref' parametresi olarak tutan bir depolama konumunu iletebilir. Bu işlev daha sonra dizeyi kopyalayabilir, kopyalanan dizgeye göre yeni bir dizge hesaplayabilir, eski dizeyi yenisiyle değiştirmeyi deneyebilir ve CompareExchange başarısız olursa tüm süreci tekrarlayabilir.
  3. Dizenin bir kopyasını oluşturabilir, dizede sağlanan işlevi çağırabilir, ardından orijinali güncellemek için CompareExchange'i kullanabilir ve CompareExchange başarısız olursa tüm işlemi tekrarlayabilir.

Ziyaretçi modeli olmadan, atomik güncellemeler yapmak, arama yazılımı katı bir kilitleme / kilit açma protokolünü takip edemezse, kilitlerin açığa çıkarılmasını ve başarısızlık riskinin alınmasını gerektirir. Ziyaretçi kalıbı ile atomik güncellemeler nispeten güvenli bir şekilde yapılabilir.


2
1. ziyaret, yalnızca genel ziyaret yöntemlerine erişiminiz olduğu anlamına gelir, bu nedenle, Ziyaretçi için yararlı olması için dahili kilitleri halk tarafından erişilebilir hale getirmeniz gerekir. 2 / Daha önce gördüğüm örneklerin hiçbiri, Ziyaretçinin ziyaret edilen kişinin durumunu değiştirmek için kullanılması gerektiği anlamına gelmez. 3. "Geleneksel VisitorPattern ile, yalnızca bir düğüme girdiğimizde belirlenebilir. Mevcut düğüme girmeden önce önceki düğümü terk edip etmediğimizi bilmiyoruz." Ziyaret girmek ve ziyaret etmek yerine sadece ziyaret ile nasıl kilit açılır? Son olarak Ziyaretçi yerine accpet () uygulamalarını sordum.
Val

Belki de kalıplar için terminolojiye tamamen hakim değilim, ancak "ziyaretçi kalıbı", X'in Y'yi bir temsilciden geçirdiği ve Y'nin yalnızca şu şekilde geçerli olması gereken bilgileri iletebildiği, kullandığım bir yaklaşıma benziyor gibi görünüyor. delege çalıştığı sürece. Belki bu kalıbın başka bir adı vardır?
supercat

2
Bu, ziyaretçi modelinin belirli bir probleme yönelik ilginç bir uygulamasıdır , ancak modelin kendisini tanımlamaz veya orijinal soruyu yanıtlamaz. "Temizlemenin gerekli olmadığı durumlarda, ziyaretçi modeli o kadar da kullanışlı değildir." Bu iddia kesinlikle yanlıştır ve genel olarak modelle değil, yalnızca sizin özel sorununuzla ilgilidir.
Tony O'Hagan

0

Değişiklik gerektiren sınıfların tümü 'kabul etme' yöntemini uygulamalıdır. İstemciler, bu sınıf ailesi üzerinde bazı yeni eylemler gerçekleştirmek ve böylece işlevselliklerini genişletmek için bu kabul yöntemini çağırır. Müşteriler, her bir belirli eylem için farklı bir ziyaretçi sınıfına geçerek çok çeşitli yeni eylemler gerçekleştirmek için bu tek kabul yöntemini kullanabilir. Bir ziyaretçi sınıfı, aile içindeki her sınıf için aynı belirli eylemin nasıl gerçekleştirileceğini tanımlayan birden çok geçersiz kılınmış ziyaret yöntemi içerir. Bu ziyaret yöntemleri üzerinde çalışılacak bir örnek iletilir.

Sabit bir sınıf ailesine sık sık işlevsellik ekliyor, değiştiriyor veya kaldırıyorsanız, ziyaretçiler kullanışlıdır, çünkü her işlevsellik öğesi her ziyaretçi sınıfında ayrı olarak tanımlanır ve sınıfların kendilerinin de değiştirilmesi gerekmez. Sınıf ailesi sabit değilse, ziyaretçi düzeni daha az işe yarayabilir, çünkü birçok ziyaretçinin her sınıf eklendiğinde veya kaldırıldığında değişmesi gerekir.


-1

Bir iyi bir örnek kaynak kod derleme içinde:

interface CompilingVisitor {
   build(SourceFile source);
}

Müşteriler bir uygulayabilirsiniz JavaBuilder, RubyBuilder, XMLValidatorvb ve bir projedeki tüm kaynak dosyaları toplayıp ziyaret için uygulama değişikliğine gerek yoktur.

Bu bir kötü her bir kaynağın dosya türü için ayrı sınıflar varsa kalıbı:

interface CompilingVisitor {
   build(JavaSourceFile source);
   build(RubySourceFile source);
   build(XMLSourceFile source);
}

Bağlama ve sistemin hangi bölümlerinin genişletilebilir olmasını istediğinize bağlıdır.


İroni şu ki, VisitorPattern bize kötü modeli kullanmamızı teklif ediyor. Ziyaret edeceği her tür düğüm için bir ziyaret yöntemi tanımlamamız gerektiğini söylüyor. İkincisi, örneklerinizin hangisinin iyi veya kötü olduğu net değil? Sorumla nasıl bağlantılılar?
Val
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.