Tìm hiểu Visitor pattern qua ví dụ
Bài đăng này đã không được cập nhật trong 9 năm
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:
Warrior
vàWizzard
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 Human
và Monster
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ủaWarrior
chỉ sượt qua thôi, mất có 10 máu, trong khi ăn phép củaWizard
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ả Human
và Monster
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.
All rights reserved