本单元重点在于UML类图,顺序图和状态图的结构化理解。三次作业要求我们逐步实现对UML模型的解析以及初步的正确性检查。
前两次作业实现了三种图的一些查询方法,重点在于如何对传入的元素进行建模,选择合适的数据结构将各个UML元素联系起来。
首先必然需要在数据传入的主类(指实现了UserApi的MyImplementation类),对所有元素做统一的保存,这里我选择嵌套的hashmap,第一层区分元素类型,第二层是某类元素根据id或name索引的hashmap。值得注意的是,由id区分的map是static的,相应地我使用public static方法将其暴露,这是便于各类元素在后面的封装时能够轻易地根据id访问到其依赖的元素从而建构联系。
private static HashMap<ElementType, HashMap<String, MyElement>> elementsById = new HashMap<>();//根据元素类型和id区分 private HashMap<ElementType, HashMap<String, HashSet<MyElement>>> elementsByName = new HashMap<>();//根据元素类型和name区分(处理同名情况) private static HashMap<String, ElementType> idToElementType = new HashMap<>();//作为辅助结构,可根据id得到元素类型,便于直接通过id找到对应元素
但是怎么建立联系呢?相当自然地,我对读入的各类UML元素做了封装,便于在相应层次添加方法进行操作,而建立联系便可通过在构造方法中(正在封装)调用相关元素的方法进行通知(即更新相关元素中的一些数据结构)。
注意到UML模型是有层次的,元素之间具有依赖,我们希望在下层元素读入时其依赖的上层元素均已读入,然而输入并未保证这一点,故我们需要多次遍历输入,每次读入一个层次的元素进行建模。
public MyImplementation(UmlElement... elements) { for (ElementType elementType : ElementType.values()) { elementsById.put(elementType, new HashMap<>()); elementsByName.put(elementType, new HashMap<>()); } for (int i = 0; i < 5; i++) { for (UmlElement element : elements) { if (Packer.getLevel(element) == i) {//getLevel用于取得UML元素的层级 MyElement myElement = Packer.pack(element);//pack用于统一进行元素封装 elementsById.get(element.getElementType()). put(element.getId(), myElement); if (!elementsByName.get(element.getElementType()). containsKey(element.getName())) { elementsByName.get(element.getElementType()). put(element.getName(), new HashSet<>()); } elementsByName.get(element.getElementType()).get(element.getName()). add(myElement); idToElementType.put(element.getId(), element.getElementType()); } } } }
通过大致如上的方法,当主类构造完毕时,我们也获得了一个各类元素的id对应表和name对应表,每个元素都管理着所有与其相关的元素信息(这是理论上可以做到的,但具体在建构联系时保留什么信息,是可以根据需要选择的),这样事情便变简单了。对于前两次作业的各类UML图查询方法,我们均可以把具体任务交给对应元素进行处理,主类仅仅需要获得调用的返回值。例如查询某个类的子类数量,主类调用该类的相应查询方法得到的返回值就是数量。我认为,这种处理方法体现了面向对象的优点,层次清晰,什么类就管什么信息,查询某类的信息自然地就调用其中的查询方法。
第三次作业要求对UML模型进行一个初步的正确性检查,检测是否违反规则。其实有了前面的数据结构,完成这些并不是很难(虽然bug也确实容易出现),我完成这些检查大致有两种方法,对于那些独立性较高的规则,比如接口的属性必须public,在读入数据并封装时就可以判断是否违规;对于那些涉及多种UML元素或者不能简单判断的规则,比如循环继承,就需要在全部数据读入后整体地进行判断。
第一单元在我看来是最难的一个单元,我们需要实现一个最终支持三角函数,求和函数以及自定义函数的表达式解析化简(要求去除非必要括号)程序。
该单元主要难在架构设计上,虽然输入的就是一行行字符串,要求的化简形式也在形式化定义中做了相对详细的规定,但这中间的化简过程完全是自由的,那么究竟如何设计类之间的继承和协作关系以及依赖的数据结构来完成这项任务就十分考验人。在借鉴了实验代码并同同学交流之后,我逐渐学会将一个较为复杂的过程如表达式化简抽象成一些功能和结构(比如项,表达式,因子,解析器),再通过构建相应的类负责一部分功能(比如表达式类描述一个表达式的结构,而解析器负责递归解析),从而达到每个类各司其职,并通过方法进行协同,这样从顶层逻辑进行层次化的展开能够使得代码逻辑清晰易于理解,同时也大幅度避免了面向过程编程中由于代码逻辑复杂度随需求复杂度剧增而产生的bug。
第二单元的重点在于多线程情况下的编程,主要难在熟悉java中多线程的互斥锁以及wait-notify机制。由于是模拟电梯情景,很多主要类都很自然地符合现实逻辑,有特点的就是线程类和缓冲区的协作。这个单元让我更加熟练了面向对象的设计方法,同学们分享的SOLID方法也十分具有启发性,我也根据这些原则审视了自己的代码,发现确实还有很多不足,比如某些类过于复杂,耦合度过高的问题。
第三、四单元有点类似,最终的任务都是实现接口的一些方法,目标十分明确,因此类的架构上并不是很困难。第三单元的JML令我印象深刻,想要运用好JML语言对代码进行工程化的规范是挺难的,不过我认为还是十分有价值的;第四单元让我对UML模型有了更为深刻的理解,我发现UML图其实是很精确的一种描述工具,图中的元素以一定的规则能够很好地展现一个工程的结构。
随着学习的深入,我越发感受到面向对象的方法天生适合工程化的开发,通过将支撑满足需求的结构抽象出来,开发者可以从不同的逻辑层次审视工程结构,而每一层次得以保持简洁清晰。这样的特点不仅有利于减少大型项目下的bug,更有利于多人的协同开发以及甲乙方之间的需求沟通。
其实各个单元的测试的基本结构都是一致的,分为数据生成、运行程序、检查输出三个部分。
运行程序的部分没什么好说的,甚至单元间都不用改动。
数据生成有自动生成和手动构造两大方式,前者根据输入的形式大量生成,随机性强且量大,适合保证程序的基本正确;手动构造在于根据程序中一些逻辑复杂或者复杂度要求较高的实现有针对性地构造数据,检测程序在压力较大情况下的正确性。理想情况下,两种方法不可偏废,我便出现过为了偷懒仅测试了自动生成的数据导致在互测中被针对性地hack的情形。
我认为适合检查输出的方法在各个单元都不尽相同。第一单元表达式化简有现成的python库可以辅助检查,自然应当使用;第二单元电梯输出的正确性仅需满足一定的合理性规则即可,故可以编写相应程序直接验证;而第三、四单元输出的正确性很大程度上依赖个人对需求的理解,依赖错误理解编写的正确性检查程序自然是无效的,故我选择利用对拍程序与同学的实现进行比对,事实证明这也是十分方便而有效的一种方法。
整体来看,我在学习过程中尤其是完成作业时,越发感觉到合理的测试之于代码的重要性,甚至不夸张地说,测试本就是代码的一部分,没有测试的代码是不完整、不可靠的。
OO课程是质量很高的一门课,虽然我的成绩比较普通,比不上那些大佬,但是还是有不少的收获:
为了课程能建设得更好,我个人在此给出三点建议,仅供参考: