一、作业架构设计

oo第四单元的主题是UML结构的理解和内容的解析。UML是一种可视化的统一建模语言,它的工具采用了一种非常特殊的方式存储图形元素:UML工具将图中的每个部分都具体为一个元素,并将每一个元素都平行放置。这样的存储方式隐藏了图中本身存在的结构,但能够极大地提升增删元素的效率。这就好比一种复杂的、混合了多种化学元素的材料,他能够展现它们组合后特殊的性质,但当我们需要提取其中某个元素的时候,却异常困难;而如果一开始这些元素就是分离的,那么提取某一种元素将变得非常简单,直接获取即可,但这些元素之间便无法展现它们结合在一起时展现的特殊性质。

由于UML工具的主要任务就是提供用户与图形元素之间的交互,而交互内容基本上以添加、移动、删除元素为主,采用平行放置所有元素的方法能够极大地改善交互的体验。但这也产生了一个弊端:UML图在使用的时候,需要对图形元素进行解析,但元素之间的关系却随着结构的隐藏而变得不直观,为了解析时的方便,我们往往需要在解析前重新建立图的结构和元素之间的关系。我们这一单元的任务,就是重建这些结构和关系,并实现一些简单的解析任务

第一次作业

1、设计思路:

第一次作业是对类图的解析。由于类图中的元素众多,可以说,类图是这个单元需要具体掌握的三种图中最复杂的一种。在我的设计中,我采用了类似树的结构去描述元素之间的关系。首先,最顶层的父节点是UMLModel,在UMLModel之下有众多的UmlInterface和UMLClass,在每个UmlInterface和和UMLClass之下,又有许多UMLAttribute和UMLOperation,UMLOperation下方又有许多UMLParameter,一共四层关系。此外,类与接口之间还有关联、继承、实现的关系,具体到图中的元素为UMLAssociation、UMLGeneralization、UmlInterfaceRealization。采用这样的结构主要原因是为了模仿Java中的类内的元素的排布关系,方便之后针对java类的一些解析任务。

第一次作业的解析任务也比较多,主要可以分为三类:一类是查询类中某种元素的数量,第二类是是查询类中符合某种特殊要求的所有元素的名字,第三类查询与类有某种特殊关系的所有类或接口的名字。第一种解析任务和第二种解析任务比较简单,因为查询的元素仅仅涉及自己类内部的某个或某些元素,而不涉及元素之间的关系。因此只需要在构建类的树形结构的时候,将属于自己的低层元素全部保存在类中,查询的时候直接返回已经保存好的对应元素地数量,或者根据要求查询保存好的元素,并将所有符合要求的元素名字放入一个集合中即可。第三类解析任务相对复杂一些,因为第三类的解析任务涉及到多个类之间的关系,并且三种关系(关联、继承、实现)之间还需要相互结合进行处理。抽象到树形结构中,相当于在第二层所有的UmlInterfaceUMLClass之间,根据三种关系进行连线,形成了一张图。由于所给的数据必须能够在Java8中实现,因此这张图中多了许多限制,所有的解析任务不需要复杂的算法实现,利用简单的bfs和dfs就能够解决。

在具体实现方面,由于课程组已经实现了所有元素的类,并提供了最终需要实现的查询类的接口。这事实上是一个强烈的暗示:目标需求是实现查询接口的所有方法,但是所提供的所有元素类中并不能够完全满足目标需求。这不正是上个单元实验中所提到的适配器模式的应用场景吗?因此在具体实现中,我创建了自己的MyClass、MyInterface等等类,将课程组实现的对应元素的类保存在其中,并增加了如相关联的类、直接继承的类等等其他数据和方法,用来满足树形结构和局部图的实现,并完成查询接口中需要完成的一些任务。

 

2UML类图:

 

 

3、度量分析:

 

 由于内容较多,因此只选取了目标接口的实现类的度量分析图。

 

4、总结

