Điểm của phương thức accept () trong mẫu khách truy cập là gì?


87

Có rất nhiều cuộc thảo luận về việc tách các thuật toán từ các lớp. Nhưng, một điều vẫn chưa được giải thích.

Họ sử dụng khách truy cập như thế này

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);
  }

Thay vì gọi trực tiếp truy cập (phần tử), Khách truy cập yêu cầu phần tử gọi phương thức truy cập của nó. Nó mâu thuẫn với ý tưởng đã tuyên bố về sự không ý thức của giai cấp về du khách.

PS1 Vui lòng giải thích bằng lời của riêng bạn hoặc chỉ vào lời giải thích chính xác. Bởi vì hai câu trả lời tôi nhận được đề cập đến một cái gì đó chung chung và không chắc chắn.

PS2 Dự đoán của tôi: Vì getLeft()trả về cơ bản Expression, việc gọi visit(getLeft())sẽ dẫn đến kết quả visit(Expression), trong khi getLeft()việc gọi visit(this)sẽ dẫn đến một lệnh gọi truy cập khác, phù hợp hơn. Vì vậy, hãy accept()thực hiện chuyển đổi kiểu (hay còn gọi là ép kiểu).

PS3 Scala's Pattern Matching = Mẫu khách truy cập trên Steroid cho thấy mẫu Khách truy cập đơn giản hơn bao nhiêu mà không có phương pháp chấp nhận. Wikipedia thêm vào tuyên bố này : bằng cách liên kết một bài báo cho thấy " accept()các phương pháp là không cần thiết khi có sự phản ánh; giới thiệu thuật ngữ 'Walkabout' cho kỹ thuật này."



Nó cho biết "khi khách truy cập cuộc gọi chấp nhận, cuộc gọi được gửi dựa trên loại bộ nhớ. Sau đó, bộ phận gọi lại phương thức truy cập cụ thể của loại khách truy cập và cuộc gọi này được gửi dựa trên loại thực tế của khách truy cập." Nói cách khác, nó nói lên điều khiến tôi bối rối. Vì lý do này, bạn có thể vui lòng nói rõ hơn được không?
Val

Câu trả lời:


154

Cấu trúc visit/ acceptcấu trúc của mẫu khách truy cập là một điều cần thiết do ngữ nghĩa của các ngôn ngữ giống C '(C #, Java, v.v.). Mục tiêu của mô hình khách truy cập là sử dụng điều phối kép để định tuyến cuộc gọi của bạn như bạn mong đợi khi đọc mã.

Thông thường khi mẫu khách truy cập được sử dụng, một hệ thống phân cấp đối tượng có liên quan trong đó tất cả các nút được bắt nguồn từ một Nodeloại cơ sở , được gọi là từ đó trở đi Node. Theo bản năng, chúng tôi viết nó như thế này:

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

Đây là vấn đề. Nếu MyVisitorlớp của chúng ta được định nghĩa như sau:

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

Nếu, trong thời gian chạy, bất kể kiểu thực tếroot là gì, cuộc gọi của chúng ta sẽ chuyển sang trạng thái quá tải visit(Node node). Điều này sẽ đúng cho tất cả các biến được khai báo kiểu Node. Tại sao thế này? Bởi vì Java và các ngôn ngữ giống C khác chỉ xem xét kiểu tĩnh , hoặc kiểu mà biến được khai báo, của tham số khi quyết định gọi quá tải nào. Java không thực hiện thêm bước nào để hỏi, đối với mỗi lần gọi phương thức, trong thời gian chạy, "Được rồi, kiểu động là rootgì? Ồ, tôi hiểu rồi. Đó là a TrainNode. Hãy xem có phương thức MyVisitornào chấp nhận tham số kiểu khôngTrainNode... ". Trình biên dịch, tại thời điểm biên dịch, xác định phương thức nào sẽ được gọi. (Nếu Java thực sự đã kiểm tra kiểu động của đối số, hiệu suất sẽ khá khủng khiếp.)

