Observers

Game Pattern – Observer

Observer patten rất phổ biến, bạn có thể thấy trong core library của Java là java.util.Observer, trong C# nó được đưa ngay vào language với keyword event.

Bây giờ chúng ta sẽ thử triển khai pattern này trên một ví dụ: “Achievement Unlocked

Giả định chúng ta đang thêm một Achivement System vào trong game. Nó sẽ đưa vào các tiêu chí để tạo ra hàng tá các huy chương mà người chơi có thể đạt được khi hoàn thành một mốc mục tiêu nào đó, ví dụ “Giết 100 con khỉ ma”, “Rơi xuống cầu”…

Việc cài đặt khá khó bởi vì chúng ta có một khoảng rộng các thành tựu (achievements) cái sẽ được trao (unlock) với tất cả các hoạt động khác nhau (behaviors). Ví dụ: “Rơi xuống cầu” một bằng cách nào đó có thể bind với physics engine, nhưng chúng ta có thể muốn gọi hàm unlockFallOffBridge() ngay trong hàm đại số tuyến tính trong thuật toán kiểm tra va chạm.

Cái chúng ta luôn mong muốn là tất cả các code liên quan đến một khía cạnh nào đó sẽ phải được tập trung tại một chỗ. Thử thách ở đây là những achievement này được kích hoạt bởi một số lượng đáng kể các khía cạnh của gameplay. Làm thế nào mà bạn có thể không móc nối mã xử lý achievement vào những chỗ đó?

Đó chính là nơi cho Observer pattern. Nó cho phép một đoạn code thông báo rằng một thứ gì đó thú vị vừa xảy ra mà không cần quan tâm ai sẽ là người nhận thông báo đó.

Giờ bắt tay vào triển khai ví dụ.

Đây là một đoạn trên physics engine:

void Physics::updateEntity( Entity& entity )
{
  bool wasOnSurface = entity.isOnSurface();
  entity.accelerate(GRAVITY);
  entity.update();
  if (wasOnSurface && !entity.isOnSurface())
  {
    notify(entity, EVENT_START_FALL);
  }
}

Ở đây physics engine phải quyết định thông báo cái gì, bởi vậy nó không phải là hoàn toàn độc lập. Nhưng trong thiết kế kiến trúc, chúng ta hầu hết thường cố gắng tạo ra một hệ thống tốt hơn chứ không phải một hệ thống hoàn hảo. (better, not perfect).

The Observer

class Observer
{
public:
  virtual ~Observer() {}
  virtual onNotify(const Entity& entity, Event event) = 0;
};

Những tham số trong onNotify() là tùy ý bạn. Đó là lý do tại sao đây là Observer pattern thông thường mà không phải là cái có sẵn chỉ việc copy vào game. Điển hình là những tham số như: object – thằng đã gửi thông báo, và một ham số dữ liệu kiểu tổng quát để bạn có thể truyền dữ liệu khác vào.

Nếu bạn đang code bằng ngôn ngữ có hỗ trợ generic hoặc templates, bạn có thể tùy chỉnh đề dùng chúng.

Bất kỳ lớp cụ thể nào cài đặt/kế thừa lớp này đều trở thành một Observer. Trong ví dụ này đó chính là achievement system, nên ta sẽ có đoạn code như sau:

class Achievements : public Observer
{
public:
  virtual void onNotify(const Entity& entity, Event event)
  {
    switch (event)
    {
      case EVENT_ENTITY_FELL:
        if (entity.isHero() && heroIsOnBridge_)
        {
          unlock( ACHIEVEMENT_FELL_OFF_BRIDGE );
        }
        break;
        // Handle other events, and update heroIsOnBridge...
    }
  }

private:
  void unlock(Achievement achievement)
  {
    // Unlock if not already unlocked...
  }
  bool heroIsOnBridge_;
};

The Subject

Cái onNotify() được gọi bởi chính đối tượng đang được quan sát. Theo cách nói trong GoF thì đối tượng đó được gọi là Subject. Nó có hai việc cần làm. Việc đầu tiên, nó giữ danh sách các observers — những thằng đang kiên nhẫn đợi một lá thư từ nó-Subject.

class Subject
{
private:
Observer* observers_[MAX_OBSERVERS];
int numObservers_;
};

Thực tế bạn sẽ dùng một mảng kích thước động thay cho mảng cố định kích cỡ này.

