面向对象 | 第一单元 | 总结

· 综述

从单纯的多项式,到单层次的幂、三角函数结合的表达式,再到多层嵌套的表达式,homework1到homework3的难度在逐步提升,面向对象思想也逐渐明显和清晰,每次作业经过需求分析、结构设计、代码实现以及调试复查的过程,我自己也对Java语言本身和面向对象的编程思想有一个大体上的认知和理解,但是这三周的代码作业让我暴露出了很多问题,比如说,对面向对象的思想虽然有自己的理解,但是在程序中的体现仍然很不成熟,对容器、继承、接口、工厂模式这类功能或概念只处于一个认知的阶段,并不能娴熟地运用在实际代码中等等,导致自己在强测和互测阶段得分并不十分理想(尤其是homework3),出现这些问题的原因,综合起来可以说是因为自己在这门课上投入的精力不够多,没有深入理解和广泛学习,没有尽善尽美和追求极致。

这第一次博客,与其说是总结性的,不如说是反思性的,反思自己在前三周的学习和作业中出现的问题,温故而知新,在第二波学习任务来临前,做好充足的准备。

· 程序结构分析

总体来说,从homework1到homework3,我的程序结构并没有很成熟地“面向对象化”,呈现出一种过程式程序“努力”向面向对象转型的状态,尤其是存在一些只提供了静态的方法的类,没有用到继承、接口、工厂模式,运用到了多态方法(也体会到了多态的便利性),反思自己在设计程序之初,并没有做到完全地对象化思考问题,依旧是在按步骤地思考程序结构。第一步应该读入并解析表达式,所以我需要一个保存表达式的数据结构,它可以成为一个类,并且我需要解析读入的字符串,将它转化进我的数据结构中,这个过程也可以成为一个类,第二步应该是生成求导后的表达式,所以我需要一个类来提供求导的功能,第三步应该是化简表达式,所以我需要一个类来提供化简的功能,第四步应该是以字符串形式输出表达式,所以我需要一个类来提供生成表达式字符串的功能。这种思考过程虽然是也是一种不差的方式,但我认为这是“披着羊皮的狼”,本质上还是在过程式设计程序,暴露的几大问题就是,第一,预设的几个类是很模糊的,它们各自拥有什么方法和属性是在编写程序的过程中才慢慢确定的;第二,实际上我可能还需要其它的一些类来完成程序的实现,但设计之初并没有想到;第三,类之间的交互关系是及其模糊的,总的来讲,这样的思考模式具有片面性和模糊性,实际上大部分的设计工作是在实现代码的过程中完成的,而边实现边设计最大的问题就是容易出现漏洞,尤其是基于代码结构或数据结构的深层次漏洞(我在homework3中就出现了这样的漏洞)。

类的基本数据表
 

类的数量

类的最大长度

类的平均长度

类的最多方法数

类的平均方法数

homework1

3

112

74.0

5

3.7

homework2

4

242

88.2

9

3.8

homework3

9

163

71.1

11

4.7

从这张表中可以看出,三次程序中都存在各别类过于冗长的情况,类的内聚性不够好,比如在homework1中,有一个多项式类,其中有一个公共方法和一个非公共方法都是用与分析字符串并生成表达式,完全可以将这两个方法合并在一个专门用于解析字符串并生成表达式的类中,完全一样的问题也出现在了homework2中,homework2的结构与homework1几乎是完全相同的。这个问题在homework3中得到了改善,但是homework3的内聚性依旧很糟糕,例如在一个用于表达式化简的类中,存在着多个用于不同情况下的化简方法,它们之间没有任何耦合关系,却有相似的结构(它们都是对表达式树的后序遍历),事实上,在homework3中有许多方法都是前序遍历或者后序遍历,而我却没有提供一个具有高内聚性的树结构类来提供前序遍历和后序遍历的方法,这导致homework3与高内聚低耦合几乎是背道而驰的,这具体会在下面的类图分析中展现。

homework1 2 UML

