面向对象第四单元做业/最终总结

1、本单元两次做业的架构设计  

1.1 第一次做业架构设计

第一次做业的主要任务是基于类图的一些查询指令,大致来看架构的实现有两种方式:java

一种是把类图的结构反映到代码的结构中,也就是为类设置相应的数据结构,每一个类是一个实体,其余的类图关系能够经过为这个类添加相应的属性实现。python

第二种方法是直接面向功能,建立类图类,而后统一在内部设计各UML元素的数据结构。git

两种方法的思路不同,第一种我称之为面向内容,由于程序的数据结构和类图的内容息息相关,第二种我称之为面向功能,由于全部的数据结构全都是为了实现某查询指令而设置的。github

我选择的方法是第二种,看起来,第一种思路是一个更天然的方法,由于你的程序的类设计能够直接按照UML来,可是为了方便功能的实现,我才用了第二种方法。正则表达式

架构设计具体细节以下:算法

Main:主控类,启动程序。数据结构

UmlInteraction:交互类,负责信息收发。多线程

ClassGraph:类图类,实现本次做业的核心要求。架构

Memo:记忆类,存储以前查询过的指令结果,加速查找。并发

1.2 第二次做业架构设计

基于第一次做业的架构直接拓展,对每一个图,新建一个对应的类,而后分别实现,同时扩大UMLinteraction类。各种的功能以下:

Main:主控类,启动程序。

UmlGeneralInteraction:交互类,负责信息收发。

ClassGraph:类图类,实现上次做业的核心要求。

InteractionGraph:交互图类,实现交互图指令的要求。

StateMachineGraph:状态图类,实现状态图的指令要求。

Memo:记忆类,存储以前查询过的指令结果,加速查找。

Checker:规则检查类,检查各个规则。

这里面又个问题,就是规则的检查其实只是检查了类图,因此更天然地应该把这个检查的方法放在类图类里,可是这样一个类行数太多,虽然没有超过代码风格检查的要求,可是分开放会好一点,因此我分红了两个类,这样Checker为了检查就必需要传入参数,就须要把类图的有关信息传入到Checker类里,这样的传输必需要保证传入的量是不可变的,不然会致使检查完规则以后类图的含义发生变化,致使出错。

虽然面向功能的架构设计未必是最简洁的,也未必是最优雅的,可是这是一种思惟的训练,这样的设计证实我对于对象的理解已经超过了实体层次,上升到了逻辑抽象的层次,也就是说我设计对象再也不是直接把研究问题内容中的对象直接抽象出来,而是对功能进行抽象。

虽然这一单元做业由于没有处理接口重名不重ID的问题不慎炸了一个点,可是实践基本上证实这样的设计仍是没有什么问题的。炸点更可能是由于我比较懒惰,没有常常看讨论区,也不太仔细看通知,尤为是不少时候已经写完了又出一些补充说明,这种状况下就心累不想改了。

2、本身在四个单元中架构设计及OO方法理解的演进  

2.1 第一单元

本单元做业一共分为三次,主要任务为多项式求导,第一次仅要求对非复合的幂函数多项式求导,第二次增长了正弦余弦的函数形式依然会出现复合,第三次则容许函数复合。要求程序在任何输入状况下都不会崩溃,且能正确识别出用户输入是否合法,对合法的输入输出尽可能短的求导结果,对不合法的输入输出WRONG FORMAT。

      首先整个程序的运行须要主控类,负责实现输入、输出、求导的过程控制。主控类可以把用户输入的字符串转化为内部的存储结构,而这个存储结构是由类实现的。通过对表达式的分析,以及编译技术学到的文法知识,不难发现,表达式由项构成。

