博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
面向对象第四单元小结
阅读量:5068 次
发布时间:2019-06-12

本文共 15071 字,大约阅读时间需要 50 分钟。

面向对象第四单元:UML类图、顺序图、状态图

一、架构设计

第一次作业

  

  第四单元第一次作业,需要完成的任务为实现一个UML类图解析器UmlInteraction,学习目标为UML入门级的理解、UML类图的构成要素及其解析方法。最终需要实现一个UML类图解析器,可以通过输入各种指令来进行类图有关信息的查询。具体来说,需要新建一个类MyUmlInteraction,并实现相应的接口方法,每个方法的代码实现均需要严格满足给出的JML规格定义。
  首先需要搞明白Uml类图中各元素的含义。我研究出各元素的属性及含义如下:

1616695-20190608153204947-1389037064.png

  既然是对UML类图的查询,最好的架构方式就是仿照Uml类图的结构,在MyUmlInteraction实现一个类似UML类图的图结构。在我的实现中,将每种UmlElement单独存储。对于每一种UmlElement,都建立一个从其id到element自身的HashMap映射。下图中所有前缀为"invert"的属性均为从其id到element自身的HashMap映射。MyUmlInteraction中所有私有属性如下图所示:

1616695-20190608154601224-1061953872.png

1616695-20190608154654209-1037372572.png

  通过以上属性,便基本建立了一个类似UML类图的结构。具体映射方式见上面图中每个属性的注释,映射关系显而易见。

  
  根据generalizationUml属性存储了所有继承关系,可以从子类向上找到所有父类,从子接口找到所有父接口。classCounter属性建立了类名到次数的映射,记录了每个类名出现多少次。classUml记录了从类名到类ID的映射,incomplete字眼意为此属性记录不完全,每个类名只记录了一个对应的ID,重名的类不会记录下多个ID。innvertClassUml为从类ID到类的映射。AttriCounter属性记录了一个类中某个名字的属性出现的次数。AttriUml记录了某个类中从属性名到属性ID的映射,同样,此属性也包含incomplete字眼。invertAttriUml为从属性ID到属性的映射。invertOperUml属性记录了某个类中从方法ID到方法的映射。paraUml记录了从方法到其所有参数的关联关系。invertParaUml为从参数ID到参数的映射。invertAssoUml是从关联关系ID到关联关系的反向映射。invertAssoEndUml是从关联的端的ID到关联的端的映射。classAsso记录了类关联的所有其他类,以便后续指令CLASS_ASSO_CLASS_LIST来查询。同理,invertFaceUml属性为从接口ID到接口的映射。realization是从类到其实现的所有接口的映射,以便后续指令CLASS_IMPLEMENT_INTERFACE_LIST进行查询。
  
  除此之外,还有一些属性的注释中标有middle字眼。含有此字眼的属性是“中间属性”,将各种查询指令所运行所查出中间结果进行存储,以便减小CPU的运行时间。
  

第二次作业

  

  第四单元第二次作业在第一次作业基础上,扩展解析器,使得能够支持对UML顺序图和UML状态图的解析,并能够支持几个基本规则的验证。一方面需要扩展类图解析器,使得可以支持对UML状态图和顺序图的解析,并可以通过输入相应的指令来进行相关查询。另一方面需要在所解析的UML模型基础上,按照规定的规则检查模型中是否存在违背规则的情况,并输出相应信息。
  
  首先依然是对新增顺序图和状态图的各种UmlElement的各种属性的理解:

1616695-20190620143105647-198782695.png

1616695-20190620142915914-671309570.png

  

  此次作业的深度不大,但广度很宽。大部分时间花在了对类图、顺序图及状态图的理解上。顺序图和状态图分别有三个函数需要填写,在理解了各种元素的各种属性后,写起来并不费很大劲。并且在讨论区助教和老师对测试进行了限制,减少了我们需要考虑的因素。在此基础上,六个函数便可用很简洁的思路完成:
  
  对于STATE_COUNT statemachine_name指令,统计state、pseudostate和finalstate元素的个数,其中这些元素的parent(Region)的parent需要为给定的state machine。
  对于TRANSITION_COUNT statemachine_name指令,统计transition元素的数量,其中transition的parent的parent需要为给定的state machine
  对于SUBSEQUENT_STATE_COUNT statemachine_name statename指令,统计从一个状态可以到达的状态的数量。
  对于PTCP_OBJ_COUNT umlinteraction_name指令,统计lifeline元素的个数,其中lifeline的parent需要保证为给定的interaction。
  对于MESSAGE_COUNT umlinteraction_name指令,统计message元素的个数,其中message的parent需要保证为给定的interaction。
  对于INCOMING_MSG_COUNT umlinteraction_name lifeline_name指令,统计message元素的个数,其中message的parent需要保证为给定的interaction,且message的target需要为给定的lifeline。
  
  我的程序中,分别创建了MyUmlCollaborationInteraction类和MyUmlStateChartInteraction类来分别处理Uml顺序图和状态图。
  
  MyUmlCollaborationInteraction类的属性如下:

1616695-20190620142341325-1591866161.png

  MyUmlStateChartInteraction类的属性如下:

1616695-20190620142235311-1352816529.png

  

  两个图中的属性的意义在图中的注释中均已标明。同上次作业一样,属性的注释中标有middle字眼的属性是“中间属性”,将各种查询指令所运行所查出中间结果进行存储,以便减小CPU的运行时间。在构造函数中,将这些属性存好,后续的函数调用这些属性进行查询。值得注意的是,虽然这些属性看似零散,但其实他们暗含着顺序图和状态图的结构。换言之,其实这些属性存储了一个完成的图结构,而后面六个函数则是对图的查询和图的算法的实现。
  

二、程序bug分析

  

  虽然本次博客作业没有对这一部分进行硬性要求,但我想从一而终,用四个单元的总结博客看到自己的问题,也看到自己的进步。

错误一:

类图如下

1616695-20190605191005728-1702086998.png

查询指令

CLASS_IMPLEMENT_INTERFACE_LIST Class1CLASS_IMPLEMENT_INTERFACE_LIST Class2CLASS_IMPLEMENT_INTERFACE_LIST Class3CLASS_IMPLEMENT_INTERFACE_LIST Class4

错误输出

Ok, implement interfaces of class "Class1" are (Interface1, Interface2).Ok, implement interfaces of class "Class2" are (Interface1).Ok, implement interfaces of class "Class3" are (Interface1, Interface3, Interface4).Ok, implement interfaces of class "Class4" are (Interface1, Interface3).

正确输出

Ok, implement interfaces of class "Class1" are (Interface1, Interface2).Ok, implement interfaces of class "Class2" are (Interface1).Ok, implement interfaces of class "Class3" are (Interface1, Interface2, Interface3, Interface4).Ok, implement interfaces of class "Class4" are (Interface1, Interface3).

错误分析

  
  错误出现在第一次作业类图的相关查询。错误原因是对于继承关系的认识不足。脑子里一直想着一个类UmlClass最多可以继承一个UmlClass,却忽略了一个接口UmlInterface可以继承多个UmlInterface。所以,在原始的错误代码中,我采用了private HashMap<String, String> generalizationUml;这样一个属性来记录继承关系,意在反映一个子类到父类(或子接口到父接口)的映射。而对于上面这个案例,Interface4继承了Interface2和Interface3这两个接口,那将这两个继承关系插入generalizationUml属性时,就会出现后面一个继承关系覆盖了前面一个继承关系的情况。在此例中,Interface4继承Interface3这个关系覆盖了Interface4继承Interface2这个关系。换言之,generalizationUml属性根本没有记录下来Interface4继承Interface2这个继承关系。所以,在查询Class3实现了那些接口时,会漏掉Interface2这个接口。于是,可以将记录所有继承关系的私有属性generalizationUml更改为:private HashMap<String, ArrayList<String>> generalizationUml;,同样反映一个子类到父类(或子接口到父接口)的映射,而一个子接口(后子类)可以有多个父接口(或父类)。对于私有属性generalizationUml的查询算法同样需要进行修正。例如,对于CLASS_IMPLEMENT_INTERFACE_LIST这样的指令,查询一个类实现了哪些接口,因为一个子接口可以继承多个父接口,这就需要用一个类似图的广度优先搜索的方法进行查询,而不能仅仅用“不断找父类,直到没有父类为止”这样简单的做法。
  

错误二:

类图如下

1616695-20190605201826022-562454296.png

查询指令

CLASS_ASSO_COUNT Class4CLASS_ASSO_CLASS_LIST Class4

错误输出

Ok, association count of class "Class4" is 4.Ok, associated classes of class "Class4" are (Class4, Class5).