Java cung cấp cho chúng ta một công cụ để tính đến kiểu thời gian chạy (tức là động) của một đối tượng khi một phương thức được gọi - điều phối phương thức ảo . Khi chúng ta gọi một phương thức ảo, lệnh gọi thực sự đi đến một bảng trong bộ nhớ chứa các con trỏ hàm. Mỗi loại có một bảng. Nếu một phương thức cụ thể bị một lớp ghi đè, mục nhập bảng hàm của lớp đó sẽ chứa địa chỉ của hàm bị ghi đè. Nếu lớp không ghi đè một phương thức, nó sẽ chứa một con trỏ đến việc triển khai của lớp cơ sở. Điều này vẫn phát sinh chi phí hiệu suất (mỗi cuộc gọi phương thức về cơ bản sẽ tham chiếu đến hai con trỏ: một con trỏ đến bảng chức năng của kiểu và một con trỏ khác của chính hàm), nhưng nó vẫn nhanh hơn việc phải kiểm tra các loại tham số.

Mục tiêu của mẫu khách truy cập là thực hiện điều phối kép - không chỉ được xem xét loại mục tiêu cuộc gọi ( MyVisitor, thông qua các phương thức ảo), mà còn là loại tham số (loại Nodechúng tôi đang xem xét)? Mẫu khách truy cập cho phép chúng tôi thực hiện điều này bằng cách kết hợp visit/ accept.

Bằng cách thay đổi dòng của chúng tôi thành:

root.accept(new MyVisitor());

Chúng tôi có thể nhận được những gì chúng tôi muốn: thông qua điều phối phương thức ảo, chúng tôi nhập chính xác lời gọi accept () như được triển khai bởi lớp con - trong ví dụ của chúng tôi với TrainElement, chúng tôi sẽ nhập cách TrainElementtriển khai của accept():

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

Trình biên dịch biết gì tại thời điểm này, bên trong phạm vi của TrainNode's accept? Nó biết rằng kiểu tĩnh của thislà aTrainNode . Đây là một mẩu thông tin bổ sung quan trọng mà trình biên dịch không nhận thức được trong phạm vi của trình gọi của chúng tôi: ở đó, tất cả những gì nó biết rootlà nó là một Node. Bây giờ trình biên dịch biết rằng this( root) không chỉ là a Node, mà còn thực sự là a . Nếu cả hai đều không tồn tại, bạn sẽ gặp lỗi biên dịch (trừ khi bạn có quá tải xảy ra ). Do đó, quá trình thực thi sẽ nhập những gì chúng tôi đã dự định từ trước đến nay:TrainNode . Do đó, một dòng được tìm thấy bên trong accept(): v.visit(this), có nghĩa là một cái gì đó hoàn toàn khác. Trình biên dịch bây giờ sẽ tìm kiếm sự quá tải của visit()nó mất a TrainNode. Nếu nó không thể tìm thấy một cái, sau đó nó sẽ biên dịch cuộc gọi thành quá tải cóNodeobjectMyVisitor việc thực hiện visit(TrainNode e). Không cần phôi, và quan trọng nhất là không cần phản chiếu. Do đó, chi phí của cơ chế này khá thấp: nó chỉ bao gồm các tham chiếu con trỏ và không có gì khác.

Bạn nói đúng với câu hỏi của mình - chúng tôi có thể sử dụng dàn diễn viên và thực hiện hành vi chính xác. Tuy nhiên, thông thường, chúng ta thậm chí không biết loại Node là gì. Lấy trường hợp của hệ thống phân cấp sau:

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; }

Và chúng tôi đang viết một trình biên dịch đơn giản để phân tích một tệp nguồn và tạo ra một cấu trúc phân cấp đối tượng phù hợp với đặc điểm kỹ thuật ở trên. Nếu chúng tôi đang viết thông dịch viên cho hệ thống phân cấp được triển khai với tư cách là Khách truy cập:

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;
  }
}

Việc truyền sẽ không giúp chúng ta đi xa được, vì chúng ta không biết các loại lefthoặc righttrong các visit()phương pháp. Trình phân tích cú pháp của chúng tôi rất có thể cũng sẽ chỉ trả về một đối tượng kiểu Nodetrỏ vào gốc của hệ thống phân cấp, vì vậy chúng tôi cũng không thể ép kiểu đó một cách an toàn. Vì vậy, trình thông dịch đơn giản của chúng tôi có thể trông giống như:

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

Mẫu khách truy cập cho phép chúng ta làm một điều gì đó rất mạnh mẽ: với một hệ thống phân cấp đối tượng, nó cho phép chúng ta tạo các hoạt động mô-đun hoạt động trên hệ thống phân cấp mà không cần phải đặt mã vào chính lớp của hệ thống phân cấp. Ví dụ: mẫu khách truy cập được sử dụng rộng rãi trong xây dựng trình biên dịch. Với cây cú pháp của một chương trình cụ thể, nhiều khách truy cập được viết hoạt động trên cây đó: kiểm tra kiểu, tối ưu hóa, phát mã máy, tất cả thường được thực hiện như những khách truy cập khác nhau. Trong trường hợp của khách truy cập tối ưu hóa, nó thậm chí có thể xuất ra một cây cú pháp mới với cây đầu vào.

Tất nhiên, nó có những hạn chế: nếu chúng ta thêm một kiểu mới vào hệ thống phân cấp, chúng ta cũng cần thêm một visit()phương thức cho kiểu mới đó vào IVisitorgiao diện và tạo các triển khai sơ khai (hoặc đầy đủ) trong tất cả khách truy cập của chúng ta. Chúng tôi cũng cần thêm accept()phương thức vì những lý do được mô tả ở trên. Nếu hiệu suất không có nhiều ý nghĩa đối với bạn, có những giải pháp để viết cho khách truy cập mà không cần đến accept(), nhưng chúng thường liên quan đến sự phản ánh và do đó có thể phát sinh chi phí khá lớn.


5
Java hiệu quả Item # 41 bao gồm cảnh báo này: " tình huống tránh nơi có cùng một tập hợp các thông số có thể được truyền cho overloadings khác nhau bằng việc bổ sung các phôi. " Các accept()phương pháp trở nên cần thiết khi cảnh báo này bị vi phạm trong khách.
jaco0646

" Thông thường khi mẫu khách truy cập được sử dụng, một cấu trúc phân cấp đối tượng có liên quan trong đó tất cả các nút được bắt nguồn từ một loại Node cơ sở ", điều này hoàn toàn không cần thiết trong C ++. Xem Boost.Variant, Eggs.Variant
Jean-Michaël Celerier

Dường như với tôi rằng trong java chúng ta không thực sự cần sự chấp nhận phương pháp bởi vì trong java chúng tôi luôn gọi phương thức loại cụ thể nhất
Gilad Baruchian

1
Wow, đây là một lời giải thích tuyệt vời. Làm sáng tỏ khi thấy rằng tất cả các bóng của mẫu là do giới hạn của trình biên dịch, và bây giờ hiển thị rõ ràng nhờ bạn.
Alfonso Nishikawa

@GiladBaruchian, trình biên dịch tạo ra lệnh gọi đến phương thức kiểu cụ thể nhất mà trình biên dịch có thể xác định.
mmw

15

Tất nhiên đó sẽ là ngớ ngẩn nếu đó là chỉ cách nhất mà Chấp nhận được thực hiện.

Nhưng nó không phải như vậy.

Ví dụ: khách truy cập thực sự hữu ích khi xử lý cấu trúc phân cấp trong trường hợp này, việc triển khai một nút không phải là nút đầu cuối có thể giống như thế này

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;
  ....
}