在第一次做业中,项的组成成分单一,因此表达式和项足以应付全部状况,而且输入输出能够直接在表达式类中一次性完成。

      第二次做业虽然加入了新的函数类,可是考虑到他们对求导运算封闭,因此其实每一个项能够写成一个a*x^b*sin(x)^c*cos(x)^d的形式,所以我依然没有建立因子类,我只须要对每一个项维护好abcd四个系数便可。

      第三次做业中,嵌套的出现,每一个项完全失去了统一性,因此必需要增长因子类,而因子有不少种,按照其种类,能够分为三个子类:幂函数子类、正弦函数子类和余弦函数子类。随着状况的复杂,对于对象的构造和输入的处理也不能一次完成,因此把对于输出输出的分析分发到各个类的构造函数中,求导过程也要如此。并且为了不递归降低子程序分析的麻烦,我对字符串进行特殊处理,维持了正则表达式的使用。

综上,我一共有主控类、多项式类、项类、因子类,而因子类做为父类有幂函数子类、正弦函数子类和余弦函数子类。

2.2 第二单元

三次做业的多线程设计出现了巨大的变化。

2.2.1 第一次做业:两线程策略

  因为第一次做业只有一部电梯,并且能够采用先来先服务的傻瓜调度,按照封装的思想,电梯内部执行任务的细节无需关心的话,和生产者消费者问题别无二致。因而采用了Collector线程读取输入,即生产者,而电梯线程执行请求,为消费者。两个线程之间须要互斥访问请求队列,因此另外抽象出Schedule类,把队列及其方法封装起来。这样的设计中,调度器并非一个线程,电梯和Collector分别调用Scheduler的方法互斥访问请求队列,根据队列是否为空使用wait和notifyAll也能够很容易地避免轮询和死锁。

2.2.2 第二次做业:三线程策略

  第二次做业虽然仍是一个电梯,可是增长了调度请求,因此调度器须要发挥做用。这里存在一个对调度器的理解问题。调度器既能够看做是一个协调区,只对各个请求进行排序,让电梯自行对选择的任务进行路径规划(捎带);或者,电梯是一个只可以执行运动装卸和开关门的机器,由调度器对输入的请求(Request)进行翻译转化,转化成简单的指令(Order)。

  因为对将来需求的误判,我把程序向着有助于调度算法扩展的方向进行构造,因此我在第二次做业中加入了Scheduler线程,让它对与传来的请求进行翻译,翻译成一系列上楼和下楼的Order,若是有新来的请求能够捎带,就会在这个Order序列上插入上人和下人的Order,也就是说,电梯全部的动做,都有Schduler安排稳当。

  这样的设计须要维护两个队列,一个队列是Scheduler和Collector互斥访问的Person request队列,一个是Scheduler和电梯互斥访问的Order队列,Scheduler封装全部的调度算法,也集中管理类全部的互斥访问的方法。

  这样的设计在当时的视角下,所具备的优势是可以比较容易的扩展到多电梯的状况,届时只要再增长一个平衡调度的算法决定请求分发给那个电梯就好了。

  缺点在于,为了防止电梯拿到一个过于巨大的请求而丧失了捎带的机会:好比直接拿到从1楼到15楼的请求,结果这个过程当中电梯一直在休眠,那么就不能相应中间能够捎带的请求。为此,必须把每上一层楼看成一个Order,避免出现上述状况,可是这样会有不少Corner Case很是恼人,好比在电梯折返楼层由于方向不明确而出现一些不合常理的捎带。

