示例2 · ServiceHallMock 模拟服务大厅
GUI 服务大厅:继承、多态、GUI、事件
示例2 · ServiceHallMock 模拟服务大厅
贯穿式教学示例(二):以「模拟服务大厅」(ServiceHallMock,包
com.un.shmock)为载体,重点介绍继承、多态、GUI、事件。素材在本仓库素材/模拟服务大厅/ServiceHallMock/src,以下代码均摘自其源码(有删节)。
一、项目概览:MVC 三层结构
模拟一个银行营业厅:取号机发号、等候区排队、服务窗口叫号办理。整个程序按 MVC 分三层:
| 层 | 类 | 职责 |
|---|---|---|
| Model(模型) | ServiceArea、RestArea、Clerk、Customer、Broadcaster、AvailableEvent | 业务状态与规则:座位、排队、叫号、空闲事件广播 |
| Controller(控制) | Controller(接口)、ClerkController、CustomerController | 处理事件、驱动模型、通知视图刷新 |
| View(视图) | ServiceHall(主窗口)、RestAreaView/ServiceAreaView/NumberGenMachineView、FacilitiesView(接口) | 绘制界面、接收用户操作 |
三层之间只通过方法调用和接口协作——Model 不知道 View 长什么样,View 不直接改 Model 的数据。
二、继承:窗口与面板的层次
public class ServiceHall extends JFrame { ... } // 主窗口
public class NumberGenMachineView extends JPanel implements FacilitiesView { ... }
ServiceHallextendsJFrame:直接获得窗口的全部能力(setSize、setTitle、setVisible),只补充自己的组装逻辑;- 三个子面板 extends
JPanel:各自负责一块区域的绘制与控件; FacilitiesView是接口,统一「视图必须能挂控制器」的约定:
public interface FacilitiesView {
public void setController(Controller c);
}
继承在这里的角色:复用组件能力 + 建立层次(窗口 → 面板 → 按钮),而不是复制代码。
三、多态:接口引用,动态分派
Controller extends ActionListener——接口也能「继承」
public interface Controller extends ActionListener {
public void update();
}
Controller 扩展了 ActionListener,所以任何 Controller 对象都可以当事件监听器用:
// NumberGenMachineView(节选)
btn.addActionListener(c); // c 的声明类型是 Controller
观察者模式:List<ServiceAvailableListener> 里的多态分派
柜员办完业务后,坐席空闲,事件要通知所有正在等候的客户:
// RestArea(节选)
public void triggerEvent(AvailableEvent ae) {
for (ServiceAvailableListener sal : sals) {
sal.onServiceAvailable(ae); // 接口引用 → 实际是某个 Customer
}
this.controller.update();
}
sals是List<ServiceAvailableListener>,里面装的其实是一个个Customer对象(Customer implements ServiceAvailableListener);- 每个客户在自己的
onServiceAvailable里判断「叫到的号是不是我的」:
// Customer(节选)
@Override
public void onServiceAvailable(AvailableEvent ae) {
if (ae.getQueueNum() == this.queueNum) {
ae.getRestArea().leaveSeats(this); // 离开等候区
ae.getServiceArea().setSeat(this, ae.getSeatNum()); // 坐到空闲坐席
}
}
同一句 sal.onServiceAvailable(ae),对每个客户执行的是各自的逻辑——这就是接口多态:调用方只认接口,行为由对象自己决定。
四、GUI:窗口组装与绘制
ServiceHall.init() 是一次完整的 GUI 组装(节选):
setSize(720, 650);
setTitle("ServiceHallMock");
this.setLayout(null); // 绝对布局
ServiceArea sa = new ServiceArea(3); // 3 个服务窗口
RestArea ra = new RestArea(48); // 48 个等候座位
// ... 创建 Broadcaster、三个 Clerk,互相 setXxx 挂接 ...
rav.setBounds(50, 250, 600, 300); // 等候区面板
sav.setBounds(200, 50, 450, 150); // 服务窗口面板
ngmv.setBounds(50, 50, 100, 110); // 取号机面板
this.getContentPane().add(rav);
this.getContentPane().add(sav);
this.getContentPane().add(ngmv);
setVisible(true);
- 绝对布局(
setLayout(null)+setBounds):每个面板用坐标定位,简单直接; - 绘制各自为政:
ServiceHall.paint只画三个区域标题,具体内容由各子面板的paint负责——绘制职责跟着组件走。
五、事件:从按钮到模型的一条链
点击「取号」按钮后发生的事情(按顺序):
- JButton 产生
ActionEvent; - 因为
btn.addActionListener(c),事件派发给 Controller(它实现了ActionListener); CustomerController让Broadcaster.next()发新号、把客户放进RestArea;controller.update()触发各视图repaint()——界面刷新。
反过来,模型主动发起的事件也走同一条链:柜员办完业务 → ServiceArea.setSeatAvailable(num) → 构造 AvailableEvent → broadcaster.broadAvailableEvent(ae) → RestArea.triggerEvent(ae)(通知所有客户)→ controller.update()(重绘)。
事件驱动的关键:谁变化,谁发事件;谁关心,谁监听。界面从不轮询模型,全靠事件推送刷新。
六、知识点映射
| 课程知识点 | ServiceHallMock 中的落点 |
|---|---|
| 继承 | ServiceHall extends JFrame、各 View extends JPanel、Controller extends ActionListener |
| 多态 | List<ServiceAvailableListener> 遍历分派;btn.addActionListener(controller) |
| 接口 | ServiceAvailableListener(观察者)、FacilitiesView(视图约定) |
| GUI | 绝对布局组装三个面板;各自 paint 分工绘制 |
| 事件 | 按钮 ActionEvent → Controller → Model → update() → repaint |
| (预告)多线程 | Clerk.serve() 里匿名 Thread + sleep 模拟办理耗时——示例3 展开 |
思考题
RestArea.triggerEvent为什么用接口类型ServiceAvailableListener声明列表,而不是直接List<Customer>?Controller extends ActionListener之后,btn.addActionListener(c)为什么能编译通过?- 如果新增一个「VIP 客户」类(办理时间更短),需要改哪些地方?(提示:观察者注册)
学习小测验
学习小测验
ServiceHallMock 中,Controller 接口扩展了哪个接口?
RestArea.triggerEvent(ae) 里 for (ServiceAvailableListener sal : sals) sal.onServiceAvailable(ae) 体现的是( )
Customer 类与 ServiceAvailableListener 接口的关系是( )
柜员办完业务后,坐席空闲事件如何通知到等候的客户?
NumberGenMachineView 里 btn.addActionListener(c) 中的 c 声明类型是( )
ServiceHall 主窗口使用什么布局摆放三个子面板?