Bạn thấy không? Những gì bạn mô tả là ngu ngốc là các giải pháp để vượt qua hệ thống phân cấp.

Đây là một bài viết sâu và dài hơn nhiều khiến tôi hiểu về khách truy cập .

Chỉnh sửa: Để làm rõ: VisitPhương thức của khách truy cập chứa logic được áp dụng cho một nút. AcceptPhương thức của nút chứa logic về cách điều hướng đến các nút liền kề. Trường hợp bạn chỉ gửi hai lần là một trường hợp đặc biệt mà đơn giản là không có các nút liền kề để điều hướng đến.


7
Lời giải thích của bạn không giải thích tại sao nó phải là trách nhiệm của Node hơn là phương thức visit () thích hợp của Khách truy cập để lặp lại các phần tử con? Ý của bạn là ý tưởng chính là chia sẻ mã truyền tải phân cấp khi chúng ta cần những người bảo trợ truy cập giống nhau cho những khách truy cập khác nhau? Tôi không thấy bất kỳ gợi ý nào từ bài báo được đề xuất.
Val

1
Nói rằng chấp nhận là tốt cho truyền tải thông thường là hợp lý và đáng giá đối với dân số nói chung. Tuy nhiên, tôi đã lấy ví dụ của mình từ câu hỏi "Tôi không thể hiểu mẫu khách truy cập cho đến khi tôi đọc andymaleh.blogspot.com/2008/04/… " của ai đó. Cả ví dụ này lẫn Wikipedia và các câu trả lời khác đều không đề cập đến lợi thế điều hướng. Tuy nhiên, tất cả đều yêu cầu sự chấp nhận ngu ngốc này (). Đó là lý do tại sao đặt câu hỏi của tôi: Tại sao?
Val

1
@Val - ý bạn là gì? Tôi không chắc bạn đang hỏi gì. Tôi không thể nói về các bài báo khác vì những người đó có quan điểm khác nhau về vấn đề này nhưng tôi nghi ngờ rằng chúng tôi đang bất đồng. Nói chung trong tính toán, rất nhiều vấn đề có thể được ánh xạ tới các mạng, vì vậy cách sử dụng có thể không liên quan gì đến biểu đồ trên bề mặt nhưng thực sự là một vấn đề rất giống nhau.
George Mauer

1
Cung cấp một ví dụ về nơi một số phương pháp có thể hữu ích không trả lời câu hỏi tại sao phương pháp đó là bắt buộc. Vì không phải lúc nào cũng cần điều hướng nên phương thức accept () không phải lúc nào cũng tốt cho việc truy cập. Do đó, chúng ta sẽ có thể hoàn thành mục tiêu của mình mà không cần nó. Tuy nhiên, nó là bắt buộc. Nó có nghĩa là có một lý do mạnh mẽ hơn để giới thiệu accept () vào mỗi mẫu khách truy cập hơn là "nó đôi khi hữu ích". Điều gì không rõ ràng trong câu hỏi của tôi? Nếu bạn không cố gắng hiểu tại sao Wikipedia lại tìm cách loại bỏ chấp nhận, bạn không muốn hiểu câu hỏi của tôi.
Val

1
@Val Bài báo mà họ liên kết đến "Bản chất của Mẫu khách truy cập" ghi nhận sự tách biệt của điều hướng và hoạt động trong phần tóm tắt như tôi đã đưa ra. Họ chỉ đơn giản nói rằng việc triển khai GOF (đó là những gì bạn đang hỏi) có một số hạn chế và khó chịu có thể được loại bỏ bằng cách sử dụng phản chiếu - vì vậy họ giới thiệu mẫu Walkabout. Điều này chắc chắn hữu ích và có thể làm được nhiều thứ giống như những thứ mà khách truy cập có thể làm nhưng nó là rất nhiều mã khá phức tạp và (khi đọc lướt qua) làm mất một số lợi ích của an toàn kiểu. Đó là một công cụ cho hộp công cụ nhưng một nặng hơn người truy cập
George Mauer

