Hướng dẫn Java Design Pattern – Visitor

Ở các bài viết trước chúng ta đã cùng tìm hiểu về các Pattern thuộc nhóm hành vi (Behavior Pattern): Chain of Responsibility, Command, Interpreter, Iterator, Mediator, Memento, Observer, State, Strategy, Template Method. Trong bài viết này, chúng ta sẽ cùng tìm hiểu về một Pattern cuối cùng thuộc nhóm Behavior Pattern được giới thiệu trong cuốn “Design Patterns: Elements of Reusable Object-Oriented Software – GOF” là Visitor Design Pattern.

1. Single Dispatch và Double Dispatch là gì?

Trước khi đi vào chi tiết về Visitor Pattern, chúng ta hãy cùng tìm hiểu về Single Dispatch và Double Dispatch.

1.1. Single Dispatch là gì?

Hãy xem đoạn code bên dưới:

package com.maixuanviet.patterns.behavioral.visitor.singledispatch;
 
import lombok.Data;
 
@Data
public class Book {
    private String name;
    private int price;
}
public class ProgramingBook extends Book {
 
}
 
public class BusinessBook extends Book {
 
}

package com.maixuanviet.patterns.behavioral.visitor.singledispatch.example1;
 
public interface Customer {
 
    void buy(Book book);
 
    void buy(ProgramingBook book);
 
    void buy(BusinessBook book);
}
package com.maixuanviet.patterns.behavioral.visitor.singledispatch.example1;
 
public class Developer implements Customer {
 
    @Override
    public void buy(Book book) {
        System.out.println("Developer buy a Book");
    }
 
    @Override
    public void buy(ProgramingBook book) {
        System.out.println("Developer buy a Programing Book");
 
    }
 
    @Override
    public void buy(BusinessBook book) {
        System.out.println("Developer buy a Business Book");
    }
}
package com.maixuanviet.patterns.behavioral.visitor.singledispatch.example1;
 
public class SingleDispatchExample {
 
    public static void main(String[] args) {
        Book book = new ProgramingBook(); // (1)
        Customer maixuanviet = new Developer();
        maixuanviet.buy(book);
 
        ProgramingBook programingBook = new ProgramingBook(); // (2)
        maixuanviet.buy(programingBook); // Developer buy a Programing Book
    }
}

Output của chương trình:

Developer buy a Book
Developer buy a Programing Book

Như bạn thấy, kết quả ở (1) không như chúng ta mong đợi, cái chúng ta cần là kết quả như (2).

Tại sao sử dụng như ở (1) lại không đúng?

=> Trong Java, chúng ta có thể định nghĩa các phương thức cùng tên nhưng khác nhau về tham số (tính đa hình). Một phương thức được gọi sẽ dựa trên 2 yếu tố: tên của phương thức và kiểu đối tượng gọi nó. Cơ chế này được gọi là Single Dispatch. Trong ví dụ trên, do ở (1) chúng ta đã cast nó về Book nên phương thức buy(Book book) được gọi.

Để giải quyết vấn đề trên, chúng ta có thể sử dụng từ khóa instanceof để kiểm tra kiểu của đối tượng gọi như sau:

package com.maixuanviet.patterns.behavioral.visitor.singledispatch.example2;
 
import com.maixuanviet.patterns.behavioral.visitor.singledispatch.Book;
import com.maixuanviet.patterns.behavioral.visitor.singledispatch.BusinessBook;
import com.maixuanviet.patterns.behavioral.visitor.singledispatch.ProgramingBook;
import com.maixuanviet.patterns.behavioral.visitor.singledispatch.example1.Customer;
 
public class Developer2 implements Customer {
 
    @Override
    public void buy(Book book) {
        if (book instanceof ProgramingBook) {
            ProgramingBook programingBook = (ProgramingBook) book;
            buy(programingBook);
        } else if (book instanceof BusinessBook) {
            BusinessBook businessBook = (BusinessBook) book;
            buy(businessBook);
        } else {
            System.out.println("Developer buy a Book");
        }
    }
 
    @Override
    public void buy(ProgramingBook book) {
        System.out.println("Developer buy a Programing Book");
 
    }
 
    @Override
    public void buy(BusinessBook book) {
        System.out.println("Developer buy a Business Book");
    }
}

Chạy lại chương trình trên với implement mới, chúng ta có kết quả như sau:

Developer buy a Programing Book
Developer buy a Programing Book

1.2. Double Dispatch là gì?

Ngoài cách sử dụng instanceof như trong ví dụ trên, chúng ta có thể sử dụng kỹ thuật Double DispatchTrong Double Dispatch, một phương thức sẽ được gọi dựa trên 3 yếu tố: tên của phương thức, kiểu của cả đối tượng gọi và kiểu của đối số truyền vào.

Vậy áp dụng Double Dispatch như thế nào, chúng ta hãy cùng theo dõi trong phần tiếp theo với Visitor Design pattern.

2. Visitor Pattern là gì?

Represent an operation to be performed on the elements of an object structure. Visitor lets you define a new operation without changing the classes of the elements on which it operates.

Visitor Pattern là một trong những Pattern thuộc nhóm hành vi (Behavior Pattern). Visitor cho phép định nghĩa các thao tác (operations) trên một tập hợp các đối tượng (objects) không đồng nhất (về kiểu) mà không làm thay đổi định nghĩa về lớp (classes) của các đối tượng đó. Để đạt được điều này, trong mẫu thiết kế visitor ta định nghĩa các thao tác trên các lớp tách biệt gọi các lớp visitors, các lớp này cho phép tách rời các thao tác với các đối tượng mà nó tác động đến. Với mỗi thao tác được thêm vào, một lớp visitor tương ứng được tạo ra.

Đây là một kỹ thuật giúp chúng ta phục hồi lại kiểu dữ liệu bị mất (thay vì dùng instanceof). Nó thực hiện đúng thao tác dựa trên tên của phương thức, kiểu của cả đối tượng gọi và kiểu của đối số truyền vào.

Visitor còn được biết đến như là Double dispatch.

3. Cài đặt Visitor Pattern như thế nào?

Các thành phần tham gia Visitor Pattern:

  • Visitor :
    • Là một interface hoặc một abstract class được sử dụng để khai báo các hành vi cho tất cả các loại visitor.
    • Class này định nghĩa một loạt các các phương thức truy cập chấp nhận các ConcreteElement cụ thể khác nhau làm tham số. Điều này sẽ hơi giống với cơ chế nạp chồng (overloading) nhưng các loại tham số nên khác nhau do đó các hành vi hoàn toàn khác nhau. Các hành vi truy cập sẽ được thực hiện trên từng phần tử cụ thể trong cấu trúc đối tượng thông qua phương thức visit(). Loại phần tử cụ thể đầu vào sẽ quyết định phương thức được gọi.
  • ConcreteVisitor : cài đặt tất cả các phương thức abstract đã khai báo trong Visitor. Mỗi visitor sẽ chịu trách nhiệm cho các hành vi khác nhau của đối tượng.
  • Element (Visitable): là một thành phần trừu tượng, nó khai báo phương thức accept() và chấp nhận đối số là Visitor.
  • ConcreteElement (ConcreteVisitable): cài đặt phương thức đã được khai báo trong Element dựa vào đối số visitor được cung cấp.
  • ObjectStructure : là một lớp chứa tất cả các đối tượng Element, cung cấp một cơ chế để duyệt qua tất cả các phần tử. Cấu trúc đối tượng này có thể là một tập hợp (collection) hoặc một cấu trúc phức tạp giống như một đối tượng tổng hợp (composite).
  • Client : không biết về ConcreteElement và chỉ gọi phương thức accept() của Element.

3.1. Ví dụ sử dụng Visitor Pattern

Visitor.java

package com.maixuanviet.patterns.behavioral.visitor.doubledispatch;
 
public interface Visitor {
 
    void visit(BusinessBook book);
 
    void visit(DesignPatternBook book);
 
    void visit(JavaCoreBook book);
}

VisitorImpl.java

package com.maixuanviet.patterns.behavioral.visitor.doubledispatch;
 
public class VisitorImpl implements Visitor {
 
    @Override
    public void visit(BusinessBook a) {
        System.out.println(a.getPublisher());
    }
 
    @Override
    public void visit(DesignPatternBook w) {
        System.out.println(w.getBestSeller());
    }
 
    @Override
    public void visit(JavaCoreBook g) {
        System.out.println(g.getFavouriteBook());
    }
}

Book.java

package com.maixuanviet.patterns.behavioral.visitor.doubledispatch;
 
public interface Book {
    void accept(Visitor v);
}

BusinessBook.java

package com.maixuanviet.patterns.behavioral.visitor.doubledispatch;
 
public class BusinessBook implements Book {
    public void accept(Visitor v) {
        v.visit(this);
    }
 
    public String getPublisher() {
        return "The publisher of business book";
    }
}

ProgramingBook.java

package com.maixuanviet.patterns.behavioral.visitor.doubledispatch;
 
public interface ProgramingBook extends Book {
 
    String getResource();
}

DesignPatternBook.java

package com.maixuanviet.patterns.behavioral.visitor.doubledispatch;
 
public class DesignPatternBook implements ProgramingBook {
 
    @Override
    public void accept(Visitor v) {
        v.visit(this);
    }
 
    @Override
    public String getResource() {
        return "https://github.com/maixuanviet/Design-Pattern-Tutorial/";
    }
 
    public String getBestSeller() {
        return "The best Seller of design pattern book";
    }
}

JavaCoreBook.java

package com.maixuanviet.patterns.behavioral.visitor.doubledispatch;
 
public class JavaCoreBook implements ProgramingBook {
 
    @Override
    public void accept(Visitor v) {
        v.visit(this);
    }
 
    @Override
    public String getResource() {
        return "https://github.com/maixuanviet/Java-Tutorial/";
    }
 
    public String getFavouriteBook() {
        return "The most favourite book of java core";
    }
}

VisitorPatternExample.java

package com.maixuanviet.patterns.behavioral.visitor.doubledispatch;
 
public class VisitorPatternExample {
 
    public static void main(String[] args) throws Exception {
        Book book1 = new BusinessBook();
        Book book2 = new JavaCoreBook();
        Book book3 = new DesignPatternBook();
 
        Visitor v = new VisitorImpl();
        book1.accept(v);
        book2.accept(v);
        book3.accept(v);
    }
}

Output của chương trình:

The publisher of business book
The most favourite book of java core
The best Seller of design pattern book

4. Lợi ích của Visitor Pattern là gì?

  • Cho phép một hoặc nhiều hành vi được áp dụng cho một tập hợp các đối tượng tại thời điểm run-time, tách rời các hành vi khỏi cấu trúc đối tượng.
  • Đảm bảo nguyên tắc Open/ Close: đối tượng gốc không bị thay đổi, dễ dàng thêm hành vi mới cho đối tượng thông qua visitor.

5. Sử dụng Visitor Pattern khi nào?

  • Khi có một cấu trúc đối tượng phức tạp với nhiều class và interface. Người dùng cần thực hiện một số hành vi cụ thể của riêng đối tượng, tùy thuộc vào concrete class của chúng.
  • Khi chúng ta phải thực hiện một thao tác trên một nhóm các loại đối tượng tương tự. Chúng ta có thể di chuyển logic hành vi từ các đối tượng sang một lớp khác.
  • Khi cấu trúc dữ liệu của đối tượng ít khi thay đổi nhưng hành vi của chúng được thay đổi thường xuyên.
  • Khi muốn tránh sử dụng toán tử instanceof.

6. Giới hạn của Visitor Pattern và Áp dụng Reflection trong Visitor Pattern

Hạn chế lớn nhất của Visitor Pattern là chúng ta cần phải biết kiểu trả về của phương thức visit() tại thời điểm thiết kế nếu không chúng ta phải thay đổi interface và tất cả các cài đặt của nó. Như trong ví dụ trên, nếu chúng ta muốn thêm một loại sách khác (chẳng hạn OthersBook), chúng ta phải thêm phương thức visit(OthersBook) trong interface và sửa đổi tất cả các cài đặt tương ứng của Visitor.

Để giải quyết vấn đề này, chúng ta có thể sử dụng kỹ thuật Reflection để xác định chính xác concrete class được gọi tại thời điểm run-time thay vì compile-time. Bằng cách này, chúng ta có thể sử dụng tham số là super class thay vì concrete class. Hãy xem ví dụ bên dưới, các lớp mới nếu không có phương thức cài đặt riêng visit(), thì nó sẽ sử dụng hàm mặc định, chúng ta không cần thêm phương thức visit() mới và tránh sửa đổi tất cả các concrete visitor đã tồn tại.

Thêm một loại sách mới OthersBook.java

package com.maixuanviet.patterns.behavioral.visitor.doubledispatch.reflection;
 
public class OthersBook implements ProgramingBook {
 
    @Override
    public void accept(Visitor v) {
        v.visit(this);
    }
 
    @Override
    public String getResource() {
        return "Undefined";
    }
}

Thay đổi Visitor.java

package com.maixuanviet.patterns.behavioral.visitor.doubledispatch.reflection;
 
import java.lang.reflect.Method;
 
public abstract class Visitor {
    public abstract void visit(Book book);
 
    protected Method getMethod(Class<?> clazz) {
        while (!clazz.equals(Object.class)) { // Check superclasses
            try {
                return this.getClass().getDeclaredMethod("visit", clazz);
            } catch (NoSuchMethodException ex) {
                clazz = clazz.getSuperclass();
            }
        }
        Class<?>[] interfaces = clazz.getInterfaces(); // Check interfaces
        for (Class<?> anInterface : interfaces) {
            try {
                return this.getClass().getDeclaredMethod("visit", anInterface);
            } catch (NoSuchMethodException ex) {
                ex.printStackTrace();
            }
        }
        return null;
    }
 
    protected void defaultVisit(Book book) {
        System.out.println("A book");
    }
}

Thay đổi VisitorImpl.java

package com.maixuanviet.patterns.behavioral.visitor.doubledispatch.reflection;
 
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
 
public class VisitorImpl extends Visitor {
 
    public void visit(Book book) {
        Method downPolymorphic = getMethod(book.getClass());
        if (downPolymorphic == null) {
            defaultVisit(book);
        } else {
            try {
                downPolymorphic.invoke(this, book);
            } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
                e.printStackTrace();
            }
        }
    }
 
    public void visit(BusinessBook a) {
        System.out.println(a.getPublisher());
    }
 
    public void visit(DesignPatternBook w) {
        System.out.println(w.getBestSeller());
    }
 
    public void visit(JavaCoreBook g) {
        System.out.println(g.getFavouriteBook());
    }
}

ReflectiveVisitorPatternExample.java

package com.maixuanviet.patterns.behavioral.visitor.doubledispatch.reflection;
 
public class ReflectiveVisitorPatternExample {
 
    public static void main(String[] args) throws Exception {
        Book book1 = new BusinessBook();
        Book book2 = new JavaCoreBook();
        Book book3 = new DesignPatternBook();
        Book book4 = new OthersBook();
 
        Visitor v = new VisitorImpl();
        book1.accept(v);
        book2.accept(v);
        book3.accept(v);
        book4.accept(v);
    }
}

Output của chương trình:

The publisher of business book
Effective Java
Head First Design Patterns
A book

Như bạn thấy, nếu chúng ta không xác định kiểu trả về của phương thức accept(OthersBook), nó sẽ gọi hàm mặc định defaultVisit(Book).