- Sản phẩm & Dịch vụSản phẩm & Dịch vụ
- Giải phápGiải pháp
- Bảng giáBảng giá
- Công tyCông ty
- Tài liệuTài liệu
vi
vi
Chuyên mục với các bài viết hướng dẫn, nghiên cứu và phân tích chi tiết về kỹ thuật, các xu hướng công nghệ mới nhất dành cho lập trình viên.
Thảo luận (0)
Đăng nhập để thảo luận
Trong quá trình phát triển phần mềm, chúng ta thường xuyên đối mặt với bài toán làm thế nào để thực hiện các thao tác (operations) trên một tập hợp các đối tượng có cùng bản chất mà không làm cho các đối tượng đó bị ‘phình to’ về mặt logic.
Đây chính là lúc các Design Pattern phát huy vai trò của mình, đặc biệt là nhóm Behavioral Design Pattern. Và hôm nay, chúng ta sẽ cùng nhau tìm hiểu sâu về một trong số đó: Visitor Design Pattern.

Như đã đề cập, Visitor Pattern là một trong các Behavioral Design Pattern, được sử dụng khi chúng ta cần thực hiện một hoặc nhiều thao tác trên một nhóm các đối tượng cùng loại. Với sự hỗ trợ của Visitor Pattern, chúng ta có thể chuyển logic vận hành từ chính các đối tượng sang một lớp khác.
Ví dụ, hãy nghĩ về một giỏ hàng mua sắm (Shopping Cart) nơi bạn có thể thêm nhiều loại mặt hàng khác nhau (Elements). Khi bạn nhấp vào nút thanh toán, hệ thống sẽ tính toán tổng số tiền phải trả. Bây giờ, chúng ta có thể đặt logic tính toán này trong các lớp mặt hàng (như Book hay Fruit), hoặc chúng ta có thể di chuyển logic này sang một lớp khác bằng cách sử dụng Visitor Pattern. Cách thứ hai sẽ giúp code của chúng ta sạch hơn, dễ bảo trì và mở rộng hơn rất nhiều.
Hãy cùng xem chúng ta sẽ triển khai điều này trong ví dụ về Visitor Pattern như thế nào.
Để triển khai Visitor Pattern, trước tiên chúng ta sẽ tạo các loại mặt hàng (Elements) khác nhau sẽ được sử dụng trong giỏ hàng mua sắm.
ItemElement.javaĐây là interface chung cho tất cả các loại mặt hàng.
package com.journaldev.design.visitor;public interface ItemElement {
public int accept(ShoppingCartVisitor visitor);}
Hãy để ý rằng phương thức accept nhận một đối số kiểu Visitor. Chúng ta có thể có các phương thức khác cụ thể cho từng loại mặt hàng, nhưng để đơn giản và tập trung vào Visitor Pattern, tôi sẽ không đi sâu vào chi tiết đó.
Tiếp theo, hãy tạo một số lớp cụ thể cho các loại mặt hàng khác nhau: Book và Fruit.
Book.javapackage com.journaldev.design.visitor;public class Book implements ItemElement {
private int price; private String isbnNumber; public Book(int cost, String isbn){ this.price=cost; this.isbnNumber=isbn; } public int getPrice() { return price; } public String getIsbnNumber() { return isbnNumber; } @Override public int accept(ShoppingCartVisitor visitor) { return visitor.visit(this); }}
Fruit.javapackage com.journaldev.design.visitor;public class Fruit implements ItemElement {
private int pricePerKg; private int weight; private String name; public Fruit(int priceKg, int wt, String nm){ this.pricePerKg=priceKg; this.weight=wt; this.name = nm; } public int getPricePerKg() { return pricePerKg; } public int getWeight() { return weight; } public String getName(){ return this.name; } @Override public int accept(ShoppingCartVisitor visitor) { return visitor.visit(this); }}
Hãy chú ý đến cách triển khai của phương thức accept() trong các lớp cụ thể này: nó gọi phương thức visit() của đối tượng Visitor và truyền chính bản thân nó (this) làm đối số. Đây là một điểm mấu chốt của Visitor Pattern, được gọi là “Double Dispatch”.
Chúng ta cần có các phương thức visit() cho các loại mặt hàng khác nhau trong interface Visitor, và chúng sẽ được triển khai bởi lớp Visitor cụ thể.
ShoppingCartVisitor.javaĐây là interface định nghĩa các phương thức visit cho từng loại mặt hàng mà Visitor có thể “thăm” (visit).
package com.journaldev.design.visitor;public interface ShoppingCartVisitor {
int visit(Book book); int visit(Fruit fruit);}
Bây giờ chúng ta sẽ triển khai interface ShoppingCartVisitor, và mỗi loại mặt hàng sẽ có logic riêng để tính toán chi phí của nó.
ShoppingCartVisitorImpl.javaĐây chính là nơi chứa logic tính toán thực tế cho từng loại mặt hàng.
package com.journaldev.design.visitor;public class ShoppingCartVisitorImpl implements ShoppingCartVisitor {
@Override public int visit(Book book) { int cost=0; //apply 5$ discount if book price is greater than 50 if(book.getPrice() > 50){ cost = book.getPrice()-5; }else cost = book.getPrice(); System.out.println("Book ISBN::"+book.getIsbnNumber() + " cost ="+cost); return cost; } @Override public int visit(Fruit fruit) { int cost = fruit.getPricePerKg()*fruit.getWeight(); System.out.println(fruit.getName() + " cost = "+cost); return cost; }}
Với Visitor Pattern, logic tính toán giá đã được tách rời hoàn toàn khỏi các lớp Book và Fruit. Điều này giúp giữ cho các lớp ItemElement gọn gàng và chỉ tập trung vào việc mô tả dữ liệu của chúng.
Cuối cùng, hãy xem cách chúng ta có thể sử dụng ví dụ về Visitor Pattern này trong các ứng dụng client.
ShoppingCartClient.javapackage com.journaldev.design.visitor;public class ShoppingCartClient {
public static void main(String[] args) { ItemElement[] items = new ItemElement[]{new Book(20, "1234"),new Book(100, "5678"), new Fruit(10, 2, "Banana"), new Fruit(5, 5, "Apple")}; int total = calculatePrice(items); System.out.println("Total Cost = "+total); } private static int calculatePrice(ItemElement[] items) { ShoppingCartVisitor visitor = new ShoppingCartVisitorImpl(); int sum=0; for(ItemElement item : items){ sum = sum + item.accept(visitor); } return sum; }}
Khi chúng ta chạy chương trình client trên, chúng ta sẽ nhận được output sau:
Book ISBN::1234 cost =20 Book ISBN::5678 cost =95 Banana cost = 20 Apple cost = 25 Total Cost = 160
Bạn có thể thấy rõ ràng cách mỗi ItemElement “chấp nhận” một ShoppingCartVisitor và chuyển quyền tính toán giá cho Visitor. Logic tính toán giá cho Book và Fruit được thực hiện hoàn toàn trong ShoppingCartVisitorImpl.
Mặc dù tôi không thể minh họa trực tiếp Class Diagram ở đây, nhưng bạn có thể hình dung nó sẽ như sau:

Ưu điểm nổi bật của Visitor Pattern chính là sự linh hoạt và khả năng mở rộng:
ShoppingCartVisitorImpl) mà không cần chạm vào các lớp Book hay Fruit. Điều này giúp duy trì Single Responsibility Principle và Open/Closed Principle.Visitor mới hoặc thêm phương thức vào ShoppingCartVisitor interface và triển khai nó, mà không làm ảnh hưởng đến cấu trúc của các lớp ItemElement hiện có.Visitor (thêm phương thức visit cho loại mới) và triển khai cụ thể của Visitor, nhưng các lớp ItemElement hiện có sẽ không bị ảnh hưởng.Tuy nhiên, Visitor Pattern cũng có một số hạn chế sau:
visit) tại thời điểm thiết kế interface Visitor. Nếu thường xuyên thêm các loại ItemElement mới, bạn sẽ phải liên tục cập nhật interface ShoppingCartVisitor và tất cả các lớp triển khai Visitor của nó, điều này có thể trở nên tốn công.Visitor (ví dụ: ShippingCostVisitor, TaxCalculationVisitor, InventoryReportVisitor), việc quản lý và mở rộng có thể trở nên phức tạp do phải duy trì nhiều lớp Visitor song song.Visitor có thể thực hiện thao tác, các lớp Element đôi khi phải “lộ” ra một số phương thức hoặc thuộc tính cần thiết, điều này có thể phá vỡ nguyên tắc đóng gói (encapsulation) ở một mức độ nào đó.Tóm lại, Visitor Design Pattern là một công cụ mạnh mẽ trong bộ sưu tập các mẫu thiết kế hành vi, giúp chúng ta đạt được sự tách biệt rõ ràng giữa cấu trúc đối tượng và các thuật toán hoạt động trên cấu trúc đó. Nó là một ví dụ điển hình cho việc áp dụng Nguyên tắc Open/Closed Principle (mở rộng mà không sửa đổi) và Single Responsibility Principle, làm cho code của chúng ta dễ bảo trì và linh hoạt hơn. Hy vọng bài viết này đã mang lại cho bạn những kiến thức hữu ích.