2.2.3 第三次做业:两类线程的回归

  事实证实,第三次做业给出的全新需求彻底不能和我以前的设计兼容,由于电梯具备个性(不一样的容量、速度、停靠楼层等)使得若是把调度问题彻底集中在调度器中会致使调度器过于复杂。因此,Scheduler应该仅仅被视为是一个请求的收发装置,协调各个电梯之间的任务分配。另外考虑到换乘,Scheduler也是各电梯进行同步的场所

  根据上面的认识,我又从新回到了第一次的设计,Scheduler再也不是一个线程,而是集中了各类共享队列和方法的集合。

  这一次的共享对象有4个,三个电梯各自的字典,这是一个楼层到各层上下人的映射。以及一个等待队列,用于电梯之间的换乘合做。Scheduler中针对这些数据结构进行操做,被电梯和Collector调用。

  Collector先从控制台获得请求,而后调用Scheduler的方法将其分发到电梯的字典中(映射了每一个楼层和该楼层上下的乘客),若是须要换乘的话,也须要将其加入到等待队列中。电梯也互斥访问字典取得请求。由于字典的存在,电梯每到一层就检查一下是否有人上下电梯,这样能够避免Corner Case,并且能够实现捎带。同时,为了兼顾效率与性能,个人调度算法具备内生随机性,陷入极差状况的几率极低。

  此外,我还专门设计了Scheme类,用来屏蔽个类电梯的停靠楼层等方面的差别,下降调配和分发的逻辑复杂度。任何请求都会先转化成一个Scheme,里面包含了请求的基本信息和换乘的要求(如前文所述,随机化选择),返回给Scheduler一个保证合法的分配方案,以后Scheduler在根据Scheme的安排拆分红Order(这里的Order和第二次做业含义不一样,只是具备换乘信息的PersonRequest而已)加入到电梯中。

 

2.3 第三单元

  三次做业的总体架构几乎不变。除了第三次增长了两个类,其余的几乎不变。固然增长的两个类主要是服务RailwaySystem,因此耦合度并不高。

      Path类。三次演化中,Path类是几乎不变的,除了第二次意识到第一次每次都查一遍点致使超时因此增长了一个变量记录DistinctNode外,就没有更改过了。

      PathContainer/Graph/RailwaySystem类,这个类实现的方法在不断增长,从PathContainer到Graph,这两次做业之间的改动是不多的,只须要增长一些方法。以前实现的方法没有任何改动。

      可是从Graph到RailwaySystem架构发生了比较大的变化。新增了两个类,其中Pair类比较简单,其实使用javafx.util.Pair便可,可是考虑到jar包运行的问题,我本身实现了一个简单的Pair。另一个类MsGraph,这是一个图类。这个类的主要做用是实现全部图相关的数据结构和算法,由于第三次做业中有不少不一样类型的图。因此,此处的MsGraph的做用是纯粹的图类,所谓纯粹,是由于其中任何的数据结构都不和本次问题发生关系,全部的节点和边都是抽象的。而RailwaySystem类则主要实现用户输入到这张纯粹图的映射,为图类屏蔽问题的差别。因此,其实第三次做业中,MsGraph才是Graph的演化,(BTW,Ms是My super的意思),MsGraph是在第二次实现的图类基础上增长了迪杰斯特拉方法求最短路产生的。而RailwaySystem则主要是创建各类索引让各个图相互配合,完成功能。

2.4 第四单元

见第一章。

2.5 理解的演进

对面向对象的理解不断加深。

第一单元做业中,类的设计依赖于实际的数学公式,加上以前编译技术对与语法的认识因此应该说没有经历不少思考就直接设计了一个大体方向,获得了类的设计。

第二单元做业中,由于设计多线程问题,这方面第一次接触,因此仔细研究了课上测试的代码,学习了课上代码对于多线程的设计理念,照猫画虎是以学习为主,本身的思考并不算太多,可是由于设计过程当中出现了一次线程个数的切换,因此对整个多线程架构的设计有了比较深刻的理解。

第三单元做业虽然联系的重点是按照规格写代码,可是针对这个问题的架构设计自己仍是值得思考。第一次做业已经明确规定了要求实现的接口,因此我就是用简单至上的原则,直接一个接口一个类的实现了,很是简单。第二次做业和第三次做业稍显复杂,可是随着对问题认识的深刻,我逐渐抽象出图的概念,并把有关图的基础运算(最短路、搜索)等集中到一块儿,并且我在构图是尽量屏蔽了和图自己不相关的信息,整个架构呈现三层,最外层用于表示,中间层用于转化,最里层是纯粹的图结构,和编码方式所有无关。为了屏蔽表示的复杂性引入中间层是计算机领域常见的方法。这样的屏蔽虽然自己形成了必定的复杂性和开销,可是却可以为程序的拓展提供便利,由于能够把全部的改动限制在中间层及以上。我这样的设计也确实为第三次做业的完成提供了巨大的方便。

