【2022春-面向对象】第四单元总结与课程总结

【2022春-面向对象】第四单元总结与课程总结

写在前面

本单元的主题是UML。

UML与JML一样都属于一种形式化的语言,而两者都是作为一种面向对象设计的工具而出现的。UML侧重于刻画类,接口及其之间的关系。本单元要做的事情则是通过这种语言形式,设计一个解析UML的工具。

一.第四单元架构

第一次作业

第一次作业仅涉及到类图的构造。输入给出的是许多带有一定信息的元素UmlElement,包括类与接口本身,以及属性,方法,参数,以及继承关系,关联关系等等。而若不经过解析,这些元素将是非常零散的”零件“。我们首先要做的工作是把这些零件有规律的组装起来,然后根据需要去给出相应的方法。

观察我们的需求,可以分为大致两类:第一类是针对某一个类或接口的询问,第二类是针对类与接口之间的继承与实现关系的询问。我们需要从需求出发,考虑如何设计当前的架构,以及如何组装这些零件。

针对类或接口的询问

对于第一类询问,我们可以设计一个MyClass类以及MyInterface类来刻画已经组装好的类与接口,它们不再是单纯的UmlClass这样的零件,而是一个完整的,具有”属性“和”方法“的类。(注:这里的”属性“与”方法“指的不是自己程序中的类本身的属性与方法,而是UML模型中对应的类所拥有的属性与方法。)有了这样的类之后,我们便可以在其上设置询问(也就是设计自己程序中MyClass中的方法)。

同理可以设计出MyOperation MyAttribute这样的类。

针对继承与实现关系的询问

对于第二类询问,其本质上是关于继承与实现的图上的问题:把类或者接口看作图中节点,把继承与实现关系看作图中的有向边。

比如”类实现的全部接口“,其实是从某一节点顺着有向边走,然后获取所有可以到达的节点。”类的继承深度“其实是从某一节点顺着”继承“边走,能走到的最远距离。在后面的作业中也出现了很多此类可以转化为图论问题的询问。

为了解决这样图论问题的询问,本次作业中设计了GeneralizationManager类,用来管理继承与实现关系。此类内部通过建图来分析解决图论问题。

如何组装

其实有了上述的类之后,解决询问只是代码实现上的问题。让人头疼的是如何”优雅“地组装出MyClass这样的类。因为实际上组装出MyClass的过程是:把Parameter放进Operation中,再把AttributeOperation放进MyClass中。这样的组装必须从下到上,先把下层组装起来,然后放入上层的元素中。

这个问题笔者并没有很好的方法,最终采用的方法非常暴力:通过强行判断输入的UmlElement列表的元素类型,根据类型及对应的从属关系从下到上组装起来。

第二次作业

涉及到状态图以及顺序图的构造。

像第一次作业一样,从需求出发反推需要如何的组装元素。其实需求已经很明朗了:需求主要分为针对状态图的询问,以及针对顺序图的询问。因此作出如下设计:

构造自己的类

MyStateMachine,MyInteration:属于自己的状态图与顺序图类,在这些类上设计方法,以解决询问。

StateMachineManager,InterationManager:考虑一个模型有多个状态图或顺序图的情况,需要设计相应的管理者去管理多个元素。

值得一提的是,如果严格按照UML元素之间的关系设计自己的元素,其实有些怪异:因为实际上还涉及到UmlCollaboration,UmlRegion等元素,但是其实际上对自己的元素没有直接关系,这些元素的存在只是为了UML的描述,只是为了构造出自己的元素。因此实际上不需要严格按照UML的结构设计自己类的结构,只需要从需求出发:保证询问能够更方便地解决即可。

图论模型的使用

再聚焦到每个询问的具体实现上,可以发现这基本上都是围绕着图论模型:顺序图中Lifeline和Endpoint是节点,Message是边;状态图中State,PsuedoState,FinalState是节点,Transition是边。而大多数询问几乎只需要枚举一个节点所连的所有边,操作非常简单。

