Visitor pattern: chuyến viếng thăm kỳ bí

Xét ví dụ sau:

interface Animal {
  ...
}

class Dog implements Animal {
  public void gogo() {...}
  public void hehe() {...}
}

class Cat implements Animal {
  public void meomeo() {...}
  public void hihi() {...}
}

void somewhere() {
  Animal a = ...;
  // PROBLEM:
  // Muốn object a nói (say) nhưng interface Animal
  // không cung cấp method phù hợp.
  // Một cách thô tục là ép kiểu ngược như sau:
  if (a instanceof Dog) {
    ((Dog)a).gogo();
  } else if (a instanceof Cat) {
    ((Cat)a).meomeo();
  }
}

Rõ ràng là bạn không thể hài lòng với cách thô tục trên, bạn rất hiểu đa hình là gì và do đó bạn sẽ thêm một method say vào interface Animal rồi cho Dog, Cat hiện thực say:

interface Animal {
  void say();
}

class Dog implements Animal {
  public void gogo() {...}
  public void hehe() {...}
  @override public void say { gogo(); }
}

class Cat implements Animal {
  public void meomeo() {...}
  public void hihi() {...}
  @override public void say() { meomeo(); }
}

void somewhere() {
    Animal a = ...;
    a.say(); // Tuyệt vời!!! Nhưng ...
}

Đừng vội mừng, vì Animal là một interface, nhiều khi bạn sẽ không thích nó bị thay đổi xoành xoạch, bởi lẽ đang có rất nhiều nơi khác phụ thuộc vào Animal. Ví dụ, giờ bạn muốn Animal có khả năng cười (smile) thế là bạn lại phải thêm method smile vào Animal, rồi phải hiện thực cho toàn bộ các lớp con. Chưa hết, ở những nơi khác kia bạn phải compile lại source, test, và deploy lại (cho dù những nơi đó không xài tới smile). Mỗi lần sửa như vậy thật là phiền phức!

Bây giờ hãy cùng nhau đi tìm một phương pháp mà chỉ cần sửa Animal, Dog, Cat một lần duy nhất, rồi từ đó về sau nó bao hết mọi sự thay đổi và không cần sửa Animal, và thậm chí không cần sửa các lớp con như Dog, Cat.

Điều chúng ta thực sự khao khát là làm sao để lấy về một tham chiếu tới chính con Animal đó, nhưng tham chiếu này lại phải thuộc một kiểu cụ thể là Dog hoặc Cat. Không một ai có thể biết con Animal đó là con gì, trừ chính nó. Điều duy nhất chúng ta có thể làm là chuẩn bị sẵn hai method: một nhận vào Dog, một nhận vào Cat, và truyền cả hai method đó cho con Animal thông qua một method mới có tên là accept. Bên trong accept của con Animal, nó biết nó là Dog hoặc Cat, nó sẽ gọi method thích hợp (một trong hai) và tất nhiên là trao chính nó (this) vào method ấy. Và thế là chúng ta đã lấy về được tham chiếu mong muốn, tại một trong hai method đó, hoàn toàn không cần ép kiểu ngược.

Trong OOP, truyền method tức là truyền object, do vậy hai method nói trên sẽ cùng nằm trong một object thuộc một kiểu gọi là Visitor.

Chúng ta đang trên con đường tái tạo lại mẫu thiết kế Visitor, một trong những mẫu thiết kế tuyệt đẹp của Gang of Four.

interface Animal {
  void accept(Visitor v);
}

interface Visitor {
  // đây là hai method được chuẩn bị sẵn
  void visit(Dog dog);
  void visit(Cat cat);
}

class Dog implements Animal {
  @override public void accept(Visitor v) {
    // trao thân mình (this, thuộc kiểu Dog) cho v:
    v.visit(this);
  }
  public void gogo() {...}
  public void hehe() {...}
}
class Cat implements Animal {
  @override public void accept(Visitor v) {
    // trao thân mình (this, thuộc kiểu Cat) cho v:
    v.visit(this);
  }
  public void meomeo() {...}
  public void hihi() {...}
}

Method accept của cả DogCat có code giống hệt nhau nhưng chúng ta không thể gom vào trong Animal được vì this trong Animal thuộc kiểu Animal chứ không phải kiểu Dog, Cat.

Sau khi lấy được tham chiếu rồi, biết nó kiểu gì rồi, thì việc muốn nó say là quá dễ dàng, ví dụ như lớp SayVisitor dưới đây:

