Khi nào tôi nên sử dụng Mẫu thiết kế khách truy cập? [đóng cửa]


315

Tôi liên tục thấy các tài liệu tham khảo về mẫu khách truy cập trong blog nhưng tôi phải thừa nhận, tôi chỉ không nhận được nó. Tôi đã đọc bài viết trên wikipedia cho mẫu và tôi hiểu cơ chế của nó nhưng tôi vẫn bối rối khi sử dụng nó.

Là một người gần đây thực sự mẫu trang trí và hiện đang thấy sử dụng nó hoàn toàn ở mọi nơi tôi cũng muốn có thể thực sự hiểu được bằng trực giác mẫu có vẻ tiện dụng này.


7
Cuối cùng cũng nhận được nó sau khi đọc bài viết này của Jermey Miller trên blackberry của tôi trong khi bị kẹt chờ trong sảnh trong hai giờ. Nó dài nhưng đưa ra một lời giải thích tuyệt vời về công văn kép, khách truy cập và kết hợp, và những gì bạn có thể làm với những điều này.
George Mauer


3
Mô hình khách truy cập? Cái nào? Vấn đề là: có rất nhiều sự hiểu lầm và nhầm lẫn thuần túy xung quanh mẫu thiết kế này. Tôi đã viết và bài viết hy vọng sẽ đặt một số thứ tự cho sự hỗn loạn này: rgomes-info.blogspot.co.uk/2013/01/ Lời
Richard Gomes

Khi bạn muốn có các đối tượng hàm trên các kiểu dữ liệu hợp nhất, bạn sẽ cần mẫu khách truy cập. Bạn có thể tự hỏi các đối tượng chức năng và loại dữ liệu hợp nhất là gì, vậy thì đáng để đọc ccs.neu.edu/home/matthias/htdc.html
Wei Qiu

Ví dụ ở đâyđây .
jaco0646

Câu trả lời:


315

Tôi không quen thuộc lắm với mẫu Khách truy cập. Hãy xem tôi đã hiểu đúng chưa. Giả sử bạn có một hệ thống phân cấp của động vật

class Animal {  };
class Dog: public Animal {  };
class Cat: public Animal {  };

(Giả sử nó là một hệ thống phân cấp phức tạp với giao diện được thiết lập tốt.)

Bây giờ chúng tôi muốn thêm một hoạt động mới vào hệ thống phân cấp, cụ thể là chúng tôi muốn mỗi động vật tạo ra âm thanh của nó. Theo như hệ thống phân cấp đơn giản, bạn có thể thực hiện với đa hình thẳng:

class Animal
{ public: virtual void makeSound() = 0; };

class Dog : public Animal
{ public: void makeSound(); };

void Dog::makeSound()
{ std::cout << "woof!\n"; }

class Cat : public Animal
{ public: void makeSound(); };

void Cat::makeSound()
{ std::cout << "meow!\n"; }

Nhưng tiếp tục theo cách này, mỗi lần bạn muốn thêm một thao tác, bạn phải sửa đổi giao diện cho mỗi lớp duy nhất của hệ thống phân cấp. Bây giờ, giả sử thay vì bạn hài lòng với giao diện ban đầu và bạn muốn thực hiện một vài sửa đổi có thể có cho nó.

Mẫu khách truy cập cho phép bạn di chuyển từng thao tác mới trong một lớp phù hợp và bạn chỉ cần mở rộng giao diện phân cấp một lần. Hãy làm nó. Đầu tiên, chúng tôi định nghĩa một hoạt động trừu tượng (lớp "Khách truy cập" trong GoF ) có một phương thức cho mọi lớp trong cấu trúc phân cấp:

class Operation
{
public:
    virtual void hereIsADog(Dog *d) = 0;
    virtual void hereIsACat(Cat *c) = 0;
};

Sau đó, chúng tôi sửa đổi cấu trúc phân cấp để chấp nhận các hoạt động mới:

class Animal
{ public: virtual void letsDo(Operation *v) = 0; };

class Dog : public Animal
{ public: void letsDo(Operation *v); };

void Dog::letsDo(Operation *v)
{ v->hereIsADog(this); }

class Cat : public Animal
{ public: void letsDo(Operation *v); };

void Cat::letsDo(Operation *v)
{ v->hereIsACat(this); }

Cuối cùng, chúng tôi thực hiện thao tác thực tế, mà không sửa đổi cả Cat và Dog :

class Sound : public Operation
{
public:
    void hereIsADog(Dog *d);
    void hereIsACat(Cat *c);
};

void Sound::hereIsADog(Dog *d)
{ std::cout << "woof!\n"; }

void Sound::hereIsACat(Cat *c)
{ std::cout << "meow!\n"; }

Bây giờ bạn có một cách để thêm các hoạt động mà không cần sửa đổi hệ thống phân cấp nữa. Đây là cách nó làm việc:

int main()
{
    Cat c;
    Sound theSound;
    c.letsDo(&theSound);
}

19
S.Lott, đi trên cây không thực sự là mẫu người truy cập. (Đó là "mẫu khách truy cập phân cấp", hoàn toàn khác biệt một cách khó hiểu.) Không có cách nào để hiển thị mẫu Khách truy cập GoF mà không sử dụng kế thừa hoặc triển khai giao diện.
khoan hồng

14
@Knownasilya - Đó không phải là sự thật. & -Operator cung cấp địa chỉ của Sound-Object, giao diện cần thiết cho giao diện. letsDo(Operation *v) cần một con trỏ
AquilaRapax

3
chỉ để rõ ràng, ví dụ này về mẫu thiết kế của khách truy cập có đúng không?
Godzilla

4
Sau rất nhiều suy nghĩ, tôi tự hỏi tại sao bạn gọi hai phương thức ở đâyIsADog và hereIsACat mặc dù bạn đã truyền Dog và Cat cho các phương thức. Tôi thích một biểu diễn đơn giản (Object * obj) và bạn truyền đối tượng này trong lớp Thao tác. (và bằng ngôn ngữ hỗ trợ ghi đè, không cần truyền)
Abdalrahman Shatou

6
Trong ví dụ "chính" của bạn ở cuối: theSound.hereIsACat(c)sẽ hoàn thành công việc, làm thế nào để bạn biện minh cho tất cả các chi phí được giới thiệu bởi mẫu? gửi đôi là sự biện minh.
franssu

131

Lý do cho sự nhầm lẫn của bạn có lẽ là Khách truy cập là một từ sai lầm nghiêm trọng. Nhiều lập trình viên (nổi bật 1 !) Đã vấp phải vấn đề này. Những gì nó thực sự làm là thực hiện gửi hai lần trong các ngôn ngữ không hỗ trợ nó nguyên bản (hầu hết trong số họ không).


1) Ví dụ yêu thích của tôi là Scott Meyers, tác giả nổi tiếng của hiệu quả C ++, người đã gọi đây là một trong những C ++ aha quan trọng nhất của mình ! khoảnh khắc bao giờ .


3
+1 "không có mẫu" - câu trả lời hoàn hảo. câu trả lời được đánh giá cao nhất chứng tỏ nhiều lập trình viên c ++ vẫn chưa nhận ra những hạn chế của các hàm ảo so với đa hình "adhoc" bằng cách sử dụng kiểu enum và trường hợp chuyển đổi (cách c). Nó có thể gọn gàng hơn và vô hình để sử dụng ảo, nhưng nó vẫn bị giới hạn trong một lần gửi. Theo ý kiến ​​cá nhân của tôi, đây là lỗ hổng lớn nhất của c ++.
dùng3125280

@ user3125280 Tôi đã đọc 4/5 bài viết và chương Mẫu thiết kế trên mẫu Khách truy cập và không ai trong số họ giải thích lợi thế của việc sử dụng mẫu tối nghĩa này so với trường hợp stmt hoặc khi bạn có thể sử dụng một mẫu khác. Thx cho ít nhất là đưa nó lên!
spinkus

4
@sam Tôi chắc rằng họ không giải thích nó - đó là những ưu đãi mà bạn luôn luôn có được từ polymorphism subclassing / runtime trên switch: switchcứng mã ra quyết định ở phía client (mã trùng lặp) và không cung cấp kiểm tra kiểu tĩnh ( kiểm tra tính đầy đủ và tính khác biệt của các trường hợp, v.v.). Mẫu khách truy cập được xác minh bởi trình kiểm tra loại và thường làm cho mã máy khách đơn giản hơn.
Konrad Rudolph