0

Mục đích của mẫu khách truy cập là để đảm bảo rằng các đối tượng biết khi nào khách truy cập kết thúc với chúng và đã rời đi, do đó các lớp có thể thực hiện bất kỳ hoạt động dọn dẹp cần thiết nào sau đó. Nó cũng cho phép các lớp hiển thị nội bộ của chúng "tạm thời" dưới dạng tham số "tham chiếu" và biết rằng nội bộ sẽ không còn được hiển thị khi khách truy cập đã biến mất. Trong những trường hợp không cần thiết phải dọn dẹp, mẫu khách truy cập không quá hữu ích. Các lớp không làm những điều này có thể không được hưởng lợi từ mẫu khách truy cập, nhưng mã được viết để sử dụng mẫu khách truy cập sẽ có thể sử dụng được với các lớp trong tương lai có thể yêu cầu dọn dẹp sau khi truy cập.

Ví dụ: giả sử một người có cấu trúc dữ liệu chứa nhiều chuỗi cần được cập nhật nguyên tử, nhưng lớp nắm giữ cấu trúc dữ liệu không biết chính xác loại cập nhật nguyên tử nào nên được thực hiện (ví dụ: nếu một chuỗi muốn thay thế tất cả các lần xuất hiện của " X ", trong khi một chuỗi khác muốn thay thế bất kỳ chuỗi chữ số nào bằng một chuỗi số cao hơn một, thì hoạt động của cả hai luồng sẽ thành công; nếu mỗi luồng chỉ đơn giản đọc ra một chuỗi, thực hiện cập nhật và viết lại chuỗi đó, thì luồng thứ hai để ghi lại chuỗi của nó sẽ ghi đè lên chuỗi đầu tiên). Một cách để thực hiện điều này là mỗi luồng có được một khóa, thực hiện hoạt động của nó và nhả khóa. Thật không may, nếu ổ khóa bị lộ theo cách đó,

Mẫu khách truy cập cung cấp (ít nhất) ba cách tiếp cận để tránh vấn đề đó:

  1. Nó có thể khóa một bản ghi, gọi chức năng được cung cấp, và sau đó mở khóa bản ghi; bản ghi có thể bị khóa vĩnh viễn nếu hàm được cung cấp rơi vào vòng lặp vô tận, nhưng nếu hàm được cung cấp trả về hoặc ném một ngoại lệ, bản ghi sẽ được mở khóa (có thể hợp lý để đánh dấu bản ghi không hợp lệ nếu hàm ném một ngoại lệ; bỏ đi nó bị khóa có lẽ không phải là một ý kiến ​​hay). Lưu ý rằng điều quan trọng là nếu hàm được gọi cố gắng lấy các khóa khác, thì có thể dẫn đến bế tắc.
  2. Trên một số nền tảng, nó có thể chuyển một vị trí lưu trữ giữ chuỗi làm tham số 'ref'. Sau đó, hàm đó có thể sao chép chuỗi, tính toán một chuỗi mới dựa trên chuỗi đã sao chép, cố gắng CompareExchange chuỗi cũ thành chuỗi mới và lặp lại toàn bộ quy trình nếu CompareExchange không thành công.
  3. Nó có thể tạo một bản sao của chuỗi, gọi hàm được cung cấp trên chuỗi, sau đó sử dụng chính CompareExchange để cập nhật bản gốc và lặp lại toàn bộ quá trình nếu CompareExchange không thành công.

Nếu không có mẫu khách truy cập, việc thực hiện cập nhật nguyên tử sẽ yêu cầu lộ các ổ khóa và có nguy cơ thất bại nếu phần mềm gọi điện không tuân theo một giao thức khóa / mở khóa nghiêm ngặt. Với kiểu khách truy cập, cập nhật nguyên tử có thể được thực hiện tương đối an toàn.


