Trong bài viết này, Stringee và các bạn sẽ cùng tìm hiểu về event-driven programming: lợi ích và các hạn chế, cách thức hoạt động và các pattern mà nó có thể mang lại. Chúng ta cũng sẽ tìm hiểu một số ví dụ để hiểu rõ hơn về cách lập trình này.

Khi nói về event-driven programming, ta có thể bắt gặp rất nhiều cụm từ mô tả chung về chủ đề này. Trong bài viết này, chúng ta sẽ đơn giản hóa việc đó bằng cách sử dụng các cụm từ được bôi đậm dưới đây:

  • Event, message, and notification
  • Producer, publisher, sender, event source
  • Consumer, receiver, subscriber, handler, event sink
  • Message queue và event queue

1. Event-driven programming là gì ?

Event-driven programming hay lập trình hướng sự kiện là một trường phái lập trình ở đó các thực thể giao tiếp một cách gián tiếp với nhau bằng cách gửi các tin nhắn cho nhau qua một phần mềm trung gian. Các tin nhắn được lưu lại trong một hàng đợi trước khi được xử lý bởi các consumer.

Không giống như sử dụng các phương thức gọi trực tiếp, lập trình hướng sự kiện hoàn toàn tách biệt producer và consumer, điều này mang lại cho chúng ta một số lợi ích rõ rệt. Có thể thấy, nhiều producer cũng như nhiều consumer có thể cùng hợp tác để xử lý các yêu cầu đang được nhận vào. Event-driven programming cũng cho phép chúng ta mở rộng các hệ thống lớn một cách dễ dàng bằng việc tăng số lượng consumer.

>>> Xem thêm bài viết:

- Khai báo và sử dụng mảng các đối tượng trong Java

- Phân biệt ArrayList, Set và Vector trong Java

- Lập trình đa luồng là gì? Hướng dẫn lập trình đa luồng bằng ngôn ngữ Java

2. Các actor cấu thành event-driven programming

Chúng ta hãy cùng tìm hiểu về các actor của một hệ thống event-driven programming: events, producers, consumers và message queues.

2.1. Event

Event(sự kiện) là một mẩu dữ liệu được gửi bởi producer và cuối cùng được xử lý bởi consumer. Ví dụ:

  • Sự kiện click chuột
  • Phím chuột nào được nhấn

Event thường được cấu trúc dưới dạng cụ thể nhưng đôi khi có thể được gửi dưới định dạng một blog miễn là consumer đã biết cách giải mã và xử lý các sự kiện này.

2.2. Producer và consumer

Producer là các thực thể tạo ra các event và gửi nó tới các message queue. Consumer có thể đăng ký để nhận các sự kiện mới hoặc định kì đọc các sự kiện mới được thêm vào trong queue. Điểm mấu chốt của lập trình hướng sự kiện đó là: producer và consumer không biết sự tồn tại của nhau mà chỉ tương tác với nhau qua message queue.

2.3. Message queue

Message queue là nơi lưu trữ các message. Các tin nhắn có thể được lưu trong RAM hoặc các bộ nhớ dài hạn. Các hàng đợi này thường được phân vùng thành nhiều topic nhỏ hơn. Producer và consumer gửi và nhận các message cho các topic cụ thể riêng biệt.

Một message broker chịu trách nghiệm đảm bảo rằng một tin nhắn được gửi đến hàng đợi thành công sẽ được gửi đến cho các consumer. Một vài message broker được sử dụng phổ biến hiện nay gồm có RabbitMQ, Redis và KafKa.

Consumer sử dụng phương phức pull hoặc push để consume message từ queue. Trong chế độ pull, hàng đợi sẽ giữ toàn bộ tin nhắn của các consumer. Khi một tin nhắn được nhận bởi toàn bộ các consumer, nó sẽ được xóa khỏi queue(tùy thuộc vào cấu hình của queue). Một vài message queue cho phép consumer có thể lấy các tin nhắn cũ. Đối với chế độ push, message queue sẽ push bất kỳ tin nhắn nào mới cho toàn bộ các subscriber của nó.

Xem thêm bài viết:

- Hướng dẫn cài đặt Web server Apache trên CentOS 7

- Cài đặt cấu hình cân bằng tải với HaProxy và Docker

- Tìm hiểu về ràng buộc (Constraint) trong SQL

3. Điều gì khiến event-driven programming trở nên đặc biệt?