正确输出

Ok, association count of class "Class4" is 5.Ok, associated classes of class "Class4" are (Class4, Class5).

错误分析

  
  错误出现在第一次作业类图的相关查询。错误原因是指令理解有误。正确的思路是:Class4自身有三个关联,关联对端分别是Class4、Class4、Class5;Class4的父类Class3自身有一个关联,关联的对端是Class5;Class3的父类Class1有一个关联,关联的对端是Interface1;Class1没有父类。所以第一条指令要求数一数Class4所有关联的数量,答案就应该是3 + 1 + 1 = 5个。那么我的错误代码漏数了哪一个呢?原本以为,是漏数了Class4的自关联,实则不然。因为自关联关系要统计两遍,而错误程序可能只统计了一遍。这个问题在讨论区有所强调,所以我格外注意,错误并不产生在这里。实际上,我是漏掉了Class1与Interface1的关联。在处理关联查询这一块儿时,脑子里总想着CLASS_ASSO_CLASS_LIST指令要求只统计关联对端是类的情况,不统计关联对端是接口的情况,而忽略了另一条关于类关联查询的指令CLASS_ASSO_COUNT既要统计关联对端是类的情况,也要统计关联对端是接口的情况。
  

错误三:

类图如下

1616695-20190620133318102-568248549.png

错误输出

(无输出)

正确输出

Failed when check R002, class/interface (W11, W112, W14) have circular inheritance.

错误分析

  
  错误出现在第二次作业规则R002的检查。R002为不能有循环继承。错误原因是检查规则R002的函数陷入了死循环,没有跳出来。具体来说,错误代码中判断某个类C是否循环继承的算法是:采用图的广度优先搜索BFS算法,若算法成功结束,则表明以C为起点的图中没有形成以C为起点和终点的环;否则,若BFS搜索时再次搜索到类C,表明产生了循环继承。值得注意的是,BFS算法的循环跳出条件是:循环队列为空再次搜索到起点。事实上,若按上述算法,对于该案例,算法可以发现类W11,W12,W14都有循环继承,而对于类W15,以W15为起点进行BFS搜索,W11,W12,W14这三个类会不断被加入队列,导致死循环。对于此错误的改进方法是:用一个Set记录所有进到过队列中的元素,BFS算法每次遍历到的结点需要先经过一层判断,若Set中包含该结点,则表明此结点形成了环,已经被遍历过,不能加入队列中。
  

三、四个单元的架构设计及OO方法理解演进

3.1 第一单元架构设计

  

  第一单元为Java语言及面向对象机制入门,三次作业难度由浅入深,从类图便可看出。第一次作业简单多项式的求导,对于Java不熟练,仿照C语言的方式写一两个函数便可完成;而第三次作业暗含嵌套结构以及继承的特征,若继续用以前C语言的编码方式,代码量将成倍地上涨。只有活学活用Java的面向对象机制,掌握继承与接口封装,才能事半功倍。第三次作业使用了继承的Class定义方式,而前两次作业都未使用。例如,Const、Power、Sin、Cos、(Expr),这几类都属于因子,因此我们可以定义一个Factor类作为父类,上述五个类作为Factor的子类拓展其属性和方法。若按此继承方法封装类,在求导、合并同类项时,Java的多态机制便可以体现得淋漓尽致,减少了编码量,同时更使得代码看起来整齐易懂。
  

