OO第四单元总结暨OO课程总结
一、总结本单元两次作业的架构设计
第一次作业完成类图Model,第二次作业在覆盖第一次作业的基础上,加入状态图StateMachine和协作图Collaborations部分。
先放两次作业的类图
第一次作业
第二次作业
下介绍自顶向下介绍架构设计:
为了完成类图,状态图,协作图,我建了UmlModel,UmlMachine,Umlcollaboration三个包来实现这些图的查询功能,下分别作介绍。
UmlModel:
其中包含Association,Attribute,Classes,Interface,Operation,Realizations,Generalization七个类分别对应处理UMLModel中对应的element,在element读入时初始化这些映射的内容,映射在java中的实现是HashMap,以便后期查询,下作这些类的细节分析。
Association:
association中存储了ClassId到AssociationEndId的映射,AssociationEndId到对端AssociationEndId的映射,AssociationEndId到ClassId的映射,AssociationEndId到AssociationEndName的映射,在简单查询时查询这些表示映射的HashMap即可,在查询一个类的所有对端时,由于要考虑继承,会调用Generalization.getFather查询继承类的对端,并采用缓存设计,即每次计算一个类的对端后都会顺便将其存储,当其子类查询对端时要得到父类的对端时可以直接读取已有的结果。
Attribute:
attribute中存储了ClassId到ArrayList<UmlAttribute>的映射,可以查询一个类到其所有的属性(自身)。当查询一个类包括其继承类的所有属性时,会调用Generalization.getFather,采用缓存设计,即每次计算一个类的所有属性(包括继承)会顺便将其存储,当其子类查询所有属性(包括继承)时只需要子类的属性加上读取存储的父类属性即可。
Classes:
classes中存储了ClassId到ClassName的映射,ClassName到ClassId的映射,提供两者相互的查询接口,比较简单,但要注意ClassId不在已存储的HashMap中的情况,进行containsKey判断,不然会造成NullPointer异常。
Interface:
interface中存储了InterfaceId到InterfaceName的映射,InterfaceId到UmlInterface的映射,提供查询接口,比较简单。
Operation:
operation中存储了ClassId到ArrayList<UmlOperation>,UmlOperationArrayList<UmlParameter>的映射,在element读入时初始化填充这些映射的内容。在实现一个类(考虑继承时)的所有operation的查询时可以遍历,要注意从Generalization.getFather中获取继承信息,同样采用了缓存设计,缓存设计同上。
Realizations:
operation中存储了ClassId到其直接实现的接口InterfaceId,查询要用到实现的所有接口(包括类的继承,接口的继承),我的实现方法是,类的继承同上通过Generalization.getFather和缓存机制实现,而接口的继承在Generalization通过拓扑排序topologicalSort(),即拓扑排序后接口中存储了其继承的所有接口,拓扑排序的实现在Generalization中讲解。这样类的继承,接口的继承都解决了就可以完成查询一个类实现的所有接口(包括类的继承,接口的继承)。
Generalization:
generalization是UmlModel包的核心,其他类中的查询需要继承关系的都需要传入这个类获得继承的信息,第二次作业中加入的checkForUml008和checkForUml009也是通过这个类来完成。主体逻辑是checkForUml008通过tarjan算法实现,checkForUml009通过拓扑排序实现,而拓扑排序会将继承的信息向下传递(包括类的继承和接口的继承,generalization中的Node是不区分类和接口的),在checkForUml009之后拓扑排序已然完成,就可以将Node的继承信息给其他类来查询,这就是这个类的工作顺序,下面讲解每一步的具体实现。
checkForUml008,tarjan算法实现,代码如下:
public HashSet<Node> getCircularNodes() { HashSet<Node> nonVisited = new HashSet<>(id2node.values()); HashSet<Node> visited = new HashSet<>(); Stack<Node> path = new Stack<>(); HashSet<Node> nodeInPath = new HashSet<>(); while (nonVisited.size() > 0) { Node aaNode = nonVisited.iterator().next(); tarjan(aaNode, path, nodeInPath, visited); nonVisited.removeAll(visited); visited.clear(); } HashSet<Node> circularNodes = new HashSet<>(); for (ArrayList<Node> nodes : color2qiang.values()) { if (nodes.size() > 1 || nodes.get(0).hasCircle()) { circularNodes.addAll(nodes); } } return circularNodes; } private void tarjan(Node theNode, Stack<Node> path, HashSet<Node> nodeInPath, HashSet<Node> visited) { time += 1; dfn.put(theNode, time); low.put(theNode, time); visited.add(theNode); path.push(theNode); nodeInPath.add(theNode); for (Node adjNode : theNode.getChilds()) { if (!dfn.containsKey(adjNode)) { tarjan(adjNode, path, nodeInPath, visited); low.put(theNode, Math.min(low.get(theNode), low.get(adjNode))); } else if (nodeInPath.contains(adjNode)) { low.put(theNode, Math.min(low.get(theNode), dfn.get(adjNode))); } } if (dfn.get(theNode).equals(low.get(theNode))) { color += 1; color2qiang.put(color, new ArrayList<>()); while (!path.peek().equals(theNode)) { Node top = path.pop(); nodeInPath.remove(top); color2qiang.get(color).add(top); } nodeInPath.remove(theNode); color2qiang.get(color).add(path.pop()); } }
tarjan算法的目的是求强连通分量,我的建图方法是有继承关系即为一条有向边,一个类或接口为一个节点,求强连通分量后,size大于2的强连通分量必在环中,都加入环的集合,size等于1的强连通分量看有没有自环如果有则加入环的集合,最后返回环的集合完成checkForUml008的检查。另外,作为一个算法小白,我的targan算法实现参考的是https://www.cnblogs.com/shadowland/p/5872257.html这篇博客。
checkForUml009,拓扑排序,代码如下:
public void topologicalSort() { Queue<Node> queue = new LinkedList<>(); for (Node anode : id2node.values()) { if (!anode.hasFather()) { queue.add(anode); } } while (!queue.isEmpty()) { Node top = queue.poll(); for (Node adjNode : top.getChilds()) { adjNode.removeConnect(top); if (adjNode.zeroInDegree()) { queue.offer(adjNode); } } } }
其中removeConnect即拓扑排序中的删边过程,并在删边的同时将父节点的继承信息传递给子节点,父节点的继承信息包括父节点继承了那些节点,进行了多少次继承,代码如下:
public void removeConnect(Node father) { inDegree -= 1; this.topId = father.getTopId(); this.totalGeneNum += father.getTotalGeneNum(); this.ancestors.addAll(father.getAncestors()); }
checkForUml009最后只需要比较每个继承的所有节点的集合的size和继承次数就可以知道是否有重复继承,即totalGeneNum > ancestors.size()即为发生了重复继承。
另外checkForUml009的拓扑排序后所有节点都有了所有的继承信息,可以在其他查询时使用,符合高内聚的编程要求。
UmlMachine:
状态图,存储了machineName到machineId的映射,machineId到regionId的映射,regionId到HashSet<UmlState>的映射,regionId到HashSet<UmlTransfer>的映射,在element读入时初始化这些映射的内容即可完成基本的查询任务,另外getSubSequent函数我通过以transfer为边,UmlState为节点的BFS算法实现,一遍BFS找到所有的SubSequent。
UmlCollaboration:
协作图,存储了InteractionName和Id的相互映射,还有InteractionId到lifeNum,incomingNum,messageNum三个计数的count,每次读入element时加1就可以,查询比较简单。
二、总结自己在四个单元中架构设计及OO方法理解的演进
多项式求导单元:
从java大白,面向对象大白开始学HashMap的使用,学习正则表达式匹配的使用,从不会面对对象编程到编写表达式,项,因子多个类来正确解析多项式。
电梯单元:
引入了多线程之后就带来了数据一致性的问题,为了确保临界区资源数据一致性的问题,要学会善用synchronized关键字。在这一系列的作业中,我采用了单例模式将管理器放入多部电梯中,使之协同工作。
JML规格单元:
这个单元立足于节点,线段,图的构建,最后要实现一张地铁图的添加查询操作。感觉这一单元的作业比较重视算法,时间复杂度的要求有很多,当然好的算法和好的设计架构是分不开的。我采用分点的方式将地铁站分在不同的线路上,针对不同的查询采用了迪杰斯特拉,BFS,并查集等算法,并且作了查询缓存方便后续的查询,并需要在更改图进行信息的更新保存,之后再查询就不用重新计算。
UML单元:
任务是解析UML建模语言,建图与查询。我用到了BFS,tarjan,拓扑排序算法。这一单元对面向对象的设计思路要求较高,要编写许多类管理对应到UML中的element,并清晰分解其各个域的数据关系,使用合适的数据结构存储。
OO方法理解的演进:
从无对象到有对象,从不了解线程安全到熟练使用各个编程模式和锁确保线程安全,从不规范到规范统一清晰的架构。
三、总结自己在四个单元中测试理解与实践的演进
测试是检测与修正自己程序不可缺少的手段。以前我测试就是输入两个简单的样例,de一下bug让程序大概能跑。
经过oo的学习,我认为要想让程序不出问题,首先要保证代码的清晰和思维的准确性,所以写完代码要自己先静态检测,自己读一读逻辑,很可能有编程过程中思维的变化导致程序前后的不一致性,这是最基础的。然后就是要学会覆盖性的测试,对每一条要求边界条件的测试样例要构造。在覆盖测试的基础上学会要使用JUnit,TestNG等自动测试手段,避免手动输入的麻烦和误输入导致的差错。更进一步,对代码分支的覆盖检测和逻辑重复的检测看有没有冗余的代码,让自己的代码更优秀更简洁。
总体就是演进就是从简单地试一试到整体逻辑的静态检测,到覆盖性测试辅以自动测试手段,到代码的分支覆盖测试优化。目的是让代码,正确,清晰,简洁。
四、总结自己的课程收获
1. 学会了很多java语言的编程技巧,让编程能力有了提升。
2. 丰富了多线程的编程知识,以前学操作系统只是理解了多线程的概念,oo阶段才真正落实多线程的编程实践。
3. 学习了面向对象的编程思维,很多问题用面向对象的编程思维很好解决而用面向过程的编程思维不易解决,提升了解决问题的能力。
4. 体验了多次架构设计的经历,以后的工作编程也是从小架构到大架构的积累过程。
5. 学习了很多测试方法和测试手段,对自己代码正确性有了更多的保证。
6. 代码风格的建立。
五、立足于自己的体会给课程提三个具体改进建议
1. 可以布置一些巩固课内知识的小编程作业或者选择题之类的,我感觉往往时间都花在每一周的project上,遇到不会的再去查资料看知识,遇到了不少困难。我觉得减少project的次数而加入一些代码量不大却很典型实用的小练习会让学习更高效一些。
2. 可以布置一些简单的预习任务,预习是学习的好方法。
3. 既然每单元的作业都是后面的作业能兼容前面的,不妨一次性告知我们三次的作业或者告知一些后续作业的信息,每次作业的时间线不变,这样我们也能有更多的时间规划总体的架构,让架构更完善,减少重构,有时候重构并不完全是架构设计的很差,只是解法的适用范围不同吧。