咱的OO结束辣!
Part1: Unit4 Summary
本单元作业,我主要使用了适配器模式和访问者模式。总体上看,代码量和文件数量有所上升,但配合分包等措施后,文件结构清晰,各部分耦合度均较低。缺点就是一个简单操作要脱好几层套,显得有些臃肿。
由于三次作业基本上只有增加对新功能的支持,因此只分析第三次作业。
Section1: 整体处理以及错误检测
总的来说,我将所有UML元素的类用MyUmlElement包裹,并添加属于自己的属性方便自己扩展。子类继承MyUmlElement并存入有关元素的引用。
对不同UML图查询的处理分别由对应的管理者类进行管理,每个管理者类首先接受元素列表和Map,利用这些提取并构建自己需要的容器。
MyUmlInteraction负责将官方包的元素转换为MyUmlElement及其子类,而对应的管理者类负责填充子类的结构,将UML元素的层级关系添加至我的包裹类中。
在管理者类将图构建过程中或者完成后,会自动调用相关的check方法进行测试,并利用ValidityLogger将发现的问题记录。
ValidityLogger其实是第三次作业我为了省事搞出来的类,类里面全是static的方法和属性,这样全局不需要拥有它的引用就可以调用。在MyUmlInteraction中调用resetStatus进行初始化,此后各模块就可以通过addXXXInfo系列方法添加相关错误的信息。MyUmlInteraction中的check方法则是通过getStatus检测问题,抛出异常。
Section2: 对类图的处理
如图所示,MyUmlXXX系列元素已经包含了树形结构的所有信息。
对于需要统计所有父亲节点数据的方法,我采用了观察者模式。
访问者(Visitor)模式:将作用于某种数据结构中的各元素的操作分离出来封装成独立的类,使其在不改变数据结构的前提下可以添加作用于这些元素的新的操作,为数据结构中的每个元素提供多种访问方式。数据的操作与数据结构一定程度上分离。
各个visitor继承自MyUcdEntityVisitor类:
public interface MyUcdEntityVisitor {
void visit(MyUcdEntity muen);
}
而在MyUmlClassDiagram中,有一个visitAllParents方法,主要操作就是对所有ue的祖先节点,将其作为调用visit方法的参数,并且返回被重复访问过的节点:
private Set visitAllParents(MyUcdEntity ue, MyUcdEntityVisitor muev) {
while (haveAncestorUnvisited) {
muev.visit(elem);
// set elem to nextAncestor of elem
}
return duplicatedList;
}
访问者模式适用于结构变化较小,但是随时可能新增操作的情况。最典型的应用是编译器中对语法树的优化。用在这里,UML类图的结构较为稳定,也可以更好地应对后续新的要求(虽说其实并没有在这个方向上提出新的要求)。
Section3: 对顺序图、状态图的处理
非常类似类图。值得注意的是,顺序图、状态图中也有许多存在层级关系的信息,这次我偷懒没有加入。如果后续有需求则可以通过改动构造方法来处理这些信息。
Section4: 复杂度
除了像构造MyUmlXXX元素的方法和各个管理者的构图方法之外因为要涉及各种元素所以复杂度高之外,各方法复杂度均较低。
Part2: 架构设计及OO方法理解的演进
先放出我对OO方法的理解吧(下面可能会包含大量我从不知道什么地方道听途说来的内容,现在也记不得出处了,还请见谅)
我们的程序写出来是为了解决现实问题的。传统的面向过程编程中,我们要思考,怎么用把问题等价转化为一个数学问题,进而写出解决这个数学问题的程序。也就是说,问题首先要经过一次转化,然后我们分步去解决转化后的问题。
面向对象编程则是尽可能的省略了问题的第一次转化。与其考虑一个问题怎么映射到计算机域,不妨直接在对应的现实生活里的问题域中思考问题。不是让问题向计算机妥协,而是用计算机去模拟现实生活中的问题,并且让计算机以模拟现实空间的方式去解决这些问题。
如何模拟现实空间中多种多样的问题呢?一种方法就是我们的OOP了。OO的思维方式,是将各种活动抽象为实体以及实体之间的交互。所以,用OO的方法编程,我们就不需要苦思冥想,怎样用程序描述一个问题。而是看,这个问题牵扯到哪些实体,每个实体分别能干什么,实体之间如何进行交互。明白了这些,我们的程序自然就有了合理的架构。
那么,需要注意的是,OO方法是不是全能的呢?显然不是,这取决于我们要分析的问题。只能说,日常学习工作中,我们接触到的大多数问题都是适合用面向对象的方式去进行分析的。但是例如,研究某些数学问题时,函数式编程的思想就比面向对象程序设计更具优势。甚至在某些场景下,面向过程的思维方式也必不可少。总之我们需要根据当前的场景来选择最合适的思维方式,来解决问题。
Section1: 第一单元,表达式求导
这个单元是我(我相信也是大多数同学)重构最多的一个时期。其实在我看来,此时的重构并不完全是我的问题,甚至时至今日,我还在后悔没有在HW1中采用简单的面向过程设计。为什么?因为在我看来,表达式求值这个问题根本就不是一个复杂的系统,而只是表达式对象的一个功能。表达式当然可以有复杂的内部结构,但是从现实系统应用的层面上看,他很大概率只能是一个复杂系统中的小对象。
面向对象方法之所以能提高开发效率,减少重构,就是因为他能包容局部的变化,进而使整体的变化可以逐步完成。很不幸,表达式的变化,就属于局部的变化。在HW1中,题目言明是多项式,我们当然可以用简单的数组来表达这个多项式,此时如果硬要上什么面向对象,要么是过度设计(你预判到了第三次作业),要么没有办法应用于后续的迭代(拆个项元素出来,结果拆除来项是常数+指数),还得改。
HW2/HW3里这样的情况就得到了改善,因为我们可以在HW2里采用面向对象的方式,认识到表达式由对象组成,表达式自身成为一个系统,建立合适的对象体系,进而将表达式对象的大变化,转变为表达式内小对象的变化。这样才能免于重构。
但是不管怎么样,我第一单元架构的设计还是很不到位。包括第一次的过度设计,以及第二三次仍没能抓住表达式本质的畸形设计带来的混乱代码,现在复盘的时候,能想到一些更好的设计,也算是一大进步吧。
Section2: 第二单元,电梯
这个单元我的重构就少了很多。电梯是一个复杂系统,所以再改动时我就可以通过对电梯本身和调度器的更改,来避免整个系统的重构。这是我第一次接触多线程,多线程的编程模式与单线程有很大不同。但可惜的是,我上的锁太重了,导致系统在总的调度时其实是以单线程形式运行的。同时,我也为了卷性能分而完全破坏了开闭原则。
但是,电梯作业的这个形式,就决定了我采用这个形式的必然性。因为电梯任务的分派,在了解信息的情况下比不了解会更优,因此我会把电梯信息暴露给总调度。电梯不是CPU繁忙的任务,所以上重锁对系统性能的影响非常有限。而调度器分派的时候,和单线程也没太大区别(其实如果上个读写锁可能会稍微好点)。
电梯其实是非常简单的多线程,只涉及电梯-总调度器这两种对象以及他们之间的交互。即使在这种情况下,二者之间的通信也是一个值得研究的内容。我最终借用了观察者模式的消息传递机制,其实和写public方法是差不多的。
Section3: 第三单元,JML
JML单元,在我的理解里,也是对象之间的交互问题。团队开发时,不同成员之间的对象,怎么才能拼到一块?靠的就是一个公用的接口。JML描述的是,与某个对象进行交流时,我们可以做的事情,和我们进行操作可以得到的结果。我们将自己的程序建立在这些公约之上,可以有效的实现责任划分和团队合作。
Section4: 第四单元,UML
UML单元,是我们真正体验团队合作的单元。JML单元是我们提供合适的接口供他人开发,UML就是在官方包的基础上进行二次开发。这单元难点其实在于对UML结构的理解。理解之后,我们就可以借助官方包,构造符合我们问题需要的MyUmlXXX系列对象来完成任务。
总的来说,OO第一单元是对一个对象的设计,同时结合了层次化的系统架构;第二单元则是一个电梯系统的架构设计配合多线程编程中对象之间的交流;第三单元继续围绕对象之间的协作,锻炼我们的实现能力;第四单元则是利用已有对象,构建新的架构和实现。1/2单元考察我们的架构能力,3/4单元考察我们的实现能力。整体结合在一起,就是OOP的主要内容了。
Part3: 四个单元中测试理解与实践的演进
良好的结构是程序正确性的第一道防线,清晰地程序架构能极大减少Bug出现的可能性。
当然,口丁出现Bug是在所难免的,这时就需要测试登场了。四个单元,我大多数作业都采用评测机进行评测。大量数据的轰炸和随机的魔力一结合,几乎能查出正确性上的所有问题。
第一单元,我采用手写+exrex库生产数据,sympy库进行正确性验证,测出了不少问题。但是,最后在提交结束后还是发现了一个问题,但为时已晚。这个Bug既有我自己编码上的混乱结构的原因,也有我测试只专注黑盒的缺陷在内。
第二单元,大概是我评测机的最高水平了。手写的数据生成、运行环境以及SpecialJudge。利用Python的多线程功能,我大幅提高了测试的效率和多线程race condition发生的可能性,有效避免了出错。同时我对自己的程序并发部分进行了分析(分析方式:瞪眼法),确保他们不会发生竞争和死锁。
第三单元(摸了摸了),采用白嫖法,极大提高愉悦度(光速溜)。主要采用对拍,自己改写数据生成,来提高生成数据的质量。这次还是有一个小bug导致tle问题,也是提交后发现的。这证明其实单纯的黑盒测试不能解决所有问题,必须配合白盒测试,形式化验证,才能有效测试程序。
第四单元,仍然是对拍+改数据生成器。(嫖来的东西真香.jpg)。这次我采用了部分手动构造的数据,作为对边缘情况的测试。
总之,测试是保证软件质量的好方法。除了黑盒测试,白盒测试也同样重要。黑盒测试的关键在与测试数据的成,在这方面我这学期收获不小。白盒测试上我也由一窍不通开始走向尝试。
我大概会在学期结束之后整理一下,将部分自动测试的代码放在GitHub上供同学们参考,届时链接也会放在这里。(如果发现过了一个月我还没放可以评论区/私信提醒某重度健忘症患者)
Part4: 课程收获
-
最主要的,提升了自己抽取对象,建立架构的能力(主要是第一、二单元)
-
学习了多线程的基本使用(
指Python评测机(溜) -
认识到基本的白盒测试、形式化验证的使用
-
对时间复杂度,合适容器的使用有了一定认识,重新(
从头)学习了数据结构图的相关内容(终于学会Dijkstra了)
刚开始写博客的时候,我还没有认识到自己一学期来的进步。然后在对作业分析时我才发现,我对对象的抽取,程序架构的理解的确有所提升。尽管真正做过的架构也就三四套,但是这三四套做下来加上反思和总结,的确对我的编程能力提高颇多。也足可见课程组作业设置的用心,在这里也想向课程组说一声,你们辛苦了!
Part5: 改进建议
其实,OO课程作业部分的设计整体已经相当完美了,这里只提一些个人的感受。
-
OO作业的难度,其实是 1 > 2 > 4 > 3 的,一上来直接接触最难的第一单元可能有点不太友好。此外,第三单元JML,我认为无需三次作业,两次足以完成训练,认识到DBC等思想,后面没得出了算法凑稍微有点多余;同时,什么工具链的使用,就不必再多做尝试了,目前看来没有作用,不适合作为博客考察点。
-
研讨课方面,还是希望课程组能更好地挑选内容,仔细审查吧。最开始的研讨课经常有能让人眼前一亮的内容。但是到了后面,某些同学的研讨就给人一种敷衍的感觉,变成为了展示
加分而研讨(个人感受,请勿对号入座),研讨课有时候真的就像罚坐(。也不是说针对某些同学,而是希望大家能更认真的对待吧。就算恰分也得用点心啊,大家都挺忙的对吧。P.S. 和yzm同学交流中,他提到可以考虑对研讨课采用打分机制,我觉得这是一个不错的主意,把研讨课的水平和加分挂钩,可以一定程度避免灌水和吃相难看的恰分。通过同学们的反馈来推动研讨课质量的提高,也能反向促进分享者好好准备。
-
实验,这其实是我OO体验最不好的一个环节。实验课每次的内容尽管按照助教所说有验题,但是我每次还是觉得实验内容存在某些问题。当然,这很可能是因为我对实验代码理解有偏差。但是实验不给分也不给正确答案,我每次在群里问一圈发现大家也是一样的迷惑,这就稍微有点不对劲。时至今日我仍然认为某些实验内容存在难以直观理解的点,这些点我也没问出来过所以然,只能说可能我将来学得多了就能悟了?总之,希望能加强对实验的验题力度,最好能在试验结束后给出标准答案,这样我们应该能获得更大的进步。
其实OO课程已经非常好了(不信的话可以去看看隔壁OS官方代码加Bug加误导性注释的神奇体验),这些改进建议除了实验课之外也都是些小问题。希望OO课程再接再厉,越来越好吧。
Part6: 线上学习oo课程的体会
线上?线上好啊(,上课内容可以回放,全程2倍速真的爽。作业本来也就是在线上完成影响不大。唯一的缺点是交流还是稍微欠缺点。微信群里打字难以准确表达意思,向同学询问作业中遇到的问题也稍显麻烦,但是这些困难都可以克服,总的来说体验还是很好的。
OO课程到这里算是要告一段落了,这学期我在OO收获颇丰,最后感谢各位助教学长学姐,各位老师的辛苦付出!