第四单元做业则是更加抽象的类设计。首先我明确区分了两个概念,咱们研究的内容和咱们的设计。由于他们在这一单元做业中是重合的。UML是用来研究架构的,咱们的设计架构刚好就是UML的内容。这样的重合性也许是一种干扰。很容易的咱们的设计思路就跟着图自己走了。可是,考虑到UML工具StarUML的内部组织,老师上课也强调过,StarUML工具里每一个元素各个字段的id是管理工具本身设计的,并不和所画的类图直接一一对应,因此我以为这种区分是正确的思路,会给咱们的设计带来必定的便利。

整体来看,对架构的设计从前两次做业直接对研究的问题内容抽象,逐步演化到一种逻辑抽象,也就是为了方便问题的解决抽象问题的解决过程,而后设计类去实现。

3、本身在四个单元中测试理解与实践的演进

3.1 第一单元测试

  首先是手动测试,手动测试的时候,我加入了死循环,而且使用IDEA中Run with Coverage的模式运行,这个模式的好处是能够在结束运行以后告诉你各段代码时否被覆盖,这种方法简单快速,并且能让你迅速有针对地把全部代码执行一遍,甚至能够起到简化代码的做用。我在第一次做业中使用这种方法,发现有几处代码不管如何也覆盖不上,后来仔细分析了一下,是由于x不管如何不会成为一个求导的结果,因此那里的逻辑组合系数和指数都是1这个分支其实永远不会进入,因此我果断删掉了这个分支。固然这个方法存在致命问题,为了测试我不得不修改已经写好的代码,这样测完了若是没改回来,就可能形成致命风险,好比卡评测。

      以后是自动化测试,对于正确的用例,我写了一个python脚本,能够根据正则表达式,生成目标表达式,在使用python中subprocess指令,调用本身的java程序,识别控制台输出,而后使用python的sympy进行求导,计算。这个方法最大的好处是真正实现了黑盒测试,把须要测试的代码使用子进程的方式启动,利用管道获取控制台输出。可是问题也很明显,就是随机生成的用例每每没有针对性,并且由于正则表达式太复杂,稍微长一点的表达式,生成和计算都须要很长时间,并且还很容易超出计算限制。不过,这个方法至少实现了大量测试,在必定程度上确保程序的正确性。

     对于错误状况分析,则比较难,理论上,只要正则表达式是正确的,看起来对于全部错误都会输出WRONG FORMAT!。因此核心是创建正确的正则表达式分析方法,借助编译技术中学习的语法分析方法,从语法树分析,按照DFA分析,最终获得的正则表达式不会出问题。

3.2 第二单元测试

3.2.1 在设计上分析进行避免

  设计的时候,抽象出Scheduler类,把全部的互斥访问工做所有集中在同一个类里,方便设计和检查,而且容易出问题的地方集中起来。

  此外,对于互斥场景的访问,必定要使用规范的格式书写代码,力求一种对称性和统一性,好比下面是我第二次做业中取出请求的代码,严格按照同步——检查(可选)——操做——唤醒的格式来写。

View Code

  此外对于多个锁的状况,必定要避免嵌套加锁,防止出现死等,为此,我采起了分别加锁处理的状况,可是若是在中间切换,就可能出现潜在的数据一致性问题。因此我对于可能出现这种问题的状况,我采起了检查A——操做B——操做A的方法,避免出现对A直接操做后线程切换,而后出错的状况。

  可是第三次做业中须要同步访问控制变得更加复杂,复杂来源主要是换乘的电梯通讯和关机指令的复杂。

  电梯的通讯要求对等待队列进行互斥访问,这个的实现和Order队列大同小异。

  关机指令则略显复杂,须要避免死锁和插入异常。为了不死锁,当Scheduler发出关机信号后须要唤醒全部线程。此外,为了防止出现插入异常(一个电梯完成换乘乘客的第一阶段任务,要从等待队列取出后,但还未插入到下一个电梯的指令队列中前,发生了线程切换,这个电梯发现没有任务插入且等待队列为空,收到关机信号后就可能会关机,致使后续的请求没有被处理),须要先插入到电梯中再将其从等待队列移除。