3.2 第二单元架构设计

  

  第二单元引入了线程的概念,用多线程机制来模拟实际生活中的多电梯调度系统。本单元可分为三次难度不断递进的小作业:单步多线程傻瓜调度(FAFS)电梯的模拟、单步多线程可捎带调度(ALS)电梯的模拟、多部多线程智能调度(SS)电梯的模拟。
  
  第一次作业我构建了一个队列Queue用来管理请求PersonRequest,它相当于生产者消费者问题中的共享数据区。电梯线程仿佛消费者,不断从Queue里取出请求并满足该请求;任务接收器仿佛生产者,只要ElevatorInput出现了新的请求,就将其加入队列。当共享数据区为空时,需要阻塞电梯线程继续消费数据;这对应于实际生活中若没有客人请求使用电梯,电梯就应该暂停运行。而与经典生产者消费者问题不同的是,Queue没有长度限制,也就是不存在队列已满无法接收请求的状况。由于任意调度策略均视为正确,我们采用最朴素的先来先服务调度(FIFO)原则,这也与共享数据区是一个队列Queue结构相符。电梯线程每次从Queue中取出一个请求,电梯内最多只有一个乘客,待满足完该乘客的请求后,再从Queue中取下一个请求进行满足。所以电梯类需要保存的属性是共享队列Queue,和电梯当前到达的楼层floor两个属性。数据共享产生冲突,致使线程不安全。在我的设计中,电梯线程和任务接收器线程(也就是消费者线程和生产者线程)不做任何wait(), notify(), lock, syncronized的处理。只要保证共享数据区Queue的安全性,生产者和消费者线程就不会数据冲突问题。电梯线程从Queue取请求,任务接收器向Queue中加入请求。可能产生的冲突在于Queue的读写冲突和写写冲突。只要将对于Queue的读写操作用锁机制保护起来,就保证了程序线程安全。
  
  第二单元第二次作业换汤不换药,除了调度策略需要有所改动外,生产者消费者机制依然可以套用。程序依然分为三个线程:主线程、电梯线程、任务接收器。构建一个队列Queue用来管理请求PersonRequest,作为共享数据区;电梯线程作为消费者;任务接收器作为生产者。与第一次作业不同的是电梯调度策略的改进,电梯内不能仅仅是一个人,运行捎带。于是对于电梯线程,每当其停靠一个楼层时,都要判断在Queue中是否有请求可以捎带。捎带请求的条件是:该请求的初始楼层与电梯当前停靠楼层一致;且该请求要行走的方向与当前电梯运行方向一致。所以,对于电梯的属性,需要在第一次的基础上有两点改进:加入direction属性记录电梯运行方向,以便对应捎带请求的判断条件;再加入一个列表记录当前电梯内的人数和当前电梯内的乘客所对应的请求信息。线程安全问题依然出在共享资源的读写冲突和写写冲突中,与前面一次作业相同,只要用锁机制保证对于共享队列Queue的操作绝对安全即可。
  
  第二单元第三次作业采用5个线程加三个共享队列的方式实现。三个线程分别负责模拟三部电梯的运行,一个线程作为任务接收器,另一个是主线程。三个共享队列分别存储着A、B、C三部电梯需要满足的请求:队列a存储着电梯A需要满足的请求,队列b存储着电梯B需要满足的请求,队列c存储着电梯C需要满足的请求。任务接收器线程除了接收请求外,还担负着初步分配请求的任务。每当ElevatorInput读到一个请求,接收器根据其起始楼层判断该乘客需要首先乘坐哪个电梯,并生成一个“中间楼层”。若某电梯可以直达乘客的目的楼层,则中间楼层等于目的楼层;否则中间楼层与目的楼层不等,表示乘客需求无法一次性满足,该乘客需要换乘其它电梯。接收器的任务是将请求拆成起始楼层,到中间楼层,再到目的楼层。将中间楼层的数值和乘客所有信息根据乘客初始需要乘坐的电梯一并存入队列a、b或c中。同时,对于需要换乘的乘客(中间楼层与目的楼层不等),生成一个新的请求,将此请求放在“等待缓冲池”里。“等待缓冲池”的意义是,请求未被激活,当乘客从起始楼层到达中间楼层后,表示乘客可以换乘,此时,等待缓冲池中的队列才被激活,处于可执行状态。这样,我们就把第三次作业成功拆分为了三个第二次作业的组合。每个电梯线程分别有各自的请求队列,按照第二次作业中的ALS可捎带调度策略按方抓药,运行各自的线程。但值得注意的是,电梯线程仍有一点需要改变。即乘客需要离开的电梯的楼层是其中间楼层,而非目的楼层。所以,需要加入一点改进:当乘客到达中间楼层离开电梯后,判断乘客的是否已达最终目的地(中间楼层是否等于目的楼层),如已达最终目的地,则乘客请求已满足;否则,需要换乘,从等待缓冲区中找出对应该乘客从中间楼层到目的楼层的请求,激活唤醒该请求,加入对应共享队列。所以,在电梯类的属性中,还需要在第二次作业电梯类的基础上加入等待缓冲池,以便乘客到达中间楼层后访问等待缓冲池。

