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 vi vì chú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 Piece
giao 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:
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 Piece
lớ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 Piece
giao 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 ( Piece
giao 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 Piece
triể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:
Ở 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à PieceMovingVisitor
giao 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 Piece
nhậ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 Piece
lớ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 Piece
lớ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à Bishop
lớ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ế, Piece
giao diện phải cung cấp một cách để làm điều đó:
void setCoordinates(Coordinates coordinates);
Trách nhiệm Piece
thay đổi tọa độ hiện đang mở cho các lớp khác ngoài các lớp Piece
con.
Di chuyển quá trình xử lý được thực hiện bởi khách truy cập trong các Piece
lớ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()
, instanceof
hoặ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ư Decorator
ví 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 Piece
và 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)