301
社区成员
发帖
与我相关
我的任务
分享大一的C语言程序设计基础和数据结构与程序设计课程将我带入了面向过程程序设计的世界,我初步理解了针对一项小任务,涉及的数据应如何组织,以及这些数据常见的处理算法。
但是,作为一个第一次接触java、没有参加面向对象设计先导课的小白,针对一项处理表达式的“工程”作业,要设计哪些数理逻辑上的数据,这些数据之间应该如何组织,如何计算设计的数据?
坦率的说,我没有办法。我唯有在了解java的基础语法和作业的要求以后,用已有的面向过程程序设计的方法,开始我人生中第一个较大的编程“工程”。
从下方的UML类图也可以看得出来,我的编程思路几乎仍然是“面向对象”的——类之间没有逻辑关联、相互间几乎无内聚和耦合。各个类之间的唯一联系只是数据处理的逻辑先后。
可以说,虽然我尝试在这一次作业从“面向过程”向“面向对象”转型,但是第一单元的尝试明显是失败的。
在“大是大非”面前,每个类的设计考虑都显得如此的不起眼——因为它们都只是错误的设计原则派生出的结果。
MainClass创建ScanExpr类用于输入字符串,随之创建顶层Expression类用于解析字符串和化简表达式,最后用PrintExpr类进行输出。Expression类是用于存储表达式的类,有解析输入的字符串、化简、深拷贝和比较是否相同的方法。Polynomial是这次设计的败笔之一,相当于在指导书的表达式-项-因子层次结构中额外插入了多项式结构,变为表达式-项-多项式-因子的抽象层次结构。因而Polynomial类的功能与Term的功能是重复的,这让我这次作业的设计更加复杂。Term和Factor类对应于指导书中的“项”和“因子”,有存储、深拷贝和比较是否相同的方法。
DeriFactor是用于处理求导因子dx()的类,有获得求导结果并化简的方法。ExpFactor是用于处理指数函数exp()的类。PowFactor是同时用于处理常数因子和幂函数的类,在此我将常数和幂函数合并成了a*x^b的形式,没有分开处理。CustomFuncFactor是用于处理自定义函数的类。在此我进行的是简单的字符串替换。
于是我要开始思考,一个真正“面向对象”的程序,应该是怎么样的呢?
可是我们平时的思维并不是基于数据结构的;我们首先接触的编程也是面向对象的。这是为什么呢?
也许是我们受教育的过程所致的吧!我们长久以来接受的教育和参加的考试的基本逻辑,都是给出若干条件并提出若干问题,然后要求学生对问题予以解决。
这意味着,学生不必发掘某个领域中的哪些要素、条件和问题,而只需要提供解决问题的过程即可。
然而在实际的生产和生活中,当人们遭遇困难时,上天却不会给人们一张写满了可以利用的条件和要解决的问题的问卷。人们必须自己找到问题所在,自己找到解决问题的条件,自己构造解决问题的流程。
随着生产力不断进步,也许人们开始逐渐意识到,无论是科学技术上的困难还是人文社会上的困难,很多都可以构造相应的数学模型,并通过计算机的强大算力加以解决。
那么这个时候,构成数学模型之一的数据结构,就应该具备更高的地位了:
除去实际生产和生活的要求,计算机本身的原理也决定了编程应该基于数据结构。
学习了计算机组成的课程,我开始明白计算机的核心根本要素是数据。一切计算机的底层逻辑都可以视作二进制数0(低电平)和1(高电平)之间的转化。每32位或64位二进制数组合起来变成一个数据的基本单元。
这样,编程应该基于数据结构便更加是是理所当然的事情。
然而我们不能忘记,我们采用计算机是为了解决实际生产和生活中的问题。恰如前文所说,人们必须自己找到问题所在,自己找到解决问题的条件,自己构造解决问题的流程。
我们构造了数学的模型,找到了对应的数据结构,就相当于找到了问题的所在,找到了解决问题需要的条件。接下来就应该要构造解决问题的流程了。
解决问题的流程,本质上就是数学模型中对于数据的处理(毕竟数学模型构建来就是这么用的吧!),也就是这个数学模型中的功能结构。
那么,我觉得我们就应该把数学模型中的两个构成部分——数据结构和功能结构结合起来!
也许,这就是第一次理论课中提到的逻辑联系紧密的数据和函数需要物理聚合在一起的原因吧!
虽然上文阐述了我对于为何要“面向对象”的理解,但是从“面向过程”转向“面向对象”的理由显得还不够充分。于是,我们不妨来分析一下我采用“面向过程”的原则书写的第一单元作业的情况。
以下列举了一些高复杂度的方法:
| 方法名称 | $CogC$ | $ev(G)$ | $iv(G)$ | $v(G)$ |
|---|---|---|---|---|
PrintExpr.printTerms(ArrayList<Factor>) | 32 | 1 | 15 | 18 |
CustomFuncFactor.subCustFunc(String) | 30 | 6 | 13 | 15 |
Polynomial.collectPolynomial() | 27 | 7 | 11 | 14 |
PrintExpr.simplifyExpo(ArrayList<Factor>, boolean, BigInteger) | 27 | 6 | 10 | 17 |
这几个复杂度很高的方法中,PrintExpr.printTerms(ArrayList<Factor>)是用于输出“项”的、CustomFuncFactor.subCustFunc(String)是用于替换表达式中的自定义函数的、Polynomial.collectPolynomial()是用于合并同类项的,PrintExpr.simplifyExpo(ArrayList<Factor>, boolean, BigInteger)是用于在输出exp()时提取公因数以使得最终输出的指数函数式子最短的。
可以轻易查询到不同参数的具体涵义。但在此,我想着重分析为何这些方法会具有如此高的复杂度,以及搞复杂度的坏处。
因为我是按照“面向过程”式原则设计这些方法的。
也就是说,在设计一个方法时,我会直接在这个方法中对一切要处理的数据直接进行操作。
无论这个方法要处理的数据究竟有几类,有什么抽象层次结构。
例如,在PrintExpr.printTerms(ArrayList<Factor>)方法中输出一个“项”时,我不会调用每一个因子的toString方法,而是直接遍历这个“项”中的所有因子,直接在PrintExpr.printTerms(ArrayList<Factor>)方法中将因子转化为字符串,随后调用System.out.println()——将因子转化为字符串并不是“项”这个层次的类应该具备的功能结构,而应该是“因子”这个层次的类应该具备的功能结构。
很明显,这里设计时就违背了“面向对象”设计的层次结构的规定。
更重要的是,这里只展示了四个复杂度很高的方法。其他方法的复杂度看似并不很高,实际上是因为我为了防止方法的有效长度(除去空行和注释后的长度)超过80行,将较复杂的函数都拆分成了多个子函数的缘故。也就是说,其余的方法也基本按照面向过程的原则设计的,复杂度也并不低。
另外,前文提到的败笔Polynomial类,它的横空出世也是基于过程的数据进行直接操作导致的抽象层次结构设计混乱的结果。Polynomial类的功能与Term的功能是重复的,这让我这次作业的设计更加复杂。
一般的说法如下:
基本复杂度高意味着非结构化程度高,难以模块化和维护。实际上,消除了一个错误有时会引起其他的错误。
软件模块设计复杂度高意味模块耦合度高,这将导致模块难于隔离、维护和复用。
圈复杂度大说明程序代码可能质量低且难于测试和维护,经验表明,程序的可能错误和高的圈复杂度有着很大关系。
上述引用的说法看似十分抽象难以理解,但是在我实际进行迭代的操作时便觉得十分地贴切。
试想,尝试在一个设计完备的流程中加入新的功能,要怎么办呢?我猜测,应该只有以下两种加入新功能的办法:
对于第1种情况,原本流程之中的相邻两步本身逻辑联系便是很紧密的,在这两步之间加入额外的步骤,需要对原本的逻辑进行重新调整,我个人认为其难度不下于在大二重修工科数学分析。
对于第2种情况,增加额外的特判则不外乎新增if...else语句或case...switch语句。事实上,这些语句正是增大方法复杂度的罪魁祸首。倘若编程者能够记得住自己的逻辑或者没有出现bug倒尚可;倘若编程者自己也记不住自己的逻辑而且还出现了bug,那么只能期待下一次阅读代码时的苦苦纠结。
我很自豪的是,我能够在如此困难的情况下完成不断的迭代,这或许是我能够学习计算机科学的资本所在吧。
当然,如果课程组决定第四次作业新增三角函数、对数函数等函数,或者新增变量、新定义算术符号,抑或是新增因式分解或者解方程等功能,那么我只能望洋兴叹了。
到了这里,我觉得我已经没有理由继续坚持面向过程的设计原则了。虽然当我真正意识到这一点的时候,已经不具备整体重构的机会了——但是我现在提出这一点也并不晚吧!
程序员与其他一切劳动者一样,都应该是值得尊重和值得提升地位的劳动者群体(我是社会主义者!)。那么首先我们就应该尊重自己:自己的言行应该优雅,我们的劳动成果——代码也应该优雅。
但是对于只完成了人生第一个java项目的我,要解决这个问题还有些难度,只能管中窥豹窥探一二。这一部分的内容肯定不会有上一部分那么饱满充实。
我个人认为,一个优雅的代码应该简洁而易懂,同时还要具有强大的功能。如何能够做到这一点呢?
恰如前文所说,我们应该构造相应的数学模型,规定好数学模型中的每一类数据数据及功能结构。
理论总是十分抽象的,因为理论本身就是从实践中抽象出来的知识,只有结合了实践才能发挥效力并让人理解。事实上课程组已经提供了很好的样例,两次实验的代码都是十分简洁优雅的。
前三次作业涉及的数据结构,课程组已经完整地给出了。十分清晰明了的表达式-项-因子层次结构,理想情况下我们只需要套用它就可以。
如果让我重新设计前三次作业的架构,我一定会设计成这样:
Factor应该设置为一个Interface接口,准备识别、求导和化简的方法。Expression类中具备Arraylist<Term>的属性,具备识别输入的字符串按'+'或'-'号切割识别、化简和求导表达式的方法,继承Factor接口Term类中具备ArrayList<Factor>的属性,具备将输入的字符串按'*'切割识别、化简和求导项的方法,继承Factor接口ExpoFactor,ConstFactor,ArguFactor和DeriFactor类分别管理指数函数、常数、幂函数和求导因子,均继承Factor接口。当然,言胜于行。虽然我已经不再具备重新设计前三次作业的条件,但是我仍有在接下来作业进行不断改进的机会。这一次,绝不能再经历失败!
在“面向过程”这样原则性错误的“大是大非”面前,几次公测和互测出现的bug都显得有些不值一提。因此在此只简单地分析。
其实说来,我出现的bug只是两类问题:
当我从exp()因子中提取出负数公因数的时候,就已经注定了我有一次犯了审题出现失误的结局。关于这一点,我觉得没有什么可以强调的。
而所谓面向过程的逻辑漏洞,实际上是在过程式程序设计中容易出现的问题:因为过于考虑整个程序过程的前后,导致在程序过程中的某一环漏了某一个特殊情况,导致出现错误。
我就在合并同类项比较两个项是否相同时出现了错误。神奇的是,这个错误不是在公测中被发现的,而是在互测中发现的——这里我吹爆互测的有效性!
在引入指数函数前,我的优化就是全部合并同类项。
在引入指数函数以后,我没有能够找到将指数函数的表达式化为最短的统一算法,因此只做到了将指数函数的公因数提取出来作为指数函数外的指数。这里,我想起来1930年哥德尔证明了“数学不具有完备性”,这意味着总存在一些数学定理不可能得到形式化证明。就像我们的指数函数化简一样,也许并不存在统一的形式化方法使得一切的指数函数都在满足我们的文法的前提下达到最短吧!
当然,对于真的在优化方面下了大功夫的同学,我向他们表示我的十分的佩服。倘若具备足够好的机制,同学们的聪明才智和逻辑思维一定能够发挥更大的效用。
坦率的说,我在互测中只获得了基础分。因为我没有构造互测的策略,只是单纯地将自己出现过的bug对应的样例各往上传了一遍。
我自己承认我的水平不足以在不参加先导课的情况下,迅速完成“面向对象”原则指导下的java工程。因此对于第一次作业就是无效作业的结果,我认为这是一件很合理的事情。
当然,我也会思考倘若课程组给予更多的时间供我们学习“面向对象”的基础知识,是否会让我们能够有更好的学习体验和更高的掌握程度——不过,我相信课程组有课程组的合理考虑和判断。
有关互测的问题,正如前文所说,我完全赞同互测的有效性,它的确可以提升我们的“面向对象设计与构造”水平。
但是作为一个社会主义者,当我想到真正热爱计算机技术的人毕竟是少数,“互测”这样技术力的实现和提高需要用分数来激励时,便只能无奈归咎于当下的资本世界了。
最后,我已经在第三次作业的bug修复说明文档中所表达过,但是仍然要表达的是,作为一个没有参加面向对象先导课、这个学期才第一次使用java编程的学生,我很感谢课程组、老师们和助教们对我的帮助!