作业集4-6总结blog

写在前面
java课程的第二单元结束了。从实验一的继承与多态选课系统,到实验五的JavaFX图形界面开发,再到穿插其中的数字电路模拟程序,这一单元以业务系统构建和复杂数据流模拟两条主线展开,我学到了很多:
• 深入理解了继承与多态的工程化应用;
• 掌握了抽象类与接口的设计取舍;
• 体验了从控制台程序到图形界面的完整演进;
• 学会了在复杂逻辑中运用拓扑排序和组合模式;
• 真正体会到了"高内聚、低耦合"不是口号,而是血泪教训换来的设计准则。
下面我将从程序结构、复杂度分析、Bug分析与测试方法几个方面,系统总结这一单元的五次实验作业。
复杂度指标说明
在进行复杂度分析之前,先明确几个核心概念:
v(G) —— 循环复杂度(Cyclomatic Complexity):程序流程图中独立路径的数量,可以理解为穷举所有执行路径所需的测试次数。值越大,方法越复杂,测试覆盖难度越高。
iv(G) —— 设计复杂度(Design Complexity):方法与其所调用其他方法的耦合程度。值越大,说明该方法调用了过多外部方法,模块独立性差。
ev(G) —— 本质复杂度(Essential Complexity):反映程序结构的"病态"程度,值越大说明结构化程度越差,通常伴随着过多的条件分支或难以重构的代码。
OCavg —— 类的所有方法的平均循环复杂度。
WMC —— 类的总循环复杂度(Weighted Methods per Class)。
实验一:继承与多态选课系统
作业要求
在实验二(基础类设计)的基础上,使用继承与多态重构选课系统。要求:
• 创建 Person 基类,Student 和 Teacher 继承之;
• 重写 displayInfo() 和 getRole() 实现多态;
• 通过 PersonManager 以统一容器管理不同角色;
• 建立 Course、MajorCourse、ElectiveCourse 的课程继承体系;
• 所有数据从文本文件读取。
实现方式
采用模板方法模式设计基类:Person 中定义 displayInfo() 为 final 方法,内部调用 displayBasicInfo()(公共实现)和抽象方法 displayDetailedInfo()(子类定制)。Course 基类同样采用 displayCourseInfo() + displayAdditionalInfo() 的模板方法结构。
PersonManager 内部维护 Person[] 数组,利用多态实现统一存储和遍历。通过 getRole() 方法返回的字符串判断类型后再进行向下转型,避免使用 instanceof。

┌─────────────┐ ┌─────────────┐
│ Person │◀─────────│ Student │
│ (abstract) │ 继承 │ │
├─────────────┤ └─────────────┘
│ -id:String │
│ -name:String│ ┌─────────────┐
│ -email │◀─────────│ Teacher │
│ -phone │ 继承 │ │
├─────────────┤ └─────────────┘
│+displayInfo │
│+getRole() │
└─────────────┘
│ 聚合

┌─────────────┐ ┌─────────────┐
│PersonManager│─────────▶│ Person[] │
├─────────────┤ └─────────────┘
│+addPerson() │
│+findById() │ ┌─────────────┐
│+displayAll()│─────────▶│ Course │
└─────────────┘ 聚合 │ (abstract) │
├─────────────┤
│+canEnroll() │
│+getType() │
└──────┬──────┘

┌────────────┼────────────┐
▼ ▼ ▼
┌──────────┐┌──────────┐┌──────────┐
│BasicCourse││MajorCourse││Elective │
└──────────┘└──────────┘└──────────┘

方法复杂度(部分高值):
┌──────────────────────┬──────┬──────┬──────┐
│ 方法名 │ ev(G)│ iv(G)│ v(G) │
├──────────────────────┼──────┼──────┼──────┤
│ displayAllPeople() │ 3 │ 5 │ 7 │
│ findPersonByName() │ 2 │ 3 │ 5 │
│ Main.main() │ 4 │ 8 │ 12 │
└──────────────────────┴──────┴──────┴──────┘

