Skip to content

Design pattern – Observer pattern

Design pattern – Observer pattern

1. Giới thiệu về Observer Design Pattern

Observer là một behavior design pattern dùng để định nghĩa một cơ chế đăng ký (Subcribe) nhằm thông báo (notify) cho nhiều đối tượng về các sự kiện xảy ra với đối tượng mà chúng đang quan sát (Observer).

Hãy tưởng tượng bạn có 2 loại đối tượng trạm khí tượngkhách hàng. Khách hàng của trạm khí tượng có thể là đủ mọi thành phần từ công nhân, học sinh,…​. Khách hàng muốn biết tình hình thời tiết hôm nay phải lên trang chủ của trạm khí tượng kiểm tra. Tuy nhiên khi có tình huống thời tiết bất thường xảy ra những lần lên trang chủ xem như thế trở nên vô ích vì mọi thử xảy ra quá nhanh.

observer2

Mặt khác trạm khí tượng có thể gửi hàng tấn email cho mọi người để thông báo về tình thời tiết hôm nay. Điều này sẽ giúp ích cho khách hàng khi đỡ phải tốn công lên trang chủ nhưng cũng đồng thời tạo ra quá nhiều tin rác cho những ai không quan tâm đến nó và gây ra sự khó chịu vì bị spam quá nhiều.

Có vẻ bài toàn bắt đầu được thấy rõ ở đây, một là các khách hàng lãng phí thời gian để lên trang chủ kiểm tra thời tiết hôm nay. Thứ hai là trạm khí tướng phải tốn rất nhiều tài nguyên cho việc thông báo.

2. Sự ra đời của mẫu thiết kế Observer design pattern

Đối tượng mà có một số trạng thái được đối tượng khác quan tâm thường được gọi là Pulisher. Tất cả các đối tượng muốn theo dõi trạng thái của Publisher được gọi là Subscribers.

observer3

Observer Design Pattern có 3 cơ chế được cho là basic nhất đó là: subscribe, removeupdate.

Cơ chế Subcribe được đối tượng sử dụng để đăng ký vào luồng của Publisher để nhận sự thay đổi (update). Trong khi đó, cơ chế remove thì ngược lại nó sẽ hủy đăng ký của phần đối tượng đó khỏi luồng của Publisher. Và khi có sự thay đổi trong đối tượng Publisher, nó chỉ cần update trạng thái thì lập tức toàn bộ các Subscribers sẽ nhận được thông báo.

Observer4
observer5
Observer6

Tuy nhiên, câu hỏi đặt ra là các Subcribers nhận được thông báo bằng cách nào và cơ chế nào để Publisher có thể làm được việc thông báo đó. Hãy cùng xem cơ chế dưới đây (trích từ sách Head of Design Pattern)

Observer8

Các đối tượng Subcriber sẽ được định nghĩa chung method update thông qua một Interface Observer. Với Publisher nó sẽ cung cấp 1 interface chứa các method về đăng ký, hủy đăng ký và thông báo. Với method đăng ký, nó sẽ tiến hành thêm đối tượng Observer vào 1 mảng, mảng này là danh sách Subcribers, khi Publisher có thay đổi và thực hiện thông báo, nó sẽ lần lượt lấy ra các Subcribers và thực hiện method của chúng, các method này sẽ tác động lên đối tượng thực thi và update trạng thái hiện tại của chúng.

3.Xây dựng mô hình Observer Patern

Dựa trên mẫu thiết kế đơn giản trên hãy cùng xây dựng một ví dụ nhỏ
Tạo ra một lớp Interface cho Publisher ở đây là IWeatherForecast chứa các method để đăng ký (register), hủy đăng ký (remove) và method thông báo update (updateWeather).

public interface IWeatherForecast {
    void register(Listener listener);
    void remove(Listener listener);
    void updateWeather(Integer humidity, Integer temperature);
}

Tiếp đến xây dựng Interface Listener chứa một method xài chung là update của tất cả các Subcriber.

public interface Listener {
    void update(Integer humidity, Integer temperature);
}