上图分别为homework1和homework2的程序结构图,可以看到两者的大体结构是一致的,homework2只是将一系列正则字符串单列出来放在一个类里,homework3沿袭了这种方式。除了MainClass类作为子类之外,两大核心类包含了程序的全部功能,其中一个类作为自定义的链表结构(实际上这几乎是毫无意义的,完全可以用Java提供的容器来代替),另一个类完成表达式的相关功能,包括读取和解析一个表达式、对表达式进行求导以及输出表达式字符串,这些功能的全部实现细节都包含在这个类中,显得这个类无比地冗长和复杂,代码阅读起来非常困难,显然这样的结构布局具有“低内聚,低耦合”的特点,为了增强内聚性,可以将解析表达式、表达式求导、表达式字符串的生成三个功能的实现细节分别放入三个类中,只向外提供一个调用函数,由表达式类来调用之。

homework3 UML上图是homework3的程序结构图,下面分两点来对第三次作业的程序结构进行反思:

· 完全没有必要的整体重构

在homework3中我采用了完全不同的代码结构,运用表达式树来储存和处理表达式,实际上这是完全没有必要的。在程序结构的设计之初,并没有清晰地认识到本次作业的嵌套特征与前两次作业之间的联系,构建表达式树不失为一种好的方法,但原先的结构并不是无法解决嵌套问题。首先,需要将Node类进行重构,原本的Node类是整合了常数、幂函数、三角函数之后的由四个参数构成的类,其本质是一个描述“项”的类,而“项”又由“因子”构成,在第三次作业中,“因子”可分为常数因子、幂函数因子、三角函数因子、表达式因子,如此一来,对原先结构的修改(也就是对Node类的重构)就豁然开朗,当然,我也应当抛弃原先自定义的链表结构,转而用Java提供的容器,不然会造成很大的不便。其次,求导的实现方式需要重构,总表达式的求导,相当于给每一项求导,每一项求导,相当于分别将这个项中的每个因子求导乘以其它因子再求和,而不同的因子求导会得到不同的结果,但结果总可以用因子的集合来表示,这样一看,求导部分的重构也清晰明了。最后,表达式字符串生成部分自然也要做相应的重构。总的来说,对原先结构的部分重构和调整,完全能够支持本次作业的功能,而且在弥补了原先代码中的不足之后,完全可以得到一个具有高内聚低耦合特征的程序结构。

· 无比混乱的表达式树结构

在homework3中我采用了表达式树的结构,分支节点存储操作符,叶节点存储操作对象,但是未经细心设计的表达式树结构使我的程序结构完全乱套,如上图所示,类之间的调用和依赖关系混乱至极。我将表达式解析、表达式化简、表达式求导和表达式字符串生成四个功能的实现细节完全放在四个不同的类中,它们全部都要对结点类进行操作,或者生成结点类,或者修改结点类,它们几乎都有各自的前序遍历或后序遍历的实现,代码重用性大大降低,这简直就是C语言的实现方式,与面向对象完全背道而驰,这就造成了程序结构的混乱,类之间依赖关系的复杂,与高内聚低耦合完全相反的结构特点。

· 程序漏洞分析与hack策略

homework3的重构可以说是失败性的,设计的缺陷造成结构的混乱,结构的混乱使得在代码在数据结构的层面上存在漏洞(甚至可以说是错误,说成漏洞简直是便宜了),在代码的实现过程中这种混乱也造成了一些小的漏洞,下面一一说明。

· 两个小漏洞

一,在将嵌套层用特殊字符来替换之后,应当以嵌套层左侧第一个字符作为起点继续检查下一个嵌套位置,而程序中却用了嵌套层右侧第一个字符,这使得当嵌套层过长,或者两个嵌套层距离较近时,会漏掉后面的嵌套层导致错误。

二,在检查sin()以及cos()内部是否为因子时,应当以因子正则表达式对其进行检查,而不是以提取的项个数和因子个数均为1作为标准,否则在面对sin(- 1)或是sin(+x)这类的测试样例时会出现错误。

