一、前言

第二阶段的面向对象程序设计作业已全部完成。这一阶段我们告别了航空器配载管理系统,迎来了全新的"数字电路模拟程序"题目。如果说前三次作业侧重于类间关系的梳理与职责分配的初步探索,那么这两次作业则将我们推向了更复杂的系统设计场景——从五个基础逻辑门扩展到九种电路元件,从单一输出引脚演变为多引脚、多类型的复杂接口体系。这是对面向对象设计能力的真正考验。

两次作业的题量延续了前一阶段"迭代式增长"的特点。第四次作业以五种基本门电路为起点,构建了引脚-元件-系统的三层架构;第五次作业在此基础上新增了三态门、译码器、数据选择器、数据分配器四种元件,同时引入了控制引脚、多输出引脚、无效状态等新概念。每一次迭代都要求在既有框架上平滑扩展,这迫使我们思考如何设计出具有良好扩展性的类结构。

难度上,这次的新挑战在于"需求的专业化"与"体系结构的复杂化"并重。数字电路涉及的专业术语(如高阻态、译码、数据选择等)需要我们主动查阅资料理解,而元件种类的增多、引脚类型的细化、输出格式的多样化则对代码架构提出了更高的要求。但令人欣慰的是,经历了前三次作业的锤炼,我在面对复杂需求时已经能够从容地进行类图设计、职责划分和迭代规划,这本身就是一种显著的进步。

与前三次作业相比,这次最大的收获在于对"开闭原则"(OCP)的深刻理解——如何设计一个对扩展开放、对修改封闭的系统。在第五次作业中,新增的四种元件几乎可以无缝接入第四次作业的框架,这验证了良好设计的价值。同时,我们也更加认识到"接口隔离"的重要性,不同类型的引脚(输入、输出、控制)需要有不同的行为,合理的抽象层次让代码既清晰又灵活。

二、设计与分析

第四次作业:数字电路模拟程序-1

· 要求

设计一个数字电路模拟系统,支持与门(A)、或门(O)、非门(N)、异或门(X)、同或门(Y)五种基本逻辑门。系统需要解析输入信号、处理元件间的连接关系,并按规定的顺序输出各元件的输出引脚电平。如果某个元件的引脚没有接有效输入,则输出忽略该元件。

· 代码规模
4

本次作业共涉及10个Java源文件,包括1个主类、1个系统控制类、5个具体门电路类、1个抽象元件类、1个抽象引脚类、2个引脚子类和1个排序工具类。总代码行数约500行。

· 类图
System4

第四次作业的类结构采用经典的继承与组合模式:

  • 抽象元件类Element:定义所有逻辑门的公共属性和行为,包括编号、引脚列表、查找引脚、计算输出信号等抽象方法。
  • 五个具体门电路类分别继承Element,实现各自的逻辑计算逻辑和输出格式。
  • 抽象引脚类Pin:定义引脚的编号、信号状态、获取信号等基本能力。
  • 输入引脚InputPin和输出引脚OutputPin继承Pin,分别实现输入引脚和输出引脚的特殊行为(输入引脚需要从上游获取信号,输出引脚需要从所属元件计算信号)。
  • CircuitrySystem系统控制类:负责管理所有元件和输入信号,处理连接关系和输出调度。
  • ElementSorter排序工具类:实现按指定顺序对元件进行排序。

· 总结

第四次作业的设计核心在于"递归计算"思想的实现。电路信号从输入端流向输出端,但计算时采用"从输出端反向追溯"的策略:当需要获取某个输出引脚的信号时,它会调用所属元件的setOutputSignal()方法,该方法又会去获取所有输入引脚的信号,而输入引脚则从与其相连的输出引脚获取信号。这种递归方式天然地解决了信号传播顺序的问题,无需显式地进行拓扑排序。

在引脚设计上,我采用了抽象类加继承的方式,将输入引脚和输出引脚的差异封装在各自的setSignal()方法中。输入引脚知道如何从上游获取信号,输出引脚知道如何从元件获取计算结果。这种设计使得信号的传递过程对于上层系统是透明的——系统只需要调用引脚的setSignal()方法,具体的信号来源由引脚自己决定。

然而,这次作业也存在一些不足。CircuitrySystem类的输出方法在处理输出引脚信号时,逻辑略显冗余:先判断信号是否为-1,若非-1则打印,否则调用计算后再判断一次。实际上,可以将判断和计算的逻辑统一封装到各元件的输出方法中,让元件自己决定何时计算、何时输出。

