[Design Patterns] Visitor Pattern

Visitor (hành động khách quan) được sử dụng khi chúng ta muốn triển khai tách rời các phương thức xử lý khỏi các class mô tả thực thể. Visitor được xếp vào nhóm các pattern Hành Vi. Trong Visitor có 2 giao diện chính được tạo ra để chúng ta có thể triển khai tách

Visitor (hành động khách quan) được sử dụng khi chúng ta muốn triển khai tách rời
các phương thức xử lý khỏi các class mô tả thực thể. Visitor được xếp vào nhóm các
pattern Hành Vi.

Trong Visitor có 2 giao diện chính được tạo ra để chúng ta có thể triển khai tách rời
các phương thức xử lý khỏi các class mô tả thực thể:

  1. Một interface có tên là Visitor để mô tả trừu tượng về một hành động khách quan.
    Các object mô tả phương thức hành động triển khai Visitor sẽ ghé thăm visit()
    các object mô tả thực thể để truy vấn dữ liệu cần xử lý.
  2. Một interface có tên là Element để mô tả trừu tượng về đối tượng của hành động.
    Các object mô tả thực thể dữ liệu Element sẽ tiếp nhận sự ghé thăm của các hành
    động khách quan bằng phương thức accept(). Ở đây accept() sẽ không chứa
    code xử lý logic mà chỉ đơn giản là được sử dụng để phát động một phương thức hoạt
    động hay một tính năng được mô tả chi tiết bởi một object Visitor được chọn.

Với tên gọi được sử dụng cho pattern thì phần mà tác giả thiết kế pattern này muốn
chúng ta chú ý tới là phần đầu tiên. Đó là các object mô tả chi tiết triển khai các hành động
khách quan. Đây cũng là phần sẽ được mở public về phía code client để các đoạn code client
có thể lựa chọn hành động muốn thực hiện bằng cách sử dụng các class thực tế triển khai
giao diện Visitor.

Ở khía cạnh còn lại thì các class thực thể dữ liệu triển khai Element lại thường được ẩn
đối với code client và các object này thường được cung cấp tới code client qua phương
thức gián tiếp nào đó. Code client sẽ không cần và không nên biết tới chi tiết kiến trúc
của các object thực thể dữ liệu.

Áp dụng triển khai

sơ đồ các class

Ở đây chúng ta có một phần mềm mô phỏng các hình phẳng 2D với một vài chức năng chính
đó là vẽ Drawing và định lượng diện tích các hình Evaluating.

Lý do triển khai áp dụng Visitor Pattern ở đây là vì chúng ta muốn tập trung vào việc “quản lý
các tính năng của phần mềm” và có thể nhìn thoáng qua cấu trúc thư mục code cũng có thể biết
được những tính năng đang có là gì? Và code triển khai chi tiết của mỗi tính năng sẽ được tập
trung trong một class Visitor cụ thể thay vì được triển khai rời rạc trong các class mô tả thực thể
CircleSquare.

Cụ thể là Drawing được sử dụng để tách rời việc triển khai phương thức vẽ hình draw()
khỏi các class thực thể CircleSquare; Và đồng thời cũng là nơi tập trung tất cả các phiên bản
khác nhau của phương thức draw() tách ra từ các class thực thể. Code triển khai của draw()
trong Circle được di chuyển vào Drawing với tên gọi mới là visit(Circle c), và tương tự thì
draw() trong Square sẽ có tên gọi mới là visit(Square s).

Drawing.java

public interface Drawing() {
   Object visit(Circle c);
   Object visit(Square s);
}

Một lý do khác để chúng ta triển khai Visitor Pattern đó là tư duy ở trên phương diện thuật toán
xử lý dữ liệu thay vì phương diện quản lý kiến trúc code. Ở đây chúng ta cung cấp cho code client
một cấu trúc dữ liệu trừu tượng là Element và một phương thức xử lý dữ liệu có tên là Evaluating
có thể áp dụng lên một Element để nhận được một giá trị kết quả.

Bước 1

Tạo cặp interface ElementVisitor để mô tả giao tiếp giữa object thực thể và object hành động.
Ở đây mục đích là thay thế việc gọi phương thức trực tiếp của object thực thể shape.draw()
bằng một phiên đón tiếp sự ghé thăm của object hành động là shape.accept(new Drawing()).