· 一个深层次漏洞

接下来就是在数据结构层面的漏洞(这个漏洞的严重性是必须要重视的,尽管强测和互测中只有两个测试样例因为该漏洞而出现了错误的输出),在结点类中,我提供了一个向上访问父亲结点的方法,而在表达式求导的过程中,对于出现完全重复的子表达式树,一律通过引用的方式处理,也就是说,这个表达式树的每个结点并不一定都占据一个独立的内存空间,它们可能是相同的,由不同的父亲结点向下连接,这样一来,向上访问父亲结点的方法就是毫无意义的,因为在内存空间的角度上看,同一个子结点可能拥有多个父亲结点,而程序在生成表达式字符串的过程中采用了向上访问父亲结点的方法,使得少部分测试用例输出错误。更正该漏洞有两个方法,一是在表达式求导时用深度克隆来替代引用,二是放弃向上访问父亲结点的方法,修改生成表达式树字符串的递归函数,使得当前结点的信息可以通过参数列表传给下一层递归中。

下面再说一说我在检查别人代码的漏洞时所用到的策略。我事先创建一些可能出错的测试样例,这其中有包含大量嵌套层的表达式、包含边间数据的表达式、包含大量正负号与空格的表达式等等一系列特殊情况,多数漏洞的发现也是归功于这类事先创建好的测试样例。当然,在遇到结构清晰的程序时,我会仔细阅读这些代码,一是出于学习的目的,二是出于发现漏洞的目的,但是这些值得学习的程序往往很难发现漏洞。

· 基于工厂模式的程序结构优化

上面已经对我混乱的表达式树结构进行了反思,实践证明,这种混乱的结构设计很容易给程序带来难以预测的bug,接下来简述一下我运用工厂模式对表达式树结构重构的设想。

首先是表达式树结构本身的重构。首先定义三个接口,分别为可求导、可简化和可字符串化,接下来定义一个表达式树结点类实现前面三个接口,该类还拥有两个方法,分别是获取左子结点和获取右子结点。考虑到表达式树的结点可以抽象地分为三大类,分别为运算对象(x或常数)、二元运算符(加、乘、幂)、一元运算符(正弦、余弦),所以定义三个继承自表达式树结点类的抽象类分别管理这三种情况,然后在每一个抽象类下面定义属于该抽象类的子类(比如一元运算符下的正弦类和余弦类),这7种子类各自拥有其求导、简化、字符串化的具体功能实现。

其次是工厂模式的设计。设计一个总工厂类,提供三个多态化创建方法,分别用于创建运算对象类、二元运算符类和一元运算符类,接下来为这三种类定义三个子工厂,用于创建7种子类中特定的类,需要指出的是,这里子工厂并非继承自总工厂,而是受总工厂多态化方法调用。这样设计的目的在于,在程序的任何其他地方,当需要创建表达式树结点类的实例时,只需要调用总工厂的多态化方法,传入适当的参数列表即可。

· 心得与总结

第一单元的学习带给我的最大的体会就是,即便在理论上理解了面向对象、工厂模式、继承与接口这些概念与方法,在实际的程序结构设计的过程中熟练运用仍然不是一件容易的事,事实证明第一单元的程序在运用这些概念与方法上面是失败的,但失败有失败的好处,这迫使我反思自己当初的设计,并且与优秀的设计作比较,使我不得不重新认知和思考到底什么是工厂模式,到底怎样合理地运用继承和接口这些问题,使我深入地思考应该如何重构我的代码,使其结构清晰明了,提高内聚性,降低耦合性。这次总结作业是极其有效的,如果没有这样的反思与回顾,我很可能不会意识到自己在程序结构设计上出的大问题,很可能在第二单元仍然无法设计出优秀的程序结构,但是现在我对接下来的任务充满信心。

posted @ 2020-03-19 12:24  DrSpace  阅读(117)  评论(0编辑  收藏  举报