@KonradRudolph cảm ơn vì điều đó. Lưu ý rằng, nó không được đề cập rõ ràng trong các mẫu hoặc bài viết wikipedia chẳng hạn. Tôi không đồng ý với bạn, nhưng bạn có thể tranh luận rằng có những lợi ích khi sử dụng trường hợp stmt vì vậy nó không tương phản chung: 1. bạn không cần một phương thức accept () trên các đối tượng của bộ sưu tập của bạn. 2. Khách truy cập ~ có thể xử lý các đối tượng không xác định. Do đó, trường hợp stmt có vẻ phù hợp hơn để vận hành trên các cấu trúc đối tượng với một bộ sưu tập các loại liên quan có thể thay đổi. Các mẫu không thừa nhận rằng mẫu Khách truy cập không phù hợp với kịch bản như vậy (tr333).
spinkus

1
@SamPinkus konrad's on - đó là lý do tại sao virtualcác tính năng rất hữu ích trong các ngôn ngữ lập trình hiện đại - chúng là khối xây dựng cơ bản của các chương trình mở rộng - theo tôi là cách c (chuyển đổi lồng nhau hoặc khớp mẫu, v.v. tùy thuộc vào ngôn ngữ của bạn) mã sạch hơn rất nhiều mà không cần phải mở rộng và tôi đã rất ngạc nhiên khi thấy phong cách này trong phần mềm phức tạp như prover 9. Quan trọng hơn là bất kỳ ngôn ngữ nào muốn cung cấp khả năng mở rộng có thể phù hợp với các mẫu công văn tốt hơn so với công văn đệ quy (nghĩa là khách thăm quan).
dùng3125280

84

Mọi người ở đây đều đúng, nhưng tôi nghĩ không thể giải quyết được "khi nào". Đầu tiên, từ các mẫu thiết kế:

Khách truy cập cho phép bạn xác định một hoạt động mới mà không thay đổi các lớp của các yếu tố mà nó hoạt động.

Bây giờ, hãy nghĩ về một hệ thống phân cấp lớp đơn giản. Tôi có các lớp 1, 2, 3 và 4 và các phương thức A, B, C và D. Sắp xếp chúng giống như trong bảng tính: các lớp là các dòng và các phương thức là các cột.

Bây giờ, thiết kế hướng đối tượng cho rằng bạn có nhiều khả năng phát triển các lớp mới hơn các phương thức mới, do đó, việc thêm nhiều dòng, có thể nói là dễ dàng hơn. Bạn chỉ cần thêm một lớp mới, chỉ định những gì khác nhau trong lớp đó và kế thừa phần còn lại.

Đôi khi, mặc dù, các lớp tương đối tĩnh, nhưng bạn cần thêm nhiều phương thức thường xuyên hơn - thêm các cột. Cách tiêu chuẩn trong thiết kế OO sẽ là thêm các phương thức như vậy vào tất cả các lớp, có thể tốn kém. Mẫu khách truy cập làm cho điều này dễ dàng.

Nhân tiện, đây là vấn đề mà mô hình của Scala phù hợp để giải quyết.


Tại sao tôi lại sử dụng mẫu khách truy cập không chỉ là một lớp tiện ích. tôi có thể gọi lớp tiện ích của mình như thế này: AnalyticsManger.visit (someObjectToVisit) vs AnalyticsVisitor.visit (someOjbectToVisit). Có gì khác biệt ? cả hai đều quan tâm đúng không? Hy vọng bạn có thể giúp đỡ.
j2emanue

@ j2emanue Vì mẫu Khách truy cập sử dụng chính xác quá tải của Khách truy cập khi chạy. Trong khi mã của bạn cần truyền kiểu để gọi quá tải chính xác.
Truy cập bị từ chối

Có đạt được hiệu quả với điều đó? tôi đoán nó tránh được việc đưa ra một ý tưởng hay
j2emanue

@ j2emanue ý tưởng là viết mã phù hợp với nguyên tắc mở / đóng, không phải lý do hiệu suất. Xem đóng mở tại chú Bob butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod
Truy cập bị từ chối

22

Mẫu thiết kế của Khách truy cập hoạt động thực sự tốt cho các cấu trúc "đệ quy" như cây thư mục, cấu trúc XML hoặc phác thảo tài liệu.

Một đối tượng Khách truy cập truy cập từng nút trong cấu trúc đệ quy: mỗi thư mục, mỗi thẻ XML, bất cứ điều gì. Đối tượng Khách truy cập không lặp qua cấu trúc. Thay vào đó, các phương thức của Khách truy cập được áp dụng cho từng nút của cấu trúc.

Đây là một cấu trúc nút đệ quy điển hình. Có thể là một thư mục hoặc một thẻ XML. [Nếu bạn là người Java, hãy tưởng tượng rất nhiều phương pháp bổ sung để xây dựng và duy trì danh sách trẻ em.]

class TreeNode( object ):
    def __init__( self, name, *children ):
        self.name= name
        self.children= children
    def visit( self, someVisitor ):
        someVisitor.arrivedAt( self )
        someVisitor.down()
        for c in self.children:
            c.visit( someVisitor )
        someVisitor.up()

Các visitphương pháp áp dụng một đối tượng khách đến mỗi nút trong cấu trúc. Trong trường hợp này, đó là một khách truy cập từ trên xuống. Bạn có thể thay đổi cấu trúc của visitphương thức để thực hiện từ dưới lên hoặc một số thứ tự khác.

Đây là một siêu lớp cho khách truy cập. Nó được sử dụng bởi visitphương pháp. Nó "đến" mỗi nút trong cấu trúc. Vì visitphương thức gọi updown, khách truy cập có thể theo dõi độ sâu.

class Visitor( object ):
    def __init__( self ):
        self.depth= 0
    def down( self ):
        self.depth += 1
    def up( self ):
        self.depth -= 1
    def arrivedAt( self, aTreeNode ):
        print self.depth, aTreeNode.name

Một lớp con có thể làm những việc như đếm nút ở mỗi cấp và tích lũy danh sách các nút, tạo ra một số phần phân cấp đường dẫn đẹp.

Đây là một ứng dụng. Nó xây dựng một cấu trúc cây , someTree. Nó tạo ra một Visitor, dumpNodes.

Sau đó, nó áp dụng dumpNodescho cây. Đối dumpNodetượng sẽ "ghé thăm" từng nút trong cây.

someTree= TreeNode( "Top", TreeNode("c1"), TreeNode("c2"), TreeNode("c3") )
dumpNodes= Visitor()
someTree.visit( dumpNodes )

visitThuật toán TreeNode sẽ đảm bảo rằng mọi TreeNode được sử dụng làm đối số cho arrivedAtphương thức của Khách truy cập .


8
Như những người khác đã tuyên bố đây là "mẫu khách truy cập phân cấp".
PPC-Coder

1
@ PPC-Coder Sự khác biệt giữa 'mẫu khách truy cập phân cấp' và mẫu khách truy cập là gì?
Tim Lovell-Smith

3
Mẫu khách truy cập phân cấp linh hoạt hơn mẫu khách truy cập cổ điển. Ví dụ, với mẫu phân cấp, bạn có thể theo dõi độ sâu của đường ngang và quyết định nhánh nào đi qua hoặc dừng ngang qua tất cả. Khách truy cập cổ điển không có khái niệm này và sẽ truy cập tất cả các nút.
PPC-Coder

18

Một cách để xem xét đó là mẫu khách truy cập là cách cho phép khách hàng của bạn thêm các phương thức bổ sung vào tất cả các lớp của bạn trong một hệ thống phân cấp lớp cụ thể.

Nó rất hữu ích khi bạn có một hệ thống phân cấp lớp khá ổn định, nhưng bạn có những yêu cầu thay đổi về những gì cần phải thực hiện với hệ thống phân cấp đó.

Ví dụ cổ điển là cho trình biên dịch và tương tự. Cây Cú pháp Trừu tượng (AST) có thể xác định chính xác cấu trúc của ngôn ngữ lập trình, nhưng các thao tác bạn có thể muốn thực hiện trên AST sẽ thay đổi khi dự án của bạn tiến triển: trình tạo mã, máy in đẹp, trình gỡ lỗi, phân tích số liệu phức tạp.

