Reading Time: 6 minutes

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.

Visitor Design Pattern trong Java

Visitor Design Pattern là gì?

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.

Ví dụ thực tế với Java: Giỏ hàng thông minh

Để 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: BookFruit.

Book.java

package 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.java

package 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 BookFruit. Đ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.java

package 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 BookFruit đượ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 của Visitor Design Pattern

Ư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:

  • Tách biệt logic: Nếu logic của một thao tác thay đổi (ví dụ: cách tính chiết khấu sách thay đổi), chúng ta chỉ cần sửa đổi trong lớp triển khai của Visitor (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.
  • Dễ dàng thêm thao tác mới: Việc thêm một loại thao tác mới (ví dụ: tính thuế, tạo báo cáo hàng tồn kho) vào hệ thống cũng trở nên dễ dàng hơn. Bạn chỉ cần tạo một 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ó.
  • Dễ dàng thêm Element mới: Việc thêm một loại mặt hàng mới vào hệ thống cũng khá thuận tiện. Nó sẽ yêu cầu thay đổi trong interface 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.

Hạn chế của Visitor Pattern

Tuy nhiên, Visitor Pattern cũng có một số hạn chế sau:

  • Phụ thuộc vào cấu trúc ổn định: Một nhược điểm đáng lưu ý của Visitor Pattern là chúng ta cần phải biết trước tất cả các kiểu đối tượng (Element) mà Visitor sẽ thăm (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.
  • Khó mở rộng nếu có quá nhiều Visitor: Nếu có quá nhiều triển khai của interface 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.
  • Lộ ra chi tiết nội bộ: Để 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.

0 Bình luận

Đăng nhập để thảo luận

Chuyên mục Hướng dẫn

Tổng hợp 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.

Đăng ký nhận bản tin của chúng tôi

Hãy trở thành người nhận được các nội dung hữu ích của CyStack sớm nhất

Xem chính sách của chúng tôi Chính sách bảo mật.

Đăng ký nhận Newsletter

Nhận các nội dung hữu ích mới nhất