类复杂度:
┌──────────────────────┬───────┬──────┐
│ 类名 │ OCavg │ WMC │
├──────────────────────┼───────┼──────┤
│ PersonManager │ 3.8 │ 23 │
│ CourseSelectionSystem│ 4.2 │ 38 │
│ MainTest │ 6.0 │ 24 │
└──────────────────────┴───────┴──────┘

3799929-20260621161729407-1932872400

3799929-20260621161803497-722863939

Main.main() 的复杂度最高,因为数据读取和业务逻辑全部集中在主方法中。虽然指导书提供了完整框架,但这种将所有读取逻辑塞入main的做法并不符合单一职责原则。合理做法是将文件解析抽取为独立的 DataLoader 类。
Bug分析
公测: 全部通过。
互测: 被指出两个问题:

  1. MajorCourse.addStudent() 中忘记调用 super.addStudent(),导致专业校验通过后学生未被真正添加到课程的 enrolledStudents 数组中。这是继承链方法调用遗忘的典型错误。
  2. Teacher.removeTeachingCourse() 中使用了 == 比较课程对象,而非 equals() 或 ID 比较,导致无法正确移除课程。
    他人的Bug: 发现3个同学的 Student.displayInfo() 未调用 super.displayInfo(),导致父类的基本信息(ID、姓名)未输出。这提醒我:重写方法时,如果父类有公共逻辑,必须显式调用 super。
    实验二:抽象类与接口(实验四)
    作业要求
    在实验一基础上,使用抽象类与接口进一步重构:
    • AbstractPerson 取代 Person,增加抽象方法和模板方法;
    • AbstractCourse 取代 Course,增加 canStudentEnroll() 抽象方法;
    • 新增 Evaluable、Enrollable、Teachable 三个接口;
    • Student 实现 Enrollable 和 Evaluable,Teacher 实现 Teachable 和 Evaluable;
    • 增加成绩管理和系统评估功能;
    • 数据从文件读取。
    实现方式
    这次重构的核心是从"是什么"抽象为"能做什么"。
    AbstractPerson 中定义了 displayInfo() 为 final 模板方法,子类只需实现 displayDetailedInfo() 即可。同时增加了邮箱和电话的格式校验(setEmail 检查是否包含 @,setPhone 检查是否为11位数字)。
    AbstractCourse 中增加了 Map<Student, Double> grades 存储成绩,以及 addGrade()、calculateAverageGrade() 等方法。不同课程类型(BasicCourse、MajorCourse、ElectiveCourse)通过重写 canStudentEnroll() 实现不同的选课条件。
    Evaluable 接口统一了学生(平均成绩)和教师(学时×0.5 + 课程数×20)的评估逻辑。EnhancedCourseSelectionSystem 中维护 List,实现了策略模式的系统评估。
    类图
    ┌─────────────────────┐ ┌──────────────────┐
    │ <> │ │ <> │
    │ AbstractPerson │──────▶│ Evaluable │
    ├─────────────────────┤ ├──────────────────┤
    │ +displayInfo():void │ │+calcGrade():double│
    │ #displayBasicInfo() │ │+getMethod():String│
    │ +getRole():String │ │+isPassed():bool │
    └──────────┬──────────┘ └──────────────────┘
    │ △
    ┌──────┴──────┐ │
    ▼ ▼ ┌───────┴────────┐
    ┌──────────┐ ┌──────────┐ │ <>│
    │ Student │ │ Teacher │──────│ Enrollable │
    │(Enrollable)│ (Teachable)│ ├────────────────┤
    └──────────┘ └──────────┘ │+enrollCourse() │
    │+dropCourse() │
    │+getCredits() │
    └────────────────┘