Nếu không có Mẫu khách truy cập, mỗi khi nhà phát triển muốn thêm một tính năng mới, họ sẽ cần thêm phương thức đó vào mọi tính năng trong lớp cơ sở. Điều này đặc biệt khó khi các lớp cơ sở xuất hiện trong một thư viện riêng hoặc được tạo bởi một nhóm riêng.

(Tôi đã nghe nói rằng mẫu Người truy cập mâu thuẫn với các thực tiễn OO tốt, bởi vì nó di chuyển các hoạt động của dữ liệu ra khỏi dữ liệu. Mẫu khách truy cập rất hữu ích trong trường hợp chính xác các thực tiễn OO thông thường thất bại.)


tôi cũng muốn ý kiến ​​của bạn về vấn đề sau: Tại sao tôi sẽ sử dụng mẫu khách truy cập chứ không chỉ là một lớp không biết. tôi có thể gọi lớp tiện ích của mình như thế này: AnalyticsManger.visit (someObjectToVisit) vs AnalyticsVisitor.visit (someOjbectToVisit). Có gì khác biệt ? cả hai đều quan tâm đúng không? Hy vọng bạn có thể giúp đỡ.
j2emanue

@ j2emanue: Tôi không hiểu câu hỏi. Tôi đề nghị bạn thịt nó ra và gửi nó như một câu hỏi đầy đủ cho bất cứ ai trả lời.
Oddthinking ngày

1
tôi đã đăng một câu hỏi mới tại đây: stackoverflow.com/questions/52068876/ từ
j2emanue

14

Có ít nhất ba lý do rất tốt để sử dụng Mẫu khách truy cập:

  1. Giảm sự tăng sinh của mã chỉ khác một chút khi cấu trúc dữ liệu thay đổi.

  2. Áp dụng cùng một tính toán cho một số cấu trúc dữ liệu, mà không thay đổi mã thực hiện tính toán.

  3. Thêm thông tin vào các thư viện cũ mà không thay đổi mã kế thừa.

Xin hãy xem một bài báo tôi đã viết về điều này .


1
Tôi đã nhận xét về bài viết của bạn với công dụng lớn nhất mà tôi đã thấy cho khách truy cập. Suy nghĩ?
George Mauer

13

Như Konrad Rudolph đã chỉ ra, nó phù hợp cho các trường hợp chúng ta cần gửi gấp đôi

Dưới đây là một ví dụ để chỉ ra một tình huống mà chúng tôi cần gửi gấp đôi và cách khách truy cập giúp chúng tôi làm như vậy.

Thí dụ :

Hãy nói rằng tôi có 3 loại thiết bị di động - iPhone, Android, Windows Mobile.

Tất cả ba thiết bị này đều có radio Bluetooth được cài đặt trong đó.

Giả sử rằng đài phát thanh răng xanh có thể từ 2 OEM riêng biệt - Intel & Broadcom.

Chỉ để làm cho ví dụ có liên quan đến cuộc thảo luận của chúng tôi, chúng ta cũng giả sử rằng các API được hiển thị bởi đài phát thanh Intel khác với các ví dụ được phát ra bởi đài Broadcom.

Đây là cách các lớp học của tôi trông -

nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây

Bây giờ, tôi muốn giới thiệu một thao tác - Bật Bluetooth trên thiết bị di động.

Chữ ký chức năng của nó sẽ giống như thế này -

 void SwitchOnBlueTooth(IMobileDevice mobileDevice, IBlueToothRadio blueToothRadio)

Vì vậy, tùy thuộc vào Đúng loại thiết bịTùy thuộc vào loại radio Bluetooth phù hợp, thiết bị có thể được bật bằng cách gọi các bước hoặc thuật toán thích hợp .

Về nguyên tắc, nó trở thành ma trận 3 x 2, trong đó tôi đang cố gắng tạo ra hoạt động đúng tùy thuộc vào đúng loại đối tượng liên quan.

Một hành vi đa hình tùy thuộc vào loại của cả hai đối số.

nhập mô tả hình ảnh ở đây

Bây giờ, mẫu khách truy cập có thể được áp dụng cho vấn đề này. Cảm hứng đến từ trang Wikipedia nêu rõ - về bản chất, khách truy cập cho phép một người thêm các chức năng ảo mới vào một nhóm các lớp mà không cần tự sửa đổi các lớp; thay vào đó, người ta tạo ra một lớp khách truy cập thực hiện tất cả các chuyên môn phù hợp của hàm ảo. Khách truy cập lấy tham chiếu thể hiện làm đầu vào và thực hiện mục tiêu thông qua công văn kép.

Công văn đôi là cần thiết ở đây do ma trận 3x2

Đây là cách thiết lập sẽ như thế nào - nhập mô tả hình ảnh ở đây

Tôi đã viết ví dụ để trả lời một câu hỏi khác, mã & giải thích của nó được đề cập ở đây .


9

Tôi thấy nó dễ dàng hơn trong các liên kết sau:

Trong http://www.remondo.net/visitor-potype-example-csharp/ Tôi đã tìm thấy một ví dụ cho thấy một ví dụ giả cho thấy lợi ích của mẫu khách truy cập là gì. Ở đây bạn có các lớp container khác nhau cho Pill:

namespace DesignPatterns
{
    public class BlisterPack
    {
        // Pairs so x2
        public int TabletPairs { get; set; }
    }

    public class Bottle
    {
        // Unsigned
        public uint Items { get; set; }
    }

    public class Jar
    {
        // Signed
        public int Pieces { get; set; }
    }
}

Như bạn thấy ở trên, Bạn BilsterPackchứa các cặp Pills ', do đó bạn cần nhân số lượng của cặp với 2. Ngoài ra, bạn có thể nhận thấy rằng việc Bottlesử dụng unitđó là kiểu dữ liệu khác nhau và cần phải bỏ.

Vì vậy, trong phương pháp chính, bạn có thể tính toán số lượng thuốc sử dụng mã sau đây:

foreach (var item in packageList)
{
    if (item.GetType() == typeof (BlisterPack))
    {
        pillCount += ((BlisterPack) item).TabletPairs * 2;
    }
    else if (item.GetType() == typeof (Bottle))
    {
        pillCount += (int) ((Bottle) item).Items;
    }
    else if (item.GetType() == typeof (Jar))
    {
        pillCount += ((Jar) item).Pieces;
    }
}

Lưu ý rằng mã trên vi phạm Single Responsibility Principle. Điều đó có nghĩa là bạn phải thay đổi mã phương thức chính nếu bạn thêm loại container mới. Ngoài ra làm cho chuyển đổi lâu hơn là thực hành xấu.

Vì vậy, bằng cách giới thiệu mã sau đây:

public class PillCountVisitor : IVisitor
{
    public int Count { get; private set; }

    #region IVisitor Members

    public void Visit(BlisterPack blisterPack)
    {
        Count += blisterPack.TabletPairs * 2;
    }

    public void Visit(Bottle bottle)
    {
        Count += (int)bottle.Items;
    }

    public void Visit(Jar jar)
    {
        Count += jar.Pieces;
    }

    #endregion
}

Bạn đã chuyển trách nhiệm đếm số Pills sang lớp được gọi PillCountVisitor(Và chúng tôi đã xóa tuyên bố trường hợp chuyển đổi). Điều đó có nghĩa là bất cứ khi nào bạn cần thêm loại hộp đựng thuốc mới, bạn chỉ nên thay đổi PillCountVisitorlớp. Ngoài ra IVisitorgiao diện thông báo là chung để sử dụng trong các kịch bản khác.

Bằng cách thêm phương thức Chấp nhận vào lớp thùng chứa thuốc:

public class BlisterPack : IAcceptor
{
    public int TabletPairs { get; set; }

    #region IAcceptor Members

    public void Accept(IVisitor visitor)
    {
        visitor.Visit(this);
    }

    #endregion
}

chúng tôi cho phép du khách ghé thăm các lớp container.

Cuối cùng, chúng tôi tính toán số lượng thuốc sử dụng mã sau đây:

var visitor = new PillCountVisitor();

foreach (IAcceptor item in packageList)
{
    item.Accept(visitor);
}

