🏺 第19章 企业人事管理系统
📄 一句话概括

系统设计产出三样东西:功能结构树、业务流程图、目录结构。 前两样是给人看的,第三样是给编译器看的。 目录结构不是「整理癖」,它决定了改一个功能要动几个文件。

📝 本章和前面几章的关系

19.2 排出了用例的序,这一篇把序变成结构。 结构一旦定下来,19.5 的界面、19.6 的公共模块都往这个格子里填。 格子错了,后面全是补丁。 这一篇的 Demo 会真的把目录建出来,不是画个示意图。

一、功能结构树:为什么是三个分支

教材的功能树是三层:一个根、三个分支、九个叶子。 三个分支对应三个业务模块,九个叶子对应九个菜单项。

+ 人事管理系统
    + 人事管理
        - 员工档案
        - 部门管理
        - 合同管理
    + 待遇管理
        - 工资计算
        - 工资发放
        - 工资条查询
    + 系统维护
        - 用户管理
        - 权限分配
        - 数据备份

「三三制」不是巧合,是照着菜单栏来的:一个菜单三个子项,界面上一眼看全。 如果变成四层,用户就要点三次才能到功能。 每多一层,找东西的成本翻一倍——这是导航设计的经验值。 所以功能树的第一条约束是【层数】,第二条才是分组。 分组可以商量,层数不行。 叶子超过十个就该分二级菜单,这条在 19.5 讲导航栏时还会用到。

二、业务流程图:拿「新增员工」走一遍

流程图不是画给客户看的,是拿来发现「这里要写两张表」的。 挑一个最典型的用例,一步一步走。

步 做什么 在哪
1 人事专员打开新增窗口 界面
2 填工号、姓名、部门、岗位 界面
3 点保存,前端校验必填 界面
4 查工号是否重复 数据库
5 查部门是否存在 数据库
6 插入员工表 数据库
7 同时生成一条初始工资记录 数据库
8 返回成功,刷新列表 界面

第 4、5 步的顺序有讲究:先查重再插,不要靠主键冲突兜。 靠主键冲突兜的写法,出错时给用户看的是 ORA-00001 这种天书。 先查一次,才能给出「工号 A001 已存在」这种能看懂的话。 代价是多一次查询,值。 第 7 步才是这张流程图真正的价值:它暴露了「一次业务写两张表」。 只要出现「写两张表」,就必须有一个事务包住。 否则第一步成功、第二步失败,就多了一个没有工资记录的员工。 流程图里的每一个「数据库」格子,都是在问一句:这里要不要事务。

三、目录结构:分层是为了改得动

教材的目录分四层:entity 放数据、dao 放 SQL、 service 放业务、view 放界面。

包 放什么 改了它影响谁
entity 数据对象 全部(但很少改)
dao 增删改查 只影响调用它的 service
service 业务规则、事务 只影响界面
view 窗口与面板 只影响它自己

这张表的第三列才是分层的目的:改一处,影响面可控。 数据库从 Oracle 换成 MySQL,只动 dao 和配置文件; 界面换个颜色,只动 view。 不分层的写法是:SQL 直接写在按钮的点击事件里。 这种代码改一个字段,要翻遍所有界面文件——而且无法判断改全了没有。 Demo 把目录真建了一遍:12 个目录、16 个文件、最深 4 层。 4 层是个上限,再深就该拆模块了。

四、系统预览:先画草图再写界面

界面草图(wireframe)不需要好看,只需要定三件事: 哪块区域放什么、谁在左谁在右、操作按钮在哪。 教材的主窗体是标准的四块:顶部工具栏、左侧导航、中间内容、底部状态。 这个布局二十年前就定了,因为它符合人眼从左到右、从上到下的扫描习惯。 草图的用处是【提前吵完架】。 在纸上改布局,成本是十分钟;代码写完再改,成本是两天。 所以草图上要标数字:宽度多少像素、能放几列。 不标数字的草图,实现时全靠猜,猜完必然不一致。

五、动手 Demo:把工程目录真建一遍,数一数层级和文件

这一篇的 Demo 是全书最朴素的一个:建目录、建空文件、数层级。 它证明不了任何性能,只能证明一件事——这个结构落得下去。 重点是最后一行数字:最深 4 层。 层级是设计约束,不是随手写的。

import os, tempfile
root = os.path.join(tempfile.gettempdir(), "hr_proj")
dirs = ["src/entity", "src/dao", "src/service", "src/view",
        "src/view/dialog", "src/util", "src/image",
        "cfg", "lib", "log", "doc", "bin"]
files = ["src/entity/Personnel.java", "src/entity/Dept.java",
         "src/entity/Wage.java", "src/entity/User.java",
         "src/dao/BaseDao.java", "src/dao/PersonnelDao.java",
         "src/dao/WageDao.java", "src/service/WageService.java",
         "src/view/MainFrame.java", "src/view/PersonnelPanel.java",
         "src/view/WagePanel.java", "src/util/ToolBar.java",
         "cfg/hibernate.cfg.xml", "cfg/Personnel.hbm.xml",
         "cfg/Dept.hbm.xml",
         "src/view/dialog/DeptTreeDialog.java"]
for d in dirs:
    os.makedirs(os.path.join(root, *d.split("/")), exist_ok=True)
for f in files:
    open(os.path.join(root, *f.split("/")), "w").close()
