BUAA OO Unit4 & Course Summary
BUAA OO Unit4 & Course Summary
一、本单元架构设计
需求简述:UML解析器
- 支持对于类图、顺序图以及状态图相关信息的查询
- 对于该
UML
模型的合法性检验
以下是三次作业总的类图:
-
总体设计:从上面的图可以看出,在
MyImplementation
类中,有MyClassDiagram
,MySequenceDiagram
以及MyStateDiagram
分别处理各自部分的解析、查询和合法性检验。同时,处于统一和规范性的考虑,将基本上所有的Uml*
都封装为My*
,储存必要的查询信息、元素之间的关系并以此构建图中的树状储存结构。 -
容器选择:基本上都采用了
Hashmap<String, MyElement>
或者Hashset<MyElement>
来储存“id-元素”,比较简单明了。仅在最后一次作业中,由于要考虑重复接口,使用了Arraylist
。 -
解析:采用多重循环读取的方式进行读取、自顶向下进行构建。由于元素之间关系都是由
parentId
给出的,与上面的容器设计相契合,所以很容易获取到元素的parent。 -
查询:由于模型的大小和输入的指令数都比较有限,所以处于简便考虑我没有在解析时进行维护,也没有采取记忆化搜索。查询逻辑大致分为以下两步:
- 按名获取目标元素,同时进行异常判断,如:
private MyClass getClassByName(String className) throws ClassNotFoundException, ClassDuplicatedException { List<MyClass> classList = this.classes.values().stream() .filter(myClass -> Objects.equals(myClass.getName(), className)) .collect(Collectors.toList()); if (classList.size() > 1) { throw new ClassDuplicatedException(className); } else if (classList.size() == 0) { throw new ClassNotFoundException(className); } else { return classList.get(0); } }
- 利用Stream等方式进行筛选、映射和收集合并,如:
public List<String> getTriggerList(String sourceId, String targetId) { return this.transitions.stream() .filter(myTransition -> myTransition.getSource().equals(sourceId) && myTransition.getTarget().equals(targetId)) .map(MyTransition::getEvents) .flatMap(Collection::stream) .map(MyEvent::getName) .collect(Collectors.toList()); }
-
合法性检验:
- 对于比较简单的检查,在解析是就进行判断,设置标志使出现一个不合法之后就无需检验其他
- 对于比较复杂的检查,因为要求完整的继承、实现接口链(图),待读入完毕后再进行,其中:
- 重复继承采用了dfs算法
- 循环继承中大于2的环采用Tarjan算法、自环进行特判
二、架构设计思维及OO方法理解的演进
第一单元
本单元作业的目标是进行表达式的括号展开与化简,其工作流程如下:
在实验课和训练的指导下,选择了用递归下降的方式进行解析,与之相配合的架构就是层次化设计:将表达式划分为表达式、项和各种因子三个层级,在解析的基础上完成复杂的计算时,我们只需要将其分配到每个对象,让每个对象完成自己负责的简单任务即可。
第二单元
本单元作业的目标是实现一个新主楼的电梯调度系统,初次接触了多线程编程,其架构设计的关键在于线程之间的协同以及线程安全的保证。我主要形成了InputHandler -> Scheduler -> Elevators
的两级生产者—消费者模型:第一层是输入队列作为生产者、全局等待队列作为共享变量、调度器作为消费者;第二层是调度器作为生产者,各个楼层/楼座的等待队列作为共享变量、电梯作为消费者。同时,在第三次作业借鉴了一点流水线模型的思想,新增加了RequestCounter
类用于结束线程、 Scheduler
类用于实现新请求及换乘的调度。
当然,本单元还有一个重难点就是多线程本身,如何利用锁的机制控制对临界区的访问、在合适的时候等待/唤醒线程,都是需要慎重考虑的。
第三单元
本单元作业的目标是根据 JML 的描述实现一个社交网络,因而架构设计的核心就是规格化设计。一方面,整体的架构必须遵循于规格的指导;另一方面,在每个方法的实现上又不能照搬规格,要在理解规格的基础上优化其性能和效率,颇有矛盾对立统一的意味。此外,在经历两个单元以后,已经初步有了面向对象的意识:会自然地将构建图模型的Graph
,Edge
以及支持算法的DisjointSets
抽象出来作为工具类,保证结构的清晰与复杂度的解耦。
第四单元
本单元作业的目标不再赘述,正如上文中所说的,重心落在对模型元素的解析和管理上。感觉上,这一单元的架构设计师最为自由的,既可以落实树形的结构、也可以“大道至简”而在查询上下功夫,算是对oo学习成果的一个检验。在本单元的设计中,我对元素的层次进行了合理划分、对有共性的部分进行了抽象合并(如MyState
和MyClassOrInterface
接口)、并选择合适的数据结构对元素进行组织等,初步地将所学融会贯通,培养更好的架构思维和能力。
此外,UML 语言本身就是一个纯面向对象的语言。图中的每个元素都是一个对象,UML 模型实际上是实例化的结果,对此的学习和了解也加深了我对面向对象的理解。
三、测试理解与实践的演进
第一单元
如上面所述,第一单元有一个顺序化的工作流程,所以可以对每一流程/模块进行阶段性的单独测试,确认基本没问题后再进行后续的开发,这样也可以避免出现bug难以定位的问题。在自动化测试上,可以根据 BNF 文法生成嵌套括号的表达式,再利用 Sympy 库(一个更nb的程序员)进行结果验证。此外,严重的问题,往往只需要简单的样例就能发现。所以手动构造一些数据也是很必要的:针对数据限制的边界情况、针对自己实现的优化等等,能更好地保障鲁棒性。
第二单元
在第二单元,首先应该保证单线程下的正确运行,再检验多线程的协同的正确性。所以,一开始可以针对单部电梯+少数几个请求测试电梯的调度策略是否符合预期、是否会出现死循环等犯病问题。私以为,第二单元的数据生成很容易,但结果的判定逻辑就比较复杂了,也是抱着大腿进行压力测试(doge):包括多电梯、随机请求、集中添加请求等情况进行正确性和效率的测试。当然,对于多线程的测试不是一次正确就能溜之大吉了,还必须重视潜在的线程安全问题。一方面由于复现性的问题需要多次测试,另一方面也需要利用println
或者专业工具 Jprofiler 协助调试——通过观察线程状态折线图以及cpu
占用时间,检查轮询和死锁的问题。
第三单元
由于第三单元输出的一致性,可以通过数据生成和多人对拍的方式进行测试,这里数据生成器的设计需要考虑如何提升数据的强度以及对于基本/异常情况的覆盖率。同时,由于对runtime有所限制,所以也需要针对时间复杂度高的指令构造边界数据:以加人 -> 加关系/加群 -> 大量指令
的方式进行。当然,在课程组的推荐下,我也尝试利用了 JUnit 进行了基本的单元测试、获得测试覆盖率。
第四单元
第四单元的黑盒测试与第三单元的思路基本一致,前两次作业可以采用随机数据+对拍的方式,也实现了比较高的测试覆盖率;而第三次作业对合法性进行检测,更好的方法可能是综合自己考虑的以及讨论区中比较”诡异“的边界情况,利用 starUml 直接画图导出数据。同时,在第三和第四单元,在给出了 JML 规格/具体要求的情况下,静态阅读代码(结合以指导书)也是很有效的一种测试方式。在最后一次作业中,正是因为忽略了一行指导书中的“特殊规定”导致了出错,也让我更加深刻地体会到白盒测试的重要。
小小总结
毫无疑问,测试是我这学期oo课中做的最差的一个部分,每个单元都只能实现很简单的数据生成、然后通过手动构造特殊、边界数据。继上学期计组的碌碌无为之后,也没有实现过一次完整的自动化测试,反而是对大佬产生了依赖,非常respect和感谢无私帮助我的朋友@FlyingAns、@Harahan还有@Longxmas。这也算我的一个遗憾了,希望在未来的学习中能在测试上有所突破,不求高深但求入门。
四、课程收获
- 对于面向对象的编程方式有了初步的理解。一开始觉得OO不过就是类、封装、继承、多态等种种;后来发现,它更是一种设计思想,要求层次化、解耦合:将一个大型的工程、任务进行分解,抽象出带有属性和操作的对象,然后进行关联组合等等。同时,从面向过程的短小代码中跳脱出来,意识到在实际项目庞大的代码中,一个优秀架构的重要性:不仅要立足当下、考虑复杂度和耦合度,更要放眼未来、考虑架构的可拓展性。
- 从零开始入门了java语言。基本熟悉了各种容器的特性和使用、浅尝了函数式编程的甜头、简单了解了JVM的内存管理和GC机制。
- 学到了很多相关知识。每个单元都有新东西:递归下降词法分析、多线程、JML规格、UML语言和契约式编程。此外还有常用的设计模式、在 checkstyle 督促下养成良好代码风格、时不时复习回顾经典的算法等等。
- 积累了写文档的经验。虽然我非常不喜欢写这种东西,但不得不承认每单元一次的博客还是很有用的。毕竟以后可能要写设计文档开发文档测试文档用户手册FAQ等等等等诸如此类的东西,现在开始学着写以后肯定也得心应手一点。
五、改进建议
提供有关测试方面的教学!
有时候并不是不想做,而是无从下手。既然多次强调了测试的重要性,又让我们完全通过自学的方式学习测试,感觉不太合理。希望至少能有一两节课或是其他方式把测试“领进门”,在以后可以帮助到和我一样能力有限的同学。
其他建议
-
完善实验课的安排。首先是给予实验结果的反馈、提供正确答案;除此之外,可能还需要以视频等方式提供必要的讲解和解析。感觉第一、二单元的实验的代码都对作业给予了很大的帮助,如果能充分理解实验的内容将会非常有益。
-
在寒假或开学增加预习内容。比如,可以加入对于递归下降的简单介绍(就和第一次实验差不多)以及多线程的简单介绍、以及各种java锁的应用方式。现在寒假预习正则表达式,然后第一单元上来就不推荐正则然后用递归下降,实话说有点剥离。而多线程的提前入门也能让第二单元开始不至于手足无措、也能让锁的选择更加多元而不是一个
synchronized
走到底。 -
研讨课的方式有待商榷。具体怎么改进我不好说,但个人感觉现在的研讨课是有点水分。首先小组讨论基本就一半的人说话,剩下都在摸;然后小组总结可能就一共就几个点、设计方式,结果让每个组都上台复读;最后就是会议记录,有点过于形式化了,身份基本是摁编、讨论内容也都是每人分一点,感觉没啥意思……
P.S. 个人分享part算是研讨课之遗珠了,感觉每次听大神们分享都很有意思也有收获。