Chuỗi phản ứng linh hoạt!

Có bao giờ bạn tự hỏi chỉ đúng bằng một câu lệnh logger.error mà lại có thể vừa hiện trên màn hình console, vừa ghi được vào file lại vừa bắn ầm ầm trên slack? đó chính là một trong những ứng dụng quan trọng của chain of responsibility design pattern.

Vấn đề trong thực tế

Để làm 3 việc ghi ra console, ghi file và thông báo đến slack, có lẽ chúng ta sẽ viết lớp logger thế này:

public class Logger {

    public void error(String message) {
        writeToConsole(message);
        writeToFile(message);
        sendToSlack(message);
    }

    private void writeToConsole(String message) {
        System.out.println(message);
    }

    private void writeToFile(String message) {
        fileWriter.write(message);
    }

    private void sendToSlack(String message) {
        slackClient.send(message);
    }   
}

Nhìn code cũng khá clean đúng không? Nhưng bây giờ lại phát sinh vấn đề. Công ty của chúng ta vừa xây dựng xong hệ thống lưu log tên là LogStore, và yêu cầu đặt ra là chúng ta phải thêm code để gửi log sang hệ thống LogStore vào lớp Logger kiểu này:

public void error(String message) {
    writeToConsole(message);
    writeToFile(message);
    sendToSlack(message);
    sendToLogStore(message);
}

private void sendToLogStore(String message) {
    logStoreClient.send(message);
}

Ồ không, chúng ta đang vi phạm quy tắc Open/Close (đóng với thay đổi, mở với mở rộng). Việc chúng ta đổi một lớp được sử dụng nhiều nơi trong chương trình như Logger sẽ yêu cầu chúng ta phải đưa ra bản đánh giá mức độ ảnh hưởng và test lại toàn bộ.

Mục tiêu ra đời

Rõ ràng giải pháp thay đổi lớp Logger như trên là không ổn, nên chúng ta cần một design pattern với mục tiêu:

  1. Cho phép xử lý một hành động hay yêu cầu qua nhiều bước, từ đó có thể chia nhỏ thành nhiều phần xử lý độc lập
  2. Tránh việc thay đổi lớp xử lý đã chạy ổn định để tránh rủi ro
  3. Dễ dàng thêm mới hoặc bỏ đi một phần xử lý trong chuỗi xử lý

Đó chính là mục tiêu ra đời của Chain of Responsibility Design Pattern

Giải quyết vấn đề

Bây giờ hãy qua trở lại với bài toán log, chúng ta sẽ cần thiết kế lớp một chút cho dễ hiểu

  1. Lớp Logger của chúng ta bây giờ sẽ không chứa các hàm xử lý nghiệp vụ nữa mà sẽ chỉ chứa một danh sách các lớp Appender
  2. Các lớp LoggerAppender sẽ chịu trách nhiệm xử lý nghiệp vụ tương ứng với chức năng của mình

Còn bây giờ là source code:

public interface LoggerAppender {
    void append(String message);
}

public class ConsoleAppender implements LoggerAppender {
    @Override
    public void append(String message) {
        System.out.println("Console: " + message);
    }
}

public class FileAppender implements LoggerAppender {
    @Override
    public void append(String message) {
        System.out.println("File: " + message);
    }
}

public class SlackAppender implements LoggerAppender {
    @Override
    public void append(String message) {
        System.out.println("Slack: " + message);
    }
}

public class LogStoreAppender implements LoggerAppender {
    @Override
    public void append(String message) {
        System.out.println("LogStore: " + message);
    }
}

public class Logger {

    private final List<LoggerAppender> appenders;

    public Logger(List<LoggerAppender> appenders) {
        this.appenders = appenders;
    }

    public void info(String message) {
        for(LoggerAppender appender : appenders) {
            appender.append(message);
        }
    }
}

public final class LoggerFactory {

    private final static Map<Object, Logger> loggers = new HashMap<>();
    private final static List<LoggerAppender> appenders = new ArrayList<>();
    static {
        appenders.add(new ConsoleAppender());
        appenders.add(new FileAppender());
        appenders.add(new SlackAppender());
        appenders.add(new LogStoreAppender());
    }

    public static Logger getLogger(Object name) {
        return loggers.computeIfAbsent(name, k -> new Logger(appenders));
    }
}

Và khi sử dụng sẽ đơn giản là thế này:

Logger logger = LoggerFactory.getLogger(LoggerDemo.class);
logger.info("Hello World");

Khi cần phải thêm một nghiệp vụ xử lý log mới, chúng ta sẽ chỉ đơn giản là tạo thêm một lớp Appender và đăng ký vào lớp LoggerFactory, như hiện nay thì logback hay log4j12 cho phép chúng ta đăng ký qua file xml hoặc file .properties. Lớp Logger của chúng ta sẽ được giữ nguyên mà không phải thay đổi gì cả. Việc thêm mới hoặc bỏ đi một Appender là cực kì dễ dàng

Tham khảo

  1. Lý thuyết
  2. Ví dụ