Điều đó có nghĩa là: Mỗi hộp đựng thuốc cho phép PillCountVisitorkhách truy cập xem số lượng thuốc của họ. Anh ấy biết cách đếm viên thuốc của bạn.

Tại visitor.Countcó giá trị của thuốc.

Trong http://butunclebob.com/ArticleS.UncleBob.IuseVisitor bạn thấy kịch bản thực trong đó bạn không thể sử dụng đa hình (câu trả lời) để tuân theo Nguyên tắc Trách nhiệm Đơn lẻ. Trong thực tế:

public class HourlyEmployee extends Employee {
  public String reportQtdHoursAndPay() {
    //generate the line for this hourly employee
  }
}

các reportQtdHoursAndPayphương pháp là để báo cáo và đại diện và điều này vi phạm các đơn Trách nhiệm Nguyên tắc. Vì vậy, tốt hơn là sử dụng mô hình khách truy cập để khắc phục vấn đề.


2
Hi Sayed, bạn có thể vui lòng chỉnh sửa câu trả lời của bạn để thêm các phần mà bạn thấy khai sáng nhất. SO thường không khuyến khích các câu trả lời chỉ liên kết vì mục tiêu là cơ sở dữ liệu kiến ​​thức và các liên kết đi xuống.
George Mauer

8

Công văn đôi chỉ là một lý do trong số những người khác sử dụng mô hình này .
Nhưng lưu ý rằng đó là cách duy nhất để thực hiện công văn gấp đôi hoặc nhiều hơn trong các ngôn ngữ sử dụng mô hình công văn duy nhất.

Dưới đây là những lý do để sử dụng mẫu:

1) Chúng tôi muốn xác định các hoạt động mới mà không thay đổi mô hình mỗi lần vì mô hình không thay đổi thường xuyên thay đổi hoạt động thường xuyên.

2) Chúng tôi không muốn kết hợp mô hình và hành vichúng tôi muốn có một mô hình có thể sử dụng lại trong nhiều ứng dụng hoặc chúng tôi muốn có một mô hình có thể mở rộng cho phép các lớp khách xác định hành vi của chúng với các lớp riêng của chúng.

3) Chúng tôi có các hoạt động chung phụ thuộc vào loại cụ thể của mô hình nhưng chúng tôi không muốn triển khai logic trong mỗi lớp con vì điều đó sẽ làm bùng nổ logic chung trong nhiều lớp và ở nhiều nơi .

4) Chúng tôi đang sử dụng một thiết kế mô hình miền và các lớp mô hình có cùng phân cấp thực hiện quá nhiều thứ riêng biệt có thể được tập hợp ở một nơi khác .

5) Chúng tôi cần một công văn kép .
Chúng tôi có các biến được khai báo với các loại giao diện và chúng tôi muốn có thể xử lý chúng theo kiểu thời gian chạy của chúng mà không cần sử dụng if (myObj instanceof Foo) {}hay bất kỳ thủ thuật nào.
Ý tưởng là ví dụ để truyền các biến này cho các phương thức khai báo một loại giao diện cụ thể làm tham số để áp dụng một quy trình xử lý cụ thể. Cách làm này không thể thực hiện được với các ngôn ngữ chỉ dựa trên một công văn vì việc được chọn được gọi trong thời gian chạy chỉ phụ thuộc vào loại thời gian chạy của máy thu.
Lưu ý rằng trong Java, phương thức (chữ ký) để gọi được chọn tại thời điểm biên dịch và nó phụ thuộc vào kiểu khai báo của các tham số, không phải kiểu thời gian chạy của chúng.

Điểm cuối cùng là một lý do để sử dụng khách truy cập cũng là một hậu quả vì khi bạn triển khai khách truy cập (tất nhiên đối với các ngôn ngữ không hỗ trợ nhiều công văn), bạn nhất thiết phải giới thiệu triển khai công văn kép.

Lưu ý rằng việc duyệt qua các phần tử (lặp) để áp dụng khách truy cập trên mỗi phần tử không phải là lý do để sử dụng mẫu.
Bạn sử dụng mô hình vì bạn tách mô hình và xử lý.
Và bằng cách sử dụng mô hình, bạn được hưởng lợi ngoài khả năng lặp.
Khả năng này rất mạnh và vượt ra ngoài việc lặp lại trên loại phổ biến với một phương thức cụ thể như accept()là một phương thức chung.
Đây là một trường hợp sử dụng đặc biệt. Vì vậy, tôi sẽ đặt nó sang một bên.


Ví dụ trong Java

Tôi sẽ minh họa giá trị gia tăng của mẫu bằng một ví dụ cờ vua trong đó chúng tôi muốn xác định xử lý khi người chơi yêu cầu một quân cờ di chuyển.

Nếu không sử dụng mẫu khách truy cập, chúng ta có thể định nghĩa các hành vi di chuyển mảnh trực tiếp trong các lớp con mảnh.
Ví dụ, chúng ta có thể có một Piecegiao diện như:

public interface Piece{

    boolean checkMoveValidity(Coordinates coord);

    void performMove(Coordinates coord);

    Piece computeIfKingCheck();

}

Mỗi lớp con Piece sẽ thực hiện nó như:

public class Pawn implements Piece{

    @Override
    public boolean checkMoveValidity(Coordinates coord) {
        ...
    }

    @Override
    public void performMove(Coordinates coord) {
        ...
    }

    @Override
    public Piece computeIfKingCheck() {
        ...
    }

}

Và điều tương tự cho tất cả các lớp con Piece.
Đây là một lớp sơ đồ minh họa thiết kế này:

[sơ đồ lớp mô hình

Cách tiếp cận này trình bày ba nhược điểm quan trọng:

- các hành vi như performMove()hoặc computeIfKingCheck()rất có thể sẽ sử dụng logic thông thường.
Ví dụ, bất kể cụ thể là gì Piece, performMove()cuối cùng sẽ đặt mảnh hiện tại đến một vị trí cụ thể và có khả năng lấy mảnh đối thủ.
Chia tách các hành vi liên quan trong nhiều lớp thay vì tập hợp chúng đánh bại chúng theo một cách nào đó theo mẫu trách nhiệm duy nhất. Làm cho khả năng bảo trì của họ khó khăn hơn.

- xử lý như checkMoveValidity()không nên là một cái gì đó mà các Piecelớp con có thể nhìn thấy hoặc thay đổi.
Đó là kiểm tra vượt ra ngoài hành động của con người hoặc máy tính. Kiểm tra này được thực hiện tại mỗi hành động được yêu cầu bởi người chơi để đảm bảo rằng di chuyển mảnh được yêu cầu là hợp lệ.
Vì vậy, chúng tôi thậm chí không muốn cung cấp điều đó trong Piecegiao diện.

- Trong các trò chơi cờ vua đầy thách thức đối với các nhà phát triển bot, nói chung, ứng dụng cung cấp API tiêu chuẩn ( Piecegiao diện, lớp con, Hội đồng quản trị, các hành vi phổ biến, v.v.) và cho phép các nhà phát triển làm phong phú chiến lược bot của họ.
Để có thể làm điều đó, chúng tôi phải đề xuất một mô hình trong đó dữ liệu và hành vi không được kết hợp chặt chẽ trong việc Piecetriển khai.

Vì vậy, hãy đi để sử dụng mô hình khách truy cập!

Chúng tôi có hai loại cấu trúc:

- các lớp mô hình chấp nhận được truy cập (các phần)

- khách truy cập ghé thăm họ (hoạt động di chuyển)

Dưới đây là một sơ đồ lớp minh họa mô hình:

nhập mô tả hình ảnh ở đây

Ở phần trên chúng ta có khách truy cập và ở phần dưới chúng ta có các lớp mô hình.

Đây là PieceMovingVisitorgiao diện (hành vi được chỉ định cho từng loại Piece):

public interface PieceMovingVisitor {

    void visitPawn(Pawn pawn);

    void visitKing(King king);

    void visitQueen(Queen queen);

    void visitKnight(Knight knight);

    void visitRook(Rook rook);

    void visitBishop(Bishop bishop);

}

Mảnh được xác định ngay bây giờ:

public interface Piece {

    void accept(PieceMovingVisitor pieceVisitor);

    Coordinates getCoordinates();

    void setCoordinates(Coordinates coordinates);

}

Phương pháp chính của nó là:

void accept(PieceMovingVisitor pieceVisitor);

Nó cung cấp công văn đầu tiên: một lời mời dựa trên người Piecenhận.
Tại thời gian biên dịch, phương thức được liên kết với accept()phương thức của giao diện Piece và tại thời gian chạy, phương thức bị ràng buộc sẽ được gọi trên Piecelớp thời gian chạy .
Và nó là accept()phương thức thực hiện sẽ thực hiện một công văn thứ hai.

Thật vậy, mỗi Piecelớp con muốn được truy cập bởi một PieceMovingVisitorđối tượng sẽ gọi PieceMovingVisitor.visit()phương thức bằng cách truyền dưới dạng chính đối số.
Theo cách này, trình biên dịch giới hạn ngay khi thời gian biên dịch, loại tham số khai báo với loại cụ thể.
Có công văn thứ hai.
Đây là Bishoplớp con minh họa rằng:

public class Bishop implements Piece {

    private Coordinates coord;

    public Bishop(Coordinates coord) {
        super(coord);
    }

    @Override
    public void accept(PieceMovingVisitor pieceVisitor) {
        pieceVisitor.visitBishop(this);
    }

    @Override
    public Coordinates getCoordinates() {
        return coordinates;
    }

   @Override
    public void setCoordinates(Coordinates coordinates) {
        this.coordinates = coordinates;
   }

}

Và đây là một ví dụ sử dụng:

// 1. Player requests a move for a specific piece
Piece piece = selectPiece();
Coordinates coord = selectCoordinates();

// 2. We check with MoveCheckingVisitor that the request is valid
final MoveCheckingVisitor moveCheckingVisitor = new MoveCheckingVisitor(coord);
piece.accept(moveCheckingVisitor);

// 3. If the move is valid, MovePerformingVisitor performs the move
if (moveCheckingVisitor.isValid()) {
    piece.accept(new MovePerformingVisitor(coord));
}

Hạn chế của khách truy cập

Mẫu khách truy cập là một mẫu rất mạnh mẽ nhưng nó cũng có một số hạn chế quan trọng mà bạn nên xem xét trước khi sử dụng.

1) Rủi ro giảm / phá vỡ đóng gói