3.3 第三单元架构设计

  

  第三单元规格化的面向对象设计方法。第一次作业,需要完成的任务为实现两个容器类Path和PathContainer。最终实现的是一个路径管理系统。可以通过各类输入指令来进行数据的增删查改等交互。第二次作业,需要完成实现容器类Path和数据结构类Graph,最终需要实现一个无向图系统。可以像PathContainer一样,通过各类输入指令来进行基于路径的增删查改管理。还可以将内部的Path构建为无向图结构,进行基于无向图的一些查询操作。第三次作业需要完成实现容器类Path和地铁系统类RailwaySystem,最终需要实现一个简单地铁系统。可以像PathContainer一样,通过各类输入指令来进行基于路径的增删查改管理;还可以将内部的Path构建为无向图结构,进行基于无向图的一些查询操作;再可以构建一个简单的RailwaySystem地铁系统,进行一些基本的查询操作。
  
  对于第三单元第一次作业,我设计的MyPath类有两个属性:trajectory属性为一个ArrayList,即对应构造方法中传入的nodeList;set属性为一个HashSet,是轨迹中编号不同的点的集合。MyPathContainer类有如下四个属性。dataset是一系列Path对象组成的ArrayList,用来记录容器中的所有轨迹。id是一系列pathId组成的ArrayList,index为i的pathId对应dataset中index为i的path。nodeCounter属性是一个HashMap,用来记录容器中不同节点出现的个数。edgeCounter属性也是一个HashMap,用来记录容器中不同边出现的个数。nodeCounter属性和edgeCounter属性都是为了应对DISTINCET_NODE_COUNT指令所生成的私有属性。并且,nodeCounter属性和edgeCounter属性这两个属性基本建立了一张图,这对于作业二和作业三的拓展也是大有裨益的。值得注意的是,edgeCounter属性在本次作业的方法中并没有应用,只是为了可扩展性的架构添加的属性。在本文下一自然段中可以发现edgeCounter属性在后续作业中求最短路径发挥了重要作用。
  
  由于第一次作业可扩展性架构做得很好,第三单元第二次作业只加了get_shortest_distance这一个最主要函数,所以第二次作业做起来较为轻松,第一周在架构和设计上花费的时间在第二周得到了回报。MyPath类没有变化。MyGraph类在MyPathContainer类的基础上变化也不大,延用第一次作业的nodeCounter属性和edgeCounter属性这两个属性基本构建图的结构。nodeCounter属性用来记录容器中不同节点及其出现次数。edgeCounter属性用来记录容器中不同边及其出现次数。图的构建完毕。针对最短路径的计算,加入distance属性。distance属性是一个HashMap,key值是无向图中的节点编号,value值是一个从节点编号映射到最短距离的HashMap。distance记录了从节点到其可达节点的最短距离。最短距离算法:考虑到相邻节点之前的距离为1,即这是一幅权重全为1的特殊无向图。于是,BFS广度优先搜索就可以派上用场,而不必应用Dijkstra算法。从某一节点fromNode出发对图进行广度优先搜索,搜索到的节点顺序其实与Dijkstra算法搜索的节点顺序是一致的,这就保证了算法的正确性。所以以某一节点fromNode出发对图进行一次BFS搜索算法,可以得到fromNode到图中所有结点的最短距离。为了节约CPU运行时间,并不是每次addPath或removePath时都进行一遍图的广度优先搜索。每次用户进行 get_shortest_distance 查询时,先查询distance属性中是否有与fromNodeId相同的键值key,若有,则表示之前以fromNode为出发点进行过BFS搜索,则直接读取相应结果,省去了BFS搜索的时间;若没有,再进行以fromNode为起点的BFS搜索,并加搜索结果加入distance属性中。换言之,distance属性并不记录无向图中所有起点到其它所有点的最短距离,而只是记录一部分起点到其它所有点的最短距离。
  
  第三次作业在第二次作业基础上主要增加了查询最低票价、最小换乘次数、最小不满意度的计算。由于是需求是不断递进关系,每次的作业其实都可以继承上一次作业的类。例如,我在此次作业实现MyRailwaySystem类时,就继承了第二次作业的Graph类,因为除了功能在递进外,本质上地铁系统图就是一个更具体的无向图。MyPath类没有变化。而MyRailwaySystem类随继承了MyGraph类,但仍然针对最低票价、最小换乘次数、最小不满意度的查询功能增加了私有属性。nodeSet属性和edgeSet属性存储了地铁图结构,price、transfer、unpleasant三个HashMap属性原理与第二次作业中distance属性类似,分别记录了从结点到图中所有其他结点的最低票价、最小换乘和最小不满意度。与distance属性类似,为了节约CPU运行时间,price、transfer、unpleasant三个属性并不记录无向图中所有起点到其它所有点的最低票价、最小换乘和最小不满意度,而只是记录一部分起点到其它所有点的最低票价、最小换乘和最小不满意度。最低票价、最小换乘和最小不满意度算法:最低票价、最小换乘和最小不满意度算法原理完全相同,都是有向图的最短路径问题,只是各自的边权重不一样。这里以最小不满意度算法具体展开。由于边权重不再全是1,所以不能简简单单像第二次作业一样使用BFS搜索算法。我这次采用了传统的Dijkstra算法,但又与传统的Dijkstra算法稍有不同。由于引入了换乘,换乘的不满意度不为0,传统的Dijkstra算法不再具有最优子结构,我的解决方法是对地铁图做一定的改进,采用拆点的做法。具体来说:对于每个地铁站node,拆分成2+x个点,其中x为经过这个地铁站的Path数。其中,前两个点为抽象出来的起点和终点,用于解决换乘和答案统计,后面的x个点可以理解为每个Path在地铁站node的站台。之后是连边,设一条Path中相邻两个点的边权为x(双向边),每个地铁站的终点到起点(没有写错)连边的边权为y(换乘,单向边)。x,y的值由需求决定(即四种图)。每个站台往它所在的终点连边的边权为0,每个地铁站的起点往它的每个站台连边的边权为0,图即建好。换言之,将图结构进行了改进后,图即变成了一个普通的有向图,传统的Dijkstra算法在求解最小不满意度时由于具有了最优子结构,可以发挥作用。

