BUAA OO Summary - Unit4

第四单元总结

一、总结本单元架构

本单元的最终目标是:实现一个UML解析器,使其支持对 UML 类图、状态图和顺序图的分析,可以通过输入相应的指令来进行相关查询,并能根据 UML 规则进行一定的规范性验证,若不符合要求,抛出异常,终止程序。

类设计入下

|-- sequencechart
|   |-- MyInteraction.java
|   `-- MySequenceChartImplementation.java
|-- statechart
|   |-- MyStateMachine.java
|   `-- MyStateChartImplementation.java
|-- MyClass.java
|-- MyInterface.java
|-- MyOperation.java
|-- MyImplementation.java
|-- UmlPreCheck.java
|-- Main.java

本单元在指令输入以后,会先在appRunner中创建MyImplementation的实例,并把解析好的UML中的元素信息elements传入,所以我们需要在MyImplementation的构造方法中将UML中的元素全部解析出来存储好,方便解析查询指令后调用MyImplementation中的方法时完成查询。由于三次作业迭代后需要完成类图、顺序图、状态图的解析,为了不让MyImplementation过于庞大,我将顺序图和状态图的解析传递到MySequenceChartImplementationMyStateChartImplementation中完成。

MyImlementation实现如下:

public MyImplementation(UmlElement... elements) {
readClassOrInterface(elements);
readAttriOrOper(elements);
readParameter(elements);
readGenerOrInterReal(elements);
readAssociationEnd(elements);
readAssociation(elements);
myStateChartImplementation = new MyStateChartImplementation(elements);
mySequenceChartImplementation = new MySequenceChartImplementation(elements);
classes = new ArrayList<>(id2class.values());
interfaces = new ArrayList<>(id2Interface.values());
interactions = mySequenceChartImplementation.getInteraction();
stateMachines = myStateChartImplementation.getStateMachines();
}

MySequenceChartImplementation实现如下:

public MySequenceChartImplementation(UmlElement... elements) {
readCollaAndInteraction(elements);
readLifelineAndEndpoint(elements);
readMessageAndAttribute(elements);
}

MyStateChartImplementation实现如下:

public MyStateChartImplementation(UmlElement... elements) {
readStatemachineAndRegion(elements);
readState(elements);
readTransition(elements);
readEventAndOpaqueBehavior(elements);
}

实现Implementation的构造函数的时候需要注意解析元素的顺序,有的元素之前是具有包含或依赖关系的,如果解析不当会导致无法将元素正确地嵌套起来。

此外,经观察,很多查询指令需要重复地抛出相同的异常,经观察分析,为了不让各方法重复实现相同功能,可以专门写一个方法完成此事,比如,关于UML状态图的查询指令中,重复出现statemachine的名字不存在或重复出现的异常,我们可以专门增加一个查找statemachine的方法,用于检查错误,其他地方类似:

private MyStateMachine searchStateMachine(String statemachineName) throws
          StateMachineNotFoundException, StateMachineDuplicatedException {
String statemachineId;
  if (!stateMachineName2ids.containsKey(statemachineName)) {
  throw new StateMachineNotFoundException(statemachineName);
  } else if (stateMachineName2ids.get(statemachineName).size() > 1) {
  throw new StateMachineDuplicatedException(statemachineName);
  } else {
  statemachineId = stateMachineName2ids.get(statemachineName).get(0);
  return id2stateMachine.get(statemachineId);
  }
}

二、总结四个单元中架构设计思维及OO方法理解的演进

第一单元

这一单元完成了表达式的化简。这一单元的实现很好地体现多态、继承、封装的重要性。

结合指导书对于架构设计的建议:

  • 对于每一种函数(常数、幂函数、三角函数、求和函数、自定义函数),分别建立类

  • 对于每一种运算规则(乘法、加减法),分别建立类

  • 对于自定义函数,可以先将其 定义表达式 展开,将展开后的结果代入进行计算。

  • 对于求和函数,可以类似于自定义函数进行代入

于是,我一共定义了8个函数类,分别为加法类(Adder)、乘法类(Multiplier)、幂函数类(PowerFunc)、常量类(Constant)、正弦函数类(Sin)、余弦函数类(Cos),他们实现了Function接口。

在通过递归下降法来解析表达式,这样的设计可以让表达式中的成分清晰有层次地进行读取并存储起来,形成一个树状结构。

第二单元

这一单元让我们初步领悟到多线程协同工作,以及同步块的设置和锁的选择の 重要性。比如在函数上加synchronized,这样锁的对象是this,这样锁的力度很大,确实很好地保证了线程安全,开销很大,性能会受到影响。我们不仅要保证程序的正确性,还应该尽可能追求高的性能。推荐使用synchronized(obj),找合适大小的锁去锁,并且synchronized里的代码尽量精简。这样的话就会需要很明确地知道同步的具体对象是什么。以后我再遇到多线程相关问题的时候我一定会采用这种方法。

整体上采用了生产者与消费者模式,将输入类调度器类电梯类继承Thread,GeneralQueueWaitingQueue则是线程安全类,作为托盘,在其中实现线程之间共享对象的增减操作,保证线程安全,从而在线程类中就不用过多考虑由于操作共享对象可能引起的线程安全问题,使问题“分而治之”,一定程度上可以降低问题的复杂度。

第三单元

这一单元学习了JML规格,感受到了即便有JML规格的约束,代码实现仍旧十分灵活,使用不同的数据结构和不同的算法,虽然最后结果的正确性都能得到保证,但是性能会大相径庭,这也让我意识到了在拿到JML规格后,不能觉得各个方法已经被设计好了而略过了架构的设计,其实这时候架构的设计仍然很重要,这将极大影响后面各方法实现的复杂度以及整体的性能。此外,也让我了解到学好数据结构和算法这两个基础的重要性。

第四单元

本单元实现了UML的解析器,之前的单元虽然画过UML的类图,但是都是直接可视化的层面直观的看到,并没有看到深层次下,UML其实也是由很多的对象组成,各元素之间也有着各种各样的依赖和嵌套关系。这让我从UML的角度深层理解了我们在利用类来实现各种对象的时候,其实这些类、类中的属性与方法以及类之间的关系本身也是由很多对象组成的。让我感慨于“面向对象”的统一性,万物皆对象,设计好类以及类之间关系的重要性。

三、总结四个单元中测试理解与时间的演进

经过一个学期的学习,测试主要分为白盒测试与黑盒测试。

白盒测试不涉及构造数据,但是确是至关重要的。个人感觉在写完所有代码后,只有自己把代码对应JML规格每个方法重新梳理对照一遍,才能一定程度上保证这次作业的正确性——这是来自白盒测试的安全感

黑盒测试也很重要,很多时候方法内部的错误很难自己肉眼检查出来错误,而通过输入数据,观察结果,可以非常直接地知道出现了错误,并且通过debug快速定位错误。这是往往会发现出现错误的地方对于自己来说,是很难找出来的,因为自己对那段代码理解的运行轨迹和实际的不一样,这种理解上的问题,不管怎么检查都检查不出,检查出的都是自己理解正确了但是实现错误了的地方。

第一单元

这一单元,我自己搭建了一个简单的自动化评测机。主要就是生成数据,输入程序,得到输出,检查正确性。用到了python的Xeger,利用正则表达式来生成最基本的常数因子和表达式因子

然后写了几个函数分别用来生成

  • 不含表达式因子的项

  • 不含表达式因子的表达式(即用来生成表达式因子)

  • 含表达式因子的项

  • 表达式

第二单元

这一单元的测试确实与第一单元都很大不同。第一单元一个输入就会得到一个输出,并且每次的结果都不会改变,容易复现,并且容易debug找到问题出现的地方。而这一单元由于这是多线程协同,很多情况不能复现,debug也比较困难。

我找bug几乎是采用黑盒模式,把输入的极端情况(比如同时多个请求、超载、1楼10楼这种边界位置的转向)考虑到以后测试程序结果是否如逾期,知道这样hack确实不太好,但是确实挺有效的。

这一单元由于多线程的错误有的很难复现,而且输出由时间戳组成,并且有几条结果的顺序并不强制,这对于我来说无论是搭建评测机还是和别人对拍都是很难的,所以白盒测试很重要,要深入理解多线程的线程同步,清楚地分析架构,以及电梯状态的转移。

第三单元

白盒测试:由于本单元的正确性很大程度取决于对JML规格的正确理解,若理解不正确造出来数据后得到输出,也不能判断代码正确与否,所以我的三次作业的测试数据都是通过仔细阅读并理解每个方法并对其正常与异常情况尽可能覆盖地进行构造数据来完成。

其中仔细观察JML的描述,会些小坑点,例如

  • addToGroup有人数上限,不得超过1111

黑盒测试:此外由于本单元作业虽然没有性能分,但是个人感觉对性能的要求其实更硬性了,因为这直接决定最后测试点的正确性,性能不好直接tle。而对于性能好坏的评测,除了通过计算代码复杂度,最简单的办法,就是在满足测试数据限制的前提下构造大量且集中攻击复杂度高的方法的数据,然后输入程序来看得到输出耗时的长短,如果输进去以后要卡一段时间才能出结果,那多半性能就是不行的,而想要解决这种性能问题,除去利用一些例如isUpdated的标志位来保证不重复计算已经计算过的值的傻瓜方法以外,多半是需要将整个方法重新再写一遍的,而这个问题集中体现在

  • 第一次作业

    • isCircle——qci

    • queryBlockSum——qbs

  • 第二次作业

    • queryGroupValueSum——qgvs

    • queryLeastConnection——qlc

  • 第三次作业

    • sendIndirectMessage——sim

通过ap,ar,ag,atg等指令构造一个复杂的人际关系网后,将这些指令反复调用,排列组合,在一定指令书数下最大化地反复调用复杂度高的方法,就很容易卡掉时间,是互测hack别人的好办法。

此外除了有可能tle以外,如果没有投入数据进行测试,很难发现一些自己看起来很对,怎么也检查不出来,但是其实蒟蒻的bug,比如

for(int i=0; i<list.size(), i++) {
list.remove(1);
}

很多类似的问题,我一般是经过仔细阅读代码白盒测试过了以后,投入数据后报NullPointer的异常了之后我才后知后觉,

其他方法,还包含课程组强推的JUnit,但是这个也需要在理解JML规格的情况下才能写的测试代码的正确性,JUnit的好处很多,可以知道测试到具体的语句,以及测试覆盖度等,是个很好的测试工具。

第四单元

这一单元,除了认真检查代码白盒测试以外,也通过画一些类图然后通过课程组给的jar包输出其元素来检查实现是否正确。由于UML图的可视性,很容易根据各指令设计相应的UML图,然后检查输出进行对比。

这一单元没有互测,所有公测数据保证所有 mdj 都是 StarUML 绘制的,但是互测后数据不一定遵守这个规则,这就需要对每一个元素字段和 UML 整体逻辑结构做验证,以保证其符合约束的规则。

四、课程收获

现在想想这学期之前,对于OO还是一脸茫然,首先JAVA的使用就非常不熟练,很多时候即使架构设计出来了,但是落实到具体代码实现的时候总是有些力不从心;其次,对于“面向对象”这一概念,我知道它的出现相对于“面向过程”,由于它更加形象,更加符合人类对事物关系的理解,大大简化了解决实际问题的复杂度,但是由于没有亲自尝试过,这些也都是看网上的帖子从字面上知道,但是并没有真正领会到。

记得寒假的时候,看到往届第一单元的表达式求导,毫不夸张的说,内心充满了忐忑,觉得自己不可能完成,在第一次课了解到课程机制是一周一次作业的强度后更是感觉自己可能会频繁挂掉。但是,现在想想,人不逼自己一下,就不知道其实很多事情没有想想的那么不可完成,这一学期也终于挺过来了,虽然平时被课程的强度压迫着,但是回头看看,真的很感谢有这么一个学期的训练,可谓是涅槃了一次。

通过四个单元的学习,我比较深刻地感受到了面向对象多态、封装、继承这三大特点的好处,并且在使用的时候都会不禁赞叹这样设计的巧妙之处。此外,我也意识到,好的程序员并不是只是解决问题,还要保证性能,这也是课程组设置性能分的初衷吧。

所以在拿到一个任务的时候,不要立马开始做,要先设计好架构,好的架构的开始,才能保证后面地基的牢固,否则“重构”早晚会到来。我们要学会根据以后在工作中的职位,把自己想象成架构师、某一具体功能的实现者以及测试人员,也就是学会将任务进行划分,不要一main到底。

现在回顾过去四个单元,感受到了课程组的良苦用心,第一单元让我们了解到设计架构的重要性,保证表达式各层次之间的合理性;第二单元让我们初步掌握了很重要的多线程的实现,初步了解到了多线程同步;第三单元JML让我知道如何读懂规格,并且知道即使已经规定了规格,其最终的实现不同也会对性能造成很大的影响,所以对程序的数据结构和算法的掌握十分重要;最后一个单元实现了UML解析器,让我们对UML的元素有了更深刻的理解。

五、课程改进建议

  • 研讨课的感想提交可以改成每个人都有一份自己的感想,不分主持人和记录的人。因为虽然这样看上去分工很好,但其实最后的结局就是不是记录的同学会感觉与自己无关,最后记录的人也会觉得没什么可写的,又是全组共同的感想,就草草了事了。(可能别的组不是这样,但是我每次的组都是这样的)。感觉可能对于不太积极的同学,只有把任务放到每个人自己身上才会上心吧。

  • 感觉博客作业可以在单元开始的时候就发布,只是截止提交时间还是按第四次作业来,因为感觉每次在座的额时候会有很多丰富的想法,可以记录很多,但是不知道博客作业的要求是什么样的,也就不知道重点记录什么,而到博客作业周的时候一些细节可能记得不是很清楚了,总结的时候也不会太完备。

  • JML规格这一单元可以多设计一些自己写规格的部分,感觉我很多时候只是能看懂规格(虽然可能这样也够用了,课程重点不再此的话可以忽略这条),如果让我写的话,可能会很不严谨,有些地方甚至不知道怎么用JML表达出来。

posted on 2022-06-25 10:03  流英成和  阅读(42)  评论(1编辑  收藏  举报

导航