Xây dựng đối tượng WeatherForecast để thực thi các phương thức trên. Theo logic, một khi method updateWeather được gọi nó sẽ phải thông báo tới các Subcribers trong nó, để làm được việc đó ta cần xây dựng 1 hàm private có tên Notification để list ra lần lượt các subcriber đã đăng ký và thực thi method của chúng.

import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;

@Slf4j
public class WeatherForecast implements IWeatherForecast{

    ArrayList<Listener> listeners = new ArrayList<>();
    private Integer humidity, temperature;

    @Override
    public void register(Listener listener) {
        listeners.add(listener);
    }

    @Override
    public void remove(Listener listener) {
        listeners.remove(listener);
    }

    @Override
    public void updateWeather(Integer humidity, Integer temperature) {
        this.humidity = humidity;
        this.temperature = temperature;
        log.info("=============== SENDING ===================");
        log.info("Weather forecast send: Temperature = {}, Humidity = {}\n", temperature, humidity);
        notification();
    }

    private void notification() {
        listeners.forEach(listener -> {
            listener.update(this.humidity, this.temperature);
        });
    }
}

Tiếp đến sẽ tạo ra các đối tượng cụ thể, các đối tượng này sẽ nhận trực tiếp thông báo từ Publisher gửi đến. Ta tạo 1 hàm private confirm để in ra thông báo này.

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class Student implements Listener{
    private Integer humidity;
    private Integer temperature;

    @Override
    public void update(Integer humidity, Integer temperature) {
        this.humidity = humidity;
        this.temperature = temperature;
        confirm();
    }

    public void confirm() {
        log.info("Student receive: Temperature = {}, Humidity = {}", temperature, humidity);
    }
}
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class Farmer implements Listener{
    private Integer humidity;
    private Integer temperature;

    @Override
    public void update(Integer humidity, Integer temperature) {
        this.humidity = humidity;
        this.temperature = temperature;
        confirm();
    }

    public void confirm() {
        log.info("Farmer receive: Temperature = {}, Humidity = {}", temperature, humidity);
    }
}
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class Worker implements Listener{

    private Integer humidity;
    private Integer temperature;

    @Override
    public void update(Integer humidity, Integer temperature) {
        this.humidity = humidity;
        this.temperature = temperature;
        confirm();
    }

    public void confirm() {
        log.info("Worker receive: Temperature = {}, Humidity = {}", temperature, humidity);
    }
}

Việc khởi tạo PublisherSubcriber đã xong, vậy làm cách nào để mô hình này thực sự hoạt dộng ?
Để làm được việc đó ta cần tạo 1 class Test và thực thi việc đăng ký và update để xem kết quả nhé.

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;

import java.security.SecureRandom;

@Component
public class Test {
    SecureRandom secureRandom = new SecureRandom();

    @Autowired
    IWeatherForecast iWeatherForecast;

    @Bean
    public void init(){
        iWeatherForecast.register(new Worker());
        iWeatherForecast.register(new Farmer());
        iWeatherForecast.register(new Student());
        new Thread(() -> {
            while (true) {
                try {
                    iWeatherForecast.updateWeather(secureRandom.nextInt(30), secureRandom.nextInt(50));
                    Thread.sleep(100);
                } catch (Exception e) {
                    Thread.currentThread().interrupt();
                }
            }
        }).start();
    }
}

Và kết quả sẽ là:

Observer9

Ví dụ trên chắc là một ví dụ huyền thoại của mẫu thiết kế này và ví dụ nào cũng na ná nhau như v thật nhàm chán. Hãy cùng thêm chút mắm muối và mẫu thiết kế này bằng cách sử dụng Generics và make it complicated hơn một chút 😀