3.2.2 大规模测试

  在设计上尽量避免了死锁和数据竞争状况的出现之后,开展大规模随机测试,随机生成请求输入后,检查输出的操做是否合法,通过了上百次测试后,能够基本保证程序的正确性。

3.3 第三单元测试

3.3.1 使用Junit进行单元测试

      不一样的方法测试难度并不同,容易测试的方法通常有两个特征,一是返回值可能的数量少好比布尔类型的方法,一种是逻辑比较简单没有算法好比增删路径。这些比较容易测试的方法主要是isConnected, CONTAINS_*以及addPath之类,能够用Junit进行单元测试,构造几个用例就基本有信心保证正确性。如下是第二次做业图类测试部分Junit测试代码。

View Code

      测试结果以下:

 

3.3.2 Corner Case测试

      对于路径中有诸多平行边,起点和终点一致的查询等等corner case进行测试。好比:

PATH_ADD 1 2 2

CONTAINS_EDGE 2 2

PATH_ADD 1 2 2 2 3

PATH_REMOVE 1 2 2

CONTAINS_EDGE 2 2

      诸如此类的corner case还有不少,通过测试后都没有问题。可是这样的测试极为有限,并且并不能保证正确性。

3.3.3 搭建对拍框架进行多人对拍

      还有一些方法并不容易测试,好比最短距离等等,即便是写一个对拍程序,对拍程序自己的正确性也不易保证。此外各个方法之间的综合做用是否会出问题也不容易使用Junit测试。因此必须进行整合测试。可是此次做业不像电梯,电梯能够有一个另外的逻辑推断结果的合理性,可是此次测试并无。但是咱们又没有标程,为此只能经过群体智慧进行测试。随机生成测试用例后,运行若干同窗的jar包,而后把结果进行比对。若是你们输出的结果都同样,那么就有比较大的把握认为程序是正确的,若是有一我的和其余人都不同,那大几率是这我的错了。

      我搭建了一个多人对拍框架,它具备如下几个特征:

      1 并发测试:同时运行多组java进程进行测试提升测试效率。采用python线程池,每一个python线程开启一个新的Java进程。

View Code

      2 计时服务:提供时间统计服务,做为算法执行效率的参考。

      3 邮件通知:运行大规模测试很耗时,因此我是在树莓派上跑的,隔一段时间check一下树莓派很麻烦,因此我设置邮件通知方法,运行完之后向我发送邮件。

      当没有发现不一样时,部分结果以下:

 

      具体技术细节见开源代码库:https://github.com/sdycodes/JavaDestroyCorner.git  (开源已通过jar包拥有者赞成)

3.4 第四单元测试

3.4.1 面向查询指令构造Corner Case

最欧一个单元的测试中,才用了先构造测试用例后写代码的方法。现根据须要完成的指令,考虑一些比较特殊的状况,以及通常的状况,构造测试用例。而后再进行代码实现。下图是几个测试的例子。

3.4.2 大规模随机测试

这一单元做业开展大规模测试是很困难的,主要是由于UML图的绘制不能随机生成,因此其实随机生成的只能是一些指令,若是类图自己不够复杂,其实再多的指令也并无太大意义,这反过来又进一步说明了手动构造测试数据的重要性。

3.5 测试的理解与演进

我从一开始就重视测试的要求,从一第一单元做业开始,我就是用了大规模测试的方法来尽量避免程序错误,可是由于一思惟惰性对于cornercase不肯意去想,总以为大规模暴力测试应该能够实现绝大多数状况的覆盖。这样虽然我测试了大量的样例,可是其实测试是比较低效的,不过采用树莓派24h不间断运行倒也没有太大问题。

第二单元做业的测试有难度,主要是由于多线程,而后模拟真实电梯的运行,速度很慢,可是检查正确性的逻辑并不复杂,能够按照每一个人的轨迹去检查。

