【面向对象】第一单元总结

前言


在本学期面向对象课程的第一单元,我们重点训练了鲁棒性设计与层次化设计,其中包括输入格式的设计与结构设计,并由易到难,编写求解多项式导函数的程序。对于在之前没有接触过面向对象的设计思想以及Java的我而言是一个不小的挑战,不仅在如何进行合理的层次划分遇到困难,随着三次作业代码量的增加,以及最开始设计的欠妥,导致后期的代码编写也曾遇到很多问题。但是,在这个过程中,我通过阅读同学们的优秀代码,阅读讨论区,与其他同学交流,也收获了很多。因此,我希望借本周的总结博客,从自己的程序结构的反思、公测与互测发现的bug、互测策略、程序的重构以及自己学到的其他知识进行总结。

基于度量的程序结构分析


进行程序结构的分析,在这里我借助了IDEA的Metrics(复杂度及依赖度)进行分析,首先先对相关术语进行说明,这里的说明参考了往届学长的博客:(https://www.cnblogs.com/qianmianyu/p/8698557.html

  • Complexity Metrics
    • ev(G):即Essentail Complexity,用来表示一个方法的结构化程度,范围在[1,v(G)]之间,值越大则程序的结构越“病态”,其计算过程和图的“缩点”有关。
    • iv(G):即Design Complexity,用来表示一个方法和他所调用的其他方法的紧密程度,范围也在[1,v(G)]之间,值越大联系越紧密。
    • v(G):即循环复杂度,可以理解为穷尽程序流程每一条路径所需要的试验次数。对于类,有OCavgWMC两个项目,分别代表类的方法的平均循环复杂度和总循环复杂度。
  • Dependency Metrics
    • Cyclic:指和类直接或间接相互依赖的类的数量。这样的相互依赖可能导致代码难以理解和测试。
    • Dcy和Dcy*:计算了该类直接依赖的类的数量,带*表示包括了间接依赖的类。
    • Dpt和Dpt*:计算了直接依赖该类的类的数量,带*表示包括了间接依赖的类。

这里对依赖度分析稍作补充,面向对象的一大原则是依赖倒转原则(Dependence Inversion Principle):是程序要依赖于抽象接口,不要依赖于具体实现。简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。(参考:https://www.cnblogs.com/xiaobai1226/p/8669232.html

  •  抽象不应该依赖于细节,细节应该依赖于抽象。 
  • 高层模块不依赖底层模块,两者都依赖抽象。

第一次作业

  • 总体结构设计: 

由于第一次要求完成的是有幂函数组成的多项式函数的导函数,因此我自然想到多项式作为一个类(Poly),提供其构造、求导、化简以及输出的方法,与此同时,将多项式中的每一项作为一个类(Term),分别实现每一项的构造、求导、化简、输出方法,而主类只负责读取和输出的简单操作。

  • 结构分析
    • 复杂度分析

            

首先,就类别复杂度而言,这里之所以Poly与Term类的平均循环复杂度过高,结合后面的方法复杂度容易知道,一方面,二者在设计上存在共同的问题,这也正是今天研讨课上同学和老师指出的,我将表达式的读入与合法性判断放到了每一项的构造函数中,这就导致在创建对象时在该类中要进行一个较长的判断过程。

实际上,构造方法的作用是初始化成员变量的属性,不应当将过于臃肿的处理放在构造方法中(这一个问题一直贯穿了我前三次作业)。实际上,我们可以通过专门构建类进行输入的处理工作,例如Parser类,可以在其中实现每个类对应的parser方法,通过在构造方法中调用对应的方法进行构造。另一方面,在Term类中,为了进行简化,toString进行了多组条件判断,而且冗长。

    • 依赖度分析

首先,我的三个模块中没有彼此之间的相互依赖,因为模块的设计最初就是根据从大到小的方向设计的。

然后,具体到每个模块的依赖度,这里可以看出,从顶层模块Main逐级向下,都会有向下的依赖,即底层模块会被上层模块依赖,由于整体依赖度比较高,进而导致当需要对底层进行扩展时,由于没有为底层设计共同的接口减少依赖,导致同时要修改上层,可拓展性差。 

第二次作业

  • 总体结构设计: 

第二次相比于第一次,由于在课上接触了Java的继承和抽象,同时也感受到增加的三角函数类与原来的幂函数类二者在方法上有很多共同的特征,因此设定了一个因子类,并让二者去继承。同时,由于研讨课上同学讲到使用抛出异常统一处理来输出格式错误,因此增加了一个异常类。此外的结构基本没有太大的变化。当时缺乏对父类与接口的区别的认识,现在想来,在这里并不需要有共同管理的数据,只是在方法上有共通性,因此使用接口应该更合适。

  • 结构分析
    • 复杂度分析

    

 这里复杂度过高的两个类是Poly类与Term类,这两个类复杂度过高,与第一次原因相近,一方面是由于对输入数据的较长的合法性判断放在了构造方法中,另一方面输出也很臃肿,这种情况同样是考虑创建Parser类来专门处理输入,同时关于输出,也可以用相同的方式。

    • 依赖度分析

首先,我的三个模块中没有彼此之间的相互依赖。

然后,具体到每个模块的依赖度,这次,我使用了一些继承,同时也可以看到Factor类被3个类依赖,即Term以及两个子类,本意是希望能够减少Term对于Factor的子类的依赖,但是发现Term的依赖仍然有3个,原因从上面的结构图也可以知道,Factor类并没有包含子类的全部方法,子类有自己不同的方法,同时我再Term类中还尝试通过判断子类的类型来使用其方法(例如get),其实这里有一点能够很容易解决这个问题。稍稍更改一下Factor的子类的设计,将Sin和Cos分别建类,这样彼此之间就会统一(Tri类不需要通过设置一个变量判断sin与cos),同时由于统一性,在求导时也可以合并大量重复代码,降低复杂度。

第三次作业

  • 总体结构设计: 

这一次,由于需要涉及到递归的嵌套,为了通过创建表达式树使用链式求导,我对布局进行了较大的重构,整体上,除之前的Poly类与Term类,还增加了因子类和运算关系类,其中各类型因子与运算关系是其子类,同时这两个类共同实现求导接口。虽然上面的因子类与运算关系类不涉及对于数据的管理,但是需要调用其中共同的方法,所以使用了类的继承。此外,是当时设计的疏忽,其实实现求导接口,只要让这两个父类去implements即可。

  • 结构分析
    • 复杂度分析

           

        

首先先从之前的老问题说起,这次我添加了一个所谓的Input类,虽然一定程度上缓解了Poly、Term等类构造方法的臃肿,但是由于其只是提供包含一些正则表达式的生成以及对数据的归一化处理,并没有将整个审核以及读入的过程包含进去,从上面的表格也可以看出,这导致没有能够降低这些类构造函数的复杂程度。

接下来还是输出的问题,在如Combine等类的toString()方法中复杂度过高,对于这种情况,我们同样可以仿照之前的方式构造一个类去进行处理。

除此之外,复杂度比较高的就是Input类中的正则表达式的生成,其实事后再想,虽然使用函数,但是根据函数的参数生成的表达式总共只有4个,因此其实完全可以将其设置为static final的常量,只要提供获得这些常量的方法即可,没有必要使用这样一个冗长的函数。

 

    • 依赖度分析

首先是循环依赖的问题,这里Sin与Cos类的循环依赖来自于它们的求导方法,即sinx求导会变成cosx之类的,所以二者彼此相互依赖,这应该在可控范围之内。而至于Term、Poly、Combine、PolyFactor之间的相互依赖,是因为存在一条递归链,即Poly->Term->Combine->PolyFactor->Poly,因此彼此相互依赖。

然后,关于依赖倒转原则,这次依然存在高层对低层依赖严重的现象。其原因与第二次是基本一样的,使用的父类或者接口不能完全覆盖子类的全部方法,子类是有自己的特别的方法的,而在使用这一类是调用了通过对其所属子类的判定而选择了子类不同的特殊方法,进而导致即使设计了父类或者接口,但仍不能做到有效的统一,导致了高层模块对于低层模块的依赖。

------------ 分割线 ------------

这三次的实验的程序结构设计中暴露出不少共性的问题。

首先,单个类和单个方法的复杂度过高,虽然代码风格检查要求单个方法不含空行不能超过60行,但是其实这已经是一个很宽的限制了,如果要用一个量化标准来考量复杂度,我记得老师在课上所提到的,单个方法控制最长不超过30行左右,单个类的长度尽量控制在100行左右。如果超过这个长度,就要考虑将其进行细分。

其次,对于继承,接口的使用,如果在数据层面上有交集,考虑使用继承,如果只是单纯在操作上有交集,考虑使用接口。这里在第二次研讨课上有同学指出了关于抽象类的问题,抽象类与接口的区别在于:首先,抽象类是类,只能单一继承,但是可以实现多个接口;其次,接口中的方法默认是抽象的方法,不能有具体的实现,而抽象类可以实现方法。

【存疑】然后就是层次之间耦合度过大、拓展性维护性差的问题,关于这一点,我暂时还没有想好,因为子类继承父类,有时除了继承或重写父类中的方法之外,确实还存在有使用自己方法的情况,感觉强行要求父类或接口包括所有子类的所有方法,感觉不太合适,暂且先存疑,好好研究一下同学们的优秀代码,然后再补充。

最后,还有关于package的使用,这一点我在完成要求内容后的部分进行讨论。

 

程序Bug分析


第一次作业

被测出的bug均来自于这一处,在原来,如果在x的指数上存在符号,此时的if条件判断仍能够满足,会错误的认为系数为-1。该bug与整体的设计没有太大关系,是因为细节处理出错。

第二次作业

第二次一共有两个bug,第一处在于当target为null时,使用substring方法会抛出异常,因此增加条件判断,第二处属于笔误,希望使用charAt方法获取String对象指定位置的字符,误写为indexOf。

第三次作业

第三次作业的bug与设计结构有关,其错误在于求导时加括号的位置不正确导致的,即输出内容不符合规范,是WRONG FORMAT。

------------ 分割线 ------------

总的来看,这几次的程序在强测和互测中被测出这种比较明显的bug,是因为中测结束后没有做更全面的测试导致的。每次构造的测试集不够完备,同时数量也不是很多。在第二次研讨课上,有同学分享了自动化测试的方法,我在接下来的测试中会尽可能地去尝试使用,同时更加重视中测后的自我测试。关于自动化测试的总结与补充,放在后面。

此外,老师在课上也讲到,我们应当去记录每一次debug的过程,包括bug的原因,测试数据,以及改动。这项记录,不仅会为后面的互测提供指导,同时更会留下宝贵的经验,值得学习。

Bug查找策略


我在这三次作业中,在这方面做的不好。老实说,我一般都是先用自己用过的数据集测试,然后再构造可能测出问题的数据进行测试,至于阅读代码,我一般只会挑选自己认为同组中写的相对较好的代码进行阅读。这种bug查找策略是不合理的。正如第二次研讨课上将自动化测试的那位同学回答问题时所言,我们不应仅仅依赖自动测评去hack别的同学的程序,而是应当将其作为一种辅助手段,他帮我们发现程序的问题,然后我们以问题为导向再去审视其他同学的代码,找出bug根源,最好的,尝试de别的同学的bug。这不仅会使得互测更有效率,同时更避免了我们盲目读代码或者干脆用数据去撞而不去研究别的同学的代码。这是更加科学的Bug查找策略,从下一次作业开始我会尝试使用。

Applying Creational Pattern


在第一部分中我对自己三次作业的使用的结构以及两次之间进行的改动及重构进行了说明。由于当时在设计时缺乏对几种常见的设计模式的了解,也正如前所述,设计出来的结构不仅可扩展性差,而且结构不清楚不合理,导致了因逻辑问题而产生的bug。所以在这一部分,我更希望对这五种常见的设计模式进行实际的了解,将设计思想应用到后续的作业中。

首先是关于这个标题的理解,下面引自Wikipedia:

The creational patterns aim to separate a system from how its objects are created, composed, and represented. They increase the system's flexibility in terms of the what, who, how, and when of object creation.

也就是说,这是指一种将设计目标进行划分,并对划分的对象进行生成、组织、使用的设计模式,具体而言包括下面几种:

  • Abstract factory pattern, which provides an interface for creating related or dependent objects without specifying the objects' concrete classes.
  • Builder pattern, which separates the construction of a complex object from its representation so that the same construction process can create different representations.
  • Factory method pattern, which allows a class to defer instantiation to subclasses.
  • Prototype pattern, which specifies the kind of object to create using a prototypical instance, and creates new objects by cloning this prototype.
  • Singleton pattern, which ensures that a class only has one instance, and provides a global point of access to it.

 下面我们结合例子具体说明。

  • 工厂模式——简单工厂模式、工厂方法模式、抽象工厂模式

(参考1:https://www.cnblogs.com/zailushang1996/p/8601808.html

(参考2:  https://www.cnblogs.com/carryjack/p/7709861.html)(先)

工厂模式用于对象的创建,使得客户从具体的产品对象中被解耦

我想在这里工厂模式的设计解决了我之前的疑惑,即降低耦合,提高扩展性、可维护性的设计理念。

1. 简单工厂模式:

组成:产品的基类、具体的产品类、工厂类。基类与产品类都比较容易理解,而实际上,我三次作业基本上都在使用基类和产品类,这里想说一下工厂类。在工厂类中主要提供的是一个creat的方法,他通过接受输出的参数,根据不同的参数返回不同的产品对象,这个方法一般被设为静态,也因此被称为静态工厂。缺点也是显然的,工厂类与产品类存在耦合,增加产品必将带来工厂类的改动。由于使用静态方法来获取对象,使其不能在运行期间通过不同方式去动态改变创建行为,因此存在一定局限性。

这里其实我开始没有太想明白,既然有产品的基类,又有其子类产品类,那工厂类的意义在哪里哪,我觉得完全可以绕过工厂类,直接去创建对象就好了?其实这是一种通过进行整合而减少耦合度的方法,虽然是工厂模式中朴素的一种,但是在创建对象时,不必在另一个类中调用产品类的构造方法,而是直接使用工厂方法,这样就减少了与子类的耦合。

2. 工厂方法模式:

参考资料的作者对其的定位是,“抽象工厂才是实际意义的工厂模式,工厂方法只是抽象工厂的一个比较常见的情况”。组成:工厂接口、工厂实现类、产品类。在这里可以看到它与前者的区别在于,这个接口不再是由产品类实现的,而是由工厂类去实现的,即“工厂接口”。借助参考1链接中的例子可以看到,这些产品类之间可以有较大的差异,同时也可以维护一个自己任意的层次。简而言之,它是在简单工厂模式的基础上,定义了一个创建对象的接口,但由子类决定要实例化的类是哪一个,这样便支持了不同种类的工厂通过实现同一个工厂接口,可以见参考2。

3. 抽象工厂模式:

上面的工厂方法模式是抽象工厂的一个特例,它的工厂接口中只有一个类,抽象工厂的工厂接口中有多个类(这个类是具体产品的父类,即抽象工厂中有多个父类,每个父类下又有多个子类,而这些子类正是最后的产品)。然而并不是所有工厂都生产抽象工厂中所有的商品,因此如果尝试使用工厂生产并不在其生产范围内的产品时,可以让其对应的creat方法返回null,再在后期对其进行处理。

至于这几种模式的实际应用,我想先按下不表,去阅读一些优秀代码,然后继续补充。【此处补充实际应用】

【此处继续补充其他设计模式】 

 

关于Package的使用


在互测阶段以及读公开的优秀代码时,看到很多同学都使用了package。至于我自己,我在这三次作业中总是将所有的类都放在了src文件夹下,尤其是到了第三次作业,类的种类稍多时,显得十分没有层次感,因此想借此机会学习一下其相关使用。

  (参考:http://www.runoob.com/java/java-package.html

  • 包的作用

(1)按照类的关系层次归类,层次清晰;

(2)不同包可以有相同的类,类似C++的namespace,可以防止命名冲突;

(3)有的时候我们经常会看到IDLE提示“Access can be package-private”,使用包可以为我们提供限定访问权限的方法。

  • 创建包

在类的第一行使用package声明包,同一个包中的类声明的包相同,如果没有显示声明,则会被放在一个无名包中

  • 使用import导入包,如果不使用import,就需要使用包的全名+类名来使用,import可以稍稍类比一下C++的using namespace ...
  • 包名必须与所在目录名相吻合,使用“.”代替目录中的“/”


关于自动化测试


(参考:https://course.buaaoo.top/assignment/29/discussion/43 中讨论区大佬们的帖子)

(参考:https://www.cnblogs.com/chanchan/p/7613261.html 关于javac与java命令;

    https://blog.csdn.net/octopusflying/article/details/53791661 依赖问题的解决;

    https://www.cnblogs.com/diegodu/p/5834339.html 编译运行含有包的project)

  • 根据得到的工程生成对应的.class文件,方便后续执行

这里主要是因为为每一个项目创建工程会非常繁琐,因此采用python与javac命令结合的方式编译生成.class文件,下面是我的小片段,可以处理package的情况,同时返回主类的名称:

 1 import os
 2 import sys
 3 
 4 """
 5 Before using, place all the projects in a folder, default directory is oo_auto_test.
 6 Then, make sure the RELATIVE files in src under the project folder.
 7 This script could generate a folder in each project folder named 'auto_out'.
 8 Then add a function finding the name of Main class.
 9 """
10 
11 
12 def class_gen(projects_dir):
13     main_class_names = []
14     projects_name = [folder for folder in os.listdir(projects_dir) if folder[0] != '.']
15     # get project names, omit those start with '.'
16 
17     for project in projects_name:
18         src_dir = set()
19         # make sure to place all the project sources in folder src
20         for root, dirs, files in os.walk(os.path.join(projects_dir, project, 'src')):
21             for name in files:
22                 if os.path.splitext(name)[-1] == '.java':
23                     src_dir.add(root)
24                     if check_main_class(os.path.join(root, name)):
25                         main_class_names.append(name)
26 
27         cmd1 = 'cd ' + os.path.join(projects_dir, project)
28         cmd2 = 'mkdir auto_out'
29         cmd3 = 'javac -d ' + os.path.join(projects_dir, project, 'auto_out ')
30 
31         for src in src_dir:
32             cmd3 += src + '/*.java '
33 
34         if 'auto_out' not in os.listdir(os.path.join(projects_dir, project)):
35             cmd = cmd1 + '&&' + cmd2 + '&&' + cmd3
36         else:
37             cmd = cmd1 + '&&' + cmd3
38 
39         os.popen(cmd)
40 
41     return main_class_names
42 
43 
44 def check_main_class(file_path):
45     fr = open(file_path)
46     for line in fr.readlines():
47         if 'public static void main(String[] args)' in line:
48             return True
49     return False
50 
51 
52 if __name__ == '__main__':
53     projects_dir = ''
54     if len(sys.argv) == 1:
55         projects_dir = '/Users/yangjiyuan/Desktop/oo_auto_test'
56     else:
57         projects_dir = sys.argv[1]
58     class_gen(projects_dir)
59     # save all the projects under the path in argv[1]
  •  运行生成的每一个工程的主类,并写入数据,获取输出,以及正则表达式的生成,这里参考oo讨论区大佬给出的程序。
原文地址:https://www.cnblogs.com/yjy2019/p/10579810.html