3.4 第四单元架构设计

  

  见本博客第一节。
  

3.5 OO方法理解的演进

  

  一个学期的面向对象课程学习下来,发现自己真的长进了不少。回首这一个学期,发现四个单元的教学设计是不断递进的,或者说是“继承”关系,或至少是强“关联”关系。
  
  本课程的4个单元模块分别是Java基础、多线程和线程安全设计、抽象与规格,测试和论证。
第一单元对象与对象化编程。主要接触了Java语言的基本语法和面向对象的设计思维,可以设计出较为简单的单线程程序,是OO课程的基础铺垫。
  
  第二单元Java运行机制及多线程。从此单元开始,难度明显上升,开始设计多线程交互系统,一方面需要实现线程间的并行,一方面需要着重注意线程安全问题。
  
  第三单元规格化的面向对象设计方法。主要进行对程序抽象与规格的设计,便于养成良好的工程化开发的习惯,同时借助对前面作业的规格设计,来进一步深入理解面向对象的编程思维。
  
  第四单元UML语言。主要则是引入了另一种语言,直接提供针对性、分离的结构与行为描述手段,而且可以在后台把描述元素整合起来。从UML类图到顺序图再到状态图,让我看到了Java程序如何与UML结合,反映自己程序的优缺点。
  
  除了四个单元之间的递进式架构外,每个单元的三次小作业也具有递进关系。从3.1 ~ 3.4小节的作业架构便可看出。比如第三单元的第一次作业中edgeCounter属性就很好地为第二次作业中最短路径的求解铺平了道路,因而我们可以认为第三单元第一次作业的架构是好的架构,是具有可扩展性的架构。
  
  除了递进式的架构外,四个单元的学习还让我学会如何工程化地开发软件。工程化开发的一般流程是需求分析—设计编码—测试验收—维护。“需求”是基本问题,既是出发点也是终结点,基于需求进行设计编码,测试验收,最终实现需求。但有时候需求也不是一成不变的,就比如我们的OO作业都有可能写着写着改指导书了,readme(对于程序员来说)是最好的解决办法,但在具体现实问题中可能事与愿违,如果前期设计包容度不够高,可能一个微小需求的变动会对整个工程开发产生很大的影响,所以,前期设计至关重要,个人认为,可能需要考虑的比需求本身更为广泛,多想一些,想长远一点。除此,一般而言,对于大型的工程项目,多是teamwork,这就需要程序员具有良好的编程习惯以及职业素养,并且在团队协作中,可能需要事先设置一些规范要求以便最终能实现高效的任务对接,但如果某个个体违背了一定的原则,且不说对整个工程开发的影响,这种行为本身就该被谴责。
  
  OO课程为我们打开了以后步入程序员岗位的一扇小窗,让我们在一轮又一轮作业中强化体会这四个环节。特别是设计和实现分离,设计好了,实现也就是解决一些小细节而已,要是没设计好就急忙动手,那必然是步步凶险且是蜗牛式前进。
  