class SayVisitor implements Visitor {
  @override public void visit(Dog dog) {
    dog.gogo(); // quá dễ dàng
  }
  @override public void visit(Cat cat) {
    cat.meomeo(); // quá dễ dàng
  }
}

Giờ đây để yêu cầu a say ta chỉ việc tạo một object SayVisitor và truyền nó cho a:

Animal a = ...;
a.accept(new SayVisitor());

Tương tự như vậy cho các hành động khác như smile, run … Code của Animal, Dog, Cat đều không thay đổi. Điều thay đổi duy nhất chỉ là thêm lớp mới. Software thay đổi chủ yếu bằng cách thêm code mới chứ không phải chủ yếu bằng cách sửa code cũ, đó là một trong những ưu điểm lớn nhất của OOP.

Lược đồ UML của Visitor Pattern áp dụng vào ví dụ này:

Sở dĩ cần phải vẽ UML ra để thấy có một chu trình trong Visitor Pattern: {Animal, Dog, Cat} phụ thuộc vào Visitor, và Visitor lại phụ thuộc ngược vào {Animal, Dog, Cat}. Nếu danh sách các lớp con của Animal (Dog, Cat) mà cố định, thì phiên bản Visitor Pattern như trên (bản gốc của Gang of Four) là phù hợp và đủ dùng.

Acyclic Visitor

Ngược lại, nếu dach sách lớp con của Animal rất hay bị thay đổi, ví dụ khi có thêm lớp Bird, thì rõ ràng interface Visitor và tất cả các lớp hiện thực nó cũng sẽ bị thay đổi để có thể viếng thăm được thêm con Bird. Nguyên nhân ở đây là do interface Visitor ôm đồm quá, nó visit được tất cả các loại con. Một biến thể của mẫu Visitor, tên là Acyclic Visitor, được dùng để giải quyết vấn đề này.

Giờ thì interface Visitor không còn ôm đồm nữa, nó hoàn toàn rỗng. Tất cả các method visit của nó được lấy hết ra và chia vào từng interface riêng biệt: DogVisitor chỉ để đi thăm DogCatVisitor chỉ để đi thăm Cat. Tất nhiên, cuối cùng thì vẫn phải có một lớp cụ thể SayVisitor hiện thực hết tất cả các interface này.

Chú ý method accept của Dog, vì v thuộc kiểu Visitor là kiểu rỗng nên chúng ta phải ép kiểu ngược nó xuống thành DogVisitor mới dùng được. Điều này khiến cho Dog có thêm một sự phụ thuộc, là phụ thuộc vào DogVisitor. Đây là cái giá phải trả của Acyclic Visitor. Nhưng được cái lợi là,

Rồi bây giờ thử thêm lớp Bird, ta sẽ thêm luôn một interface là BirdVisitor. Các interface cũ như Visitor, DogVisitorCatVisitor vẫn bình yên vô sự.  Còn lớp SayVisitor, nó có muốn thăm cả Bird hay không là quyền của nó. Nếu muốn, nó sẽ phải hiện thực thêm interface BirdVisitor; nếu không thì thôi, như cũ.

Mẫu Acyclic Visitor quả thực rất phức tạp hơn so với mẫu Visitor gốc, mà lại còn khiến các con như Dog, Cat có thêm sự phụ thuộc, cho nên ta chỉ dùng nó khi nào danh sách lớp con của Animal là rất rất hay biến động mà thôi.


Vẫn có lúc bạn sửa interface Animal mà không cảm thấy phiền phức, vậy thì bạn cứ thoải mái, không cần phải áp dụng mẫu Visitor nữa.

Thực ra thì các method visit không nhất thiết phải overload, bạn hoàn toàn có thể đặt tên chúng rạch ròi là visitDog, visitCat … Tuy nhiên, nên dùng overload cho ngắn gọn.

Kỹ thuật mà mẫu Visitor áp dụng gọi là double dispatch, vì có hai sự message dispatch mỗi khi gọi method accept: lần một là xác định xem con Animal nào (Dog hay Cat) sẽ nhận message accept, tiếp theo, lần hai là xác định xem cái Visitor nào (SayVisitor hay SmileVisitor) sẽ nhận message visit.

Advertisements

One thought on “Visitor pattern: chuyến viếng thăm kỳ bí

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s