┌─────────────────────┐ ┌──────────────────┐
│ <> │ │ <> │
│ AbstractCourse │──────▶│ Gradable │
├─────────────────────┤ ├──────────────────┤
│ +addGrade() │ │+addGrade() │
│ +calcAvgGrade() │ │+getGrade() │
│ +canStudentEnroll() │ └──────────────────┘
└──────────┬──────────┘

┌──────┼──────┐
▼ ▼ ▼
┌──────┐┌──────┐┌──────────┐
│Basic ││Major ││Elective │
└──────┘└──────┘└──────────┘
方法复杂度(部分高值):
┌─────────────────────────┬──────┬──────┬──────┐
│ 方法名 │ ev(G)│ iv(G)│ v(G) │
├─────────────────────────┼──────┼──────┼──────┤
│ AbstractCourse.addGrade │ 1 │ 2 │ 3 │
│ Student.calculateGrade │ 3 │ 4 │ 6 │
│ Teacher.calculateGrade │ 2 │ 3 │ 5 │
│ Main.main (读取) │ 5 │ 9 │ 14 │
└─────────────────────────┴──────┴──────┴──────┘

类复杂度:
┌─────────────────────────┬───────┬──────┐
│ 类名 │ OCavg │ WMC │
├─────────────────────────┼───────┼──────┤
│ EnhancedCourseSelection │ 4.5 │ 40 │
│ AbstractCourse │ 3.2 │ 29 │
│ Student │ 3.8 │ 23 │
└─────────────────────────┴───────┴──────┘

3799929-20260621162001020-12915623

3799929-20260621161946451-293578930

Main.main() 的复杂度进一步升高(14),因为输入格式更加复杂(支持三种课程类型的不同字段)。这说明将数据解析与业务逻辑分离是当务之急。
Bug分析
公测: 全部通过。
互测: 发现一个隐蔽问题:AbstractCourse.removeStudent() 中调用 student.dropCourse(this),而 Student.dropCourse() 中又调用 course.removeStudent(this),形成了双向递归调用,虽然最终能正确执行,但增加了不必要的调用栈深度。优化方案是让一方负责主逻辑,另一方只做数据同步。
他人的Bug: 多名同学的 ElectiveCourse.canStudentEnroll() 中未处理 openToGrades 为 null 的情况,导致空指针异常。这提醒我:任何从外部读取的数据都应进行防御性校验。
实验三:图形用户界面(实验五)
作业要求
以选课系统为背景,使用 JavaFX 实现图形界面。要求:
• 四个主选项卡:学生管理、教师管理、课程管理、选课管理;
• 支持增删改查(表单输入 + TextArea 列表显示);
• 学生选课/退课、教师分配/取消授课;
• 使用 Lambda 表达式处理按钮事件;
• 数据内嵌在代码中(硬编码示例数据)。
实现方式
采用 MVC 模式的变体:
• Model:Student、Teacher、Course 三个数据类(与实验一基本相同);
• View:JavaFX 组件(TabPane、GridPane、TextArea、ComboBox 等);
• Controller:事件处理中的 Lambda 表达式,直接操作 Model 列表并刷新 View。
数据通过 initSampleData() 硬编码,避免了文件读取的复杂性。refreshStudentDisplay() 等方法负责将 Model 列表转换为 TextArea 的字符串表示。
┌─────────────────────────────────────────┐
│ CourseManagementSystem │
│ (extends Application) │
├─────────────────────────────────────────┤
│ - students:List
│ - teachers:List
│ - courses:List
│ - studentDisplay:TextArea │
│ - teacherDisplay:TextArea │
│ - courseDisplay:TextArea │
│ - resultDisplay:TextArea │
├─────────────────────────────────────────┤
│ + start(Stage):void │
│ + initSampleData():void │
│ + createStudentTab():Tab │
│ + createTeacherTab():Tab │
│ + createCourseTab():Tab │
│ + createSelectionTab():Tab │
│ + refreshStudentDisplay():void │
│ + refreshTeacherDisplay():void │
│ + refreshCourseDisplay():void │
│ + updateStudentCombo():void │
│ + updateTeacherCombo():void │
│ + updateCourseCombo():void │
│ + showAlert():void │
│ + addResult():void │
│ + main(String[]):void │
└─────────────────────────────────────────┘
方法复杂度(部分高值):
┌─────────────────────────┬──────┬──────┬──────┐
│ 方法名 │ ev(G)│ iv(G)│ v(G) │
├─────────────────────────┼──────┼──────┼──────┤
│ createStudentTab() │ 4 │ 7 │ 12 │
│ createTeacherTab() │ 4 │ 7 │ 12 │
│ createCourseTab() │ 4 │ 8 │ 13 │
│ createSelectionTab() │ 3 │ 6 │ 9 │
│ createStudentSelection │ 4 │ 5 │ 10 │
│ createTeacherSelection │ 4 │ 5 │ 10 │
└─────────────────────────┴──────┴──────┴──────┘