唯一有困难的询问是”状态图关键状态“的询问。根据定义,关键状态的核心是”删除某个状态之后无法从 Initial State 到达任意一个 Final State“,那就根据定义来:对于每个状态都尝试删掉它,然后重新建图,判断是否可以从某个Initial State到达某个Final State。尽管很暴力,但是足够了。

第三次作业

涉及到模型有效性的检查,即模型是否满足相应的限定条件。

其实这些有效性检查仍然是针对已有的图结构,以及借助一些数据结构来解决的。有了前两次作业的架构,第三次作业基本上只是代码的堆砌。

三次作业出现的bug几乎都是没好好审题的结果。譬如“R009:一个状态不能有两个条件相同的迁出”,笔者没有对于每个状态分别判断,而是把所有迁出揉到了一起判断。

二.架构设计思维及OO方法理解的演进

经过四个单元的”洗礼“,架构设计的重要性不言而喻。一个好的架构,自己编写代码时写得舒服,别人看自己的代码也看得舒服。

第一单元:架构设计初尝试

第一单元是架构设计的初尝试:对表达式进行层次上的分解,是从抽象到具体的过程。事物本身都是抽象的,而事物所蕴含的具体的规律实际上是人们依靠着主观能动性而提取发现出来的。而这样的具体规律需要人们的不断思考加工。面对表达式及其计算,平时的我们在数学的学习中也不曾思考过表达式的结构,而是直接拿来计算。但是计算机没有人们那样聪明。因此当我们把这项计算任务交给计算机去完成时,则必须由人们帮助计算机提取表达式的结构,把抽象的事物转化成具体的问题,具体的架构,具体的代码,交给计算机去解决。

第二单元:多线程下的架构

第二单元是多线程下的架构设计:设计一套高效且安全的电梯系统。这一单元是我认为最难的一个单元,因为涉及到多线程这个复杂且未知的事物。在这个单元中我对设计模式的印象最深,因为设计模式使我从一开始的无从下手找到了突破口,对我的帮助很大。关于设计模式,前人已经为我们铺好了道路,而我们要做的则是更好更灵活的使用它们。当遇到棘手的问题时,不妨先在设计模式中找找思路,总有一个模型适合你。

电梯单元中我最先接触到的是生产者-消费者模式:用最简单的话说,就是有一个容器,生产者放东西,消费者取东西。听起来很简单。然而在多线程场景下运用生产者-消费者模式,则涉及到同步互斥这样的多线程概念,需要花时间去理解。这也是这单元最大的难点之一。借助这样的设计模式,我得以生产出最初的设计架构:等待队列和电梯(处理队列)都是容器,然后调度器负责从等待队列拿东西(乘客),放进电梯里。

然后我接触到了单例模式:用单例模式构建调度器可以有效减小类之间的耦合度,代码写起来清爽了很多。诚然设计模式种类很多,并不意味着写代码时可以生搬硬套。但是在迷茫之时,设计模式可以拉你一把,在不迷茫的时候设计模式也起到了锦上添花的效果。

第三单元:用JML规范设计架构

第三单元借助JML这样一个极其规范的语言,设计了一套社交网络架构。这单元的重点从设计整体架构转移到了JML的学习上,但是架构设计思想仍然不可或缺。JML的作用在于,给每个方法都设计了严格的行为准则,用以消除设计者与实现者的认知偏差,弥补了自然语言容易产生歧义的缺点。也许今后用不上JML,但至少通过学习,理解了应该规范的设计架构,当自己作为设计者提出了某个架构,并着手设计各类方法时,一定要注意规范性,把需求像JML一样明确的提出来。

第四单元:用UML形象描述架构

第四单元主要学习了UML,并完成了一个简单的UML解析器。UML的特点就是形象,将这种语言代码转化成图,将会很方便人们设计架构以及进行架构的分享交流:摆个图上去,一目了然,只要图画的好看,架构便清晰呈现在人们的眼前。第三单元和第四单元的编写代码过程当然也是架构设计的练习,好在这样的架构设计在经过前两个单元的“折磨”之后显得容易了不少。

关于增量开发与重构