四、四个单元的测试理解及实践演进

  

  值得说明的是,16级高工没有OO互测,没有机会去学习其他同学的代码。但发现别人程序Bug的策略和发现我自己程序Bug的策略其实是基本一致的。本博客中仅根据四个单元中在弱中强测中发现的Bug,以及自我的其它程序测试进行分析。
  
  在码好代码后,首先要根据自己的代码结构思考可能出现的问题,再加之各种覆盖性测试和鲁棒性测试。即先进行白盒测试,再进行黑盒测试。白盒测试是通过程序的源代码进行测试而不使用用户界面。这种类型的测试需要从代码句法发现内部代码在算法,溢出,路径,条件等等中的缺点或者错误,进而加以修正。黑盒测试又被称为功能测试、数据驱动测试或基于规格说明的测试,是通过使用整个软件或某种软件功能来严格地测试, 而并没有通过检查程序的源代码或者很清楚地了解该软件的源代码程序具体是怎样设计的。测试人员通过输入他们的数据然后看输出的结果从而了解软件怎样工作。
  
  每次作业测试着重测试作业的创新点。例如第一单元的第三次作业在前两次作业的基础上又允许三角函数中嵌套因子的新规则,就着重设计对嵌套因子的求导的测试。再如第一单元第三次作业重构了代码,五种因子类都是Factor的子类,覆盖测试时就要覆盖全面,各种因子都嵌套一遍,都相加减一遍,都相乘一遍。我自己书写了两个程序:①随机生成项和因子的代码,用于生成覆盖性测试的测试集;②提取自己程序的求导结果,代入MATLAB中验证正确性的比对代码。
  
  再如第二单元的作业,我同样编写了Python程序已完成覆盖性测试。随机生成一系列的请求,PersonId依次递增,fromFloor和toFloor为在楼层范围内的随机整数(fromFloor不等于toFloor)。模型产生大量随机请求,验证程序的正确性和效率是否在datacheck给出的时间范围之内。
  
  鲁棒性测试一样重要。每次作业的需求都有不小的改变,第一单元中字符串的输入处理会千变万化,很有可能考虑不周;第二单元单次运行程序很可能不会有错,但多次运行程序可能才会发现线程安全问题;此外,没有任何请求的系统、只有单一请求的系统、只用到一部电梯的请求、错误请求、很多人同时从一层上电梯的请求......等等特殊情况都要覆盖全面。所以第二单元我着重设计了鲁棒性测试。与第一单元进行黑盒测试和白盒测试相比,第二单元更侧重线程安全的角度,所以更注重鲁棒性测试,看看程序是否会出现死锁或无限等待的情况。第三单元对于给定的规格,同样需要将规格的每个分支测试都覆盖全面。
  
  除了上述测试之外,我还学会了如何使用JUnit进行测试。通过JMLUnitNG的安装,可以自动生成相应的测试代码对代码段进行测试。从测试样例可见,使用JUnit进行测试,测试强调了方法的鲁棒性。例如对整数范围内的最大值、最小值、0、空值及其组合都会进行相应测试。所以,在第四单元和今后的Java程序中,除了自主编写测试程序和测试用例外,我还学会了结合JMLUnitNG进行自动的程序测试。
  

五、课程收获

  

  一个学期的面向对象课程学习下来,发现自己真的长进了不少。纵观四个单元面向对象编程作业的完成过程,从臃肿的代码方式到后面较为抽象的设计实现,我的编程思维发生了不小的转变。
  
  在设计上,在对功能需求进行分析之后,带着“高内聚、低耦合”的思想,可以抽离出相应的类来均衡实现相应的功能,并使这些类相互交互来实现整个系统的功能。例如,第三单元中的作业是逐级递进的。这与一般用户的需求相近,每一次的功能是单调递增的,而且具备一定的继承性。这个系列作业,从PathContainer,到Graph再到RailwaySystem,很形象地描述了实际OOP开发中,一个功能模块的演化过程。从低层次抽象,逐步形成一个面向实际需求的模块。这对我今后步入工作岗位或科学研究都是很有帮助的。另外,每次做作业前应注意架构和设计,所有在设计上花费的时间都会在未来收到回报。不必着急开始动手编写代码,要事先考虑好图的结构用什么存储,最短路径用什么算法实现等等,此外还要考虑好结构的可扩展性。
  
  在测试上,除了会应用Python编写黑盒测试用例,还能够不断针对特殊点完善自己程序的鲁棒性。最后还学到了拥有JML规格后基于junit的自动化测试,来对程序进行逻辑验证。
  
  在程序质量上,除了鲁棒性的考虑更加完善外,程序容错率也有所提升。之前写代码不会考虑一些不可能的因素,现在编写会尽量将if-else或switch的每个分支写全面,再如对HashMap的查询尽量使用getOrDefault方法,而不去使用get方法。尽管在设计时会认为有些分支条件不可能被触发,但一个完整的、规格化的设计方式不但可以避免很多bug的出现,还能让自己和其它程序维护的人看起来清楚明了。
  