Cái quan trọng là Subject phải phơi những public API cho phép thay đổi danh sách các Observer đó, như mô tả dưới đây:

class Subject
{
public:
void addObserver(Observer* observer)
{
// Add to array ...
}

void removeObserver(Observer* observer)
{
// Remove from array ...
}

// Other stuff ...
};

Điều này sẽ cho phép mã bên ngoài có thể kiểm soát được ai là người sẽ nhận những thông báo. Subject có thể giao tiếp với Observer tuy nhiên chúng không bị liên kết chặt (couple) với nhau. Trong ví dụ này, không có dòng  nào trong physics engine đề cập đến achievement system. Tuy vậy, nó vẫn có thể nói chuyện được với achievement system. Đó là cái thông minh của Observer pattern.

Một điểm quan trọng khác là Subject sẽ có một danh sách các Observers chứ không phải chỉ có một. Ví dụ: ngoài achievement system, thì audio system cũng là một Observer khác của sự kiện “Rơi xuống cầu”.

Bây giờ là lúc Subjectgửi thông báo đi

class Subject
{
protected:
  void notify(const Entity& entity, Event event)
  {
    for (int i = 0; i < numObservers_; i++)
    {
      observers_[i]->onNotify(entity, event);
    }
  }
  // Other stuff ...
};

Lưu ý rằng đoạn mã này giả định bản thân Observer không làm thay đổi danh sách các Observer bằng phương thức onNotify() . Sẽ cần một cài đặt mạnh mẽ hơn để có thể ngăn chặn hoặc có thể xử lý một cách duyên dáng hơn cho vấn đề thay đổi đồng thời này.

Apply Subject: Observable Physics Engine

Bây giờ chúng ta chỉ cần móc những thứ này vào Physics Engine như vậy nó có thể gửi các thông báo và achievement system có tự kết nối đến và nhận các thông báo đó. 

class Physics : public Subject
{
public:
void updateEntity(Entity& entity);
};

Điều này cho phép chúng ta để notify() method ở dạng protected . Như vậy thì lớp con của Subject trong trường hợp này là physics engine có thể gửi thông báo. Trong khi đó thì addObserver()removeObserver()public vì vậy mọi thứ trong physics system đề có thể được đưa vào thành các Observer .

Code thực tế thì bạn tránh sử dụng kế thừa ở đây. Thay vào đó, ban sẽ để Physis System has an instance of Subject . Thay cho việc giám sát chính bản thân physics engine, thì cái Subject sẽ là một đối tượng “falling event” tách biệt. Khi đó Observer sẽ có thể đăng ký nhận thông báo theo kiểu như sau.

physics.entityFell() .addObserver(this);

Đây là một sự khác biệt giữa Observer system và Event system. Observer system quan sát cái đã làm một điều gì đó thú vị, còn Event system quan sát đối tượng đại diện cho điều thú vị đã xảy ra.

Detractors – Những người chỉ trích:

“Nó quá chậm”

Đây là phát ngôn thường xuất phát từ các lập trình viên không thực sự hiểu một cách chi tiết về pattern. Họ có một giả định mặc nhiên rằng những thứ có mùi “design pattern” phải bao gồm một đống class , những điều hướng gián tiếp, cũng như những cách thức sáng tạo để phung phí các CPU cycles.

Observer pattern đang mang một tiếng xấu ở đây, vì nó được biết đến loanh quanh những nhân vật mờ ám có tên là events , messages, và thậm chí là data binding . Một vài cái trong những hệ thống đó có thể chậm (thường là cố tình và lý do tốt). Chúng bao gồm những thứ như kiểu queuing hoặc cấp phát động cho các thông báo.

Đây là lý do việc viết tài liệu cho pattern là quan trọng. Khi bạn mơ hồ về thuật ngữ, bạn sẽ mất khả năng kết nối một cách sáng sủa và xúc tích. Bạn có thể nói Observer và một ai đó nghe là Events hoặc Messaging bởi vì không ai bận tâm đến việc viết ra sự khác nhau giữa chúng hoặc cũng có thể họ không tình cờ đọc được về nó ở đâu đó.