Trong một số loại hoạt động, mẫu khách truy cập có thể làm giảm hoặc phá vỡ sự đóng gói của các đối tượng miền.

Ví dụ, vì MovePerformingVisitor lớp cần đặt tọa độ của mảnh thực tế, Piecegiao diện phải cung cấp một cách để làm điều đó:

void setCoordinates(Coordinates coordinates);

Trách nhiệm Piecethay đổi tọa độ hiện đang mở cho các lớp khác ngoài các lớp Piececon.
Di chuyển quá trình xử lý được thực hiện bởi khách truy cập trong các Piecelớp con cũng không phải là một tùy chọn.
Nó thực sự sẽ tạo ra một vấn đề khác khi Piece.accept()chấp nhận bất kỳ triển khai của khách truy cập. Nó không biết khách truy cập thực hiện những gì và vì vậy không biết về cách và cách thay đổi trạng thái Piece.
Một cách để xác định khách truy cập sẽ là thực hiện xử lý bài đăng Piece.accept()theo cách triển khai của khách truy cập. Đây sẽ là một ý tưởng rất tồi vì nó sẽ tạo ra sự kết hợp cao giữa các triển khai của Khách truy cập và các lớp con Piece và bên cạnh đó có thể sẽ phải sử dụng thủ thuật như getClass(), instanceofhoặc bất kỳ điểm đánh dấu nào xác định việc thực hiện của Khách truy cập.

2) Yêu cầu thay đổi mô hình

Trái ngược với một số mẫu thiết kế hành vi khác như Decoratorví dụ, mẫu khách truy cập bị xâm nhập.
Chúng tôi thực sự cần phải sửa đổi lớp người nhận ban đầu để cung cấp một accept()phương thức để chấp nhận được truy cập.
Chúng tôi không có bất kỳ vấn đề nào Piecevà các lớp con của nó vì đây là các lớp của chúng tôi .
Trong các lớp tích hợp hoặc bên thứ ba, mọi thứ không dễ dàng như vậy.
Chúng ta cần bọc hoặc kế thừa (nếu có thể) chúng để thêm accept()phương thức.

3) Chỉ định

Các mô hình tạo ra nhiều chỉ dẫn.
Công văn kép có nghĩa là hai yêu cầu thay vì một lần duy nhất:

call the visited (piece) -> that calls the visitor (pieceMovingVisitor)

Và chúng ta có thể có các chỉ định bổ sung khi khách truy cập thay đổi trạng thái đối tượng đã truy cập.
Nó có thể trông giống như một chu kỳ:

call the visited (piece) -> that calls the visitor (pieceMovingVisitor) -> that calls the visited (piece)

6

Cây Horstmann có một ví dụ tuyệt vời về nơi áp dụng Khách truy cập trong sách Thiết kế và mẫu OO của mình . Ông tóm tắt vấn đề:

Các đối tượng hợp chất thường có cấu trúc phức tạp, bao gồm các yếu tố riêng lẻ. Một số yếu tố có thể lại có các yếu tố con. ... Một hoạt động trên một phần tử truy cập các phần tử con của nó, áp dụng thao tác cho chúng và kết hợp các kết quả. ... Tuy nhiên, không dễ để thêm các hoạt động mới vào một thiết kế như vậy.

Lý do không dễ dàng là vì các hoạt động được thêm vào trong chính các lớp cấu trúc. Ví dụ: hãy tưởng tượng bạn có Hệ thống tệp:

Sơ đồ lớp FileSystem

Dưới đây là một số hoạt động (chức năng) mà chúng tôi có thể muốn thực hiện với cấu trúc này:

  • Hiển thị tên của các thành phần nút (danh sách tệp)
  • Hiển thị kích thước được tính toán của các phần tử nút (trong đó kích thước của thư mục bao gồm kích thước của tất cả các phần tử con của nó)
  • Vân vân.

Bạn có thể thêm các hàm cho mỗi lớp trong Hệ thống tệp để thực hiện các hoạt động (và mọi người đã làm điều này trong quá khứ vì cách làm rất rõ ràng). Vấn đề là bất cứ khi nào bạn thêm một chức năng mới (dòng "vv" ở trên), bạn có thể cần thêm nhiều phương thức hơn vào các lớp cấu trúc. Tại một số điểm, sau một số thao tác bạn đã thêm vào phần mềm của mình, các phương thức trong các lớp đó không còn ý nghĩa nữa về mặt gắn kết chức năng của các lớp. Ví dụ: bạn có một FileNodephương thức calculateFileColorForFunctionABC()để thực hiện chức năng hiển thị mới nhất trên hệ thống tệp.

Mẫu khách truy cập (giống như nhiều mẫu thiết kế) được sinh ra từ nỗi đau và sự đau khổ của các nhà phát triển, người biết rằng có một cách tốt hơn để cho phép mã của họ thay đổi mà không đòi hỏi nhiều thay đổi ở mọi nơi và cũng tôn trọng các nguyên tắc thiết kế tốt (độ gắn kết cao, khớp nối thấp ). Theo ý kiến ​​của tôi, thật khó để hiểu được sự hữu ích của rất nhiều mẫu cho đến khi bạn cảm thấy nỗi đau đó. Giải thích nỗi đau (như chúng tôi cố gắng làm ở trên với các chức năng "vv" được thêm vào) chiếm không gian trong phần giải thích và là một sự phân tâm. Hiểu các mẫu là khó vì lý do này.

Trình truy cập cho phép chúng tôi tách rời các chức năng trên cấu trúc dữ liệu (ví dụ FileSystemNodes:) từ chính cấu trúc dữ liệu. Mẫu cho phép thiết kế tôn trọng sự gắn kết - các lớp cấu trúc dữ liệu đơn giản hơn (chúng có ít phương thức hơn) và các chức năng cũng được gói gọn trong các Visitortriển khai. Điều này được thực hiện thông qua việc gửi hai lần (là phần phức tạp của mẫu): sử dụng accept()các phương thức trong các lớp cấu trúc và visitX()phương thức trong các lớp của Khách truy cập (chức năng):

