Observer Pattern trong Java
design pattern
23
tirbc
1
White

Trần Đức Tâm viết ngày 24/08/2018

Observer Pattern

(Nội dung bài viết hoàn toàn hư cấu nhưng không nhất thiết phải sai sự thật)
(Bài viết không hẳn là Today I learned nhưng vì không có tag Today I read my book carefully nên đành dùng tag #til)

Một ngày mưa buồn lặn lội lên Shinjuju thăm lầu vọng nguyệt trên toà nhà chính phủ. Với tôi mà nói, những lúc buồn được đứng từ trên cao nhìn toàn bộ thành phố trải dài trong tầm mắt thật là yên bình. Sự yên bình đến lạ kì đầy tĩnh lặng phút chốc tan biến khi tôi lần đầu được nhìn thấy một cô gái đẹp nhường ấy. Vẻ đẹp làm tôi bất thần không nói lên lời, tâm trí rối bời lẩn quẩn bởi bao nhiêu là câu hỏi. Rõ ràng nhất xuất hiện trong đầu tôi lúc này là làm sao có thể nói chuyện được cùng cô gái ấy. Tôi lấy tập danh thiếp vốn thường để sẵn trong ví cẩn trọng lựa chọn. Nếu chỉ là danh thiếp của một gã lập trình viên quèn ngoại quốc dù cho có làm ở một tập đoàn hơn 3000 người thì cũng hẳn sẽ chẳng thể ấn tượng với nàng. Một thoáng chần chừ, tôi chọn tấm danh thiếp có đề tựa giám đốc một công ty trách nhiệm hữu hạn một thành viên có văn phòng ở Roppongi hills. Giả vờ bước qua nhau, tôi thả nhẹ tấm danh thiếp vào trong túi của nàng rồi chờ đợi. Liệu phải mất bao lâu để tôi có được cuộc điện thoại đầu tiên? Chúng ta có lẽ cần phải phân tích một chút.

Gợi mở bài toán

Hành vi của tôi vừa làm được gọi là đặt một callback vào trong túi của người phụ nữ tôi cảm mến. Ha ha. Cũng giống như trong lập trình, lập trình phản ánh lại thực tế cuộc sống để giải quyết các bài toán mà cuộc sống gặp phải. Trong Java, tôi thể hiện bài toán này như sau:

class Girl {
    private Bag mLVBag;

    public void addToBag(CardVisit callback) {
        mLVBag.add(callback);
    }

    public void run() {
        comeBackHome();
        CardVisit callback = checkBag();
        if (callback != null) {
            callback.callTo();
        }
    }
}

interface CardVisit {
    void callTo();
}

class Me {
    private World mWorld = World.getInstance();

    private String mPhoneNumber = "09035689245";

    private CardVisit mCEOCard = new CEOCardVisit(mPhoneNumber);

    protected void flirt(Girl girl) {
        girl.addToBag(mCEOCard);
    }

    public void run() {
        if (mWorld.meetBeautifulGirl()) {
            flirt(mWorld.thatsGirl);
        }
    }
}

Cách thức giải bài toán trên có hai điểm key point cần quan tâm. Thứ nhất là cô gái phải nhận thức được interface CardVisit có chứa hàm callTo() nếu không thì hỏng. Cô ấy hoàn toàn có thể nạp danh thiếp của tôi thành parameter (tham số) cho hàm callPolice() nếu muốn. Thật tai hại. Vì vậy mà cả class Girl cô gái ấy và class Me là tôi đều phải có nhận thức chung. Lúc này, chúng tôi định nghĩa ra interface CardVisit. Điểm cần quan tâm thứ hai đó là hành vi của tôi không bị phụ thuộc vào hành vi của cô gái trong hoàn cảnh bất kì. Hãy để cô gái là của hiện tại. Không phải quá khứ cũng chẳng phải tương lai. Tôi vẫn phải tự do đi ngắm thành phố, ăn tối và cô gái thì có thể gọi cho tôi, có thể không. Trong trường hợp tôi được cô gái ấy gọi tới, tôi phải hoàn toàn rảnh để thực hiện cuộc gọi hoặc giả như tôi có cách để rảnh rang để hoàn thành cuộc gọi đó. Nếu thoả mãn được điều này, sẽ chẳng có callback hell nào xảy ra cả. Mặc dù cách thức này có phần không được nam tính, phụ thuộc nhiều vào ấn tượng trên tấm danh thiếp cũng như không thể hiện được sự tôn trọng cần có dành cho phụ nữ, tuy nhiên, giải pháp này trong trường hợp này được gọi là good enough (Đủ dùng).

Vấn đề của giải pháp truyền thống

  • Từ điều kiện tiên quyết của giải pháp trên, ta nhận định được khá nhiều yếu điểm mà callback mang theo. Tôi không phải lúc nào cũng rảnh. Và khi tôi không rảnh, cô gái buộc phải chờ tôi.
  • Đôi khi tôi rảnh nhưng mà rảnh đến cái độ phưỡn ra. Bởi vì tôi bị đi tù, tôi không còn tại ngoại nữa. Cô ấy sẽ phải đợi tôi mỏi mòn. Ôi cái giấc mơ làm phu nhân CEO ở Roppongi hills sẽ đi vào quên lãng.
  • Tôi, vì quá mến thương cô gái ấy mà không để ý rằng cũng có nhiều chàng trai như tôi. Cùng thả vào và đợi chờ. Cô ấy có thể sẽ bỏ quên tôi trong góc túi trong khi đang bận hẹn hò với những chàng trai khác. Có thể họ không bằng tôi, nhưng vì họ thả sau tôi nên vô tình tôi bị trở thành người thứ ba trong cuộc tình mà tôi đến trước. Đúng người sai thời điểm ôi đau đớn lắm thay.
  • Cũng có khi, tôi bị kẻ gian hãm hại làm mất số. Cay. Các bạn biết đấy. Cạnh tranh trên thương trường thì làm gì họ cũng có thể. Vấn đề là làm thế nào để cho cô ấy biết đến số mới của tôi? Tôi còn chẳng biết được địa chỉ nhà của cô ấy ở đâu trên thanh ram. Chính phủ OS chắc chắn sẽ không cho tôi làm điều này. Nếu như tôi lưu lại số nhà của cô ấy. Chẳng lẽ tôi không cho cô ấy cơ hội được chuyển nhà hay sao?

Thực lòng mà nói, các bạn không cần quan tâm nhiều đến việc những vấn đề trên sẽ xảy ra như thế nào trong lập trình. Chúng ta mới chỉ đang xét đến chiến lược dành cho việc giải quyết bài toán. Để đi tiếp cho vấn đề này, có lẽ cô gái đó cần phải thuê một thư kí riêng. Dẫu sao cái túi cũng chỉ là một vật đựng. Vật đựng có thể là một List, một Array, một Hashmap... nhưng dù sao thì một vật vô chi vô giác cũng chỉ có thể giải quyết một vài bài toán cụ thể mà không thể giúp giải quyết vấn đề tổng quát một cách linh hoạt được. Và giải pháp của chúng ta cho bài toán này sẽ khác.

Tư tưởng của Observer Pattern

Tư tưởng của Observer Pattern gói gọn trong việc thay vì một thread A phải dừng lại chờ một tác vụ nằm ở thread B được hoàn thành thì A sẽ đăng kí một observable lắng nghe sự thay đổi của tác vụ vủa thread B mà A có thể phải chờ để tiếp tục làm công việc của A. Về bản chất, đây cũng chỉ là một dạng callback tuy nhiên điểm khác biệt nằm ở ba vấn đề:

  • Thread B vì không cần phải nắm giữ callback của A nên khi A muốn, A hoàn toàn có thể rút lui khỏi cuộc chơi bất kì lúc nào.
  • Thread B không cần nắm giữ liên kết đến A. Nhờ vậy mà A có thể được thay thế bằng C, D hay E tuỳ thích. Thậm chí là tất cả cùng lao lên cũng được.
  • Cách thức mà thread B trả lại giữ liệu cho A cũng trở nên phong phú hơn. Thay vì bị giới hạn bởi 1 callback nhất định, B hoàn toàn có thể trả về cho A một prototype. Nhược điểm duy nhất của Observer Pattern chính là người code buộc phải hiểu rõ hệ thống để biết rút lui đúng lúc. Khác với callback, điểm mạnh cũng là điểm yếu của Observer Pattern chính là nằm ở việc thread A có thể nhận được nhiều hơn một lần gọi. Vì vậy, khi A không còn cần biết đến sự thay đổi ở B nữa, cần phải loại A ra khỏi cuộc chơi để tránh khỏi Memory Leak.

Triển khai của Observer Pattern

Trong Observer Pattern chúng ta sẽ quan tâm tới các đối tượng chính sau:

  • Publisher: Đối tượng thực hiện việc yêu cầu.
  • Subcriber: Đối tượng đăng kí lắng nghe các lượt gọi của Publisher.
  • Observer: Đối tượng trung gian chịu trách nhiệm nhận lời gọi của Publisher rồi phân phối lại cho Subcriber trên kênh tương ứng.

Như vậy, chúng ta có mô hình như sau:

  • Subcriber sẽ đăng kí bản thân với Observer.
  • Subcriber yêu cầu Publisher thực hiện một nhiệm vụ nào đó.
  • Publisher sau khi hoàn thành nhiệm vụ sẽ thực hiện thông báo đến Observer.
  • Observer sẽ gửi lại thông báo cho tất cả những Subcriber đang đăng kí với nó.

Publisher → Observer → Subcriber

Như vậy trong tình huống ví dụ. Tôi sẽ là Subcriber và tôi sẽ theo đuôi (follow) cô gái Publisher bằng cách ấn vào nút follow ở tài khoản Instagram Observer của cô gái đó. Khi nào cô ấy rảnh, cô ấy đăng bài, tôi sẽ được Instagram thông báo.

Tuy nhiên, trong mô hình này, phân tích một chút thì có rất nhiều kẻ giống tôi, nên Observer buộc phải định nghĩa sẵn hai phương thức để chúng tôi có thể follow được cô gái đó. Phương thức thứ nhất là làm sao để follow thông qua việc giới thiệu nút follow cho chúng tôi. Bản thân Observer tự mình định nghĩa một method cho chính nó. Thế nhưng với phương thức thứ hai, cách để thông báo cho chúng tôi. Nếu như chúng tôi không bật máy lên thì có thông báo mãi cũng như không. Vậy thì Observer sẽ phải định nghĩa ra một interface Observerable và bắt buộc các Subcriber phải implement nếu muốn được đăng kí. Ngó qua source code một chút sẽ như sau:

interface Observerable {
    void notify();
}
class Subcriber implement Observerable {
    public void observe() {
        Observer.getInstance().register(this);
    }

    @Override
    public void notify() {
        // Làm điều khiến bạn vui
    }
}
class Observer {
    private static Observer sObserver = null; // Cài đặt Lazy init
    private ArrayList<Observerable> mObserverableList = new ArrayList();

    private Observer() {
        // Cài đặt Singleton.
    }

    public void register(final Observerable subcriber) {
        if (subcriber != null && !mObserverableList.contains(subcriber)) {
            mObserverableList.add(subcriber);
        }
    }

    public void unregister(final Observerable subcriber) {
        if (subcriber != null && mObserverableList.contains(subcriber)) {
            mObserverableList.remove(subcriber);
        }
    }

    // Thêm accept modifier synchronized nếu làm việc trên môi trường multi-thread.
    public static Observer getInstance() {
        if (sObserver == null) {
            sObserver = new Observer();
        }

        return sObserver;
    }

    public void notifyAll() {
        for (Observerable subcriber: mObserverableList) {
            subcriber.notify();
        }
    }
}
class Publisher {
    public void run() {
        // Làm việc bạn phải làm
        Observer.getInstance().notifyAll();
    }
}

Observer Pattern cheatsheet

Phần trên là tư tưởng tổng quát của Observer Pattern. Kế đến chúng ta sẽ đi vào một số bài toán con cho các giải pháp cần dùng đến Observer Pattern.

Nhiều loại Subcriber đăng kí

Bài toán được phát biểu rằng có nhiều Subcriber cùng đăng kí lên Observerable nhưng lại lắng nghe những sự kiện khác nhau. Vậy phương án giải quyết là gì?

Hướng giải quyết của bài toán này là buộc phải kiểm tra tính hợp lý của yêu cầu. Yêu cầu là dành cho Subcriber nào. Vì mối quan hệ này là mối quan hệ giữa Subcriber và Observer thế nên việc kiểm tra sẽ chỉ có thể nằm ở trong một trong hai đối tượng này.

Kiểm tra tại Observer

Để có thể kiểm tra tại Observer thì Observer phải nhận thức được ra sự khác nhau của các Subcriber. Cách đơn giản nhất để có được sự nhận thức này là thông qua instance của Subcriber tức việc mỗi loại Subcriber sẽ extends từ một Observerable khác nhau.

class Observer {
// ...

    public void notifyDirector() {
        for (Observerable subcriber: mObserverableList) {
            if (subcriber instanceof Director) {
                subcriber.notify();
            }
        }
    }
}

Nhược điểm của cách này đó là mỗi khi có thêm một loại Subcriber mới thì Observer phải lao vào sửa code. Điều này xem ra là không ổn lắm.

Kiểm tra tại Subcriber

Các thức thứ hai là thay vì Observer phải thay đổi thì bản thân Observer sẽ truyền một message vào theo cùng với hàm notify của mình như sau:

interface Observerable {
    // Message có thể không chỉ giới hạn bởi String mà có thể là những cấu trúc dữ liệu phức tạp hơn nếu muốn
    void notify(String message);
}
class DirectorSubcriber implement Observerable {
    private final String KEY = DirectorSubcriber.getClass().getName();

// ...

    @Override
    public void notify(final String message) {a
        if (KEY.equals(message)) {
            // Làm điều khiến bạn vui
        }
    }
}
class Observer {
// ...

    public void notifyAll(final String message) {
        for (Observerable subcriber: mObserverableList) {
            subcriber.notify(message);
        }
    }
}

Nhược điểm của cách này đó là tất cả các Subcriber đã được đăng kí đều sẽ nhận được thông báo. Cũng là một dạng được mất nhỉ. Việc lựa chọn xem cách nào phù hợp lại sẽ phụ thuộc vào bài toán mà bạn định giải.

Update soon.

Kết bài

Để trả lời câu hỏi ở đầu bài. Liệu phải mất bao lâu để tôi có được cuộc điện thoại đầu tiên? Ngoại trừ việc cô gái ấy đẹp thật thì thực ra câu chuyện này hoàn toàn là bốc phét để làm ví dụ cho bài viết. Thread của bản thân tôi chắc chắn là sẽ rơi vào một vòng lặp hữu hạn như bên dưới chứ chẳng có nổi một callback nào đâu.

public void run() {
    while(!mWorld.thatsGirl.sheGone()) {
        mBody.getHead().updateFaceColor("#BA6C49");
        sleep(5 * 1000);
    }
}
Bình luận


White
{{ comment.user.name }}
Bỏ hay Hay
{{comment.like_count}}
Male avatar
{{ comment_error }}
Hủy
   

Hiển thị thử

Chỉnh sửa

White

Trần Đức Tâm

6 bài viết.
133 người follow
Kipalog
{{userFollowed ? 'Following' : 'Follow'}}
Cùng một tác giả
White
56 9
Phần I.I Machine Learning là gì? TL;DR Mình nghiên cứu Machine Learning(Link) bắt đầu từ khoảng một năm về trước, mất khoảng sáu tháng để tự luyệ...
Trần Đức Tâm viết 2 năm trước
56 9
White
40 9
$ Flutter và Flutter Architecture Components Chapter 1] Ngụp lặn với Flutter (Dive to Flutter) Sau sáu tháng cày cuốc tiếng Nhật, năm mới, thử th...
Trần Đức Tâm viết 6 tháng trước
40 9
White
37 13
Phần I.2 Tensorflow và bài toán Hồi quy đơn giản đầu tiên TL;DR Qua bài viết trước, chúng ta đã biết được đến sự tồn tại của một vài khái niệm cơ ...
Trần Đức Tâm viết 2 năm trước
37 13
Bài viết liên quan
Male avatar
10 5
Facade Design Patern Facade Patern thuộc vào họ mô hình cấu trúc (structural patern). Facade patern phát biểu rằng : "just provide a unified an...
DuongVanTien viết gần 3 năm trước
10 5
White
5 3
Observer pattern (python example) 1. Observer là gì : Theo như (Link) Observer Pattern là : A software design pattern in which an object, calle...
Khôi Trọng Nguyễn viết gần 3 năm trước
5 3
{{like_count}}

kipalog

{{ comment_count }}

bình luận

{{liked ? "Đã kipalog" : "Kipalog"}}


White
{{userFollowed ? 'Following' : 'Follow'}}
6 bài viết.
133 người follow

 Đầu mục bài viết

Vẫn còn nữa! x

Kipalog vẫn còn rất nhiều bài viết hay và chủ đề thú vị chờ bạn khám phá!