OO架构设计总结
OO架构设计总结
第四单元
本次作业考察UML相关的内容,目标学会熟练使用UML进行软件设计并使用模型化思维对数据进行建模和管理。
- 第一次:UML入门级的理解、UML类图的构成要素,编写解析方法(支持对类图的解析)
- 第二次:扩展解析器,使得能够支持对UML顺序图和UML状态图的解析
- 第三次:支持对模型进行有效性检查
第四单元作业架构设计
public class UmlTreeNode {
private UmlElement elm;
private ElementType type;
private String id;
private HashSet<UmlTreeNode> sons = new HashSet<>();
private UmlTreeNode parent;
private String name;
/* ... */
}
仍然使用去年的装饰者模式,其允许向一个现有的对象添加新的功能,同时又不改变其结构,而仅作为现有的类的一个包装。就增加功能来说,装饰器模式相比生成子类更为灵活。此次将原来的UmlElement
类型包装起来进行拓展为UmlTreeNode
,并将其实现类继承此装饰类,使这种策略,可以把原有的稀少的,功能不够全面的结构,用新的,具有强扩展能力的类包装起来(本文参考)。例如,可以增加设置、获取其具有parentID
作为id的结点元素(父节点)的功能,同时存储以此结点元素的ID为parentID
的结点元素(子节点)。
对于第一次作业,在UmlTreeNode
下有UmlClassLevelNode
、UmlLizeNode
、UmlStaticNode
、UmlOptNode
子类,而UmlClassLevelNode
下又有UmlClassNode
、UmlInterfaceNode
两类。
除此之外,我还建立了NodeFactory
类用于对不同类型的UmlElement
进行分类实例化我的装饰者类并组织存储,同时再循环访问所有UmlClassNode
、UmlOptNode
和UmlInterfaceNode
类型的元素,以向其添加静态元素属性(UML_PARAMETER
和UML_ATTRIBUTE
的装饰者类都是UmlStaticNode
)。
其中,我对于结点对象的存储方式比较丰富,便于各种查询添加,例如:
private final HashMap<String, ArrayList<UmlStaticNode>> umlStaticHM = new HashMap<>();
//以其parentID作为key的静止结点,可以记录每个同父节点的子节点。
private final HashMap<String, UmlClassLevelNode> umlClassLevelIdHM = new HashMap<>();
//以自身id为key的class或interface结点,用于遍历查询。
private final HashMap<String, ArrayList<UmlClassNode>> umlClassNodeNameHM;
//以自身name为key的class结点,用于判断重复异常,同时此图会被传出到MyUmlInteraction。
private final HashMap<String, UmlClassNode> umlClassNodeIdHM;
//以自身id为key的class结点,传到MyUmlInteraction后用于查询。
对于第二次作业仅添加了UmlInteractionNode
、UmlLifelineNode
、UmlMessageNode
、UmlRegionNode
、UmlStateMachineNode
、UmlStateNode
、UmlTransitionNode
类以达到查询需求(实际上还有一些冗余,例如UmlEvent
并没有方法涉及,但我设计了接口)。
对于一些需要遍历搜索的方法,例如getImplementInterfaceList
,在第一作业中我使用BFS查询元素,并将方法放于MyUmlInteraction
中,这使得其十分冗长,于是在第二次作业中,对于需要BFS查询后继状态的方法,我将其放在了新添加的类中作为结点的方法,同时得到的结果作为结点的属性,这样可以避免重复查询。同时我添加了ExceptionJudgement
类专门作为抛出异常的实现类,还有UmlMethods
类用于存储MyUmlInteraction
中的实现过程方法。
对于第三次作业,我在ExceptionJudgement
类中添加了抛出异常类型,其中频繁使用了之前存储的各种HashMap
以及BFS来遍历搜索。例如对于R002
,我分别遍历了umlClassNodeIdHM
和umlInterfaceIdHM
,对于每一个结点都使用BFS判断其是否可以找到一个环回到自己从而说明其是否重复继承。对于其他几种规则都是如此,总的来说都是利用本身装饰者类的功能和属性来查询判断,如果有更多需求,添加即可。
然而最后一次作业因为没有考虑到一种极为特殊的情况,因为架构的解耦反而使得我在增量开发时忽略了一个潜在边界问题。
架构设计和OO方法
面向对象程序设计(英语:Object-oriented programming,缩写:OOP)是一种具有对象概念的程序编程典范,同时也是一种程序开发的抽象方针。它可能包含数据、属性、代码与方法。对象则指的是类的实例。它将对象作为程序的基本单元,将程序和数据封装其中,以提高软件的重用性、灵活性和扩展性,对象里的程序可以访问及经常修改对象相关连的数据。
面向对象程序设计可以看作一种在程序中包含各种独立而又互相调用的对象的思想,这与传统的思想刚好相反:传统的程序设计主张将程序看作一系列函数的集合,或者直接就是一系列对电脑下达的指令。面向对象程序设计中的每一个对象都应该能够接受数据、处理数据并将数据传达给其它对象,因此它们都可以被看作一个小型的“机器”,即对象。 ——Wikipedia
四个单元的内容是由层次化设计 ➡️ 线程安全设计 ➡️ 规格化设计 ➡️ 模型化设计的逻辑进行的,而其内在又蕴含了OOA(面向对象系统分析)和OOD(面向对象系统设计)两方面的内容。
- 面向对象的分析方法是利用面向对象的信息建模概念,如实体、关系、属性等,同时运用封装、继承、多态等机制来构造模拟现实系统的方法。
- 面向对象设计方法是面向对象程序设计方法中一个环节。其主要作用是对分析模型进行整理,生成设计模型提供给OOP作为开发依据。OOD包括:架构设计、用例设计、子系统设计、类设计等。
四个单元都贯穿了这些理念,只是各个侧重点有所不同。
多项式求导单元,我抽象化出了Polynomial
多项式因子、Monomial
单项式因子、SinFactor
以及他们的共同继承的父类Factor
等等模块,其间我运用了封装、继承、多态、接口等机制,这是OOA方法的体现,同时对模型的模块化抽象体现出面向对象的层次化设计,这与表达式树中类与接口的构造关系都是OOD思路的一环。多线程电梯单元,在多线程场景下学习面向对象思维、解决线程安全问题、理解消息传递机制,这些在现代计算机系统中是不可或缺的组成。在JML单元,规格化设计将运行细节抽象为用户所需求的行为,体现了面向对象的关注点。UML单元中,图形化设计明确、清晰、唯一,更便于交流、维护,有效性检查确保了交流有效性。
每个单元三次作业的递进也可以解释这些设计哲学。
Walking on water and developing software from a specification are easy if both are frozen.
走在结冰的河边不会湿鞋,开发需求不变的项目畅通无阻。 -Edward V. Berard
OO作业的三次作业是难度的提升,是需求的改变,现实中这种变化是普遍发生的,做到
- 面向对象
- 复用
- 能以最小的代价满足变化
- 不用改变现有代码满足扩展
因为今年是在去年的基础上写的代码,因此尤其是三四单元基本上原有代码几乎没有改动,尤其是第四单元,基本上做的是纯增量开发,因此可以说第四单元的设计较好,但是因为第四单元第二次作业出现了一个麻烦的指令,使得在本架构上需要做不是很优雅的变动才能够实现需求,这让我对之前的设计有了更深的理解。
而第二单元则是完全重构了。代码不可避免地会有重构地情况出现,尤其是之前的代码出现了大问题的情况下,避免之前的坑,并且利用之前代码设计中意识到的性能与coding方便性上可优化的空间,充分考虑以对类似或者说迭代的需求设计新的结构(从去年到今年可以人为需求上是相似的,也即目标上是相似的;从每个单元内部的不同次作业而言是迭代的),并进行新的coding。
测试理解与实践的演进
测试主要分为构造测试数据,运行测试数据和对比结果三个方面。因此对于黑盒测试,主要用到的工具无外乎就是构造数据的python代码,运行程序的脚本代码和比对结果的python/脚本代码。而对于单元测试,那就是JUnit了,编写其代码的方法网上比较全。下面我大致说明一下历次作业的测试思路:
第一单元作业照着去年别人在结束后分享的的思路写了一个测试。使用了python中的Xeger
函数,根据正则表达式随机生成相应的字符串。然后利用管道编译java文件对其输入输出,最后利用sympy
中的求导函数diff
对正则表达式生成的数据求导,最后按照评测机的测评方法,比对python求导后的值与程序跑出的结果,验证程序的正确性。不过感觉生成的仍然不是很好,边界数据仍然是手动构造。
第二单元作业我用python完成了一个定时输入程序,可以向打包好的jar输入完整格式的请求。并且编写了判断结果正确性的程序,主要用是否将乘客送到终点、是否无中生有、是否电梯吃人、乘客是否穿墙等条件判断。在debug时,由于调试和在IDEA里面运行都效率极低且无法复现出问题,我逐渐领悟出了在代码间插入输出,然后用自动化测试工具运行的方法,效率大大提升。
第三单元作业使用python构造数据集,因为有明确的命令条数和格式要求,所以比较方便实现,注意需要用数组存住生成的person来构造环。需要使用他人的代码进行对拍,同时使用脚本比较文件(fc命令)。
第四单元没测试,因为不好构造数据。
总的来说,黑盒测试能在很大程度上解决问题,但是有些时候构造数据的程序针对性不强,导致效率十分低,同时需要找别人的程序对拍,这是一个缺陷。所以黑盒测试只能拿来找到编码上的、对题意理解不当的和比较明显的程序逻辑上的问题,JUnit测试同样只能解决上述bug。而对于情况未充分考虑的以及性能问题则需要形式化验证,计算和思考,这其中,需要积极与同学讨论,多查资料,关注数据结构细节。
课程收获
由于今年已经是重修,因此避免了很多去年出现的问题。然而去年出现问题的几次作业,今年仍然出现了问题。
在去年发挥不佳的第二单元里面,今年取得了不错的成绩,因为相较去年,设计出了相对简洁而又强大的架构。这一方面得益于去年失败设计的经验,另一方面也得益于去年讨论区一个大佬的无私的经验分享。从而可见经验互享可以互相促进(虽然此处似乎是单项促进)。汲取前人经验可以最大程度地避免无知的损失。
最大的感悟应该还是在今年仍然强测出了bug的几次作业上,总的来说,需求有变化,但是基本上可以归在大的框架里解决,然而却屡屡因为边界问题失去大量分数。具体而言,重修的过程中,虽然需求变化并不是很大,但是有时候测试会发生变化,今年的代码出现了复用去年的模块却被新的边界测试检出bug的情况。对于开发而言,永远不应该面向测试编程,对于边界条件应该有系统性的、根源性的思考。没有测出bug不代表没有bug,就像真正流入市场的app无法控制用户做出怎样的边际行为,对于OO程序而言,通过课程组的测试只是通过了一重检验,实际上它的强大应该在于其架构内核、其精准的实现、其全面的考虑,再加上各种测试全方位构建的鲁棒性。测试对于程序员而言是内功,然而对于程序而言就像外功,程序的内功(内部结构)才是应对外部变化的最核心的法宝。
改进建议
- 希望有官方测试教程,系统地并且结合作业例子教大家如何进行测试
- 希望可以开放测试的Docker镜像,以方便感知OO作业测试的精髓,了解大规模测试的布局
- 希望可以有针对每次实验的系统性的答案或讲解