目录
题目
某软件公司欲开发一套CRM系统,其中包含一个客户信息管理模块,所设计的“客户信息管理窗口”界面效果图如图所示。组件之间的交互关系如下:
(1) 当用户单击“增加”按钮、“删除”按钮、“修改”按钮或“查询”按钮时,界面左侧的“客户选择组合框”、“客户列表”以及界面中的文本框将产生响应。
(2) 当用户通过“客户选择组合框”选中某个客户姓名时,“客户列表”和文本框将产生响应。
(3) 当用户通过“客户列表”选中某个客户姓名时,“客户选择组合框”和文本框将产生响应。
请使用中介模式实现该系统,并补充程序中相应Java代码。
最终效果图
理解中介者模式的核心思想
按照我的理解和思路来总结一下。
以即时聊天为例子,A用户给B用户发消息,并不是A用户和B用户之间建立了点对点的连接。假如两个聊天用户之间都建立一对一、点对点的连接,那么要建立的连接就非常多了,就成为了个复杂网状结构。
实际上,A用户和B用户都与一台服务器建立了连接,A用户发消息给B用户,消息会经由服务器中转来发送。这台服务器就充当了中介者的角色,它持有了所有的用户连接。由于它的存在使得原先的网状结构变成了一(服务器)对多(多个连接)的发散的星状结构。
思路分析
先抛开代码如何实现,抛开界面如何绘制。先来宏观角度来想想。
在上述UI界面中,我们不妨定义 "组件"(component)的概念来类比上述的 "用户"。根据题意,我们可以得出以下几个组件:
Component 我们可以定义为接口(为什么不是抽象类?因为组件还需要集成Swing的组件类,而Java只能单继承),里面有两个方法:
change():当前组件发生了改变,作为事件源
update() :当前组件作为监听者,监听到了事件,从而更新来做出响应
而中介者(Mediator),必定持有所有的 Component,而且持有所有的 User,因此中介者(Mediator)需要是全局唯一的,是单例的。当某个 Component 作为事件源调用了 change() 方法,中介者(Mediator)统一做通知分发:调用除了事件源以外的其他组件的 update() 方法。
代码
项目目录结构
Mediator(中介者)
- 饿汉单例模式
- 使用 List 存储所有的 Component(使用接口,利用多态)
- 作为 User 的数据源,用 Map 存储所有的 User,key 是 username
- componentChanged(Component component, User user) 是给作为事件源组件调用的来通知更新的,第1个参数是作为事件源的组件,第2个参数是更新的数据
还挺简洁的吧。。。
package demo;
import demo.component.Component;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 中介者
* @author passerbyYSQ
* @create 2021-06-03 0:01
*/
public class Mediator {
// 所有的组件
private List<Component> list = new ArrayList<>();
private Map<String, User> users = new HashMap<>();
// 中介者的单例
private static final Mediator INSTANCE;
static {
INSTANCE = new Mediator(); // 饿汉
}
private Mediator() {}
public static Mediator getInstance() {
return INSTANCE;
}
public void addComponent(Component component) {
list.add(component);
}
public void putUser(User user) {
users.put(user.getUsername(), user);
}
public void removeUser(String username) {
users.remove(username);
}
public String[] getUsernameList() {
return users.keySet().toArray(new String[0]);
}
public User getUser(String username) {
return users.getOrDefault(username, new User(username));
}
public void componentChanged(Component component, User user) {
for (Component com : list) {
if (!com.equals(component)) { // component是事件源
com.update(user);
}
}
}
}
Component(组件)
- Component 肯定需要持有 Meditor (不然怎么建立双向联系?)Meditor 是全局单例,可以在任何地方方便的获得,也可以在实现类中以构造方法参数的形式传入。但为了方便,直接在接口中定义也是允许的。那么在实现类中可以直接使用
- 同时利用 jdk 1.8 的新特性在接口中就为 change() 方法提供了默认实现。
- update() 方法是根据 User 数据,来对当前组件进行界面上的更新。各个组件的界面更新都不一样,所以需要留到子类来实现。
package demo.component;
import demo.Mediator;
import demo.User;
/**
* @author passerbyYSQ
* @create 2021-06-02 23:57
*/
public interface Component {
Mediator mediator = Mediator.getInstance();
void update(User user);
default void change(User user) {
mediator.componentChanged(this, user);
}
}
Component(组件)的实现类
1. SearchTextField(搜索文本框)
- 直接在空参构造里面就将当前组件对象注册到中介者(Mediator)中。这一个操作可以放到外面去做。但是组件本身已经持有了Mediator,为了方便我们可以放到构造方法里面。
- update():通过调用父类JTextField 来更新界面
其他组件依葫芦画瓢...
/**
* @author passerbyYSQ
* @create 2021-06-03 0:00
*/
public class SearchTextField extends JTextField implements Component {
public SearchTextField() {
mediator.addComponent(this);
}
@Override
public void update(User user) {
setText(user.getUsername());
}
}
2. UsernameCombBox(用户名下拉选择框)
- Mediator 持有了 User 数据源。而新增 User 之后,下拉选择框中必然多一条记录。因此在做 "选中" 操作之前,我们先拿到数据源,重置下拉选择框的数据。然后再去找与 User 对应的选项,将其选中
package demo.component;
import demo.User;
import javax.swing.*;
/**
* 下拉选择框
* @author passerbyYSQ
* @create 2021-06-03 0:15
*/
public class UsernameCombBox extends JComboBox<String> implements Component {
public UsernameCombBox() {
mediator.addComponent(this);
}
@Override
public void update(User user) {
// 数据源可能有更新,先重置数据源
setModel(new DefaultComboBoxModel<>(mediator.getUsernameList()));
boolean isFound = false;
ComboBoxModel<String> model = getModel();
for (int i = 0; i < model.getSize(); i++) {
String item = model.getElementAt(i);
if (item.equals(user.getUsername())) {
setSelectedItem(item);
//setSelectedIndex(i);
isFound = true;
}
}
if (!isFound) { // 没有任何匹配项,取消选中
setSelectedItem(null);
}
}
}
3. UsernameList(用户名列表)
这个跟 UsernameCombBox 很像,不多说了。直接贴代码
package demo.component;
import demo.User;
import javax.swing.*;
/**
* @author passerbyYSQ
* @create 2021-06-03 0:20
*/
public class UsernameList extends JList<String> implements Component {
public UsernameList() {
setAutoscrolls(true);
mediator.addComponent(this);
}
@Override
public void update(User user) {
// 可能有新增数据
setModel(new DefaultListModel(mediator.getUsernameList()));
boolean isFound = false;
ListModel<String> usernameList = getModel();
for (int i = 0; i < usernameList.getSize(); i++) {
String item = usernameList.getElementAt(i);
if (item.equals(user.getUsername())) {
setSelectedIndex(i);
isFound = true;
}
}
if (!isFound) {
clearSelection();
}
}
public static class DefaultListModel extends AbstractListModel<String> {
private String[] model;
public DefaultListModel(String[] model) {
this.model = model;
}
@Override
public int getSize() {
return model.length;
}
@Override
public String getElementAt(int index) {
return model[index];
}
}
}
4. UserDetailPanel(用户详细信息面板)
这是一个复合组件,先抛开界面怎么画,剩下的麻烦的点有两个:
- 界面更新比较繁琐
- "增加 & 修改" 按钮的点击事件,"删除" 按钮的点击事件。基于前面的封装,把思路屡屡会发现。其实就两步:(1)先更新数据源;(2)再调用 change() 方法做事件分发,提供新的 User数据,让其他组件做出界面改变
package demo.component;
import demo.User;
import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Enumeration;
/**
* @author passerbyYSQ
* @create 2021-06-03 1:24
*/
public class UserDetailPanel extends JPanel implements Component {
private JTextField usernameText;
private JTextField phoneText;
private ButtonGroup genderGroup;
public UserDetailPanel() {
setBounds(204, 144, 335, 263);
setLayout(null);
JLabel usernameLabel = new JLabel("用户名");
usernameLabel.setBounds(24, 16, 45, 24);
add(usernameLabel);
usernameText = new JTextField();
usernameText.setBounds(94, 16, 161, 24);
add(usernameText);
usernameText.setColumns(10);
JLabel phoneLabel = new JLabel("手机");
phoneLabel.setBounds(39, 124, 30, 18);
add(phoneLabel);
phoneText = new JTextField();
phoneText.setBounds(94, 121, 161, 24);
add(phoneText);
phoneText.setColumns(10);
JLabel genderLabel = new JLabel("性别");
genderLabel.setBounds(34, 64, 30, 24);
add(genderLabel);
JRadioButton maleRadio = new JRadioButton("男");
maleRadio.setBounds(102, 64, 43, 27);
add(maleRadio);
JRadioButton femaleRadio = new JRadioButton("女");
femaleRadio.setBounds(170, 64, 43, 27);
add(femaleRadio);
// 加入按钮组就能单选
genderGroup = new ButtonGroup();
genderGroup.add(maleRadio);
genderGroup.add(femaleRadio);
JButton addOrUpdateBtn = new JButton("增加 & 修改");
addOrUpdateBtn.setBounds(70, 188, 106, 27);
add(addOrUpdateBtn);
addOrUpdateBtn.addActionListener(e -> {
User user = getUser();
if (user.getUsername().isEmpty()) {
return;
}
mediator.putUser(user); // 新增或修改
change(user); // UserDetailPanel 是事件源
});
JButton deleteBtn = new JButton("删除");
deleteBtn.setBounds(206, 188, 63, 27);
deleteBtn.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
User user = getUser();
if (user.getUsername().isEmpty()) {
return;
}
mediator.removeUser(user.getUsername());
change(new User());
}
});
add(deleteBtn);
mediator.addComponent(this); // 不要忘了!!!
}
@Override
public void update(User user) {
selectGender(user);
usernameText.setText(user.getUsername());
phoneText.setText(user.getPhone());
}
// 选中对应的性别
public void selectGender(User user) {
// 删除的时候需要,但未生效
clearSelectedGender();
Enumeration<AbstractButton> radios = genderGroup.getElements();
while (radios.hasMoreElements()) {
AbstractButton radio = radios.nextElement();
if (radio.getText().equals(user.getGender())) {
radio.setSelected(true);
}
}
}
// 获取选中的性别
public String getSelectedGender() {
Enumeration<AbstractButton> radios = genderGroup.getElements();
while (radios.hasMoreElements()) {
AbstractButton radio = radios.nextElement();
if (radio.isSelected()) {
return radio.getText();
}
}
return null;
}
// 清除单选
public void clearSelectedGender() {
Enumeration<AbstractButton> radios = genderGroup.getElements();
while (radios.hasMoreElements()) {
AbstractButton radio = radios.nextElement();
radio.setSelected(false);
}
}
public User getUser() {
String username = usernameText.getText();
String gender = getSelectedGender();
String phone = phoneText.getText();
return new User(username, gender, phone);
}
}
MainFrame(主窗口界面)
核心的点就是3个点击事件
- 搜索按钮的点击事件,而事件源是搜索输入框(SearchTextField)
- 用户名列表的某一项的选中事件
- 用户名下拉选择框的选中事件
此处做总结,实现见代码。思路理清,代码就不难了
另外,再来说一个最繁琐的问题:如何画界面???
写代码我是用 idea 写的,而画界面我是用 Eclipse 来画的,因为 Eclipse 有一款免费的Java GUI构建插件 WindowBuidler,可以比较方便地通过可视化界面的方式画界面。我在 Eclipse 将界面画好,变量名改好(强迫症患者)。然后再粘贴到 idea 来改。
package demo;
import demo.component.SearchTextField;
import demo.component.UserDetailPanel;
import demo.component.UsernameCombBox;
import demo.component.UsernameList;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
/**
* @author passerbyYSQ
* @create 2021年6月2日 下午10:23:45
*/
public class MainFrame extends JFrame {
private Mediator mediator = Mediator.getInstance();
/**
* Launch the application.
*/
public static void main(String[] args) {
EventQueue.invokeLater(() -> {
try {
MainFrame frame = new MainFrame();
frame.setVisible(true);
} catch (Exception e) {
e.printStackTrace();
}
});
}
/**
* Create the frame.
*/
public MainFrame() {
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setBounds(100, 100, 627, 455);
JPanel contentPane = new JPanel();
contentPane.setBorder(new EmptyBorder(5, 5, 5, 5));
setContentPane(contentPane);
contentPane.setLayout(null);
JLabel titleLabel = new JLabel("用户信息管理");
titleLabel.setFont(new Font("宋体", Font.PLAIN, 26));
titleLabel.setBounds(226, 13, 170, 48);
contentPane.add(titleLabel);
JLabel keywordLabel = new JLabel("关键词");
keywordLabel.setBounds(226, 96, 57, 26);
contentPane.add(keywordLabel);
SearchTextField keywordText = new SearchTextField(); // 搜索框
keywordText.setToolTipText("请输入用户名");
keywordText.setBounds(284, 96, 182, 26);
contentPane.add(keywordText);
keywordText.setColumns(10);
JButton searchBtn = new JButton("查询");
searchBtn.setBounds(494, 96, 63, 27);
searchBtn.addActionListener(new AbstractAction() { // 搜索按钮的点击事件
@Override
public void actionPerformed(ActionEvent e) {
String keyword = keywordText.getText();
if (!keyword.isEmpty()) {
keywordText.change(mediator.getUser(keyword));
}
}
});
contentPane.add(searchBtn);
JScrollPane scrollPane = new JScrollPane();
scrollPane.setBounds(33, 165, 126, 208);
contentPane.add(scrollPane);
UsernameList usernameList = new UsernameList();
scrollPane.setViewportView(usernameList);
usernameList.addMouseListener(new MouseAdapter() { // 鼠标点击事件
@Override
public void mouseClicked(MouseEvent e) {
String username = usernameList.getSelectedValue();
usernameList.change(mediator.getUser(username));
}
});
UsernameCombBox usernameSelect = new UsernameCombBox();
usernameSelect.setBounds(33, 96, 126, 26);
usernameSelect.addActionListener(new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
String username = (String) usernameSelect.getSelectedItem();
usernameSelect.change(mediator.getUser(username));
}
});
contentPane.add(usernameSelect);
UserDetailPanel userDetailPanel = new UserDetailPanel();
contentPane.add(userDetailPanel);
setLocationRelativeTo(null);
setResizable(false);
}
}
最后,贴出最没有技术含量的一个类,User类
package demo;
/**
* @author passerbyYSQ
* @create 2021-06-02 23:58
*/
public class User {
private String username;
private String gender;
private String phone;
public User() {
}
public User(String username) {
this.username = username;
}
public User(String username, String gender, String phone) {
this.username = username;
this.gender = gender;
this.phone = phone;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
}