前两次做业自动化测试都是比较简单的,由于检查正确性有另外的方法,好比表达式求导的正确性,只要带入数值检查,电梯的正确性,只要检查每一个人的乘电梯的轨迹是否合理便可。可是第三次第四次做业,检查起来就有难度了,由于没有另外的逻辑,检查须要的逻辑和求解问题同样,好比求解最短路是用来Dijstra算法,那么验证的时候仍是须要算一个Dijstra,这样若是两个dij出自一人之手,其实检查并没有意义。因此,我搭建了多人对拍框架,相似对答案的方式,同窗之间相互认证,出现不同你们一块儿探讨,保证程序正确性。

前面三次做业中,我高度依赖自动化测试,缘由很简单,测试用例的构造能够所有随机生成,因此与其处心积虑构造测试用例,不如直接自动生成测试来的简单稳妥。每次测试都部署在树莓派上7*24h不间断运行,因此也不太须要担忧有什么特殊的测试点没有测到一类的问题。可是第四次做业倒是遇到了自动化测试的困境,由于UML图实在不能随机生成,因此这又逼迫我从新回归了手动设计。

整体来看,前三个单元的做业让我实现自动化测试的水平不断提升,而第四单元的做业有让我从新回归对问题自己的思考。此外,测试和实现的顺序也在第四次做业发生转换。

这个过程还伴随着其余工具的使用,好比JUnit,JProfiler以及IDEA自带的插件等等,这些工具的使用也都可以方便我去测试,发现bug,特别是Coverage的评估可以指导我构造出高效的测试代码。

4、课程收获

4.1 工具链的使用

这门课接触了不少工具。

IDE:IntelJ IDEA

单元测试工具:JUnit

线程工具:JProfiler

UML工具:StarUML、z3

了解了不少语言和表示方法:

Java语言、JML语言、UML图的规范

4.2 工程代码能力的提高

由于代码风格的检查,指引我造成良好的命名、缩进、加括号的习惯。这样写出的代码才有可能成为有质量的代码。

结合IDEA的自动补全、代码建议、快捷键的使用,写代码的速度和效率极大的提高,灵活使用条件断点、变量监视等方法极大地加快了debug速度,认识到一个高效的生产力工具是何等重要。

这也是第一次进行系统性的Java代码书写,Java是比较广泛使用的语言,掌握之颇有必要。不过Java里面的还有不少复杂的语法包括面向对象的特性我尚未用到,往后还要继续学习。

多线程能力的训练,第一次实战多线程,在OS课上学过管程,当时就发现Java实现并发控制本质上就是管程,因此还算比较快的认识到这一点。多线程程序是比较有意思的,不过出了错误不能复现确实比较有挑战。

4.3 面向对象思惟的训练

这是这门课一个很重要的学习目标,也是我收获最大的一部分。

从第一单元开始,这门课就不断强调关于面向对象的设计理念,除了掌握了一些基本的说法和面向对象的概念之外,在这四个单元的做业中反复强化的架构设计才是对面向对象理念进行学习的最好方法。而这种潜移默化的能力训练很重要,可是却有不易表达,可是从几回做业的架构设计演进中能够略知一二。

5、立足于本身的体会给课程提三个具体改进建议

5.1 关于性能分的意见。

比谁短、比谁快我以为很差,助教也已经说过这个东西主要是给学有余力的同窗作,那么问题就来了,首先,学有余力的同窗是否须要这点分数的激励,其次,学有余力的同窗想不想作这个事,我以为这些都应该思考。有想积极探索的同窗,这个应该鼓励,可是是否是能够在其余方面给予奖励。

5.2 分数计算方法

听说是按照排位给分,这个很残忍,我知道大家会说竞争很重要,社会很残酷,我也不反对,但在公开场合我想走人道主义路线。

5.3 关于课上实验

高工每次实验的时候都已经对所学知识很熟练了,没有起到趁热打铁的做用。

相关文章
相关标签/搜索