这次作业写起来的体验是:UML元素关系的理解比较复杂和费时,代码实现起来相当繁琐,但是很多地方的代码都是相似的,因此写起来的难度并不大。从这次作业的结果来看,自己重建的元素结构和关系,以及自己实现时所使用的模式的效果还是非常不错的,有许多同学使用了相同的结构和模式,也印证了这样的实现方式能够被大多数人理解和接受。用树形结构实现类图中元素的关系使得作业中各种解析查询变得相对容易编写,因为大部分的所属关系都已经在树形结构的构建过程中建立好了,只有特殊的三类关系需要进一步使用图的方式进行查询。

当然,这次作业的实现上还是有一些值得改进的地方。首先正如上一段所说,在树形结构搭建的过程中,大部分的所属关系都已经建立好了,这也使得用于搭建结构的构造函数拥有巨大的代码量,甚至比真正实现解析功能的代码还要多。为了贪图方便,我仅仅是将构造函数内的代码根据所构建的元素不同分成了多个方法,使得构造函数内只需要短短地调用每个元素的搭建方法即可。但事实上,这些方法依然在类中占据了大量的空间,且与这个类进行类图的解析的功能无关。因此,最适合的解决方法是专门创建一个工厂类,实现将平行的元素搭建成树形结构的功能,而原来的类则专门用于实现类图的解析。

此外,在这次作业中,我写的图中某个点是否可达的dfs又一次忘记将已经到达的点做上标记,使得强测中有一个点超时了。虽然值得庆幸的是,助教们只放了一个可能超时的数据,但我仍然有些懊恼,明明上个单元刚刚出现过错误的问题,明明上个单元信誓旦旦地说下次一定注意,结果才隔了一星期就又忘了……或许该写张纸条贴在床前提醒自己才行了……

 

第二次作业

1、设计思路:

第二次作业增加了对状态图和顺序图的解析。经历了第一次作业对元素间的结构和关系的思考,第二次作业写起来容易多了。虽然顺序图和状态图中没有能够在Java中直接找到模仿的对象,但我们依旧可以使用树形结构重建元素之间的关系。相比于第一次作业的类图来说,状态图和顺序图的属性结构更加简单,因为元素之间的关系从三个变为一个,在状态图中是状态转移,对应的元素为UMLTransition,在顺序图中是消息,对应的元素为UMLMessage。结构的搭建完全参照第一次作业中类图的方法即可。

第二次作业的解析任务也比第一次作业的简单许多,只有查询某种元素的数量这一种类型的解析任务,直接将建立树形结构时保存好的对应元素的数量返回即可。但是事实上,这两种图的解析可以非常复杂。对于状态图来说,如果解析的内容涉及到状态转移的Guard、Event和Behavior的话,就有可能进行一些逻辑合理性的分析,甚至可以与上一单元的JML的相关内容相结合。对于顺序图来说,如果涉及到消息块的处理,也会使难度提升一个档次。不过要真涉及到上边这些内容,我还真不知道该如何解决。

在具体实现方面,我们采用两次适配器的方式进行实现。对于状态图和顺序图来说,模仿第一次作业的方式,将对应的已经实现的类放入自己的类中,然后添加新的属性和方法。对于最终需要实现的目标接口,我们再次使用适配器模式,将三个图作为属性保存在目标类中,实现目标接口的各种解析功能时,我们只要调用对应的图中的方法即可。采用这样的方式能够最大程度地将功能进行分离,并减少最终需要实现的目标接口的代码量。

 

2UML类图:

 

 

3、度量分析:

 

 

4、总结

这次作业写起来比第一次作业要轻松许多。对于理解各个元素之间的所属关系和关联上,助教和同学都已经在讨论区中给出了详细的总结;对于结构的搭建上,由于采用了和第一次作业相同的结构,好多代码都是类似的,复制过来稍作修改即可;对于解析任务上,出题的助教们也没有特意为难,基本上都是简单的查询元素数量的问题。

在这次作业中,我觉得自己迭代开发的效果还挺好的。由于第二次作业中对于类图的解析与上次作业完全相同,再加上采用了适配器的模式,很容易就能够做到在上一次作业的基础上继续编写这一次作业的代码。这也值得我去仔细地思考,未来写代码的时候该如何寻找到这样既能够将功能很好地分离,又能够方便地进行迭代开发的模式。

 

第三次作业

1、设计思路:

第三次作业主要是对三种模型进行有效性检查。由于没有涉及新的UML模型图,因此第三次作业中不再需要进行新的结构搭建,只需要进行一些特殊的解析任务,即在普通解析任务之前的有效性检查。

这次有效性检查的解析任务主要分为三类:第一类是对元素数量的检查,第二类是对元素属性的检查,第三类是对元素关系的检查。第一和第二类比较简单,直接遍历元素或元素属性即可。第三类问题由于涉及到元素之间的关系,因此又需要运用图的知识。第三类问题看似比较复杂,但是将问题化简之后,都可以利用简单dfs直接解决。比如求所有处于循环继承环上的点,其实就是每个类或接口能否通过继承链到达自己;求是否重复继承或实现接口,其实就是每个类的继承链或接口实现链是否是一棵树。在使用dfs或bfs的时候,要注意标记已经到达的点,否则可能陷入死循环。

对于具体的实现,我依旧采用了适配器的模式,建立了一个有效性检查的类,并作为最终需要实现的类的一个属性,实现对应接口的有效性检查方法时,直接调用有效性检查的属性中的方法即可。

 

2UML类图:

 

 

3、度量分析:

 

 

4、总结

这次作业由于只涉及解析任务,因此代码的实现上不再繁琐,也不再需要理解新的模型图元素之间的关系,最主要的是分析和化简给出的有效性检查问题的实质。从类图中可以看出,在这次实现上,由于结构的搭建全部集中在前两次作业完成的三个UML模型图的类中,而有效性检查需要运用到这些结构,因此为了贪图方便,我就直接将有效性检查的代码分布到对应的模型图的类中,而新建的检查类事实上是对前两次作业中完成的三个UML模型图类内的有效性检查方法的调用。这么做似乎没什么问题,但事实上修改了前两次作业完成的代码,违背了迭代开发的要求;还使得模型图类内存在与解析功能不相关的有效性检查的代码,让类的功能变得更加复杂。因此仍然有改进的空间。

经过思考之后,我有一个改进的思路。其实方法也非常简单,就是在第一次作业中提出的:将搭建结构的功能交给工厂类。这样一来既能够将搭建结构的功能与解析功能进一步分离,又能够使UML模型图的结构不再成为对应的类的专属,只要向工厂类申请,就能够获得一套对应的UML模型图的结构。采用这种方式,新建的有效性检查类就不需要依赖前两次作业中完成的模型图类去获取图的结构,直接在自己内部就能够对模型有效性进行检查,从而最大程度地将功能分离,并实现迭代开发的要求。

 

二、架构设计和oo方法的演进

第一单元

第一单元主要是从之前的面向过程编程向面向对象编程进行转变。我仔细地观察了我第一单元写的代码,实在是看不出有啥架构设计,基本上就是怎样写得爽怎样来,并没有考虑迭代开发的时候要对代码进行补充还是重构的问题。

但是,在第一单元中,对于oo方法使用的进步还是很大的。在预习作业中,我编写的所有代码都是用的普通数组,大量的不同功能的代码全部集中在同一个类中。但是到了第一次作业乃至以后,我几乎再也没有使用过普通数组了,基本上采用java内已经封装好的容器进行数据的存储。还明白了类中不同的功能是需要拆分的:由于第一单元是多项式的处理,输入输出比较复杂,我特地设置了专门的输入输出类来对数据进行拆分和化简。此外,在最后一次作业中,运用了数据的抽象:由于最后一次作业中涉及到的项比较多,因此我设计了因子的接口去统一管理所有的因子。这样一来,不仅存储起来非常方便,进行求导和求和的计算时其行为也相对统一。

第二单元

第二单元主要学习的是多线程的编程。在这个单元中,由于需要处理的场景是用户乘坐电梯,因此我主要采用了生产者-消费者模式的架构设计,这应当是多线程编程中最经典的架构设计了。我将楼层作为生产者,不停地“生产”出乘客;电梯作为消费者,将楼层“生产”出的人接受并运送到相应的楼层;在生产者和消费者之间还有一个调度器,既作为缓冲区,用来存放生产出来但还未被消费的乘客,又承担分配任务,将缓冲区的乘客分派给合适的电梯。可惜的是,在当时我还是没有迭代开发的意识,每次新的作业到来的时候,虽然不是全部重构,但依然在上次作业已经完成的代码上填填补补,实现的方式不够优雅。事实上,将生产者、消费者以及调度器都设计成接口,然后每次迭代开发的时候,都继承上次已经完成的代码,会使得迭代开发的效果更好一些。