Hãy suy nghĩ về lượng data mà một Subcriber nhận được, giả sử Publisher là nơi tổng hợp data cùng cực kỳ nhiều topic nhưng với mỗi Subcriber họ chỉ muốn nhận đủ lượng data họ cần và không muốn nhận dư các trường dữ liệu không cân thiết. Đồng thời để hạn chế spam thì Publisher một lần gửi sẽ giới hạn lượng Subcriber nhận được chẳng hạn. Xa hơn nữa, khi nhận được dữ liệu, mỗi Subcriber sẽ có cách sử lý dữ liệu khác nhau, nôm na như nhận được điểm thì Student sẽ đi khoe với má, nhận được tin hôm nay xưởng cháy thì công nhân nghỉ làm chẳng hạn,…​.. Hãy cùng xem mẫu thiết kế bên dưới.

Observer10

Có thể dễ dàng nhận ra FarmerStudent đều subcribe vào DataCenter để nhận thông tin nhưng kiểu Dto nó dùng để xử lý lại khác nhau.

Trước tiên tiến hành tạo các Interface xài chung

import java.util.List;

public interface IDataCenter {
    void register(Listener listener);
    void remove(Listener listener);
    void update(Message message);
    List<String> getListSubscriber();
}
public interface Display<T> {
    void broadcast(T data);
}
public interface Listener {
    void update(Message data);
    String getNameListener();
}

Khởi tạo các Dto

DataDto là các trường dữ liệu DataCenter sẽ được nhận, trường này có thể thêm dữ liệu tùy thích vì data là vô tận mà

import lombok.Data;

@Data
public class DataDto {
    private Integer humidity;
    private Integer temperature;
    private Integer money;
    private Integer candy;
    private Integer score;

    public DataDto(Integer humidity, Integer temperature, Integer money, Integer candy, Integer score) {
        this.humidity = humidity;
        this.temperature = temperature;
        this.money = money;
        this.candy = candy;
        this.score = score;
    }
}

FarmerData và StudentData quy định các trường dữ liệu mà mỗi đối tượng muốn nhận được, ở đây là đối tượng Student và Farmer.

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;

@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class FarmerData {
    private Integer temperature;
    private Integer humidity;
}
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;

@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class StudentData {
    private Integer score;
    private String money;
    private String candy;
}

Message quy định format data gửi đi của DataCenter bao gồm dữ liệu kiểu Json String convert từ dữ liệu nhận được là DataDto và danh sách các receiver nhận được trong 1 lần gửi.

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import lombok.Data;

import java.util.List;

@Data
public class Message {
    static Gson gson = new GsonBuilder().create();

    private String data;
    private List<String> receiver;

    public Message(String data, List<String> receiver) {
        this.data = data;
        this.receiver = receiver;
    }

    public static Message buildMessage(DataDto dataDto, List<String> receiver) {
        return new Message(gson.toJson(dataDto), receiver);
    }
}

Tiếp theo tiến hành xây dựng lớp DataCenter implement Interface IDataCenter để thực thi các method định sẵn

import lombok.extern.slf4j.Slf4j;

import java.util.*;

@Slf4j
public class DataCenter implements IDataCenter{

    private final Map<String, Listener> listenerMap = new HashMap<>();
    private Message message;

    @Override
    public void register(Listener listener) {
        listenerMap.put(listener.getNameListener(), listener);
    }

    @Override
    public void remove(Listener listener) {
        listenerMap.put(listener.getNameListener(), listener);
    }

    @Override
    public void update(Message message) {
        this.message = message;
        notification();
    }

    @Override
    public List<String> getListSubscriber() {
        List<String> result = new ArrayList<>();
        for (Map.Entry<String, Listener> m : listenerMap.entrySet()) {
            result.add(m.getKey());
        }
        return result;
    }

    private void notification() {
        log.info("----------Data send----------");
        log.info("List receivers {}", message.getReceiver());

        for (String s : message.getReceiver()) {
            if (getListSubscriber().contains(s)) {
                listenerMap.get(s).update(message);
            }
            else {
                log.error("{} is not subscriber", s);
            }
        }
        log.info("---------------------------\n");
    }
}

Về cách thức hoạt động của lớp này về cơ bản khá giống với bài cơ bản trên, chỉ là thêm phần lọc receiver để gửi đi thay vì gửi cho toàn bộ Subcriber như ví dụ trước.

