面向对象设计与构造 —— 数字电路单元总结
面向对象设计与构造——数字电路单元总结
写在前面
OO课程的数字电路单元结束了。本单元以数字电路模拟程序为核心,通过三次迭代逐步扩展功能,从基础逻辑门运算到组合电路元件,再到子电路嵌套与异常检测,我在实践中收获了很多:
熟悉了Java面向对象的进阶用法;
初步建立了迭代式开发的设计思路;
认识到了设计模式对代码可拓展性的重要作用;
学会了构建自动化测试工具与边界用例构造方法。
Complexity Metrics(复杂度分析)
因为下面要用到复杂度分析,所以先在此给出一些相关概念。
我们需要使用的主要是方法和类的复杂度分析。方法的复杂度分析主要基于循环复杂度的计算。循环复杂度是一种表示程序复杂度的软件度量,由程序流程图中的“基础路径”数量得来。
ev(G):即Essentail Complexity,用来表示一个方法的结构化程度,范围在[1,v(G)]之间,值越大则程序的结构越“病态”,其计算过程和图的“缩点”有关。
iv(G):即Design Complexity,用来表示一个方法和他所调用的其他方法的紧密程度,范围也在[1,v(G)]之间,值越大联系越紧密。
v(G):即循环复杂度,可以理解为穷尽程序流程每一条路径所需要的试验次数。
对于类,有OCavg和WMC两个项目,分别代表类的方法的平均循环复杂度和总循环复杂度。
下面我将从程序结构,公测、互测以及bug分析几个方面来总结我本单元的三次作业。
第一次作业
作业要求
实现五种基础逻辑门(与门、或门、非门、异或门、同或门)的电路模拟,支持多输入引脚的与门、或门,解析电路输入信号与引脚连接关系,按指定顺序输出所有有效元件的输出电平。
实现方式
采用面向对象分层设计:抽象出基础元件类,五种逻辑门继承自该类并实现各自运算逻辑;使用HashMap存储全局输入信号与引脚连接关系,通过拓扑排序确定元件计算顺序,依次推导各元件输出。
代码规模
第一次作业整体代码体量较小,结构简单清晰,完整代码规模统计如下:
从统计结果可以看出,本次作业总代码行数386行,有效代码318行,注释占比8%,空行占比10%,代码结构十分紧凑,没有冗余内容。
类图设计
本次作业的类结构以Gate抽象基类为核心,各逻辑门继承实现,Circuit负责电路整体调度,完整类图如下:
复杂度分析
第一次作业的方法循环复杂度统计如下:
可以看出Circuit的parseConn方法和printResult方法的复杂度很高,因为这两个方法有一个共同点,就是用了很复杂的if/else条件判断语句。
而parseConn方法条件语句复杂,是对元件名格式解析与正则表达式应用不熟练导致。
Bug分析
公测
笔者的程序在公测中没有出现正确性错误,但是由于同类元件编号排序未处理不连续编号,导致一个点输出顺序错误,性能分不满。
互测
笔者的程序在互测中被发现一个bug,原因是笔者在处理单输入与门时,边界判断逻辑错误,导致输出结果异常。
笔者在互测中,hack了其他人12次,大部分都是字符串解析的边界处理错误导致的,主要表现在对元件名格式、空白字符处理考虑不周全。
测试方法
手动构造边界测试用例,覆盖单输入门、全0全1输入等场景;
编写Python脚本批量生成随机电路,与手动编写的参考程序对拍验证结果正确性;
阅读每个人的代码寻找漏洞,在互测中有2个bug是我读代码读出来的。
第二次作业
作业要求
在基础逻辑门之上新增三态门、译码器、数据选择器、数据分配器四类元件;引入控制引脚概念,引脚按“控制-输入-输出”顺序编号;调整各类元件的输出格式,适配多输出元件与无效状态。
实现方式
扩展元件类层级,新增带控制引脚的抽象元件类,四类新增元件继承自该类并重写运算逻辑;重构引脚解析模块,统一管理不同类型引脚的编号映射;重写输出模块,根据元件类型生成对应格式的输出。
代码规模
第二次作业的代码规模统计如下。
类图设计
第二次作业新增CtrlGate抽象类作为带控制引脚元件的基类,整体类图如下:
复杂度分析
第二次作业的复杂度分析如下。
其中几个方法复杂度稍高,下面给出分析:
printResult方法需要处理9种元件的不同输出格式,包含大量条件分支,因此循环复杂度较高。
Decoder的calcOutput方法需要同时判断控制引脚状态与输入编码,逻辑分支较多。
而parseConn方法随着元件类型增加,引脚名解析的判断分支也随之增加,复杂度上升。
Bug分析
公测
笔者的程序在公测中,未出现正确性错误,但由于译码器输出引脚编号顺序与题目要求相反,故性能分未能拿到满分。
互测
笔者的程序在互测中被发现3个bug,主要来源于几个方面:
三态门控制引脚为低电平时,未正确标记输出为无效状态,仍输出了电平值。
数据选择器控制端与数据输入端的对应关系颠倒,导致选通通道错误。
解析元件名时,未正确区分译码器的输入引脚数与数据选择器的控制引脚数,造成元件初始化错误。
笔者在本次互测时发现他人bug 8个,主要集中在译码器控制逻辑错误、无效状态判断遗漏以及引脚编号映射错误。
测试方法
针对新增元件单独构造测试用例,覆盖正常工作与无效状态两种场景;
复用第一次作业的对拍框架,扩展元件类型支持,进行批量随机测试;
重点核对各类元件的引脚编号规则,针对性构造边界用例。
第三次作业
作业要求
新增子电路定义与实例化引用功能,采用组合模式实现子电路与基础元件的统一抽象;新增五类输入异常检测,按优先级输出错误信息;支持子电路的嵌套使用与独立命名空间。
实现方式
采用组合模式设计:定义统一的Component接口,基础元件作为叶子节点,子电路作为组合节点,二者对外提供一致的引脚访问与计算接口;新增子电路解析模块,处理子电路的定义、输入输出映射与实例化;新增异常检测模块,按题目指定的优先级依次检查每一条连接信息,仅输出优先级最高的首个异常。
代码规模
第三次作业的代码规模统计如下。
类图设计
第三次作业基于组合模式设计,Component为统一接口,基础元件与子电路分别实现
复杂度分析
第三次作业的复杂度分析如下。
对于其中几个比较复杂的方法,分析如下:
ExceptionChecker的checkConn方法需要按优先级依次检查五类异常,包含多层条件判断,因此结构化复杂度与循环复杂度都较高。
Parser.parseElement方法需要识别九类元件与子电路实例,分支判断多,且包含复杂的字符串解析。
SubCircuit.calcOutput方法需要管理内部元件的计算顺序与引脚映射,调用多个内部方法,设计复杂度较高。
结合三次作业的复杂度分析,仔细思考了可能的解决方法:
利用多态和继承,将每一类元件的输出、解析逻辑分散到各自类中,可以有效避免都塞进一个类的复杂度爆表。
对于各种条件语句,可以多写几种方法,然后拆分功能为独立子方法,单一方法只处理单一逻辑,减少复杂if-else嵌套。
采用工厂模式统一管理元件创建,将解析逻辑与元件逻辑解耦,降低类之间的耦合度。
Bug分析
公测
笔者的程序在公测中未出现计算正确性错误,但子电路输出时的元件名前缀格式有误,少了分隔符,被扣格式分;另外异常优先级判断中,将输入输出顺序错误的优先级排在了无输入之前,不符合题目要求。
互测
笔者的程序在互测中没有被发现功能Bug
笔者发现其他人6个Bug,主要集中在异常优先级判断错误、子电路引脚映射错位、多个子电路实例命名空间冲突三类问题。
而其中前两个Bug是笔者用手动构造测试集发现的,说明针对性手动测试用例比随机自动生成数据更有效。
测试方法
针对每一类异常单独构造测试用例,并构造多异常叠加的用例验证优先级;
编写多层嵌套子电路的测试用例,验证组合模式的层级调用正确性;
扩展对拍脚本,加入异常输出的校验逻辑。
关于设计模式的思考
在本单元的学习中,自我感觉对组合模式和工厂模式的理解在迭代中逐步加深,第一次作业的时候还只是简单的继承分层,到第三次作业引入子电路后,才真正体会到组合模式的强大。将子电路和基础元件统一抽象为Component,上层电路不需要关心内部是单个门还是复杂子电路,都可以用统一的方式调用,极大地提升了代码的可扩展性。
工厂模式也在本次迭代中体现出了明显的优势:将元件解析和创建的逻辑封装在Parser中,上层只需要传入元件名称就能得到对应的元件对象,新增元件类型时只需要新增对应类并在工厂中注册即可,符合开闭原则。如果在第一次作业就引入工厂模式,后面两次迭代新增元件时会轻松很多。
在之后的多线程学习中,我会继续深化面向对象设计模式的理解与工程落地应用。
标签: Java , 面向对象 , 数字电路 , 组合模式 , 迭代开发

浙公网安备 33010602011771号