Nhưng, bây giờ với những cái mà bạn đã nhìn thấy cách mà pattern này được triển khai, bạn thấy nó không phải “quá chậm” như nhận xét trên? Việc gửi một thông báo chỉ đơn giản là lướt trên một danh sách và gọi một vài virtual method. Ngay cả cứ công nhận là như vậy, thì nó chỉ chậm hơn một chút so với một lời gọi dispatch tĩnh (statically dispatched call), nhưng cái giá đó không đáng kể.

Thực sự thì nó quá nhanh, bạn phải cẩn thận bởi vì Observer pattern là synchronous . Thằng Subject gọi các Observer của nó một cách trực tiếp, điều đó có nghĩa nó chưa quay trở lại công việc khi nào toàn bộ các Observer chưa được return từ các phương thức onNotify() của nó. Một thằng Observer chậm sẽ làm block Subject .

Điều này nghe thật kinh khủng, xong nó không phải là tận cùng của thế giới. Nó chỉ là một thứ gì đó mà bạn cần nhận biết trước. Những nhà lập trình giao diện — những người đang code dạng event-based thích điều sau qua hàng thập kỷ — có một phương châm cho việc này: “tránh xa cái UI thread”.

Nếu bạn phản hồi một sự kiện theo kiểu đồng bộ (synchronous), bạn cần kết thúc nó và trả lại quyền điều khiển cho UI thread nhanh nhất có thể, như vậy thì UI mới không bị khóa (treo). Nếu cần làm việc gì nặng/chậm, hãy đưa nó sang một thread khác hoặc vào một work queue.

Bạn phải cẩn thận khi kết hợp observer pattern, threadingexplicit locks. Nếu một observer cố gắng giữ một khóa mà thằng subject đang có, thì bạn có thể tạo ra deadlock trong game. Những trường hợp cần thiết bạn nên dùng kết nối bất đồng bộ qua Event Queue.

“Nó thực hiện quá nhiều cấp phát động”

Nhiều lập trình viên đã chuyển sang những ngôn ngữ có hỗ trợ bằng garbage collected, như vậy việc cấp phát động không còn thấy xuất hiện nhiều nữa. Nhưng đối với những phần mềm ở mức độ “hiệu năng tới hạn” như là game, cấp phát động vẫn là vấn đề, thậm chí ngay cả trong managed languages. Việc cấp phát động sẽ mất thời gian, ngay cả khi nó cần thực hiện thu hồi bộ nhớ (reclaiming memory) thậm chí khi nó được thực hiện một cách tự động.

Trong ví dụ trên đây, chúng ta đang dùng một mảng cố định (fixed array) để cho ví dụ đơn giản đi. Tuy nhiên trong cài đặt thực tế, danh sách các Observer luôn luôn là những tập hợp cấp phát động (dynamic allocated collection) — tăng/giảm kích thước tùy theo việc thêm/loại bỏ observer vào/ra khỏi danh sách này. Việc cấp phát động này có thể khuyấy động một số người. Tất nhiên, điều nhận thấy đầu tiên là nó chỉ cấp phát bộ nhớ khi một observer được liên kết. Việc gửi một thông báo không yêu cầu cấp phát bộ nhớ nữa-vì nó chỉ là một lời gọi method. Nếu bạn móc nối các observer khi khởi chạy game và không gây rối với chúng nhiều (dont mess with them), thì số lượng cấp phát sẽ là tối thiểu.

Nếu nó vẫn là vấn đề, bạn có thể xử lý thêm và bớt Observer mà không sử dụng cấp phát động như mô tả dưới đây.

Linked observers:

Trong code mà bạn nhìn thấy đến thời điểm hiện tại, thì Subject đang sở hữu một danh sách các con trỏ chỉ đến các Observer đang theo dõi nó. Bản thân Observer class này không tham chiếu đến danh sách đó. Vì nó chỉ là một pure virtual interface. Đây cũng là việc apply theo nguyên tắc thiết kế: Interfaces được ưa chuộng hơn concrete, stateful class.

Nhưng nếu chúng ta sẵn lòng đặt thêm một chút memory về state trong Observer thì chúng ta có thể giải quyết vấn đề cấp phát bằng cách xâu chuỗi danh sách của Subject qua các Observer . Như vậy, thay cho việc Subject có một danh sách tách biệt các con trỏ, thì các đối tượng Observer sẽ trở thành các node trong một linked list

để cài đặt, đầu tiên chúng ta sẽ loại bỏ biến array trong Subject và thay thế nó bằng một con trỏ tới đầu (head) của danh sách các Observer