visitorpattern/Element.java

packagevisitorpattern;publicinterfaceElement{Objectaccept(Visitor v);}

Lúc này do chưa viết các class thực thể nên chúng ta sẽ tạm để trống interface Visitor.

visitorpattern/Visitor.java

packagevisitorpattern;publicinterfaceVisitor{// các phiên bản visit() sẽ được bổ sung sau}

Bước 2

Tạo các class mô tả thực thể hình học 2D là CircleTriangle.
Đồng thời triển khai giao diện Element cho các class này. Ở đây
chúng ta cũng sẽ tạo thêm class hỗ trợ mô tả điểm Point trong mặt
phẳng tọa độ.

Các class này sẽ không để mở public về phía code client để tránh việc
bị ghi đè logic hoạt động.

visitorpatter/Point.java

packagevisitorpattern;classPoint{finalint x;finalint y;Point(int x,int y
   ){this.x = x;this.y = y;}doubledistanceTo(Point p){int dX = x - p.x;int dY = y - p.y;returnMath.sqrt(dX*dX + dY*dY);}@OverridepublicStringtoString(){return"("+ x +", "+ y +")";}}

visitorpatter/Circle.java

packagevisitorpattern;classCircleimplementsElement{PointO;int radius;@OverridepublicObjectaccept(Visitor v){return v.visit(this);}}

visitorpattern/Square.java

packagevisitorpattern;classSquareimplementsElement{PointA,B,C,D;@OverridepublicObjectaccept(Visitor v){return v.visit(this);}}

Tới đây thì chúng ta đã biết về 2 class thực thể và có thể bổ sung thêm
2 phương thức visit() tương ứng cho interface Visitor, để có thể
đảm bảo rằng các class mô tả phương thức hành động khách quan sẽ
phải được triển khai tính đa hình đầy đủ tương đương với mô hình kế thừa
truyền thống của OOP.

Cụ thể là thay vì override phương thức abstract draw() của một class
cơ sở Shape trong code của CircleSquare, thì ở đây chúng ta
cần đảm bảo Drawing sẽ phải có có đủ các phiên bản overload của
visit() để có thể áp dụng lên cả CircleSquare.

visitorpattern/Visitor.java

packagevisitorpattern;publicinterfaceVisitor{Objectvisit(Circle c);Objectvisit(Square s);}

Bước 3

Triển khai các tính năng vẽ hình Drawing và định lượng diện tích hình Evaluating.

visitorpatter/Drawing.java

packagevisitorpattern;publicclassDrawingimplementsVisitor{@OverridepublicObjectvisit(Circle c){PointA=newPoint(c.O.x,c.O.y + c.radius);System.out.println("Bắt đầu vẽ hình tròn...");System.out.println("Chấm điểm O làm tâm hình tròn tại tọa độ: "+c.O);System.out.println("Chấm điểm A làm điểm bắt đầu vẽ đường tròn tại tọa độ: "+A);System.out.println("Vẽ đường tròn tâm O "+c.O+" với bán kính "+ c.radius +" bắt đầu từ điểm A "+A);System.out.println("Tô kín diện tích bên trong đường tròn vừa vẽ.");return c;}@OverridepublicObjectvisit(Square s){System.out.println("Bắt đầu vẽ hình vuông...");System.out.println("Chấm điểm A tại tọa độ: "+s.A);System.out.println("Chấm điểm B tại tọa độ: "+s.B);System.out.println("Chấm điểm C tại tọa độ: "+s.C);System.out.println("Chấm điểm D tại tọa độ: "+s.D);System.out.println("Vẽ đoạn thẳng nối từ điểm A "+s.A+" tới điểm B "+s.B);System.out.println("Vẽ đoạn thẳng nối từ điểm B "+s.B+" tới điểm C "+s.C);System.out.println("Vẽ đoạn thẳng nối từ điểm C "+s.C+" tới điểm D "+s.D);System.out.println("Vẽ đoạn thẳng nối từ điểm D "+s.D+" tới điểm A "+s.A);System.out.println("Tô kín diện tích bên trong các đường vừa vẽ.");return s;}}

visitorpattern/Evaluating.java

packagevisitorpattern;publicclassEvaluatingimplementsVisitor{@OverridepublicObjectvisit(Circle c){double pi =3.14;return pi * c.radius * c.radius;}@OverridepublicObjectvisit(Square s){double width =(s.A).distanceTo(s.B);return width * width;}}

Bước 4

Viết code client để chạy thử 2 tính năng DrawingEvaluating đã được
triển khai xong. Ở đây code client sẽ không thể khởi tạo các object mô tả thực thể
bằng việc trực tiếp sử dụng các class CircleSquare mà chỉ truy vấn một
mảng các Element từ đâu đó. Chúng ta sẽ viết thêm 1 class public Test
trong package visitorpattern để cung cấp cho main các object thực thể.

visitorpattern/Test.java

packagevisitorpattern;importjava.util.ArrayList;importjava.util.List;publicclassTest{publicstaticList<Element>generateSomeShapes(){List<Element> shapeList =newArrayList<Element>();Circle c =newCircle();c.O=newPoint(1,2);
      c.radius =100;
      shapeList.add(c);Square s =newSquare();s.A=newPoint(0,1);s.B=newPoint(1,1);s.C=newPoint(1,0);s.D=newPoint(0,0);
      shapeList.add(s);return shapeList;}}

Code client tại main sử dụng phương thức static của Test để nhận được một List<Element>.
Sau đó chạy thử 2 tính năng DrawingEvaluating trên mỗi Element.

PatternDemo.java

importvisitorpattern.Drawing;importvisitorpattern.Element;importvisitorpattern.Evaluating;importvisitorpattern.Test;importjava.util.List;publicclassPatternDemo{publicstaticvoidmain(String[] args){// truy vấn dữ liệu từ đâu đó để có được vài object hình họcList<Element> shapeList =Test.generateSomeShapes();// thực hiện tính năng vẽ hình
      shapeList.forEach((shape)->{System.out.println("==============");
         shape.accept(newDrawing());});// thực hiện tính năng định lượng diện tích các hình
      shapeList.forEach((shape)->{System.out.println("==============");System.out.println("Kiểu hình: "+ shape.getClass().getSimpleName());System.out.println("Diện tích: "+ shape.accept(newEvaluating()));});}}

Bước 5

Kiểm chứng lại kết quả được in ra ở console.

console

==============Bắt đầu vẽ hình tròn...Chấm điểm O làm tâm hình tròn tại tọa độ:(1,2)Chấm điểm A làm điểm bắt đầu vẽ đường tròn tại tọa độ:(1,102)Vẽ đường tròn tâm O(1,2) với bán kính 100 bắt đầu từ điểm A(1,102)Tô kín diện tích bên trong đường tròn vừa vẽ.==============Bắt đầu vẽ hình vuông...Chấm điểm A tại tọa độ:(0,1)Chấm điểm B tại tọa độ:(1,1)Chấm điểm C tại tọa độ:(1,0)Chấm điểm D tại tọa độ:(0,0)Vẽ đoạn thẳng nối từ điểm A(0,1) tới điểm B(1,1)Vẽ đoạn thẳng nối từ điểm B(1,1) tới điểm C(1,0)Vẽ đoạn thẳng nối từ điểm C(1,0) tới điểm D(0,0)Vẽ đoạn thẳng nối từ điểm D(0,0) tới điểm A(0,1)Tô kín diện tích bên trong các đường vừa vẽ.==============Kiểu hình:CircleDiện tích:31400.0==============Kiểu hình:SquareDiện tích:1.0

Nguồn: viblo.asia

Bài viết liên quan

WebP là gì? Hướng dẫn cách để chuyển hình ảnh jpg, png qua webp

WebP là gì? WebP là một định dạng ảnh hiện đại, được phát triển bởi Google

Điểm khác biệt giữa IPv4 và IPv6 là gì?

IPv4 và IPv6 là hai phiên bản của hệ thống địa chỉ Giao thức Internet (IP). IP l

Check nameservers của tên miền xem website trỏ đúng chưa

Tìm hiểu cách check nameservers của tên miền để xác định tên miền đó đang dùn

Mình đang dùng Google Domains để check tên miền hàng ngày

Từ khi thông báo dịch vụ Google Domains bỏ mác Beta, mình mới để ý và bắt đầ