Sơ đồ lớp FileSystem với Khách truy cập được áp dụng

Cấu trúc này cho phép chúng ta thêm các chức năng mới hoạt động trên cấu trúc dưới dạng Khách truy cập cụ thể (không thay đổi các lớp cấu trúc).

Sơ đồ lớp FileSystem với Khách truy cập được áp dụng

Ví dụ, một PrintNameVisitorcái thực hiện chức năng liệt kê thư mục và một PrintSizeVisitorcái thực hiện phiên bản với kích thước. Chúng ta có thể tưởng tượng một ngày nào đó có một 'ExportXMLVisitor` mà tạo ra các dữ liệu trong XML, hoặc một khách truy cập mà tạo ra nó trong JSON, vv Chúng tôi thậm chí có thể có một khách truy cập mà hiển thị cây thư mục của tôi sử dụng một ngôn ngữ đồ họa như DOT , được hình dung với một chương trình khác

Như một lưu ý cuối cùng: Sự phức tạp của Khách truy cập với công văn kép của nó có nghĩa là khó hiểu hơn, để mã hóa và gỡ lỗi. Nói tóm lại, nó có yếu tố đam mê cao và đi ngược lại nguyên tắc KISS. Trong một cuộc khảo sát được thực hiện bởi các nhà nghiên cứu, Khách truy cập đã được chứng minh là một mô hình gây tranh cãi (không có sự đồng thuận về tính hữu ích của nó). Một số thí nghiệm thậm chí cho thấy nó không làm cho mã dễ bảo trì hơn.


Cấu trúc thư mục tôi nghĩ là một mẫu tổng hợp tốt nhưng đồng ý với đoạn cuối cùng của bạn.
zar

5

Theo tôi, khối lượng công việc để thêm một thao tác mới ít nhiều giống nhau bằng cách sử dụng Visitor Patternhoặc sửa đổi trực tiếp từng cấu trúc phần tử. Ngoài ra, nếu tôi thêm lớp phần tử mới, giả sử Cow, giao diện Hoạt động sẽ bị ảnh hưởng và điều này lan truyền đến tất cả các lớp phần tử hiện có, do đó yêu cầu biên dịch lại tất cả các lớp phần tử. Vậy vấn đề là gì?


4
Hầu như mỗi lần tôi sử dụng Khách truy cập là khi bạn làm việc với việc phân cấp một hệ thống phân cấp đối tượng. Hãy xem xét một menu cây lồng nhau. Bạn muốn thu gọn tất cả các nút. Nếu bạn không triển khai khách truy cập, bạn phải viết mã truyền tải biểu đồ. Hoặc với khách truy cập : rootElement.visit (node) -> node.collapse(). Với khách truy cập, mỗi nút thực hiện duyệt đồ thị cho tất cả các phần tử con của nó để bạn hoàn thành.
George Mauer

@GeorgeMauer, khái niệm về công văn kép đã làm sáng tỏ động lực cho tôi: hoặc logic phụ thuộc vào loại là với loại hoặc, thế giới của nỗi đau. Ý tưởng phân phối logic truyền tải vẫn khiến tôi tạm dừng. Có hiệu quả hơn không? Có phải nó dễ bảo trì hơn? Điều gì xảy ra nếu "gấp đến mức N" được thêm vào như một yêu cầu?
nik.shornikov

@ nik.shornikov hiệu quả thực sự không phải là một mối quan tâm ở đây. Trong hầu hết mọi ngôn ngữ, một vài lời gọi hàm là chi phí không đáng kể. Bất cứ điều gì ngoài đó là tối ưu hóa vi mô. Có phải nó dễ bảo trì hơn? Vâng, nó phụ thuộc. Tôi nghĩ rằng hầu hết thời gian, đôi khi không. Đối với "gấp đến cấp N". Dễ dàng vượt qua trong một levelsRemainingtruy cập như là một tham số. Giảm nó trước khi gọi cấp độ tiếp theo của trẻ em. Bên trong của khách truy cập của bạn if(levelsRemaining == 0) return.
George Mauer

1
@GeorgeMauer, hoàn toàn đồng ý về hiệu quả là mối quan tâm nhỏ. Nhưng khả năng duy trì, ví dụ như ghi đè chữ ký chấp nhận, chính xác là những gì tôi nghĩ rằng quyết định sẽ được đưa ra.
nik.shornikov

5

Mẫu khách truy cập giống như triển khai ngầm đối với lập trình Aspect Object ..

Ví dụ: nếu bạn xác định một hoạt động mới mà không thay đổi các lớp của các thành phần mà nó hoạt động


lên đề cập đến Aspect Object Lập trình
milesma

5

Mô tả nhanh về mẫu khách truy cập. Tất cả các lớp yêu cầu sửa đổi đều phải thực hiện phương thức 'chấp nhận'. 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 đối với họ các lớp đó do đó mở rộng chức năng của chúng. Khách hàng có thể sử dụng 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 nhau 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 được ghi đè xác định cách đạt được cùng một hành động cụ thể cho mọi lớp trong gia đình. Các phương thức truy cập này được thông qua một ví dụ để làm việc.

Khi bạn có thể cân nhắc sử dụng nó

  1. Khi bạn có một gia đình các lớp, bạn biết rằng bạn sẽ phải thêm nhiều hành động mới, nhưng vì một số lý do, bạn không thể thay đổi hoặc biên dịch lại gia đình của các lớp trong tương lai.
  2. Khi bạn muốn thêm một hành động mới và hành động mới đó hoàn toàn được xác định trong một lớp khách truy cập thay vì trải rộng trên nhiều lớp.
  3. Khi sếp của bạn nói rằng bạn phải sản xuất một loạt các lớp phải làm một cái gì đó ngay bây giờ ! ... nhưng thực tế không ai biết chính xác đó là cái gì.

4

Tôi đã không hiểu mô hình này cho đến khi tôi bắt gặp bài viết của chú và đọc bình luận. Hãy xem xét các mã sau đây:

public class Employee
{
}

public class SalariedEmployee : Employee
{
}

public class HourlyEmployee : Employee
{
}

public class QtdHoursAndPayReport
{
    public void PrintReport()
    {
        var employees = new List<Employee>
        {
            new SalariedEmployee(),
            new HourlyEmployee()
        };
        foreach (Employee e in employees)
        {
            if (e is HourlyEmployee he)
                PrintReportLine(he);
            if (e is SalariedEmployee se)
                PrintReportLine(se);
        }
    }

    public void PrintReportLine(HourlyEmployee he)
    {
        System.Diagnostics.Debug.WriteLine("hours");
    }
    public void PrintReportLine(SalariedEmployee se)
    {
        System.Diagnostics.Debug.WriteLine("fix");
    }
}

class Program
{
    static void Main(string[] args)
    {
        new QtdHoursAndPayReport().PrintReport();
    }
}

Mặc dù nó có thể trông tốt vì nó xác nhận Trách nhiệm đơn lẻ nhưng nó vi phạm nguyên tắc Mở / Đóng . Mỗi khi bạn có loại Nhân viên mới, bạn sẽ phải thêm nếu kiểm tra loại. Và nếu bạn sẽ không bao giờ biết điều đó vào thời gian biên dịch.

Với mẫu khách truy cập, bạn có thể làm cho mã của mình sạch hơn vì nó không vi phạm nguyên tắc mở / đóng và không vi phạm Trách nhiệm đơn lẻ. Và nếu bạn quên thực hiện truy cập, nó sẽ không biên dịch:

public abstract class Employee
{
    public abstract void Accept(EmployeeVisitor v);
}

public class SalariedEmployee : Employee
{
    public override void Accept(EmployeeVisitor v)
    {
        v.Visit(this);
    }
}

public class HourlyEmployee:Employee
{
    public override void Accept(EmployeeVisitor v)
    {
        v.Visit(this);
    }
}

public interface EmployeeVisitor
{
    void Visit(HourlyEmployee he);
    void Visit(SalariedEmployee se);
}

public class QtdHoursAndPayReport : EmployeeVisitor
{
    public void Visit(HourlyEmployee he)
    {
        System.Diagnostics.Debug.WriteLine("hourly");
        // generate the line of the report.
    }
    public void Visit(SalariedEmployee se)
    {
        System.Diagnostics.Debug.WriteLine("fix");
    } // do nothing