六、改进建议

  

  1、首先是大的教学方法上,面向对象课程和计算机组成课程的授课方法是有很大区别的。计算机组成课程的教师会对课下的编程作业做很多架构上的分析,老师带领所有同学一起思考如何架构会是最佳方式;而面向对象课程的老师则是课上带着大家一起讨论如何架构的时间有限,更多是让同学课下思考与讨论最合理、可延续性的架构方式。因为如果不能自己做出架构,就不会有真正的技术掌控力。这样的做法虽然让我走了不少弯路,比如某次作业由于架构不合理,需要在上一次作业的基础上重构代码;但也在一个学期下来培养了我高内聚、低耦合,面向对象的思维方法。可能是因为计算机组成是我们进入计算机专业后第一座大山,同学经验有限,需要老师更多带领,而OO课程的老师想培养我们自己探索、自己思考的能力。我无法确定地说,这两种教学方式哪一种更好,但是这个问题我想在这里提出来,供老师和各位同学思考讨论。
  
  2、其次,可能也是课程组考虑到了上一自然段所述的问题,同学们可能在自己思考探索后还是没有做到最佳的架构方式。在每个单元结束后,助教都会整理出几份推荐代码供同学们学习。由于这部分内容并非强制学习,且推荐的内容是上一个单元的作业,往往与当下手头的作业没有直接的关系,导致大部分同学无暇去看,或懒得去看。建议是这部分内容变成强制性学习内容,比如可以纳入到总结性博客作业中。
  
  3、此外,是课程指导书的下发。这一年OO课程的教学内容较前一年改变很大,所以有时指导书下发推迟或内容不完善也可以理解。但是我们希望能麻烦OO课程组对这一学期下来同学们提出的指导书的Bug和解释不充分的地方进行补充完善,能为下一届学习OO的同学提供更多便利。
  
  以上建议虽然语言上可能看似比较犀利,但是发自肺腑。我在一个学期的学习下来确实提升了自我,所以也由衷地希望这么课能越来越棒!
  
  总的来说,我认为从去年的课程内容再到今年,面向对象课程组的教学内容和方式在不断地完善和升华。也特别感谢助教们对于我们编程作业的细心解答。更要感谢吴际和荣文戈两位老师的教学,课程清楚明白,切中重点,培养了我面向对象的设计思维和设计方法。祝愿OO课程组越来越好。
  

posted @
2019-06-21 20:35  阅读(
...) 评论(
...) 收藏

转载于:https://www.cnblogs.com/16231181guorongchen/p/10981802.html

你可能感兴趣的文章
javascript学习笔记1
查看>>
LVM调整磁盘分区大小
查看>>
sql使用row_number()查询标记行号
查看>>
PowerDesigner安装教程(含下载+汉化+破解)
查看>>
hdu5236 Article
查看>>
源码 springmvc 请求加载过程
查看>>
linux-memcache安装及memcached memcache扩展
查看>>
python logging 日志使用
查看>>
Effective Java 74 Implement Serializable judiciously
查看>>
Java Concurrency In Practice -Chapter 2 Thread Safety
查看>>
13.constexpr
查看>>
15.map映射
查看>>
flask简单应用以及配置文件的写法。
查看>>
n阶螺旋矩阵问题
查看>>
工作中碰到的一些问题以及解决方法
查看>>
C#-WinForm-对话框控件
查看>>
支持多个版本的ASP.NET Core Web API
查看>>
D - Xenia and Bit Operations
查看>>
tar用法
查看>>
Spring框架初识(二)
查看>>