Tại đây, chúng ta sẽ tìm hiểu một số đặc điểm khiến lập trình hướng sự kiện trở nên thú vị và đáng để chúng ta cân nhắc sử dụng.

3.1. Tightly-coupled vs loosely-coupled workflows

Các cách gọi hàm truyền thống là tightly-coupled. Nếu đối tượng A có một vài thông tin X có liên quan đến đối tượng B, khi đó A cần phải thỏa mãn các điều kiện sau để có thể gửi thông tin tới B:

  • A phải biết là B có quan tâm tới X
  • Nó cần biết một tham chiếu của B
  • Cần phải biết về interface của B, hay phương thức để truyền thông tin X cho B
  • Phải call B để truyền X
  • Nếu không thực hiện được hoặc không thành công thì A phải biết làm gì tiếp theo Quả là quá nhiều đòi hỏi.

Well, vậy còn lập trình hướng sự kiện thì sao nhỉ? Đương nhiên là một hệ thống đảm bảo được loosely-coupled cũng sẽ có yêu cầu của riêng nó, cùng xem nhé:

  • A gửi X tới event queue

Và chỉ có vậy, giờ đây không chỉ có B mà có thể C và D cũng có thể nhận được thông tin X nếu chúng có quan tâm. Các framework event-driven thường có các quy chuẩn về việc retry và handle error và đương nhiên bạn hoàn toàn có thể làm các điều này từ phía consumer.

Điểm hạn chế của loosely-coupled là chúng ta khó có thể nhìn ra được các thực thể nào đang quan tâm tới message queue, khó để vẽ ra biểu đồ tuần tự.

3.2. Request-response vs publish-subscribe

Request-response là cách giao tiếp cơ bản của các nền tảng web truyền thống. Ví dụ như khi bạn nhập một địa chỉ website lên thanh công cụ của trình duyệt và server trả về thông tin trang web đó.

Publish-subscribe(PubSub) xử lý các vấn đề khác với cách tiếp cận trên: fire and forget. Publisher đẩy một event tới queue mà không cần nhận về một sự xác nhận hay phản hồi nào.

3.3. Synchronous vs asynchronous calls

Khi bạn thực hiện gọi một function với một vài biến và nhận lại kết quả trong cùng một thread, đó chính là một synchronous call. Thread sẽ bị block cho đến khi phương thức đó trả về kết quả.

Đối với asynchronous call thì khác, vì nó là bất đồng bộ nên kết quả sẽ được trả về ngay lập tức và luồng xử lý sẽ không chờ quá trình tính toán hoàn tất. Kết quả sẽ đến trong một luồng khác vào một thời gian sau đó. Event-base system phụ thuộc vào các hệ thống PubSub thường sử dụng cách tiếp cận này để nhận các event. Các consumer subscribe tới các topics cụ thể hoặc các event và các event sẽ được xử lý trong một luồng riêng biệt.

3.4. Retry và replay

Các event framework có thể cho phép ta cấu hình thực hiện gửi lại các event tới consumer, điều này giúp chúng ta có thể thực hiện lại các hành động dễ dàng hơn. Tuy nhiên, nếu một consumer nhận một event nhưng không thể xử lý được nó, sẽ không có một các trực tiếp nào để có thể báo cáo lại sự cố này cho producer. Trong cấu trúc lập trình hướng sự kiện, điều này không phải là một vấn đề vì production vốn dĩ không hề biết sự tồn tại của consumer và nó cũng sẽ không chịu trách nghiệm cho việc xử lý bất kỳ sự cố nào.

Các event framework quản lý hàng đợi có thể thử lại nhiều lần trong trường hợp thất bại là tin nhắn gửi đi bị gián đoạn. Nếu thất bại này được ghi nhận lại, event sẽ được lưu lại để xử lý sau và một error message sẽ được ghi nhận lại.

4. Event-driven programming hoạt động như thế nào?