第二单元的oo方法自然是学到了很多。因为多线程的编程以前从来没有接触过,几乎所有的内容都是新学到的,比如synchronize同步锁、wait、join、notifyAll等等多线程编程中专用的方法。多线程编程最重要的也是最困难的内容就是线程之间的互斥和同步,熟练地运用好上面提到的同步锁以及wait和notifyAll等等,才能够保证自己写得多线程程序不会陷入死锁或是原子操作被打断这样的错误中。另外,在咱们电梯多线程作业中,还有一个需要重点考虑的地方:线程的安全退出。由于一开始没有很好地理解和总结退出的规律,在课堂实验上吃了苦头。不过后来归纳出第二单元总结中提到的线程安全退出的条件之后,在处理生产者-消费者模式的线程安全退出上就游刃有余了。

第三单元

第三单元主要学习的是JML规格化语言的使用。这个单元实现的场景是一个人机关系网络,但由于对所有的方法都给了具体和完整的JML规格化描述,再加上所有的重要的查询方法全部集中在一个Network类中,喜好偷懒的我就完全不考虑结构设计,所有的方法全部堆在自己实现的Network类中,最后恰好499行,压着线过了checkstyle。但事实上这样并不好,达不到oo学习的效果。后来在第三单元的总结中也重新思考了一番架构设计,发现采用按照作业的顺序继承开发,或者采用适配器模式分离单个类中的功能,都是可行的、适合迭代开发的架构设计。

第三单元学到的oo方法主要有两点。第一个不是具体的方法,而是JML规格化语言所采用的思想。因为未来真正参与项目和工程的时候,必然是多人协作的模式。如何寻找一种合适的语言去破除自然语言的模糊性是非常重要的。JML语言虽然目前还有很多缺点,但是这种完全精确的逻辑语言描述,为我们未来工程中准确交流提供了一种思路。第二个关键的oo方法是Junit单元测试。这是一种运用非常广泛的单元测试工具。在我接触Junit之前的所有测试手段,无论是黑箱测试、手测等等,都是对整个代码进行测试,有时发生了问题,要准确定位到具体方法还需要一定时间。但是Junit是单元测试工具,他最大的好处就是能够直接测试单个方法,并且是自动化测试,还提供了不少测试模式。听说在真正的工程开发中,还有专门面对单元测试进行开发的TDD模式,可见熟练地掌握这种单元测试工具还是非常重要的。

第四单元

第四单元主要学习的是UML模型图的理解和使用。在这个单元的作业中主要实现了一个UML模型图的解析器。正如前面所总结的那样,UML模型图中的元素全部都是平行放置的,如果想要方便快速地解析UML模型图中的信息,并适应迭代开发,一个合理的模型结构和架构设计是非常重要的。在这次作业中我使用了适配器模式,在迭代开发的问题上解决的不错,基本上三次作业下来没怎么修改前一次作业写过的代码。说实话,这四个单元下来,只有这一次算是比较好地进行了迭代开发以及功能分离,也从中尝到了甜头。这也提醒了我未来写代码的时候,要更加重视架构设计,越好的架构设计能带来越棒的编程体验。

第四单元学到的oo方法主要是UML的使用以及通过不同的UML模型图用不同的视角观察同一个代码。我们平时观察一个面向对象的代码,更多地可能是从类图的角度进行观察,但是事实上,用流程图、状态图、协作图等等图的视角进行观察,往往能看到不同层面的代码抽象,从而更加全面地了解一个代码。

 

三、测试理解与实践的演进

说实话,在测试这方面,这学期我做的并不好,与那些亲手搭建评测机并创建数据集的同学相比,我的测试方法属实有点low。原本也想自己做个评测机啥的,结果自己既不会又懒得学。搞到最后自己没有评测集进行大量数据的测试,也没有数据集和同学进行对拍,只能自己孤苦伶仃地做着简单的测试……在这里就提一下我所使用的几种测试方法。

首先这学期,我所使用的最主要的测试方法,是一种挺有名气,但做起来却显得有些尴尬和难为情的测试方法:小黄鸭测试法。要是在学校,我还真不敢用这个方法,被别人看到怕被当做傻子,但这学期碰巧一个人在家学习,所以用起来也无妨。不得不说,这个方法虽然看上去和开玩笑似的,但效果真的很好。我使用的时候主要分成两步:在写整个程序和一些相对复杂的方法之前,问自己一句:“你想怎么写?”然后自己回答一遍;在写完整个程序以及那些复杂的方法之后,指着代码问:“你凭什么这么写?”然后根据代码回答一遍。这样做对于理清整个代码中类与类之间的关系,以及具体方法的实现过程有很大的帮助。做完后的询问,既能检查设计时的缺漏,也能够查出一些实现时错误地逻辑。

第二种方法,应该是大家都会使用的一种方法,就是自己构造一些简单,但是处于边界状态的数据进行测试。因为设计的时候我们往往以及考虑了大部分的情况,但容易忽略一些边界点,而边界点恰恰是强测以及未来真正开发时最喜欢的检查点和出错点。因此,这些简单的数据虽然看起来简陋,但有的时候比巨量的自动造的数据更有测试效果。

此外,还有在第三单元中学到的Junit单元测试。这个测试工具应当是比较成熟的了,但是我不知道如何快速地将制造的数据输入进去,只知道一个一个实例化然后加入。这也使得我使用单元测试的效率往往还没有直接黑箱测试+错误定位的效率高,因此用的很少,只在第三单元有些不方便直接调试的方法内使用了Junit单元测试的技术。

前两个测试方法相互结合,已经能够解决大部分测试的问题了。但毕竟是相对简陋的测试方法,这两种测试法往往无法发现代码中复杂度超标的问题。因此在四个单元的强侧中,除了彻底爆炸的两次,其余的错误都是复杂度超标了但自己测试时发现不了。

最后是我四个单元的作业做完之后对测试的感悟,也是对以后学弟学妹的提醒:千万不要因为代码简单就不做测试,不要因为使用的测试方法简陋就轻视测试,不要因为测试没发现问题就觉得时间被浪费了。测试应该被认为是编写代码过程中一个极其关键和有意义的固定环节。像我上边提到的三个挺low的方法,最后依然能够帮助我找到不少的bug,也使我的程序在强测中有较好的表现。而我唯二的两次强测爆炸的程序,一次是因为觉得代码简单没必要测试,一次是写得太急没时间做测试,从中也可以看出测试的重要性。

 

四、课程收获

应当说,这学期面向对象编程的学习,让我收获了很多。毕竟这对我来说是一个以前从未接触的新的知识点,一个学期学下来能够比较熟练地运用java中的各种常用的库函数,写出自己想要的程序,已经是一个巨大的进步和收获了。下面具体说说我对一学期oo课程的收获。

首先是一个老生常谈的问题:对面向对象编程的理解,毕竟之前学习的都是面向过程的编程,为啥还要再学一个面向对象编程?在我看来,面向对象编程与面向过程变成最大的差距在于:面向对象编程所操作的目标是一个个实例,注重的是每个实例之间的关系;而面向过程编程所操作的是一个个函数,注重的是函数调用的顺序。尽管有如此大的差距,两者之间仍有结合:面向对象编程在构建完实例之间的关系之后,还需要对这些关系进行处理和分析,即我们所说的方法,这些方法的实现又需要运用到面向过程编程的思想。因此面向对象编程是对面向过程编程的具体化和丰满化,他将一些面向过程编程中的函数和属性包装成一个具体的事物和对象,在程序中发挥它的作用。

在这学期的oo学习中,让我觉得收获最大,最有意思的单元,应当是学习多线程编程的第二单元。首先这个单元学习了之前从未接触过,但未来应当是应用最广泛的多线程编程,在实用性上有非常高的价值。此外,这个单元还是最综合的一个单元,既需要思考每个多线程函数实现时的同步和互斥问题,又需要设计更合适的电梯调度算法使得能够在最终测试上拿到更多性能分数,另外,由于三次作业迭代更新的内容有些并不可知,还需要选择合适的架构设计,使得它能最大程度地适应未来可能的电梯功能,并减少对已实现的代码进行修改。但限于这是第二单元的内容,自己对面向对象编程以及各种架构设计的理解不深,再加上出题难度也有所限制,使得一些更有拓展性的地方无法展示,自己回望当时写的代码也有很多不满意之处。我觉得,倒不如把第三第四单元的对面向对象编程理解的部分前移,然后将这个最实用、最综合的单元放到最后,去验证同学们整个学期对面向对象编程的理解程度。

当然,这学期也有一些不满意的地方。第一个就是对于代码的测试方法不够好。看到好多同学都通过学习,自己搭建自己的评测机和创建自己的数据集去自动测试,反观自己是纯手动的简陋测试,总是偷懒结果让自己失去了学会一项新技能的好时机。还有一点就是在oo的学习中与他人的交流分享太少,没有参加研讨课,甚至连讨论区都没发过问题。其实之前有的时候,自己还是有一些对当前作业的一些思考的,但是总是觉得这些思考比起那些大佬们的想法显得太幼稚,都不敢说明,最后只能放在课后总结中给自己看。或许把这些想法分享出来,让大家讨论甚至批判一下,自己反而能收获更多呢。

 

五、改进建议

我觉得需要改进的第一点是平时对于作业的讨论环境,我觉得有必要专门建立一个oo作业的讨论群,供同学、助教和老师们在里边交流每周作业的问题。虽然已经有了讨论区供同学提出问题并寻求解答,但有的同学确实不适应通过严肃的讨论区去发表自己的疑问,但这学期又没有专门的讨论群供大家交流解答,结果问题全部堆到灌水群里边去了。灌水群里边有大部分内容都是同学之间的聊天交流,但中间有时却夹杂着很关键的、能够解答我当前疑问的一些回答,但又没人把这些发到讨论区,只能自己从中翻找,感觉体验不是很好,还不如像操作系统课一样,专门建个交流大群,为一些不愿意发讨论区的同学建立另一个专门的讨论渠道。

第二点是预习作业。这学期的预习作业希望我们能够掌握一些面向对象编程的基本思路和理念。但事实上,我到了第一单元的第二甚至第三次作业之后才开始慢慢理解面向对象编程应该怎么做。现在反观预习作业,当时我连java很多功能具体怎么用都不知道,写的代码基本上就是用面向过程的方式硬写面向对象的题目,写得很辛苦,但是效果一般。我觉得,倒不如更基础一些,讲讲java语言体系中一些关键的工具用法,比如各种容器、正则表达式的使用,继承和实现,抽象类和接口的具体使用方式会好一些。这样在进入第一单元时,大家对java语言的使用上已经比较熟悉,再灌输面向对象编程思想,可能会更平滑一些。

最后就是实验课的设计。不知为啥,每次实验课总让我觉得题目有问题,有的地方真不知道该怎么写。但是实验课结束之后,这次实验的反馈又没有,题目看不了,解答也没有,分数又不公布,最后一些实验中的疑问就烂在肚子里,自己忘掉了。感觉做一做实验课结束之后的解答和反馈还是有必要的,不一定要公布分数和解答,至少应该分析和解释一下题目的思想和流程。

 

六、线上oo学习体会

线上oo学习感觉和线下的应当也差不多吧,没有特殊的感受。对于理论课来说,线下的学习与老师的交流会更多一些,线上的学习更自由一些,听不懂的地方也能够重复听。对于每周作业来说,我觉得基本没差别,都是自己坐在电脑前对着IDE一阵敲。

这次总结应当是oo学习最后一步了。oo课与计组一样,好像也是经典的折磨人课。但与上学期计组完成之后一样,虽然写的时候觉得很蛋疼很烦躁,但结束之后回望,看着自己一点点码出来的代码,不谈收获大不大,至少成就感还是满满的。从同学们的反馈中可以看出,咱们oo课程设计中,依然有一些不令人满意的地方,但我相信,未来的oo课程一定会越来越好。

posted on 2020-06-17 13:15  marvinhehehe  阅读(159)  评论(0)    收藏  举报