    public void PrintReport()
    {
        var employees = new List<Employee>
        {
            new SalariedEmployee(),
            new HourlyEmployee()
        };
        QtdHoursAndPayReport v = new QtdHoursAndPayReport();
        foreach (var emp in employees)
        {
            emp.Accept(v);
        }
    }
}

class Program
{

    public static void Main(string[] args)
    {
        new QtdHoursAndPayReport().PrintReport();
    }       
}  
}

Điều kỳ diệu là trong khi v.Visit(this)trông giống nhau thì thực tế lại khác vì nó gọi quá tải khách khác nhau.


Vâng, tôi đặc biệt thấy nó hữu ích khi làm việc với các cấu trúc cây, không chỉ các danh sách phẳng (danh sách phẳng sẽ là trường hợp đặc biệt của cây). Như bạn lưu ý, nó không quá lộn xộn chỉ trong danh sách, nhưng khách truy cập có thể là một vị cứu tinh vì việc điều hướng giữa các nút trở nên phức tạp hơn
George Mauer

3

Dựa trên câu trả lời tuyệt vời của @Federico A. Ramponi.

Chỉ cần tưởng tượng bạn có thứ bậc này:

public interface IAnimal
{
    void DoSound();
}

public class Dog : IAnimal
{
    public void DoSound()
    {
        Console.WriteLine("Woof");
    }
}

public class Cat : IAnimal
{
    public void DoSound(IOperation o)
    {
        Console.WriteLine("Meaw");
    }
}

Điều gì xảy ra nếu bạn cần thêm phương pháp "Đi bộ" ở đây? Điều đó sẽ gây đau đớn cho toàn bộ thiết kế.

Đồng thời, việc thêm phương pháp "Đi bộ" sẽ tạo ra các câu hỏi mới. Còn "Ăn" hay "Ngủ" thì sao? Chúng ta phải thực sự thêm một phương thức mới vào hệ thống phân cấp Động vật cho mọi hành động hoặc hoạt động mới mà chúng ta muốn thêm? Điều đó xấu và quan trọng nhất, chúng ta sẽ không bao giờ có thể đóng giao diện Animal. Vì vậy, với mẫu khách truy cập, chúng ta có thể thêm phương thức mới vào cấu trúc phân cấp mà không sửa đổi cấu trúc phân cấp!

Vì vậy, chỉ cần kiểm tra và chạy ví dụ C # này:

using System;
using System.Collections.Generic;

namespace VisitorPattern
{
    class Program
    {
        static void Main(string[] args)
        {
            var animals = new List<IAnimal>
            {
                new Cat(), new Cat(), new Dog(), new Cat(), 
                new Dog(), new Dog(), new Cat(), new Dog()
            };

            foreach (var animal in animals)
            {
                animal.DoOperation(new Walk());
                animal.DoOperation(new Sound());
            }

            Console.ReadLine();
        }
    }

    public interface IOperation
    {
        void PerformOperation(Dog dog);
        void PerformOperation(Cat cat);
    }

    public class Walk : IOperation
    {
        public void PerformOperation(Dog dog)
        {
            Console.WriteLine("Dog walking");
        }

        public void PerformOperation(Cat cat)
        {
            Console.WriteLine("Cat Walking");
        }
    }

    public class Sound : IOperation
    {
        public void PerformOperation(Dog dog)
        {
            Console.WriteLine("Woof");
        }

        public void PerformOperation(Cat cat)
        {
            Console.WriteLine("Meaw");
        }
    }

    public interface IAnimal
    {
        void DoOperation(IOperation o);
    }

    public class Dog : IAnimal
    {
        public void DoOperation(IOperation o)
        {
            o.PerformOperation(this);
        }
    }

    public class Cat : IAnimal
    {
        public void DoOperation(IOperation o)
        {
            o.PerformOperation(this);
        }
    }
}

đi bộ, ăn uống không phải là những ví dụ phù hợp vì chúng cũng phổ biến cho cả hai Dogcũng như Cat. Bạn có thể tạo chúng trong lớp cơ sở để chúng được kế thừa hoặc chọn một ví dụ phù hợp.
Abhinav Gauniyal

âm thanh khác tho, mẫu hay, nhưng không chắc nó có liên quan gì đến mẫu khách truy cập không
DAG

3

Khách thăm quan

Trình truy cập cho phép một người thêm các hàm ảo mới vào một nhóm các lớp mà không cần sửa đổi các lớp đó; thay vào đó, người ta tạo một lớp khách truy cập thực hiện tất cả các chuyên môn phù hợp của hàm ảo

Cấu trúc khách truy cập:

nhập mô tả hình ảnh ở đây

Sử dụng mẫu Khách truy cập nếu:

  1. Các hoạt động tương tự phải được thực hiện trên các đối tượng thuộc các loại khác nhau được nhóm lại trong một cấu trúc
  2. Bạn cần phải thực hiện nhiều hoạt động khác biệt và không liên quan. Nó tách hoạt động từ các đối tượng Cấu trúc
  3. Các hoạt động mới phải được thêm vào mà không thay đổi cấu trúc đối tượng
  4. Tập hợp các hoạt động liên quan vào một lớp duy nhất thay vì buộc bạn thay đổi hoặc lấy các lớp
  5. Thêm các hàm vào các thư viện lớp mà bạn không có nguồn hoặc không thể thay đổi nguồn

Mặc dù mẫu Khách truy cập cung cấp tính linh hoạt để thêm thao tác mới mà không thay đổi mã hiện có trong Đối tượng, tính linh hoạt này đã đi kèm với một nhược điểm.

Nếu một đối tượng Có thể truy cập mới đã được thêm vào, nó yêu cầu thay đổi mã trong các lớp của Khách truy cập & ConcreteVisitor . Có một cách giải quyết để giải quyết vấn đề này: Sử dụng sự phản chiếu, điều này sẽ có tác động đến hiệu suất.

Đoạn mã:

import java.util.HashMap;

interface Visitable{
    void accept(Visitor visitor);
}

interface Visitor{
    void logGameStatistics(Chess chess);
    void logGameStatistics(Checkers checkers);
    void logGameStatistics(Ludo ludo);    
}
class GameVisitor implements Visitor{
    public void logGameStatistics(Chess chess){
        System.out.println("Logging Chess statistics: Game Completion duration, number of moves etc..");    
    }
    public void logGameStatistics(Checkers checkers){
        System.out.println("Logging Checkers statistics: Game Completion duration, remaining coins of loser");    
    }
    public void logGameStatistics(Ludo ludo){
        System.out.println("Logging Ludo statistics: Game Completion duration, remaining coins of loser");    
    }
}

abstract class Game{
    // Add game related attributes and methods here
    public Game(){

    }
    public void getNextMove(){};
    public void makeNextMove(){}
    public abstract String getName();
}
class Chess extends Game implements Visitable{
    public String getName(){
        return Chess.class.getName();
    }
    public void accept(Visitor visitor){
        visitor.logGameStatistics(this);
    }
}
class Checkers extends Game implements Visitable{
    public String getName(){
        return Checkers.class.getName();
    }
    public void accept(Visitor visitor){
        visitor.logGameStatistics(this);
    }
}
class Ludo extends Game implements Visitable{
    public String getName(){
        return Ludo.class.getName();
    }
    public void accept(Visitor visitor){
        visitor.logGameStatistics(this);
    }
}

public class VisitorPattern{
    public static void main(String args[]){
        Visitor visitor = new GameVisitor();
        Visitable games[] = { new Chess(),new Checkers(), new Ludo()};
        for (Visitable v : games){
            v.accept(visitor);
        }
    }
}

Giải trình:

  1. Visitable( Element) là một giao diện và phương thức giao diện này phải được thêm vào một tập hợp các lớp.
  2. Visitorlà một giao diện, chứa các phương thức để thực hiện một thao tác trên Visitablecác phần tử.
  3. GameVisitorlà một lớp, thực hiện Visitorgiao diện ( ConcreteVisitor).
  4. Mỗi Visitableyếu tố chấp nhận Visitorvà gọi một phương thức Visitorgiao diện có liên quan .
  5. Bạn có thể coi Gamenhư Elementvà các trò chơi cụ thể Chess,Checkers and Ludonhư ConcreteElements.