此外,排序逻辑虽然独立成了ElementSorter类,但排序算法使用了冒泡排序,效率不高。在元件数量较少时尚可接受,但作为一种通用设计,采用Comparator接口并结合Java内置排序方法会是更优雅的方案。

第五次作业:数字电路模拟程序-2

· 要求

在第四次作业的基础上,新增三态门(S)、译码器(M)、数据选择器(Z)、数据分配器(F)四种元件。新增控制引脚类型,多输出引脚元件需要按特定格式输出。译码器输出"输出为0的引脚编号",数据分配器按顺序输出所有输出引脚的信号,无效状态用"-"表示。

· 代码规模
5

本次作业代码量显著增长,所有类合并到单个Java文件中(出于提交便利),总行数约800行。新增了ControlPin控制引脚类、以及三态门、译码器、数据选择器、数据分配器四个具体元件类,并对系统控制类和引脚连接逻辑进行了扩展。

· 类图
System5

第五次作业在第四次架构基础上进行了扩展:

  • 新增ControlPin类:继承Pin,表示控制引脚,其行为与输入引脚类似(从上游获取信号),但在语义上独立,便于后续扩展。
  • 四个新增元件类继承Element,实现各自复杂的引脚创建逻辑和信号计算逻辑。
  • 输入引脚的连接方式升级:从单个前向引脚变为前向引脚列表,支持一个输入引脚连接多个输出引脚的情况(虽然约束条件规定不允许,但为健壮性考虑做了泛化)。
  • 系统控制类新增连接方法重载:用于处理外部输入信号直接连接到元件引脚的情况。

· 总结

第五次作业最大的挑战在于三种复杂元件(译码器、数据选择器、数据分配器)的引脚创建与信号计算逻辑。它们的共同特点是:引脚数量不固定(取决于输入引脚数或控制引脚数)、引脚类型多样(控制/输入/输出混合)、输出格式特殊(不是简单的"引脚号:信号值")。

以译码器为例,其引脚顺序为:3个控制引脚、n个输入引脚、2的n次方个输出引脚。在创建引脚时需要精确控制每个引脚的编号和类型,并且在计算信号时需要先解析控制引脚信号判断是否有效,再解析输入引脚信号计算哪个输出引脚为0。这一过程涉及大量细节,稍有不慎就会导致索引错位。

数据选择器和数据分配器则引入了"控制信号决定选通"的逻辑。前者根据控制信号从多个输入中选择一路送到输出,后者根据控制信号将一路输入送到多个输出中的一路。两者的实现思路相似,但输出格式截然不同——数据分配器需要按顺序输出所有输出引脚的状态,无效引脚显示为"-"。

在迭代过程中,我遇到了一些典型的"扩展性陷阱"。例如,在第四次作业中,输入引脚的前向引脚设计为单个对象,这在第五次作业中面对更复杂的连接场景时显得不够灵活,我将其升级为列表。这提醒我在设计初期就应该考虑到未来可能的扩展需求,选择更具泛化能力的数据结构。

另一个值得反思的地方是信号计算的"短路"逻辑。在或门的计算中,最初的设计是"只要有一个输入为1就输出1并立即返回",但如果后续有输入引脚信号为-1(未计算出),这个返回值就可能是错误的。修正后的逻辑先全面评估所有输入的状态再决定输出值,这说明在递归计算中,不能简单地"见好就收",而必须完整地考虑所有依赖。

三、踩坑心得

1. 正则表达式的使用陷阱

在解析输入时,我多次遇到正则匹配与取值配合不当的问题。group()方法必须在find()返回true后才能调用,否则会抛出异常。在第四次作业中,我一度因为在条件判断中直接使用group()而忽略了对find()结果的检查,导致程序在某些输入格式下崩溃。修正后的代码严格遵循"先find再group"的顺序,并且在访问分组内容前确保匹配成功。这一教训让我深刻理解了API使用规范的重要性。

2. 字符串比较的疏忽

在查找元件的逻辑中,我最初使用==运算符来比较两个字符串是否相等。这在C语言中是不被允许的(需要strcmp),但在Java中语法上可以通过,实际上比较的是引用地址而非内容。这个bug导致即使两个字符串内容相同也无法匹配成功。排查这个问题花费了不少时间,让我认识到语言特性差异带来的隐患,以及Java中比较字符串必须使用equals()方法这一基本规则。

3. 递归计算的出口条件不明确