deep = max(len(f.split("/")) for f in files)
print("目录 %d 个,文件 %d 个,最深 %d 层" % (len(dirs), len(files), deep))
print("根目录 %s" % root)

实跑输出:

--------------------------------------------------------------------
第八十六课:系统设计——功能树、流程图、目录一次成型
--------------------------------------------------------------------

  **这一课把上一课的用例变成三张图:功能结构树、业务流程图、目录结构。**
  **三张图里只有第三张能在沙箱真跑,另外两张画出来看。**

  一、功能结构树
  --------------------------------------------------------------------
  + 人事管理系统
      + 人事管理
          - 员工档案
          - 部门管理
          - 合同管理
      + 待遇管理
          - 工资计算
          - 工资发放
          - 工资条查询
      + 系统维护
          - 用户管理
          - 权限分配
          - 数据备份

  **三个一级分支,九个二级功能。**
  **这个「三三制」不是巧合,是照着菜单栏来的:一个菜单三个子项,界面上一眼看全。**
  **十个以上就该分二级菜单了,那时候用户要点两次才能到功能。**

  二、业务流程图:新增一个员工
  --------------------------------------------------------------------

  步 做什么                   在哪
  ----------------------------------
  1  人事专员打开新增窗口     界面
  2  填工号、姓名、部门、岗位 界面
  3  点保存,前端校验必填     界面
  4  查工号是否重复           数据库
  5  查部门是否存在           数据库
  6  插入 tb_personnel        数据库
  7  同时生成一条初始工资记录 数据库
  8  返回成功,刷新列表       界面
  已选择 8 行
  **第 4、5 步的顺序有讲究:先查重再插,不要靠主键冲突来兜。**
  **靠主键冲突兜的写法,出错时给用户看到的是 `ORA-00001` 这种天书。**
  **先查一次,能给出「工号 A001 已存在」这种能看懂的话。**
  **代价是多一次查询。**
  **但第 7 步暴露了一个更重要的问题:新增员工要同时写两张表,必须放在一个事务里。**
  **不然第一步成功、第二步失败,就多了一个没有工资记录的员工。**

  三、目录结构:真的建一遍
  --------------------------------------------------------------------

  统计项   数值
  -------------
  目录数   12
  文件数   16
  最深层级 4
  已选择 3 行
  **这个结构要记住的是分层:`entity` 放数据、`dao` 放 SQL、`view` 放界面、`util` 放工具。**
  **分层的意义在「改一处不动别处」。**
  **数据库从 Oracle 换成 MySQL,只动 `dao` 和配置文件;界面换个颜色,只动 `view`。**
  **不分层的写法是:SQL 直接写在按钮的点击事件里。**
  **这种代码改一个字段,要翻遍所有界面文件。**

输出三行:12 个目录、16 个文件、最深 4 层。 12 和 16 这两个数看着平淡,但它们是可以验收的。 「目录结构设计好了」这句话没法验收,「12 个目录」可以。 16 个文件分布在 4 个包里,平均每包 4 个。 这个密度是健康的:一个包超过十个文件,就该想想是不是能拆。 src/img 和 bin、log、doc 是空目录,所以文件数比目录数少。 真实项目里这几个目录往往被忽略——结果图片散落在项目根目录,日志打进控制台,没有任何文档。 cfg 里放了四个 XML:一个主配置、三个映射文件。 19.6 会真的去解析其中那个主配置。 要说明一点:这里只建了「骨架」,没有写 Java 代码。 Java 编译不了,所以这一课能真跑的只有目录和文件本身。

30 秒实验

  1. 把 dirs 里的 "src/image" 改成 "src/img",看统计变不变——目录名和代码里的引用必须一致, 这一改就得同步改代码。
  2. 往 files 里加一个 "src/view/dialog/SelectEmpDialog.java", 看最深层级还是不是 4——层级由路径决定,不由文件数决定。
  3. 把 dirs 删掉三项再跑,看 makedirs 会不会报错——它带了 exist_ok=True, 但父目录不存在时会自动补建,这是它和 mkdir 的区别。

常见误区

  1. 「目录结构是小事,先写代码再说」。 结构决定影响面, 写到一半再调整,等于把已经写好的 import 全改一遍。
  2. 「分层越多越专业」。 四层够用,再多会出现「改一个字段要动五个文件」的情况, 那叫过度设计。
  3. 「流程图是给客户看的」。 它是给自己看的, 作用是发现「这里要写两张表」「这里要事务」这类实现级问题。
  4. 「草图不用标数字」。 不标宽度,实现时全靠猜;猜出来的界面在 1024 的机器上一定出滚动条。
自测三问(点开对答案) 1. 功能树为什么控制在三层? —— **每多一层,用户找到功能就多一次点击,成本翻倍。** **三层(菜单→子菜单→功能)是桌面软件的惯例上限。** 2. 「新增员工」流程图里,哪一步最重要? —— **第 7 步「同时生成工资记录」。** **它把一次业务变成两次写库,从而逼出一个事务边界。** 3. 分层的目的是什么? —— **让「改一处」的影响面可控。** 不是为了好看,**是为了能判断改了之后哪些地方要跟着改。** 4. 为什么草图必须标数字? —— **因为布局的本质是算术**:窗口宽度减去固定区域,剩下的才是内容区。 **不标数字就没法算,只能猜。**

下一篇进入数据库:四张表、22 个字段、4 个外键。 还要做一次范式体检,把「以后一定要返工」的字段提前挑出来。

相关笔记