Trong ví dụ trên, Chess, Checkers and Ludolà ba trò chơi khác nhau (và Visitablecác lớp). Vào một ngày đẹp trời, tôi đã gặp phải một kịch bản để ghi lại số liệu thống kê của từng trò chơi. Vì vậy, không cần sửa đổi lớp riêng lẻ để thực hiện chức năng thống kê, bạn có thể tập trung trách nhiệm đó trong GameVisitorlớp, đây là mẹo cho bạn mà không sửa đổi cấu trúc của mỗi trò chơi.

đầu ra:

Logging Chess statistics: Game Completion duration, number of moves etc..
Logging Checkers statistics: Game Completion duration, remaining coins of loser
Logging Ludo statistics: Game Completion duration, remaining coins of loser

Tham khảo

bài viết thiết kế tốt

bài viết chua

để biết thêm chi tiết

Người trang trí

mẫu cho phép hành vi được thêm vào một đối tượng riêng lẻ, tĩnh hoặc động, mà không ảnh hưởng đến hành vi của các đối tượng khác từ cùng một lớp

Bài viết liên quan:

Mẫu trang trí cho IO

Khi nào nên sử dụng mẫu trang trí?


2

Tôi thực sự thích mô tả và ví dụ từ http://python-3-potypes-idioms-test.readthedocs.io/en/latest/Visitor.html .

Giả định là bạn có một hệ thống phân cấp lớp chính được cố định; có lẽ đó là từ một nhà cung cấp khác và bạn không thể thay đổi thứ bậc đó. Tuy nhiên, ý định của bạn là bạn muốn thêm các phương thức đa hình mới vào hệ thống phân cấp đó, điều đó có nghĩa là thông thường bạn phải thêm một cái gì đó vào giao diện lớp cơ sở. Vì vậy, vấn đề nan giải là bạn cần thêm các phương thức vào lớp cơ sở, nhưng bạn không thể chạm vào lớp cơ sở. Làm thế nào để bạn có được xung quanh này?

Mẫu thiết kế giải quyết loại vấn đề này được gọi là khách truy cập trực tiếp (một phần cuối cùng trong sách Mẫu thiết kế) và nó được xây dựng trên sơ đồ gửi kép được hiển thị trong phần cuối.

Mẫu khách truy cập cho phép bạn mở rộng giao diện của loại chính bằng cách tạo một hệ thống phân cấp lớp riêng của loại Khách truy cập để ảo hóa các hoạt động được thực hiện theo loại chính. Các đối tượng của loại chính chỉ đơn giản là Chấp nhận khách truy cập, sau đó gọi hàm thành viên bị ràng buộc động của khách truy cập.


Trong khi về mặt kỹ thuật, mẫu Khách truy cập thực sự chỉ là công văn kép cơ bản từ ví dụ của họ. Tôi sẽ tranh luận rằng tính hữu dụng không đặc biệt có thể nhìn thấy từ điều này một mình.
George Mauer

1

Trong khi tôi đã hiểu cách thức và thời gian, tôi chưa bao giờ hiểu lý do tại sao. Trong trường hợp nó giúp bất cứ ai có nền tảng về ngôn ngữ như C ++, bạn muốn đọc nó rất cẩn thận.

Đối với người lười biếng, chúng tôi sử dụng mẫu khách truy cập vì "trong khi các chức năng ảo được gửi động trong C ++, quá tải chức năng được thực hiện tĩnh" .

Hoặc, nói cách khác, để đảm bảo rằng CollideWith (ApolloSpacecraft &) được gọi khi bạn chuyển qua tham chiếu SpaceShip thực sự bị ràng buộc với một đối tượng ApolloSpacecraft.

class SpaceShip {};
class ApolloSpacecraft : public SpaceShip {};
class ExplodingAsteroid : public Asteroid {
public:
  virtual void CollideWith(SpaceShip&) {
    cout << "ExplodingAsteroid hit a SpaceShip" << endl;
  }
  virtual void CollideWith(ApolloSpacecraft&) {
    cout << "ExplodingAsteroid hit an ApolloSpacecraft" << endl;
  }
}

2
Việc sử dụng công văn động trong mẫu khách truy cập hoàn toàn làm tôi bối rối. Đề xuất sử dụng mô hình phân nhánh có thể được thực hiện tại thời điểm biên dịch. Những trường hợp này dường như sẽ tốt hơn với một mẫu hàm.
Praxeolitic

0

Cảm ơn lời giải thích tuyệt vời của @Federico A. Ramponi , tôi chỉ thực hiện điều này trong phiên bản java . Hy vọng nó có thể hữu ích.

Cũng như @Konrad Rudolph đã chỉ ra, đây thực sự là một công văn kép sử dụng hai trường hợp cụ thể với nhau để xác định các phương thức thời gian chạy.

Vì vậy, thực sự không cần phải tạo một giao diện chung cho người thực thi thao tác miễn là chúng ta có giao diện hoạt động được xác định đúng.

import static java.lang.System.out;
public class Visitor_2 {
    public static void main(String...args) {
        Hearen hearen = new Hearen();
        FoodImpl food = new FoodImpl();
        hearen.showTheHobby(food);
        Katherine katherine = new Katherine();
        katherine.presentHobby(food);
    }
}

interface Hobby {
    void insert(Hearen hearen);
    void embed(Katherine katherine);
}


class Hearen {
    String name = "Hearen";
    void showTheHobby(Hobby hobby) {
        hobby.insert(this);
    }
}

class Katherine {
    String name = "Katherine";
    void presentHobby(Hobby hobby) {
        hobby.embed(this);
    }
}

class FoodImpl implements Hobby {
    public void insert(Hearen hearen) {
        out.println(hearen.name + " start to eat bread");
    }
    public void embed(Katherine katherine) {
        out.println(katherine.name + " start to eat mango");
    }
}

Như bạn mong đợi, một giao diện chung sẽ mang lại cho chúng ta sự rõ ràng hơn mặc dù thực sự nó không phải là phần thiết yếu trong mẫu này.

import static java.lang.System.out;
public class Visitor_2 {
    public static void main(String...args) {
        Hearen hearen = new Hearen();
        FoodImpl food = new FoodImpl();
        hearen.showHobby(food);
        Katherine katherine = new Katherine();
        katherine.showHobby(food);
    }
}

interface Hobby {
    void insert(Hearen hearen);
    void insert(Katherine katherine);
}

abstract class Person {
    String name;
    protected Person(String n) {
        this.name = n;
    }
    abstract void showHobby(Hobby hobby);
}

class Hearen extends  Person {
    public Hearen() {
        super("Hearen");
    }
    @Override
    void showHobby(Hobby hobby) {
        hobby.insert(this);
    }
}

class Katherine extends Person {
    public Katherine() {
        super("Katherine");
    }

    @Override
    void showHobby(Hobby hobby) {
        hobby.insert(this);
    }
}

class FoodImpl implements Hobby {
    public void insert(Hearen hearen) {
        out.println(hearen.name + " start to eat bread");
    }
    public void insert(Katherine katherine) {
        out.println(katherine.name + " start to eat mango");
    }
}

0

Câu hỏi của bạn là khi nào cần biết:

tôi không mã đầu tiên với mẫu khách truy cập. i mã tiêu chuẩn và chờ cho sự cần thiết xảy ra và sau đó tái cấu trúc. vì vậy, giả sử bạn có nhiều hệ thống thanh toán mà bạn đã cài đặt cùng một lúc. Tại thời điểm thanh toán, bạn có thể có nhiều điều kiện if (hoặc instanceOf), ví dụ:

//psuedo code
    if(payPal) 
    do paypal checkout 
    if(stripe)
    do strip stuff checkout
    if(payoneer)
    do payoneer checkout

bây giờ hãy tưởng tượng tôi đã có 10 phương thức thanh toán, nó trở nên xấu xí. Vì vậy, khi bạn thấy kiểu khách truy cập đó xuất hiện một cách khéo léo để ngăn cách tất cả những điều đó và cuối cùng bạn sẽ gọi một cái gì đó như thế này sau đó:

new PaymentCheckoutVistor(paymentType).visit()

Bạn có thể xem cách triển khai nó từ số lượng ví dụ ở đây, tôi chỉ cho bạn thấy một usecase.

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.