在系统输出方法的设计中,我最初只判断了输出引脚信号非-1就打印,忽略了信号为-1时需要触发计算。这导致程序在初始状态下一片空白,所有元件都没有输出。究其原因,是我在设计时没有清晰地定义"何时触发计算"——计算应该由输出引脚在被需要时主动发起,而不是由系统预先计算好所有值。修正后的设计遵循了"惰性计算"的思想:只有在需要输出时才递归地计算信号值,计算完成后再次判断是否有效。

4. 引脚连接时的空指针防护不到位

在引脚连接的实现中,我最初没有对传入的参数进行判空处理,直接添加到列表中。当某个输入引脚在连接信息中被遗漏,或者解析出错时,列表中会出现空元素,后续遍历时抛出空指针异常。添加判空逻辑后,程序对不完整输入的鲁棒性大大提升。这也让我意识到,防御性编程在处理外部输入时是必不可少的。

5. 多输出元件的输出格式实现偏差

数据分配器的输出要求按引脚编号顺序打印所有输出引脚信号,无效状态用"-"表示。我最初的做法是直接拼接字符串,但忽略了"所有输出都无效时忽略该元件"的规则。修正后的逻辑先检查是否至少有一个输出引脚有效,如果没有则直接返回,否则才构建输出字符串。这种"先校验再执行"的模式在处理复杂输出格式时非常有效,可以避免输出空数据或错误格式。

四、改进建议

1. 引入设计模式优化架构

当前的元件创建采用简单的多分支选择语句,每次新增元件都需要修改系统类的创建方法,违反了开闭原则。可以考虑引入工厂模式,将元件创建的逻辑封装到独立的工厂类中,新增元件时只需添加新的工厂子类,无需修改已有代码。这样不仅提高了扩展性,也使创建逻辑更加清晰集中。

2. 优化排序逻辑

排序类目前使用冒泡排序和硬编码的排序顺序数组。建议改用Comparator接口,结合枚举类型定义元件优先级,并使用Java内置的排序方法,既提高效率又增强可读性。同时,编号的比较应该统一使用整数比较方法,避免手动转换和比较带来的潜在错误。

3. 增强引脚连接的合法性校验

目前对于"一个输入引脚不能连接多个输出引脚"的约束没有强制校验,虽然存储结构支持多个,但并不检查是否超过限制。应该在校验方法中添加检查,若已存在连接则给出明确提示,这能帮助开发者在调试阶段及时发现非法输入。

4. 完善注释和文档

本次作业的注释率依然偏低,核心算法(如译码器的信号计算、递归信号传播过程)缺少必要的解释性注释。特别是涉及专业领域知识(如数字电路)的代码,良好的注释可以极大提升可维护性。在后续学习中,应该养成"为复杂逻辑写注释"的习惯,不仅写"做了什么",更要写"为什么这样做"。

5. 分离输入解析与业务逻辑

主类承担了过多的输入解析职责,正则表达式的编译和匹配逻辑混在主流程中,使得代码可读性下降。可以设计一个专门的输入解析类,负责将原始输入转化为结构化的数据对象,系统控制类只接收这些结构化对象进行处理。这样既降低了耦合度,也便于后续扩展新的输入格式。

五、总结

在第二阶段的数字电路模拟程序开发中,我从基础逻辑门到复杂组合元件,完成了一次完整的系统迭代演进。这个过程让我深刻体会到,面向对象程序设计不仅仅是语法层面的"用类组织代码",更是一种"如何应对变化"的设计哲学。

两次作业的核心挑战都围绕着"如何设计一个可扩展的系统"。第四次作业提供了基础框架,第五次作业则考验这个框架的弹性——新增四种元件、新增引脚类型、新增输出格式,所有这些变化都应该在尽量不修改已有代码的前提下完成。虽然最终实现中仍有诸多不足,但我已经初步体会到开闭原则的实践意义:一个好的抽象可以让新增功能成为"添加"而非"修改"。

与此同时,递归计算思想的运用让我对"职责驱动设计"有了更直观的认识。每个引脚负责获取自己的信号,每个元件负责计算自己的输出,系统只负责调度——这种各司其职的设计让复杂的电路模拟变得清晰可控。这也呼应了单一职责原则:让每个类只做一件事,并把它做好。

展望未来,我需要在以下几个方面继续深入:一是设计模式的系统学习与实践,让代码架构更加优雅;二是单元测试的引入,确保迭代过程中既有功能不被破坏;三是代码质量意识的提升,包括注释规范、命名规范、异常处理等。编程之路漫漫,每一次作业都是一次成长的机会,从航空器配载到数字电路,从类间关系到设计模式,我正一步步走进面向对象编程的世界,期待在后续的学习中继续探索与进步。