Tiếp đến xây dựng lớp BaseListener. Sử dụng Generics type kiểu dữ liệu bởi vì Farmer và Student đều quy về tính chất của 1 Listener trong khi kiểu dữ liệu của nó lại mang tính chất tùy biến tức là mỗi loại sẽ có 1 kiểu khác nhau.

public abstract class BaseListener<T> implements Listener {
    private final Display<T> display;

    public BaseListener(Display<T> display) {
        this.display = display;
    }

    protected void broadcast(T data) {
        display.broadcast(data);
    }
}

Các Listener này cũng đều có chung một hành động đó là broadcast với kiểu dữ liệu của chính nó vậy nên sử dụng một constructor với tham số là một Interface có kiểu dữ liệu của Listener tương ứng.

Xây dựng các lớp con kế thừa từ BaseListener là Student và Farmer, sử dụng Singleton Design Patern để chỉ tạo ra 1 đối tượng từ nó.

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

public class Student extends BaseListener<StudentData>{

    private static Student instance;

    Gson gson = new GsonBuilder().create();

    public Student(Display<StudentData> display) {
        super(display);
    }

    @Override
    public void update(Message data) {
        broadcast(gson.fromJson(data.getData(), StudentData.class));
    }

    @Override
    public String getNameListener() {
        return this.getClass().getSimpleName();
    }

    public static Student getInstance(Display<StudentData> display) {
        if (instance == null) {
            return new Student(display);
        }
        return null;
    }
}
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

public class Farmer extends BaseListener<FarmerData> {

    Gson gson = new GsonBuilder().create();
    private static Farmer instance;

    public Farmer(Display<FarmerData> display) {
        super(display);
    }

    @Override
    public void update(Message data) {
        broadcast(gson.fromJson(data.getData(), FarmerData.class));
    }

    @Override
    public String getNameListener() {
        return this.getClass().getSimpleName();
    }

    public static Farmer getInstance(Display<FarmerData> display) {
        if (instance == null) {
            return new Farmer(display);
        }
        return null;
    }
}

Cuối cùng để thực thi ví dụ này ta xây dựng lớp Test để tạo ra một luồng truyền nhận data như sau

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.hoang.myproject.observerpattern2.dto.DataDto;
import com.hoang.myproject.observerpattern2.dto.Message;
import com.hoang.myproject.observerpattern2.observer.Farmer;
import com.hoang.myproject.observerpattern2.observer.Student;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;

import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

@Component
@Slf4j
public class Testing {

    Gson gson = new GsonBuilder().create();
    SecureRandom secureRandom = new SecureRandom();
    List<String> receivers = Arrays.asList(Farmer.class.getSimpleName(), Student.class.getSimpleName(), "Dog", "Cat");

    @Bean
    public void initTest() {
        new Thread(() -> {

            Student student = Student.getInstance(data -> {
                log.info("This is Student ");
                log.info("Student receive : {} \n", data);
            });

            Farmer farmer = Farmer.getInstance(data -> {
                log.info("This is Farmer ");
                log.info("Farmer receive : {} \n", data);
            });

            while (true) {
                try {
                    DataCenter dataCenter = new DataCenter();
                    dataCenter.register(student);
                    dataCenter.register(farmer);

                    // Build message
                    DataDto dataDto = new DataDto(secureRandom.nextInt(), secureRandom.nextInt(), secureRandom.nextInt(),
                            secureRandom.nextInt(), secureRandom.nextInt());
                    Collections.shuffle(receivers);

                    Message message = Message.buildMessage(dataDto, List.of(receivers.get(0), receivers.get(1)));
                    dataCenter.update(message);

                    Thread.sleep(1000);
                } catch (Exception e) {
                    Thread.currentThread().interrupt();
                }
            }
        }).start();
    }
}

Và đây là kết quả:

Observer11

Hy vọng kiến thức mình tìm hiểu được sẽ giúp ích cho những ai đọc bài viết này. Peace out !!!!!

Để lại một bình luận

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *