1. Visitor Patern là gì

Để trả lời cho câu hỏi trên, trước hết ta hãy thử dạo một vòng qua Wikipedia tiếng Việt xem sao nhé:

Trong thiết kế hướng đối tượng, Visitor là mẩu thiết kế(Design Patterns) 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.

Ok, chả hiểu gì cả 😄, thử Wiki tiếng Anh xem

In object-oriented programming and software engineering, the visitor design pattern is a way of separating an algorithm from an object structure on which it operates. A practical result of this separation is the ability to add new operations to existing object structures without modifying those structures. It is one way to follow the open/closed principle.

:-< Cũng khó hiểu không kém.

Siêu nhân nào đọc xong wiki hiểu luôn visitor pattern thì bài viết mình đến đây là kết thúc, hẹn gặp lại các siêu nhân ở bài viết khác. Còn bạn nào đọc xong vẫn lơ ngơ (như mình) thì hi vọng ví dụ tiếp sau đây sẽ giúp các bạn hiểu được về design pattern này.

2. Ví dụ minh họa

Chúng ta sẽ cùng bắt tay làm một game điển hình Human vs Monster. Con người, với bản tính hung hãn của mình tấn công quái vật, trong khi loài quái vật hiền lành nhút nhát chỉ biết lãnh đòn thôi.

Đầu tiên, yêu cầu game đơn giản như sau:

  • Có 2 loại người tham chiến: WarriorWizzard
  • Warrior trẻ khỏe đập quái 1 hit 50 máu, Wizard toàn mấy ông già lụ khụ 1 vụt chỉ 10 máu thôi.

Với yêu cầu trên, trước hết ta sẽ định nghĩa 2 interface tương ứng cho HumanMonster

public interface Human {
    void hit(Monster monster);
}

public interface Monster {
    void damaged(int hp);
}

Từ đó, ta viết các implement cho 2 interface này

public class Warrior implements Human {
    public void hit(Monster monster) {
        System.out.println("Let me introduce you: my hammer!!!");
        monster.damage(50);
    }
}

public class Wizard implements Human {
    public void hit(Monster monster) {
        System.out.println("Avada Kedavra! ⚡");
        monster.damage(10);
    }
}

public class CuteDogie implements Monster {
    public void damaged(int hp) {
        System.out.println("Woof! I lost " + hp + "hp (❍ᴥ❍ʋ)");
    }
}

Chạy thử xem sao

Human warrior = new Warrior();
Human wizard = new Wizard();

Monster monster = new CuteDogie();

warrior.hit(monster);
wizard.hit(monster);

Kết quả thu được, ai cũng biết được là:

Let me introduce you: my hammer!!!
Woof! I lost 50hp (❍ᴥ❍ʋ)
Avada Kedavra! ⚡
Woof! I lost 10hp (❍ᴥ❍ʋ)

Giả sử ở version 2 của trò chơi, sếp thêm vài luật:

  • Thêm một Monster nữa: Dracula
  • Dracula có thể hóa dơi, nên búa của Warrior chỉ sượt qua thôi, mất có 10 máu, trong khi ăn phép của Wizard thì thụt 80 máu luôn.

Ở version 1, method hit được gọi phụ thuộc vào kiểu của instance Human, chính là single dispatch.

Trong version 2 này, ta cần gọi method dựa trên cả kiểu của instance Human cũng kiểu như giá trị đầu vào Monster. Nói cách khác, ta cần double dispatch.

Hầu hết các ngôn ngữ hiện nay như C++, Java, Python, Javascript... chỉ hỗ trợ single dispatch.

Ta sẽ sử dụng Visitor Pattern để thực hiện double dispatch với ngôn ngữ chỉ hỗ trợ single dispatch.

Trước hết là interface:

public interface Human {
    void hit(Monster monster);
}

public interface Monster {
    void hitBy(Warrior warrior);
    void hitBy(Wizard wizard);
}

Trong method hit của Human sẽ gọi đến hitBy của Monster truyền vào, tham số đầu vào chính là chính Human đấy. Nhờ có vậy, method hitBy sẽ được chọn theo kiểu của cả HumanMonster liên quan.

Update lại theo interface mới

public class Warrior implements Human {
    public void hit(Monster monster) {
        System.out.println("Let me introduce you: my hammer!!!");
        monster.hitBy(this);
    }
}

public class Wizard implements Human {
    public void hit(Monster monster) {
        System.out.println("Avada Kedavra! ⚡");
        monster.hitBy(this);
    }
}

public class CuteDogie implements Monster {
    public void hitBy(Warrior warrior) {
        damaged(50);
    }

    public void hitBy(Wizard wizard) {
        damaged(10);
    }

    private void damaged(int hp) {
        System.out.println("Woof! I lost " + hp + "hp (❍ᴥ❍ʋ)");
    }
}

Như vậy, Dracula cũng tương tự

public class Dracula implements Monster {
    public hitBy(Warrior warrior) {
        damaged(10);
    }

    public hitBy(Wizard wizard) {
        damaged(80);
    }

    private void damaged(int hp) {
        System.out.println("Owie! I lost " + hp + "hp ᙙᙖ");
    }
}

Chạy thử

Human warrior = new Warrior();
Human wizard = new Wizard();

Monster dogie = new CuteDogie();
Monster dracula = new Dracula();

warrior.hit(dogie);
wizard.hit(dogie);

warrior.hit(dracula);
wizard.hit(dracula);

Kết quả thu được sẽ là

Let me introduce you: my hammer!!!
Woof! I lost 50hp (❍ᴥ❍ʋ)
Avada Kedavra! ⚡
Woof! I lost 10hp (❍ᴥ❍ʋ)
Let me introduce you: my hammer!!!
Owie! I lost 10hp ᙙᙖ
Avada Kedavra! ⚡
Owie! I lost 80hp ᙙᙖ

Nhận xét: Có thể thấy, việc thêm một Monster mới rất đơn giản, chỉ cần implement các method của Monster thôi. Trong khi, nếu thêm Human thì ta phải thay đổi cả interface Monster và cập nhật các class implement của Monster.

Chính vì vậy, tùy vào trường hợp cụ thể, xem các đối tượng thuộc loại nào hay bị thay đổi mà cài đặt Visitor Patern cho phù hợp.