类复杂度:
┌─────────────────────────┬───────┬──────┐
│ 类名 │ OCavg │ WMC │
├─────────────────────────┼───────┼──────┤
│ CourseManagementSystem │ 4.8 │ 96 │
└─────────────────────────┴───────┴──────┘

3799929-20260621162138055-2067429381

3799929-20260621162122215-2146604745

WMC 高达96,说明该类承担了过多职责。理想情况下应该拆分为:
• StudentView、TeacherView、CourseView、SelectionView 四个独立的视图类;
• 一个 DataManager 管理所有数据;
• 一个 Controller 协调视图与数据。
Bug分析
开发阶段发现的问题:

  1. ComboBox 更新不及时:添加或删除学生/教师/课程后,选课管理界面的下拉列表未刷新。解决方案是在每次数据变更后调用 updateStudentCombo()、updateTeacherCombo()、updateCourseCombo()。
  2. 选课与退课按钮功能颠倒:在 createStudentSelectionPanel() 中,enrollButton 误调用了 course.removeStudent(studentId),导致点击"选课"执行的是退课操作。这是复制粘贴代码忘记修改方法名的经典错误。
  3. 教师授课分配与取消功能颠倒:同样的问题出现在 createTeacherSelectionPanel() 中,assignButton误调用了 teacher.removeCourse()。
  4. 窗口大小导致内容被截断:Scene 初始大小为 900×700,但选项卡内容较多时需滚动。未添加 ScrollPane 导致部分用户界面不完整。
    测试策略: 由于是 GUI 程序,采用了手工测试 + 边界值测试(如添加空字符串、添加重复 ID、课程满员选课等)。
    实验四:数字电路模拟程序1(题目集四)
    作业要求
    实现五种基本逻辑门的数字电路模拟:
    • 与门(A)、或门(O)、非门(N)、异或门(X)、同或门(Y);
    • 输入:INPUT 行定义外部信号,连接行定义信号传播路径;
    • 输出:按 A→O→N→X→Y 顺序输出各门输出引脚电平;
    • 忽略输入不全的元件。
    实现方式
    采用图传播算法:
  5. 解析 INPUT 行,建立外部信号映射;
  6. 解析连接行([ ... ]),建立信号源到目标引脚的邻接表;
  7. 解析连接过程中动态创建 Gate 对象;
  8. 将外部信号加入队列,BFS 传播;
  9. 每当一个门的所有输入就绪,计算输出并加入队列;
  10. 最终收集所有 outOk 的门,排序输出。
    复杂度分析
    方法复杂度:
    ┌─────────────┬──────┬──────┬──────┐
    │ 方法名 │ ev(G)│ iv(G)│ v(G) │
    ├─────────────┼──────┼──────┼──────┤
    │ main() │ 6 │ 8 │ 15 │
    │ decode() │ 3 │ 4 │ 6 │
    │ Gate.calc() │ 2 │ 3 │ 7 │
    └─────────────┴──────┴──────┴──────┘
    实验五:数字电路模拟程序2(题目集五)
    作业要求
    在程序1基础上新增四种元件:
    • 三态门(S)、译码器(M)、数据选择器(Z)、数据分配器(F);
    • 元件引脚分为控制、输入、输出三类;
    • 译码器输出为"输出0的引脚编号";
    • 数据分配器输出为"各输出引脚信号(无效用-)"。
    实现方式
    重构了 Component 类,新增 controlCount、inputCount、outputCount 字段以及 ctrlPins、dataPins、outPins 数组。getNonOutputPinIndices() 返回所有需要就绪的引脚(包含控制引脚),解决了三态门控制信号未到就提前计算的bug。
    List getNonOutputPinIndices() {
    List pins = new ArrayList<>();
    switch (type) {
    case S: pins.add(0); pins.add(1); break;
    case M: case Z: case F:
    for (int i = 0; i < controlCount + inputCount; i++) pins.add(i);
    break;
    // ...
    }
    return pins;
    }

