Java 面向对象 · 交互课程

示例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 { ... }
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();
}
// 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);

五、事件:从按钮到模型的一条链

点击「取号」按钮后发生的事情(按顺序):

  1. JButton 产生 ActionEvent;
  2. 因为 btn.addActionListener(c),事件派发给 Controller(它实现了 ActionListener);
  3. CustomerController 让 Broadcaster.next() 发新号、把客户放进 RestArea;
  4. 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 展开

思考题

  1. RestArea.triggerEvent 为什么用接口类型 ServiceAvailableListener 声明列表,而不是直接 List<Customer>?
  2. Controller extends ActionListener 之后,btn.addActionListener(c) 为什么能编译通过?
  3. 如果新增一个「VIP 客户」类(办理时间更短),需要改哪些地方?(提示:观察者注册)

学习小测验

学习小测验

Q1

ServiceHallMock 中,Controller 接口扩展了哪个接口?

Q2

RestArea.triggerEvent(ae) 里 for (ServiceAvailableListener sal : sals) sal.onServiceAvailable(ae) 体现的是( )

Q3

Customer 类与 ServiceAvailableListener 接口的关系是( )

Q4

柜员办完业务后,坐席空闲事件如何通知到等候的客户?

Q5

NumberGenMachineView 里 btn.addActionListener(c) 中的 c 声明类型是( )

Q6

ServiceHall 主窗口使用什么布局摆放三个子面板?