一、作业架构设计
-
第一次作业
作业需求:
实现一个UML类图解析器,可以通过输入各种指令来进行类图有关信息的查询。
难点:
CLASS_ASSO_COUNT,CLASS_TOP_BASE,CLASS_IMPLEMENT_INTERFACE_LIST
架构设计:
类图中的元素有明显的层次性,根据层次重写三个类,对应Class,Interface,Operation。
MyClass属性,MyInterface类似。
public class MyClass extends MyElement implements MyObject {
private HashMap<String, MyOperation> idOpMap = new HashMap<>(); //Operation id->MyOperation
private HashMap<String, HashSet<MyOperation>> nameOpMap = new HashMap<>(); //Operation name->MyOperation
private HashMap<String, UmlAttribute> idAttriMap = new HashMap<>(); //Attribute id->UmlAttribute
private HashMap<String, HashSet<UmlAttribute>> nameAttriMap = new HashMap<>(); //Attribute name->UmlAttribute
private MyClass father = null; //Father MyClass
private HashSet<MyInterface> interfaceRealizationSet = new HashSet<>(); //Interface MyInterfaceprivate HashSet<MyClass> assEndSet = new HashSet<>(); //AssociationEnd MyClass
}
MyOperation属性
public class MyOperation extends MyElement {
private Visibility visibility; //Visibility
private HashMap<String, UmlParameter> idParaMap = new HashMap<>(); //parameter(in)
private UmlParameter returnp = null; //parameter(return)
}
实现思路:
-
- 由于不保证元素出现的顺序,先遍历元素并分类。相当于给每个uml元素准备一个口袋,按照类别放到各自的口袋。
- 遍历Class和Interface对应的口袋,生成对象。这两种对象类似一个容器,通过add方法,添加自己的方法,属性,或者关联。
- 再遍历Operation,Attribute,Association,Generalization,Realization,添加到对应容器。
- 实现查询指令,这里只探讨刚才提出的三个难点。他们有一个相似的解决思路——dfs。比如CLASS_ASSO_COUNT的计数函数,先调用父类的函数,返回父类Association的集合,再自己的Association合并。CLASS_IMPLEMENT_INTERFACE_LIST指令应该注意,既要有父类实现的接口,又要有自己的接口继承的父接口。
- 应用缓存思想:把查询过的结果储存好,接到指令先判断是不是有存储的结果。
-
第二次作业
-
- 扩充状态图和顺序图的查询指令。
- 对类图进行模型有效性检查。
检查循环继承,检查重复继承;查询一个状态的后继。
有四个功能相对独立的接口:
UmlClassModelInteraction
UmlCollaborationInteraction
UmlStateChartInteraction
UmlStandardPreCheck把方法实现在一个类里面代码会超长,功能也会十分混乱。应该写四个单独的类来实现方法,在主类里复用这些方法。代码复用有两个途径,一是继承,二是在类里创建对象,我选择了后者。
状态图和顺序图采取了和类图相似的层次性架构。我还写了一个有向图的类(Graph),封装求解可达节点的算法。这是状态图的结构。
- 循环继承:类是节点,继承关系是有向边,从任意一个类开始dfs,记录经过的路径,把路径传入更深一层的递归。如果新遇到的类已经在路径里出现过,说明在这条路径上,从这个类上一次出现到这一次出现之间的节点形成一个环,把环上的节点加入一个集合。从剩下类中挑出一个不在该集合内的类,继续dfs,直到所有类要么遍历过,要么出现在集合里。集合里就是循环继承的类。
- 重复继承:接口重复继承接口,对于一个接口,先dfs判断所有的父接口如果有重复继承,那这个接口也重复继承;再判断父接口继承的接口集合之间有没有交集,有交集则重复继承。类重复实现接口,对一个类,先dfs判断父类如果重复实现,或接口重复继承,那这个接口也重复继承;再判断父类实现的接口集合和接口继承的接口集合之间有没有交集,有交集则重复继承。
二、四个单元中架构设计的演进
-
第一单元:带三角函数嵌套的表达式求导。
我的架构是给每一种运算写一个类,回看代码有很多面向过程的写法,几个运算类之间的联系比较混乱。代码复用性很不好,后两次作业全部重构了。第三次作业难度激增,由于对嵌套运算的不合理递归和for循环,很多点都超时了。这是因为我过于看重架构实现,忽略了算法选择,按照自己的方法一写到底。
-
第二单元:多线程电梯调度。
这个单元我认识了几种设计模式:生产消费者模式,工厂模式,单例模式。电梯问题中的调度器是单例模式,考虑的是代码安全性;三个主要的类构成生产消费者模式,电梯是消费者,请求输入是生产者,调度器进行请求分配。第一次的分配算法用了傻瓜电梯的先到先服务方法,第二三次是主请求的捎带方法,第三次还需要考虑换乘问题。我前两次作业的复用性很好,第三次作业写的飞快,正确性也有保障。我总结出提高前期代码拓展性的方法是,架构设计越清晰越好,函数职责越简单越好,线程的交互越少越好。比如电梯的第二次作业,所有分配任务都是调度器完成的,电梯只需要上下楼开关门,并从调度器内部的一个队列里取出第一个的请求。第三次作业增加到三部电梯,变化只在调度器现在需要维护三个请求队列,三架电梯从各自队列取请求,电梯类不用修改。
-
第三单元:JML规格。
JML相比自然语言优越在其简洁性,逻辑性和无二义性。熟练掌握JML是第三单元的学习目标。
这次作业的类也是层次化的,节点和边是最底层的类,上面一层是Path类,最上面的RailwaySystem类。另外封装一个计算最短路径的算法类。三次作业的区别就是图该怎么建的问题,第三次引入换乘问题,我使用了拆点法。如果数据结构忘的差不多了,这个单元就该还债了。最短路径算法有dijkstra和folyd可选,我三次作业一直沿用了dij算法,没有堆优化的dij是没有灵魂的dij,写着没有灵魂的dij我几乎T了第三次强测所有点。分析算法的复杂度应该成为我的一个习惯,不能只满足于通过中测,应该持续探索优化的空间。
-
第四单元:解析UML图。
这次我理解源码和指导书花费了大量时间,代码实现起来反而简单,类的结构是高度层次化的。顶层类一级级调用底层类的函数,完成一个查询功能。在思考了几种算法的可行性之后,仍然选择了最开始想到的dfs。这两个单元的作业复习了图结构的很多经典算法,包括最短路径,可达节点,寻找环路,寻找根节点。
我注意到一个规律,对原始数据的预处理方式,严重影响后续实现的复杂程度。第三次作业搭建地铁路线的时候,如果每新加一条路径,都添加到现有的图上会很复杂,重新建图反而简单,时间也不会耽误太多。第四次作业对于所有UML元素,第一次遍历先将元素分类,再逐类处理更简单。
三、四个单元中测试理解的演进
从构造方法分类,分为人工构造测试集和自动生成测试集。从测试目的分类,分为正确性测试和压力测试。
第一次作业是表达式求导,适合人工构造和自动生成相结合。先人工构造一些特殊的点,比如WRONG FORMAT的个例。然后自动生成复杂嵌套的测试点,测试求导的正确性。构造极端数据点进行压力测试也是必要的,可以揪出爆栈问题。
第二次作业是多线程电梯,最适合对拍,用随机生成的一些电梯请求测试。
第三次作业是JML规格,认识了JUnit插件,可以自动生成一些边界条件上的测试用例。其他测试方法无法相比的一个优点是,JUnit可以针对每一个方法测试,这样发生了错误更好定位。TLE是这次作业最大的问题,构造压力测试很重要。
第四次作业是UML图查询,需要用staruml画图生成测试数据。画图要考虑全面所有情况,比如循环继承的继承环应该有所交叠,单继承多继承都出现。
四、总结自己的课程收获
- 从面向过程转变成面向对象。
- 获得了完成千行级代码的工程能力。
- 写出有拓展性的代码,方便功能拓展。
- 用插件分析代码复杂度。
- 自动生成测试用例。
- JML规格描述功能实现。
- UML图整理架构思路。
- 寻找算法资料,优化设计的能力。
五、改进建议
- 理论课增加JAVA易混淆知识点的讲解,少一些工程思想的介绍。十二次写代码作业把我们的实践能力练习的很充分,但知识上有很多漏洞。类继承和接口实现有很多繁琐的知识点我们始终没有触及,我在几次作业中应用到的继承也是特别简单的继承结构。有时候架构不好也不是思考的少,而是从练习中学习的这种教育模式,让我们只有用到了才去查,浅尝辄止,基础没有巩固,不如老师提前点播一下。
- 上机课的题目应该公布出来,有些课上没有完成的同学,之后还能继续研究。感觉我每次上机都是慌里慌张,从来没有时间好好研究一下上机的那些问题。上机过去就过去了,也没有条件细想,收获很少。
- 在每个单元的第一次作业时就公布三次作业的大致需求。我完成第一次作业时,不知道该怎么设计架构才能方便后面的拓展,因为不知道还要增加哪些功能,提高拓展性就是盲猜。有时候写一个性能最优但实现复杂的算法,反而是最不好拓展的。