这里想着重说说增量开发与重构的事情。在每个单元的迭代中,我们需要对当前的设计进行增量开发:增加新的功能。不过在这个过程中可能会很艰难,因为如果架构设计不好,增加新功能所写的代码会越来越丑,越来越难以维护。其实无论架构设计好与坏,增量开发必然会多多少少导致代码可读性变差,难以维护的问题,只是架构设计越好,这样的过程会越慢。

当增量开发到一定程度时,也许代码问题很大,这个时候就面临一个选择:保持当前架构开发,或者打破当前架构进行重构。后者的代价无疑是巨大的,但确实是改良架构最直接的方法。

我在第一单元进行了大重构,第二单元进行了小重构,第三单元第四单元没有重构。可见自己的架构设计水平是有所提升的(也不排除是三四单元难度小的缘故)。关于是否需要重构,我个人的看法是:能不重构就不重构。因为代价属实是非常大,对于一些数以万计的代码行数的大型项目更是如此。在不重构的前提下,就需要初始的架构越完备越好,使增量开发的难度降至最低。因此在项目的开始一定要“三思而后行”。

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

软件的测试与设计一样是重要的一环。测试这个事情在大一的程序设计以及大二计组的过程中早有体现,通常采用的模式是:需要自行设计输入数据,然后检查程序的输出结果是否与期望一致。有时也需要通过随机化的方式得到输入数据,实现更全面的测试。

上面所提到的测试方法叫“黑箱测试”,就是把程序当作一个未知的黑箱,只通过获取其结果判断正确性。这样做的缺点是,如果程序很大,这样的测试对代码的覆盖率很低,有很多代码段是无法测试到的。

与之相对的是“白箱测试”:程序的架构已经清楚,测试是针对某一个类或者某几个类而进行的。这样的测试相对来说覆盖率更高。

在第一单元中,黑箱测试还是很有用的。通过随机输入表达式,检查输出的表达式是否与输入一致。但是第二单元则不一样了:因为存在多线程的不确定性问题,有时问题无法复现,往往涉及到线程不安全的问题。这类问题只得靠观察代码来解决,看看有没有死锁等问题。记得当时看一个死锁问题看了两天才发现,确实很头疼。第三第四单元开始利用JUnit工具来进行一定程度的白箱测试。但是个人感觉JUnit只是一个工具,使用这个工具的前提一定是对需求清晰。因为后期出现的bug大多都是需求不清晰(审题不到位)所造成的。

四.课程收获

整个课程下来,感觉最重要的收获是代码力的提升。计算机专业无疑要长期与代码打交道,写代码的能力一定是硬实力。当然“代码力”往细了说包括代码的可读性,代码的复杂度优化,代码的可维护性,以及写代码出bug的几率等等。这些能力的提升要靠理论知识的积累,但最重要的还是“多练”。熟能生巧,对于写代码同样如此。

此外我也理解到了沟通的重要性。借助于研讨课这个形式,我得以学习其他同学关于架构设计的看法,切实帮助自己解决了一些问题,同时也可以把自己的想法与他人交流,看看是否有优化的方法。

五.致课程组

在此提出的建议主要针对课程的难度梯度方面:

  1. 可以将pre部分选择性纳入第一单元前的必做范围。
  2. 可以将第一单元和第二单元的第一次作业的时间延长。
  3. 可以适当减少第三单元和第四单元的工作量。

能够明显感觉到,最难做的作业是前两个单元的第一次作业。第一单元第一次是面向对象设计的初探,对于刚上手的同学难度较大,即使做过pre也可能难以下手。如果把pre的部分放到必做部分(比如叫做第零单元,跟计组P0,操作系统lab0的地位一样),大家做pre质量会更好,也利于上手第一单元。然后由于前两个单元难度大,可以适当延长前两个单元的时间。最后的三四单元难度低,从写代码的收获层面来讲感觉收获没有前两个单元那么大,而更多的是按部就班完成任务的感觉,所以如果期末时间紧的话可以减少第三单元和第四单元工作量,比如少写几个方法,或者少一次作业。

最后,感谢课程组对课程的精心设计与维护,感谢老师和助教对OO课程的尽心尽力~

posted @ 2022-06-29 14:40  infinity0  阅读(14)  评论(0编辑  收藏  举报