2
1. truy cập ngụ ý rằng bạn chỉ có quyền truy cập vào các phương pháp đã truy cập công khai, do đó cần phải làm cho các khóa nội bộ có thể truy cập công khai để hữu ích với Khách truy cập. 2 / Không có ví dụ nào tôi đã thấy trước đây ngụ ý rằng Khách truy cập được sử dụng để thay đổi trạng thái đã truy cập. 3. "Với VisitorPattern truyền thống, người ta chỉ có thể xác định khi nào chúng tôi đang vào một nút. Chúng tôi không biết liệu chúng tôi đã rời khỏi nút trước đó hay chưa trước khi vào nút hiện tại." Làm cách nào để bạn mở khóa chỉ với lượt truy cập thay vì visitEnter và visitLeave? Cuối cùng, tôi hỏi về các ứng dụng của accpet () chứ không phải là Khách truy cập.
Val

Có lẽ tôi không hoàn toàn bắt kịp thuật ngữ cho các mẫu, nhưng "mẫu khách truy cập" dường như giống với một cách tiếp cận mà tôi đã sử dụng trong đó X chuyển cho Y một đại biểu, sau đó Y có thể chuyển thông tin chỉ cần hợp lệ miễn là đại biểu đang chạy. Có thể mô hình đó có một số tên khác?
supercat

2
Đây là một ứng dụng thú vị của mẫu khách truy cập cho một vấn đề cụ thể nhưng không mô tả chính mẫu hoặc trả lời câu hỏi ban đầu. "Trong những trường hợp không cần thiết phải dọn dẹp, mẫu khách truy cập không thực sự hữu ích." Tuyên bố này chắc chắn là sai và chỉ liên quan đến vấn đề cụ thể của bạn chứ không phải mẫu nói chung.
Tony O'Hagan

0

Các lớp yêu cầu sửa đổi đều phải triển khai phương thức 'accept'. Khách hàng gọi phương thức chấp nhận này để thực hiện một số hành động mới trên họ lớp đó, do đó mở rộng chức năng của chúng. Khách hàng có thể sử dụng một phương thức chấp nhận này để thực hiện một loạt các hành động mới bằng cách chuyển vào một lớp khách truy cập khác cho từng hành động cụ thể. Một lớp khách truy cập chứa nhiều phương thức truy cập bị ghi đè xác định cách đạt được cùng một hành động cụ thể đó cho mọi lớp trong họ. Các phương thức truy cập này được thông qua một phiên bản để hoạt động.

Khách truy cập rất hữu ích nếu bạn thường xuyên thêm, thay đổi hoặc xóa chức năng cho một nhóm lớp ổn định vì mỗi mục chức năng được xác định riêng biệt trong mỗi lớp khách truy cập và bản thân các lớp không cần thay đổi. Nếu nhóm lớp không ổn định thì kiểu khách truy cập có thể ít được sử dụng hơn, vì nhiều khách truy cập cần thay đổi mỗi khi thêm hoặc xóa lớp.


-1

Một tốt ví dụ là trong mã nguồn biên soạn:

interface CompilingVisitor {
   build(SourceFile source);
}

Khách hàng có thể thực hiện một JavaBuilder, RubyBuilder, XMLValidator, vv và thực hiện để thu thập và quý khách đến thăm tất cả các file nguồn vào một dự án không cần phải thay đổi.

Đây sẽ là một mô hình xấu nếu bạn có các lớp riêng biệt cho từng loại tệp nguồn:

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

Nó phụ thuộc vào ngữ cảnh và những phần nào của hệ thống mà bạn muốn có thể mở rộng được.


Điều trớ trêu là VisitorPattern đề nghị chúng ta sử dụng mô hình xấu. Nó nói rằng chúng ta phải xác định một phương thức truy cập cho mọi loại nút mà nó sẽ truy cập. Thứ hai, không rõ những tấm gương của bạn là tốt hay xấu? Chúng liên quan đến câu hỏi của tôi như thế nào?
Val
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.