class Subject
{
  Subject()
  : head_(NULL)
  {}

// Methods ...
private:
  Observer* head_;
};

Sau đó chúng ta sẽ mở rộng Observer với một con trỏ đến Observer tiếp theo trong danh sách:

class Observer
{
  friend class Subject;

public:
  Observer()
  : next_(NULL)
  {}

// Other stuff ..
private:
  Observer* next_;
};

Do danh sách Observer ở đây cần được thêm/bớt chính trong Observer nên đơn giản nhất thì ta để nó có một friend.

Việc đăng ký một Observer mới đơn giản chỉ là nối nó vào danh sách.

void Subject::addObject( Observer* observer )
{
  observer->next_ = head_; // old node address
  head_ = observer         // new node address

Đây là code hủy đăng ký

void Subject::removeObserver( Observer* observer )
{
  if (head_ == observer)
  {
    head_ = observer->next_;
    observer->next_ = NULL;
    return;
  }
  Observer* current = head_;
  while (current != NULL)
  {
    if (current->next_ == observer)
    {
      current->next_ = observer->next_;
      observer->next_ = NULL;
      return;
    }
    current = current->next_;
  }
}

Gửi thông báo

void Subject::notify( const Entity& entity, Event event )
{
  Observer* observer = head_;
  while (observer != NULL)
  {
    observer->onNotify(entity, event);
    observer = observer->next_;
  }
}

Không quá tệ phải không nào? ta đã có một Subject với số lượng Observer tùy ý mà không cần cấp phát động. Việc đăng ký/hủy bỏ Observer nhanh như một mảng đơn giản. Tuy vậy chúng ta vừa hy sinh một tính năng. Vì bạn đang sử dụng đối tượng Observer như là một node trong danh sách, điều đó ngầm định nó chỉ có thể thuộc một danh sách Observer của Subject . Nói cách khác, một Observer chỉ có thể giám sát một Subject tại một thời điểm. Còn ở cài đặt truyền thống thì nó có thể đồng thời thuộc nhiều hơn một Subject

Những vấn đề tồn đọng

Destroying Subjects and Observers

Nếu bạn delete một vài Observer , Subject có thể vẫn giữ một con trỏ đến nó. Nó sẽ là con trỏ treo (dangling pointer) đến một vùng nhớ đã bị thu hồi. Vậy khi Subject cố gửi một thông báo, … chỉ có thể nói rằng bạn sẽ không có thời gian vui vẻ.

Việc hủy Subject đơn giản hơn vì Observer không có bất kỳ tham chiếu nào đến Subject. Tuy nhiên vấn đề phát sinh sau khi huỷ Subject đó là những Observer vẫn mong đợi nhận được những thông báo trong tương lai, và chúng không biết rằng điều đó bây giờ sẽ không xảy ra nữa. Chúng không còn là các Observer nữa, chỉ là chúng đang nghĩ vậy thôi.

Giải pháp đầu tiên là Observer thực hiện unregister khi bị delete. Thường thường, Observer biết nó đang theo dõi Subject nào, vì vậy việc unregister đơn giản chỉ là đưa hàm removeObserver() vào hàm destructor 

Còn về việc hủy Subject trước, thì giải pháp đơn giản là Subject sẽ gửi một thông báo cho các Observer trước khi nó tự hủy.

Tuy vậy bạn không cần lo lắng quá, bạn đã có Gabage Collector.

What’s going on?

Vấn đề khác, vấn đề sâu xa hơn với Observer pattern là hậu quả trực tiếp từ mục đích sử dụng của nó. Chúng ta dùng nó để làm lỏng sự liên kết. Nó để cho Subject kết nối gián tiếp với một vài Observer mà không cần rằng buộc tĩnh giữa chúng.

Tuy nhiên nếu có lỗi phát sinh trong quá trình xử lý nhận thông báo, thì với một explicit coupling, việc tìm ra hàm gây lỗi sẽ dễ dàng hơn. Kiểu như một trò chơi trẻ con trên IDE khi liên kết là tĩnh. Nhưng nếu lỗi xảy ra trên danh sách các Observer  , thì cách duy nhất để biết ai đã được gọi là xem cái Observer nào được gọi ở runtime. Để xử lý vấn đề này, nếu bạn thường cần suy nghĩ về cả hai phía của một vài kết nối để hiểu các phần của chương trình, đừng sử dụng Observer pattern để thể hiện sự rằng buộc này (linkage). Lựa trọn giải pháp cụ thể hơn (explicit). 

Khi bạn chọc vào một hệ thống lớn, bạn thường có xu hướng nhóm các thứ bạn làm vào cùng một chỗ. Điều này đánh mất cái gọi là “separation of concerns” hay “cohenrence and cohension” và “modularity”, nhưng chúng được rút xuống thành “những thứ đi cùng nhau và không đi cùng những thứ khác”

Observer pattern là một cách tuyệt vời để cho các mảng tách biệt có thể nói chuyện với nhau mà không cần gom lại thành cả mảnh lớn. Như vậy nó sẽ không hiệu quả trong một khối nhỏ chuyên biệt cho một chức năng hoặc một khía cạnh nào đó của ứng dụng.

Đây là lý do tại sao nó lại phù hợp tốt với ứng dụng của chúng ta: achievements system và physics engine là những domain hoàn toàn không liên quan, có thể phát triển bởi những người khác nhau. Chúng ta muốn một kết nối trần trụi tối giản nhất giữa chúng, bởi vậy công việc trên từng domain không cần nhiều hiểu biết về thằng còn kia.

Observers Hôm Nay

Design Patterns đi đến năm 1994. Từ đó, lập trình hướng đối tượng đã trở thành một mô hình lập trình được ưa chuộng. 

Bởi vì một Observer chỉ có một onNotify() method, nên nếu nó đang giám sát nhiều subject, thì nó cần có khả năng nhận biết thằng nào đang gọi nó.

Có một cách tiếp cận hiện đại là một “observer” đơn giản chỉ là một tham chiếu đến một method hoặc function. Trong những ngôn ngữ với “first-class functions”, đặc biệt với closure, đây là cách phổ biến để thực hiện Observer pattern.

Ví du, C# có “events” trong bản thân ngôn ngữ, với thứ đó, cái observer bạn đăng ký là một “delegate”, đó là một thuật ngữ trong ngôn ngữ C# để mô tả một tham chiếu đến một phương thức. Trong JavaScript’s event system, observers có thể là các objects hỗ trợ giao thức đặc biệt EventListerner, nhưng chúng cũng có thể chỉ đơn giản là một hàm. Và cách đơn giản này là cách phổ biến đối với các lập trình viên.

Nếu là tôi thì tôi cũng ưa thích thiết kế theo dạng function-based hơn dạng class-based. Thậm chí với C++, bạn sẽ hướng tới một hệ thống cho bạn đăng ký function pointers như là những observer thay cho những instance của một vài Observer interface. (tham khảo ví dụ)

Observers Ngày Mai

Event systems và những mẫu giống Observer pattern là cực kỳ phổ biến ngày nay. Các bước phổ biến của pattern:

  • Thực hiên thông báo khi một vài state thay đổi
  • Thực hiện cưỡng bách thay đổi một số UI để phản ánh trạng thái mới.

Sau một số cải tiến, một số cái tên khác đã được đưa ra: “dataflow programming”, “function reactive programming”, vân vân. Trong khi có một vài thành công, thường đến trong những lĩnh vực hạn chế như audio processing hoặc thiết kế chip. Trong lúc đó một tiếp cận ít tham vọng hơn đã thu hút được sự chú ý. Nhiều application framework gần đây đã sử dụng nó — “data binding”

Không giống những mô hình cơ bản, “data binding” không cố gắng loại bỏ hoàn toàn mã chỉ định (imperative code) và không cố gắng kiến trúc toàn bộ ứng dụng của bạn trên một đồ thị luồng khai báo khổng lồ (a giant declarative dataflow graph). Cái nó thực hiện là tự động hóa cái công việc bận nơi bạn đang thay đổi một UI element hoặc calculated property để phản anh một thay đổi.

Giống những declarative system khác, “data binding” có thể chậm và phức tạp để phù hợp với bên trong phần core của game engine. Nhưng tôi sẽ bất ngờ nếu không nhìn thấy nó xâm nhập vào các lĩnh vực không đòi hỏi hiệu năng cao trong game như các mảng UI.

Trong khi đó, cái Observer pattern cổ điển – rất hữu dụng vẫn ở đây đợi bạn.

Enjoy!

Leave a Reply

Your email address will not be published. Required fields are marked *