Như chúng ta đã cùng nhau phân tích ở trên, event-driven programming hoạt động trên nguyên tắc trao đổi các tin nhắn giữa các thực thể với nhau một các không trực tiếp thông qua một message queue. Dưới đây sẽ là các thao tác cần thực hiện trong lập trình hướng sự kiện.

  • Tạo ra event: Producer sẽ cần tạo ra các event và gửi nó tới một message queue cụ thể nào đó.
  • Subscribe event: Các consumer sẽ subscribe vào các message queue. Tùy thuộc vào cấu hình của queue, một message có thể được gửi tới nhiều queue hoặc chỉ một queue. Điều này dẫn tới hệ quả là cùng một message nhưng cũng có thể sẽ có hai consumer nhận được.
  • Cấu hình lưu trữ(retention): Một vài queue sẽ ngay lập tức xóa bỏ tin nhắn một khi nó được chuyển tới các subscriber. Các queue khác có thể sẽ giữ các event ở lại trong nó. Với các queue này, điều chúng ta cần phải làm là thực hiện cấu hình thời gian các tin nhắn được giữ lại. Chúng ta có thể cấu hình thời gian tồn tại của tin nhắn, số tin nhắn trong hàng đợi hoặc là tổng số lượng message của toàn bộ các queue event.

5. Ví dụ về triển khai chương trình với event-driven programming

Bài toán chúng ta giải định ở đây là bạn đã triển khai cài đặt một lớp EmailListener có thể nhận biết được sự kiện email tới ứng dụng của mình. Mỗi khi có một email tới, bạn muốn thực hiện một vài công việc khác và không muốn sửa đi sửa lại logic của class này để thêm các nghiệp vụ khác. Đây là một trường hợp mà chúng ta có thể triển khai event-driven programming.

5.1. Tạo một consumer subscribe tới message queue

public class Consumer implements Handler {

    private final Channel channel = new Channel();



    void subscribe(String queueName) {

        channel.subcribe(queueName);

    }



    @Override

    public void onMessage(String message) {

        if (!message.startsWith("EMAIL_")) {

            return;

        }

        System.out.println(message);

    }

}

5.2. Publish message từ Email Listener

public class EmailListener implements IEmailListener{

    private final Channel channel = new Channel();

    @Override

    public void onEmailReceived(String email) {

        System.out.println("Got new email " + email);

        String codec = "EMAIL_";

        channel.publish(codec + email, "email_queue");

    }

}

Trên đây là một ví dụ cơ bản để minh họa cho những gì chúng ta đã tìm hiểu trong bài viết ngày hôm nay, các bạn có thể tìm hiểu các ví dụ trên thực tế với các message broker như Kafka, RabbitMQ nhé.

Kết

Event-driven programming là một mô hình cực kỳ hữu ích, với các trường hợp sử dụng từ giao diện người dùng trong hệ thống xử lý đơn lẻ đến giao tiếp giữa các dịch vụ trong hệ thống phân tán quy mô lớn. Bản chất kết hợp lỏng lẻo của Event-driven programming mang lại nhiều lợi ích, mặc dù nó có thể yêu cầu bạn thay đổi cách suy nghĩ về giao tiếp giữa các đối tượng.

Với xu hướng phát triển phần mềm chuyển từ các ứng dụng nguyên khối sang các hệ thống phân tán và các dịch vụ siêu nhỏ tách rời, một mô hình hướng sự kiện cho kiến trúc và lập trình chắc chắn sẽ tồn tại. Giải pháp hàng đợi sự kiện mà bạn chọn sử dụng cho dù đó là Kafka (hay bất kỳ message broker nào khác) là một giải pháp cần được cân nhắc sáng suốt. Với xu thế phát triển phần mềm phân tán, bạn hoàn toàn có thể đánh giá sự tăng trưởng và mở rộng của dịch vụ mà mình đang cung cấp để thực hiện chuyển đổi sang lập trình với Event-driven programming.


Stringee Communication APIs là giải pháp cung cấp các tính năng giao tiếp như gọi thoại, gọi video, tin nhắn chat, SMS hay tổng đài CSKH cho phép tích hợp trực tiếp vào ứng dụng/website của doanh nghiệp nhanh chóng. Nhờ đó giúp tiết kiệm đến 80% thời gian và chi phí cho doanh nghiệp bởi thông thường nếu tự phát triển các tính năng này có thể mất từ 1 - 3 năm.

Bộ API giao tiếp của Stringee hiện đang được tin dùng bởi các doanh nghiệp ở mọi quy mô, lĩnh vực ngành nghề như TPBank, VOVBacsi24, VNDirect, Shinhan Finance, Ahamove, Logivan, Homedy,  Adavigo, bTaskee…

Quý bạn đọc quan tâm xin mời đăng ký NHẬN TƯ VẤN TẠI ĐÂY: