Hướng dẫn Java Design Pattern – Mediator

Để có một thiết kế hướng đối tượng tốt, chúng ta phải tạo ra nhiều class tương tác với nhau. Nếu một số nguyên tắc nhất định không được áp dụng, cuối cùng hệ thống trở thành một mớ hỗn độn trong đó mỗi đối tượng phụ thuộc (dependency) vào nhiều đối tượng khác để thực thi. Để tránh các class kết hợp chặt chẽ, chúng ta cần một cơ chế để tạo thuận lợi cho sự tương tác giữa các đối tượng theo cách mà các đối tượng không nhận thức được sự tồn tại của các đối tượng khác. Một trong những cách để giải quyết vấn đề này là áp dụng Mediator Pattern.

1. Mediator Pattern là gì?

Define an object that encapsulates how a set of objects interact. Mediator promotes loose coupling by keeping objects from referring to each other explicitly, and it lets you vary their interaction independently.

Mediator Pattern là một trong những Pattern thuộc nhóm hành vi (Behavior Pattern). Mediator có nghĩa là người trung gian. Pattern này nói rằng “Định nghĩa một đối tượng gói gọn cách một tập hợp các đối tượng tương tác. Mediator thúc đẩy sự khớp nối lỏng lẻo (loose coupling) bằng cách ngăn không cho các đối tượng đề cập đến nhau một cách rõ ràng và nó cho phép bạn thay đổi sự tương tác của họ một cách độc lập”.

Mediator Patern (mô hình trung gian) được sử dụng để giảm sự phức tạp trong “giao tiếp” giữa các lớp và các đối tượng. Mô hình này cung cấp một lớp trung gian có nhiệm vụ xử lý thông tin liên lạc giữa các tầng lớp, hỗ trợ bảo trì mã code dễ dàng bằng cách khớp nối lỏng lẻo.

Khớp nối lỏng lẻo ở đây được hiểu là các đối tượng tương đồng không “giao tiếp” trực tiếp với nhau mà giao tiếp thông qua người trung gian, và nó cho phép thay thay đổi cách tương tác giữa chúng một cách độc lập.

Mediator Patern thúc đẩy mối quan hệ nhiều – nhiều (many-to-many) giữa các đối tượng tượng với nhau để đạt đến được kết quả mong muốn.

Hình bên trên là một ví dụ về mẫu thiết kế Mediator. Theo hình ta đang có một website với 4 trang. Website cho phép khách hàng duyệt qua kho hàng và đặt mua. Khách hàng có thể đi  từ trang này qua trang khác theo đường vẽ trên hình. Ở đây có một vấn đề phát sinh. Tại từng trang, bạn phải viết mã để nhận biết khi nào khách hàng muốn nhảy qua trang khác và kích hoạt trang đó. Tại một trang bạn có quá nhiều đường để đi tới trang khác, và vì vậy sẽ phát sinh nhiều đoạn code trùng lặp trên nhiều trang khác nhau.

Chúng ta có thể sử dụng mẫu Mediator để đóng gói tất cả các đường dẫn tới trang vào một module duy nhất, và đặt nó vào trong một đối tượng Mediator. Từ bây giờ, từng trang chỉ cần phải thông báo bất cứ sự thay đổi nào cho Mediator, và Mediator tự biết điều hướng trang cần thiết cho khách hàng, như trong hình bên dưới.

Chúng ta có thể tạo ra một Mediator với chức năng điều hướng trang. Tại đây chúng ta có thể chỉnh sửa và thay đổi dễ dàng. Đó chính là chức năng của Mediator (Người trung gian).

2. Cài đặt Mediator Pattern như thế nào?

Các thành phần tham gia Mediator Pattern:

  • Colleague : là một abstract class, giữ tham chiếu đến Mediator object.
  • ConcreteColleague : cài đặt các phương thức của Colleague. Giao tiếp thông qua Mediator khi cần giao tiếp với Colleague khác.
  • Mediator : là một interface, định nghĩa các phương thức để giao tiếp với các Colleague object.
  • ConcreteMediator : cài đặt các phương thức của Mediator, biết và quản lý các Colleague object.

2.1. Ví dụ Mediator Pattern trong hệ thống chat

Trong một ứng dụng chat, một user sẽ có thể send và recieve message. Khi một user muốn send message đến group thì user đó phải tìm xem tất cả những người đang online hoặc trong trạng thái có thể message để send. Nếu bình thường user gửi tin phải tự làm hết mọi thứ, phải tự kiểm tra từng member và thực hiện việc gửi tin nhắn.

Khi sử dụng Mediator: user không cần tự kiểm tra, không quan tâm ai có thể nhận message, user chỉ việc gửi thông tin đến Mediator của group. Mediator sẽ tự điều phối message này đến người nhận.

Chương trình của chúng ta như sau:

User.java

package com.maixuanviet.patterns.behavioral.mediator.chat;
 
/**
 * Colleague
 */
public abstract class User {
    protected ChatMediator mediator;
    protected String name;
 
    public User(ChatMediator med, String name) {
        this.mediator = med;
        this.name = name;
    }
 
    public abstract void send(String msg);
 
    public abstract void receive(String msg);
 
    @Override
    public int hashCode() {
        return name.hashCode();
    }
 
    @Override
    public boolean equals(Object obj) {
        if (obj == null) {
            return false;
 
        }
 
        if (this.getClass() != obj.getClass()) {
            return false;
        }
 
        User user = (User) obj;
        return name.equals(user.name);
    }
}

UserImpl.java

package com.maixuanviet.patterns.behavioral.mediator.chat;
 
/**
 * ConcreteColleague
 */
public class UserImpl extends User {
 
    public UserImpl(ChatMediator mediator, String name) {
        super(mediator, name);
    }
 
    @Override
    public void send(String msg) {
        System.out.println("---");
        System.out.println(this.name + " is sending the message: " + msg);
        mediator.sendMessage(msg, this);
    }
 
    @Override
    public void receive(String msg) {
        System.out.println(this.name + " received the message: " + msg);
    }
}

ChatMediator.java

package com.maixuanviet.patterns.behavioral.mediator.chat;
 
public interface ChatMediator {
 
    void sendMessage(String msg, User user);
 
    void addUser(User user);
}

ChatMediatorImpl.java

package com.maixuanviet.patterns.behavioral.mediator.chat;
 
import java.util.ArrayList;
import java.util.List;
 
public class ChatMediatorImpl implements ChatMediator {
     
    public ChatMediatorImpl(String groupName) {
        System.out.println(groupName + " group already created");
    }
 
    private List<User> users = new ArrayList<>();
 
    @Override
    public void addUser(User user) {
        System.out.println(user.name + " joined this group");
        this.users.add(user);
    }
 
    @Override
    public void sendMessage(String msg, User user) {
        for (User u : this.users) {
            if (!u.equals(user)) {
                u.receive(msg);
            }
        }
    }
}

ChatClient.java

package com.maixuanviet.patterns.behavioral.mediator.chat;
 
public class ChatClient {
     
    public static void main(String[] args) {
        ChatMediator mediator = new ChatMediatorImpl("Java design pattern");
         
        User admin = new UserImpl(mediator, "GP Coder");
        User user1 = new UserImpl(mediator, "User 1");
        User user2 = new UserImpl(mediator, "User 2");
        User user3 = new UserImpl(mediator, "User 3");
         
        mediator.addUser(admin);
        mediator.addUser(user1);
        mediator.addUser(user2);
        mediator.addUser(user3);
 
        admin.send("Hi All");
        user1.send("Hi Admin");
    }
}

Output của chương trình:

Java design pattern group already created
VietMX joined this group
User 1 joined this group
User 2 joined this group
User 3 joined this group
---
VietMX is sending the message: Hi All
User 1 received the message: Hi All
User 2 received the message: Hi All
User 3 received the message: Hi All
---
User 1 is sending the message: Hi Admin
VietMX received the message: Hi Admin
User 2 received the message: Hi Admin
User 3 received the message: Hi Admin

2.2. Ví dụ Mediator Pattern trong ứng dụng điều khiển đèn giao thông

Đèn giao thông có 3 màu: đỏ, vàng, xanh. Khi một đèn được mở thì những đèn còn lại sẽ tắt.

Chương trình của chúng ta như sau:

  • Light: lớp chứa thông tin đèn giao thông, trạng thái (ON/OFF), giữ bộ thông tin bộ điều khiển tín hiệu đèn (Mediator).
  • LightMediator : bộ điều khiển đèn thông thông, nhận thông báo khi một tín hiệu đèn được mở, thực hiện tắt những đèn còn lại.
  • TrafficLightApp : khởi tạo Mediator, đăng ký các Light với Mediator. Thực hiện bật các đèn theo khoảng thời gian 3 giây.

Light.java

package com.maixuanviet.patterns.behavioral.mediator.trafficlight;
 
/**
 * Colleague
 */
public class Light {
 
    enum State {
        ON, OFF
    }
 
    private String color;
    private LightMediator lightMediator;
    private State currentState;
 
    public Light(String color, LightMediator lightMediator) {
        this.color = color;
        this.currentState = State.OFF;
        this.lightMediator = lightMediator;
        lightMediator.registerLight(this);
    }
 
    public void turnOn() {
        this.currentState = State.ON;
        lightMediator.notifyMediator(this);
    }
 
    public void turnOff() {
        this.currentState = State.OFF;
        lightMediator.notifyMediator(this);
    }
 
    public String getColor() {
        return color;
    }
 
    public State getCurrentState() {
        return currentState;
    }
 
    @Override
    public int hashCode() {
        return color.hashCode();
    }
 
    @Override
    public boolean equals(Object obj) {
        if (obj == null) {
            return false;
 
        }
 
        if (this.getClass() != obj.getClass()) {
            return false;
        }
         
        Light light = (Light) obj;
        return color.equals(light.color);
    }
}

LightMediator.java

package com.maixuanviet.patterns.behavioral.mediator.trafficlight;
 
import java.util.HashSet;
import java.util.Set;
 
/**
 * Mediator
 */
public class LightMediator {
    private Set<Light> trafficSignals = new HashSet<>();
 
    public void registerLight(Light light) {
        trafficSignals.add(light);
    }
 
    public void unRegisterLight(Light light) {
        trafficSignals.remove(light);
    }
 
    public void notifyMediator(Light light) {
        System.out.printf("%s is turned %s \n", light.getColor(), light.getCurrentState());
        if (light.getCurrentState() == Light.State.ON) {
            turnOffAllOtherLights(light);
        }
    }
 
    private void turnOffAllOtherLights(Light light) {
        for (Light otherLight : trafficSignals) {
            if (!(light.equals(otherLight))) {
                otherLight.turnOff();
            }
        }
        System.out.println("---");
    }
}

AutoRotateTrafficLightApp.java

package com.maixuanviet.patterns.behavioral.mediator.trafficlight;
 
import java.util.concurrent.TimeUnit;
 
public class AutoRotateTrafficLightApp {
     
    public static void main(String[] args) {
 
        LightMediator lightMediator = new LightMediator();
        Light[] lights = {
                new Light("Red", lightMediator),
                new Light("Green", lightMediator),
                new Light("Yellow", lightMediator)
        };
         
        int currentLightIndex = 0;
        Light light;
        while (true) {
            if (currentLightIndex >= lights.length) {
                currentLightIndex = 0;
            }
            light = lights[currentLightIndex];
            light.turnOn();
            timer();
            currentLightIndex++;
        }
    }
 
    private static void timer() {
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

Output của chương trình như sau:

Red is turned ON 
Yellow is turned OFF 
Green is turned OFF 
---
Green is turned ON 
Red is turned OFF 
Yellow is turned OFF 
---
Yellow is turned ON 
Red is turned OFF 
Green is turned OFF 
---
Red is turned ON 
Yellow is turned OFF 
Green is turned OFF 
---
....

3. Lợi ích của Mediator Pattern là gì?

  • Đảm bảo nguyên tắc Single responsibility principle (SRP) : chúng ta có thể tách phần giao tiếp giữa các thành phần (component) ra một nơi khác.
  • Đảm bảo nguyên tắc Open/Closed Principle (OCP) : chúng ta có thể implement thêm một Mediator mới mà không ảnh hưởng đến các component hiện có.
  • Giảm khớp nối giữa các component.
  • Tái sử dụng các component dễ dàng hơn.
  • Đơn giản hóa cách giao tiếp giữa các đối tượng. Một mediator sẽ thay thế mối quan hệ nhiều-nhiều (many-to-many) giữa các component bằng quan hệ một-nhiều (one-to-many) giữa một mediator với các component.
  • Quản lý tập trung, giúp làm rõ các component tương tác trong hệ thống như thế nào trong hệ thống.

4. Sử dụng Mediator Pattern khi nào?

  • Khi tập hợp các đối tượng giao tiếp theo những cách thức được xác định rõ ràng nhưng cách thức đó quá phức tạp. Sự phụ thuộc lẫn nhau giữa các đối tượng tạo ra kết quả là cách tổ chức không có cấu trúc và khó hiểu.
  • Khi cần tái sử dụng một đối tượng nhưng rất khó khăn vì nó tham chiếu và giao tiếp với nhiều đối tượng khác.
  • Điều chỉnh hành vi giữa các lớp một cách dễ dàng, không cần chỉnh sửa ở nhiều lớp.
  • Thường được sử dụng trong các hệ thống truyền thông điệp (message-based system), chẳng hạn như hệ thống chat.
  • Khi giao tiếp giữa các object trong hệ thống quá phức tạp, có quá nhiều quan hệ giữa các object trong hệ thống. Một điểm chung để kiểm soát hoặc giao tiếp là cần thiết.

5. So sánh Mediator Pattern với các Pattern khác

Các Pattern như Chain of Responsibility, Command, Mediator, Observer: giải quyết nhiều cách khác nhau để kết nối người gửi và người nhận yêu cầu.

  • Chain of Responsibility : nhận yêu cầu của người gửi, và gửi yêu cầu đó (request) dọc theo một chuỗi những người nhận tiềm năng (processor) cho đến khi một trong số chúng xử lý nó.
  • Command : thiết lập các kết nối đơn hướng giữa người nhận với người gửi với một phân lớp.
  • Mediator : loại bỏ các kết nối trực tiếp giữa người gửi và người nhận, buộc chúng phải liên lạc gián tiếp thông qua một đối tượng Mediator.
  • Observer : định nghĩa một interface tách biệt cho phép nhiều người nhận đăng ký và hủy đăng ký nhận yêu cầu tại thời điểm run-time.

Mediator tương tự như Facade, chúng cố gắng tổ chức sự hợp tác giữa nhiều class được liên kết chặt chẽ.

  • Facade : định nghĩa một interface đơn giản hóa cho một hệ thống con của các đối tượng, nhưng nó không giới thiệu bất kỳ chức năng mới nào. Bản thân hệ thống con không biết về Facade. Các đối tượng trong hệ thống con có thể giao tiếp trực tiếp với nhau.
  • Mediator : tập trung giao tiếp giữa các thành phần (component) của hệ thống. Các thành phần chỉ biết về đối tượng Mediator và không thể giao tiếp trực tiếp với nhau.

Sự khác biệt giữa Mediator và Observer là Observer phân phối giao tiếp bằng cách giới thiệu các đối tượng “quan sát viên (observer)” và “chủ thể (subject)”. Trong khi đó, một đối tượng Mediator đóng gói giao tiếp giữa các đối tượng khác. Thông thường, việc tạo ra các Observer và Subject có thể tái sử dụng dễ dàng hơn so với việc tạo các Mediator có thể tái sử dụng. Mặt khác, Mediator giải có thể tận dụng Observer để đăng ký động các thành phần và liên lạc với chúng.

Tuy nhiên, sự khác biệt giữa Mediator và Observer thường không rõ ràng. Trong hầu hết các trường hợp, chúng ta có thể thực hiện một trong hai mẫu này, nhưng đôi khi có thể áp dụng cả hai cùng một lúc.

  • Mục tiêu chính của Mediator là loại bỏ sự phụ thuộc lẫn nhau giữa một tập hợp các thành phần hệ thống. Thay vào đó, các thành phần này trở nên phụ thuộc vào một đối tượng Mediator. Mục tiêu của Observer là thiết lập các kết nối một chiều động giữa các đối tượng, trong đó một số đối tượng đóng vai trò là cấp dưới của những người khác.
  • Có một triển khai phổ biến của mẫu Mediator dựa trên Observer. Đối tượng Mediator đóng vai trò là nhà xuất bản (publisher) và các thành phần đóng vai trò là người đăng ký (subscriber) có thể đăng ký và hủy đăng ký khỏi các sự kiện của Mediator. Khi Mediator được triển khai theo cách này, nó có thể trông rất giống với Observer.