方法复杂度(部分高值):
┌─────────────────────────┬──────┬──────┬──────┐
│ 方法名 │ ev(G)│ iv(G)│ v(G) │
├─────────────────────────┼──────┼──────┼──────┤
│ Component.initPins() │ 4 │ 6 │ 15 │
│ Component.computeOutput │ 5 │ 7 │ 18 │
│ parseComponent() │ 3 │ 5 │ 10 │
│ main() │ 6 │ 9 │ 18 │
└─────────────────────────┴──────┴──────┴──────┘

类复杂度:
┌─────────────────────────┬───────┬──────┐
│ 类名 │ OCavg │ WMC │
├─────────────────────────┼───────┼──────┤
│ Main │ 5.6 │ 62 │
│ Component │ 4.8 │ 43 │
└─────────────────────────┴───────┴──────┘
Component.computeOutput() 的 v(G)=18,因为需要处理9种不同元件的计算逻辑。可以通过工厂模式 + 多态将每种元件的计算逻辑分离到独立的子类中,大幅降低复杂度。
实验六:数字电路模拟程序4(题目集六)
作业要求
在程序2基础上新增:
• 子电路:使用组合模式支持层次化电路;
• 异常检测:对连接信息进行合法性校验;
• 优先输出异常信息,无异常则正常计算输出。
实现方式
采用组合模式:
• CircuitComponent 为抽象组件;
• LogicGate 为叶子节点;
• SubCircuit 为容器节点,包含内部门和连接规则。
异常检测在解析阶段完成,包括:

  1. 多个输出源;
  2. 无目标输入引脚;
  3. 无信号输出源;
  4. 连接行首尾顺序错误;
  5. 引脚信号冲突。
    复杂度分析
    方法复杂度:
    ┌─────────────────────────┬──────┬──────┬──────┐
    │ 方法名 │ ev(G)│ iv(G)│ v(G) │
    ├─────────────────────────┼──────┼──────┼──────┤
    │ checkConnectError() │ 6 │ 7 │ 14 │
    │ parseSubCircuit() │ 4 │ 6 │ 10 │
    │ runSimulate() │ 3 │ 5 │ 8 │
    │ main() │ 4 │ 7 │ 11 │
    └─────────────────────────┴──────┴──────┴──────┘
    checkConnectError() 的 v(G)=14,因为需要检测5种不同的异常情况。合理做法是为每种异常编写独立的检测方法,然后由一个方法统一调用。
    各次作业Bug汇总与心得
    作业
    公测Bug
    互测被发现的Bug
    发现的他人Bug
    核心问题类型
    实验一
    0
    2
    3
    继承方法调用遗漏、对象比较错误
    实验二
    0
    1
    4
    空指针防御不足、双向递归
    实验三
    N/A
    N/A
    N/A
    GUI逻辑颠倒、ComboBox未刷新
    题目集四
    0
    0


    题目集五
    初期多个
    0

    控制引脚未纳入就绪检查
    题目集六
    初期多个
    0

    子电路解析顺序、异常检测不完整
    核心教训
  6. 重写方法必须考虑父类逻辑:displayInfo() 和 addStudent() 的重写中,忘记调用 super 会导致功能缺失。记住:重写 ≠ 替换,重写 = 扩展。
  7. 对象比较用 equals() 而非 :特别是从文件读取的对象,即使内容相同, 也可能返回 false。
  8. 防御性编程:任何从外部(文件、用户输入)获取的数据都可能为 null 或不合规,必须校验。
  9. 复杂度可控的设计:当单个方法的 v(G) 超过10时,必须拆分。将9种元件的计算逻辑放在同一个 switch 中,是复杂度爆表的根源。
  10. 双向关联需要明确主控方:Student 和 Course 相互引用时,应让一方主导删除/添加逻辑,另一方只做同步。
    关于设计模式的思考
    通过本单元五次实验,我对几个关键设计模式有了更深的理解:
    模板方法模式
    在 AbstractPerson 和 AbstractCourse 中,将不变的算法骨架(显示信息流程)放在父类中,将可变的部分(详细信息、选课条件)定义为抽象方法由子类实现。这种模式既保证了流程的一致性,又提供了足够的灵活性。
    组合模式
    在数字电路模拟程序4中,CircuitComponent 作为统一接口,LogicGate 和 SubCircuit 分别作为叶子和容器。这使得单个门和整个子电路对上层完全透明,递归计算成为可能。这是处理层次化结构最优雅的方式。
    工厂模式
    虽然我在第三次作业中未使用工厂模式,但在阅读他人代码后深刻认识到:如果每种元件都有独立的创建逻辑,使用工厂模式可以将创建逻辑从主流程中剥离,大大提高可维护性。
    接口隔离原则
    Evaluable、Enrollable、Teachable 三个接口分别对应不同的能力维度,Student 和 Teacher 根据自身功能选择实现。与设计一个包含所有方法的庞大接口相比,这种细粒度接口更加灵活,也更容易扩展。
    总结
    本单元的五次实验是一次从面向对象基础到复杂系统设计的完整训练。继承与多态让我们学会了代码复用,抽象类与接口让我们理解了"是什么"与"能做什么"的分离,JavaFX让我们体验了图形界面的构建,而数字电路模拟则让我们在复杂数据流处理中实践了图算法和组合模式。
    核心收获:
  11. 继承是"is-a"关系,接口是"can-do"关系。Student 是 Person,所以继承;Student 能选课,所以实现 Enrollable。这个区别贯穿了整个单元的设计。
  12. 复杂度是设计质量的量化指标。v(G) 超过10的方法需要重构,WMC 过高的类需要拆分。
  13. 从"能跑"到"能扩展"再到"能容错",这是软件质量的三重境界。本单元的前三次实验教会了我"能跑",第四次让我理解"能扩展",第五、六次则让我认识到"能容错"和"能应对复杂数据"的重要性。
  14. 测试是设计的一部分。无论是手工测试、对拍器还是边界值测试,测试用例的构造本身就是对设计的一次审视。
    需要进一步学习的方向:
    • 设计模式的系统学习:本单元接触了模板方法、组合、工厂、策略等模式,但应用还比较浅。后续需要深入理解各模式的适用场景和变体。
    • JavaFX的进阶应用:当前只是基础的控件使用,后续可以探索图表、动画、CSS样式等高级特性。
    • 多线程与并发:数字电路仿真在真实场景中需要并行计算,后续可以研究多线程信号传播。
    • 事件驱动架构:GUI 的事件处理本质上是事件驱动模型,可以进一步学习 Reactor 模式、消息队列等。
    最后,感谢老师和助教们精心设计的五次实验。从继承到接口,从控制台到图形界面,从简单逻辑门到层次化电路,每一步都在逼迫我突破舒适区,真正理解了面向对象的精髓。
posted @ 2026-06-24 22:28  曼波爱吃凉皮  阅读(2)  评论(0)    收藏  举报