在第四单元的作业中,我的架构是逐步演进的。设计第一次作业的架构时并没有考虑到后续作业会增加新的图,所以直接把类图的实现放在UmlInteraction
中。第一次作业的这种设计导致第二次作业在添加新的图时,需要将放在UmlInteraction
中的类图实现代码剥离出来并封装到单独的类中。
第一次作业的简约类图如下:
1.类的继承关系
下图是第一次作业中的类继承关系:
首先分析其中的继承关系,在UML
的规范中,允许接口拥有属性、类继承接口、接口继承类、类实现接口等关系,所以在建模层面类和接口没有本质的区别。但是在java8中接口和类的区别很大,需要在体系架构中体现出来。在第一次作业中,Node
类作为ClassNode
和InterfaceNode
的父类出现,用来实现类和接口所体现的共同属性和行为,从而达到具有相同功能的代码只写一次的目的。
2.作业架构和官方架构的交互
在第一次作业中,建立平行于官方包的类:ClassNode
、InterfaceNode
、Operation
以及Attribute
。这些类实际上是官方包中UmlElement
类的子类的包装类。官方已经将从文本到类的转换实现,呈现给我们的UmlElement
类的子类互相之间的关联关系已经建立完毕,只不过是通过字符串ID引用的形式储存。我们实际上要做的只是将字符串ID引用转化为java的指针引用。要储存java的指针引用,就要有容器,继而要有类。所以在第一次作业中建立UmlElement
类的子类的包装类将引用的容器作为自己的属性,从而建立起整个UML
类图。在容器类的建立中体现出第一次作业架构不成熟的地方:所有的容器类都应该有一个Element
父类。Element
父类作为UmlElement
的包装类,用来存储基础字段和方法。这样既可以将getName()
和getId()
等方法封装在Element
父类中来避免重复编码,也可以使类的结构更有层次,使得对元素的管理变得简单容易。
3.架构、算法、核心数据结构
第一次作业的实质是将UML
中的类图映射到java中并提供官方指定的查询方法。所以架构的任务总共有两个:解析官方提供的UmlElement
子类将其建立为图;在图中提供特定的查询方法。从第一次作业的类图中可以看到MyUmlInteraction
是承担架构功能的主体,架构的两大任务都在其中完成。第一个任务通过对包装类的建立就可以完成,而第二个任务则通过在ClassNode
类的实例组成的图中得到完成。
官方要求的查询任务可以分为两类:对ClassNode
类实例,也就是图节点本身状态的查询、对图中各个节点之间的关系的考察。第一个任务十分容易解决:在Node
类内部配备相应的查询方法即可解决,复杂度取决于每个需求本身的特性。通过记忆化查询做到对查询中间结果的保存,就可以大幅检查查询时间。第二个任务因为官方保证测试样例中不会有环,所以只需要实现记忆化的深度遍历就可以实现,复杂度可以压倒O(n)。
第一次作业中的核心数据结构为HashMap
,三大核心HashMap
分别是Id到节点、name到节点和name到出现次数。用这三个HashMap
就可以完成对类图中节点的管理和查询。
在第二次作业中,因为考试压力减小和可支配时间的增多,所以在进行第二次作业时较第一次作业进行了更充分的架构设计。在最后一次编程作业中,我的架构体现出我在OO作业中自己总结出来的软件实现原则和在课程中学习到的软件实现原则。虽然此次作业的架构仍然称不上完美,也存在许多我能够继续优化的问题,但较第一单元第一次作业已经进步良多。
第二次作业的简单类图如下:
1.类的继承关系
下图是类的继承关系:
在第二次作业中,包装类都被设置了一个父类Element
。有关UML
元素的共同特性和行为都在Element
类中得到实现,在子类中不必进行重复的编码。实际上,面向对象的继承机制是代码封装和重用的有效途径。将共同的行为和属性封装在父类中,既可以减少重复实现代码的工作量,也可以对代码进行统一管理,减少出错的概率。在第二次作业中,虽然继承的体系十分简单,但是给我的代码实现带来了很大的便利。面向对象的多态特性的优秀完全体现出来,对于相同的对象,在继承体系上提供不同的引用类型,天然的提供了从不同角度管理对象实例的接口,给代码实现提供了很多便利。写过的代码不用再重复实现,这是继承特性的另一大好处。
在我的编程经验中,复制粘贴十分容易出错,虽然编程时简单,但是debug时却能逼死人。没有两段代码是在完全相同的环境下运行的,复制粘贴之后需要对粘贴过来的代码进行微调,这个过程极容易出错。用函数的方法也可以实现对代码的高度复用,但是如果没有面向对象对函数进行承载和组织,函数之间的协调会变得十分困难。然而怎么进行代码的复用却是一个困难的问题,许多需要复用的代码在编写时并没有复用它的打算。面向对象的继承机制提供了一种代码复用的思考方向:进行类的继承就自然地完成代码的复用;将数据和代码绑定起来,同样复用了代码和数据之间的关系。
2.类功能的单一化
第一次作业中软件体系所要实现的两大功能都集成在UmlInteraction
中,造成该类过于臃肿,对其拓展也十分困难。在第二次作业中,对类的功能进行单一化处理,首先从UmlGeneralInteraction
中剥离出类图的功能,将实现代码转移到Node
类中;其次,并行的建立时序图类和状态图类,各自负责时序图和状态图的建立和管理。类的功能单一化使得代码管理变得简单,debug时也很容易能找到问题出现在何处。
在我的眼中,编程规范和设计原则的目的是使得软件工程变得工作量少、不易出错以及容易维护。而这三者之间往往是相互关联的,不能满足其中一点会导致其他两点也得不到满足;其中一点的提升也会导致其他两点的提升。第二次作业中虽然将类功能单一化的工作看似十分简单,但是为我的后续的设计实现带来了很大的便利。这一点是我在进行类功能单一化时没有想到的,让我对前人总结出的编程规范和设计原则有了更深的理解。
3.架构、算法、核心数据结构
第二次作业和第一次作业相比,任务并没有本质的变化:解析并建立图,在图上实现查询方法。
解析官方传进的UmlElement
子类list是在UmlGeneralInteraction
中完成的,该类在识别出UmlElement
的子类类型后,就将UmlElement
子类传入相应的图类中,由图类建立相应的图。查询方法在各大图类中实现,UmlGeneralInteraction
只调用相关的查询方法并将查询结果返回给调用者。UmlRuleCheck
是一个特殊的类,规则检查的代码在该类中实现,它会调用各大图类的容器,对每个图进行需要的合法性检查。
对于图中节点本身状态的查询比较容易实现,大多是直接可以返回结果的查询。比较复杂的查询有(不包括第一次作业中的查询指令):状态图中指定状态的后继状态的查询、UML规则检查中的环路检查和UML规则中的接口重复实现检查。对于状态图中指定状态的后继状态查询,因为状态图允许环的出现,一次遍历实现该功能不太现实。所以我采用对每个状态运行一次深度遍历,将状态深度遍历的结果储存下来用来加速之后其他状态的深度遍历,时间复杂度\(O(n^{2})\)。对于环路检查,因为类图是有向图,所以环路等价于类图的强连通分支(单点强连通分支除外),所以采用有向图的强连通分支算法:tarjan
算法,时间复杂度\(O(n)\)。对于接口重复实现检查,只需要对第一次作业中类实现接口列表查询的代码进行复用,将HashSet<Node>
换为HashMap<Node,Integer>
从而实现对每个接口实现次数的统计,时间复杂度\(O(n)\)。
第二次作业中的核心数据结构依然是为HashMap
,三大核心HashMap
分别是Id到节点、name到节点和name到出现次数。用这三个HashMap
就可以完成对类图、状态图和时序图中节点的管理和查询。在进行UML规则检查时,需要应对重复元素的情况,所以采用的是List数据结构。
针对类状态转移的测试
刚学到UML中类的状态图就遇到因为类的状态没有准备好就访问其中的数据从而导致的bug。类图的建立并不是一行代码就可以完成的,这导致在建立类图过程中访问类图类访问到的是类图类的未定义状态。导致这个bug的根本原因是我在完成第二次作业时修改第一次作业中类图类的代码,从而破坏了类图类状态的原子性。
应对这种bug的方法就是在设计架构时建立状态转移图,在实现代码时严格按照状态转移图进行实现,并在测试时构造针对状态转移图的样例。
在面向对象的学习过程中,我发现面向对象的架构设计和面向对象思想的理解是紧密结合在一起的。如果一个程序的代码精妙地呈现出面向对象的思想,那么其架构设计一定不会差。
第一单元总共有三层功能要求:解析字符串输入、求导、生成字符串输出。解析字符串用输入解析类完成,而求导和生成字符串输出都是由代数类完成。因为求导和生成字符串具有相似的嵌套结构。第一单元的架构设计中面向过程的成分还很大,例如只是因为求导和生成字符串的逻辑相似就把这两种功能集成到一个类中。在设计第一单元的架构时,我并没有考虑到核心数据结构和算法的影响,只是考虑系统内部的各个组件是如何合作的。
但我们知道,一个软件的架构应该是从多个视角展现的,例如UML中的类图、状态图和时序图。而第一单元中只体现出架构的组件视图,也就是类图。
第二单元是多线程电梯调度系统。我认为在第二单元中的教学核心是核心数据结构,因为多线程的同步和互斥都是围绕着核心数据结构进行的,充分体现出核心数据结构和系统中不同组件的交互。在第二单元的架构设计中,各个组件通过互斥队列进行交互,指令从一个互斥队列移到另一个互斥队列的过程就是指令处理的过程。这个过程体现出架构设计中的数据流视图,和核心数据的枢纽作用。
从第一单元到第二单元,架构视图增加了数据流视图和核心数据结构视图。但是,算法的重要性并没有被展现出来。虽然在第二次作业中性能分很重要,但是并没有多个算法同时出现的情况,不能很好的体现出算法和架构之间的交互。
在第三单元中,学生被要求在JML的框架下完成地铁查询系统。第三单元的作业实际上可以被分成一下三部分:将官方的路径集转换为图、在图上配置算法、完成官方要求的查询方法。在第三单元中,充分的体现出架构和算法之间的交互。算法的选择是系统效率的关键,并且为实现算法需要配置一些算法独有的数据结构。那么这里就有一个问题:如果将算法配置在图类中,代码耦合性太强,类图过于臃肿并且失去可复用性。在第三单元的架构设计中,需要对算法和核心数据结构进行解耦,从而达到后期的易修改、易维护、易替换算法。
一直到第三单元,架构设计中都没有体现出面向对象的特性。这三个单元的架构设计实际上用过程式编程也能够轻易地实现。在第四单元的架构设计中真正地体现出面向对象的特性。
在第四单元中类的构建平行于java的类抽象模型,使得第四单元的架构设计天然适合与类的继承机制相配合。在上文第四单元的总结中已经表述过,第四单元的任务是:转化官方的UmlElement List
到图、在图上配置算法、完成官方的查询方法。到这里我们会发现第四单元的任务和第三单元没有什么区别。但是在第四单元中需要管理很多不同种类的UML元素,如果不使用面向对象的继承机制,管理的代码将十分冗长,繁杂;如果使用继承机制,通过父类的引用进行,管理代码简洁高效。看似只使用了一个继承机制,实际上是从面向对象和架构结合的飞跃。
直到第四单元,架构的设计中集合了组件视图、核心数据结构视图、数据流视图、算法视图,完成了与面向对象特性的结合。
在《java语言程序设计(基础篇)》这本书中提到面向对象的三大特性:封装、继承、多态。知道第四次作业我才深刻的体会到面向对象这三大特性的威力,它使我的程序工程能力提升到一个新的水平。
在第一单元的作业中,刚接触到面向对象思想和方法,还不是很适应。再加上多项式本身就是数学领域的抽象存在,所以第一次作业的实现整体上是面向过程的。虽然强行使用类的继承机制,但是并没有使继承发挥任何作用。但是java的可见性机制使得以java编写的程序天生具有封装的特性。我在第一单元的作业中第一次体会到不同类之间定义的函数和方法不会互相干扰所带来的好处。
在搜索引擎上查到面向对象方法指的是:所谓面向对象就是基于对象概念,以对象为中心,以类和继承为构造机制,来认识、理解、刻画客观世界和设计、构建相应的软件系统。
在第二单元中,我的编程思路逐渐从面向过程转换到面向对象。在第二单元的代码中面向对象方法得到了更深刻的体现。电梯类、调度器类和输入类在现实中都能找到相应的对应,可以很轻松的进行代码建模。第二单元困难的是在核心数据结构的基础上进行对象间的同步和互斥。对象间同步和互斥的过程对应的是现实实体之间交互的过程,只要遵循实体交互的模型,对象交互的编写就变得简单。封装、继承和多态则是面向对象方法赖以实现的机制,对应对象所具有的特性,对面向对象方法的体现从某种层面上说就是对面向对象特性的应用。
第三单元的主题是JML模型化编程。通过官方三次作业和JML规范的引导,我逐渐学会了如何正确地使用面向对象的封装机制。好的封装能够使得实例对象较为独立的运行,整个系统耦合度较低。明显能感受到第三单元中bug的影响范围变小了,虽然代码行数大大增加但是编程却没有变得更加吃力。这些好处有很大一部分是合理的封装带来的。
到了第四单元,继承和多态开始发挥出作用,对UML元素的管理变得得心应手。在架构设计的演进中我已经详细论述继承和多态被合适地利用所发挥的巨大作用。当从继承和多态的角度来看,程序中很多杂乱无章的行为就被整合起来到不同继承链条上的相同行为,程序的结构性得以保证。
软件的测试实际上就是从不同的角度对软件的数据行为进行考察。好的角度需要软件工程师经验的积累才能得到,所以测试十分依赖于工程师的经验积累或是全面高效的测试方法论。
第一单元
测试思想:黑盒测试:随机样例
在第一单元中我采取评测机生成随机数据加自动化测试的方法。在随机的样例足够多的情况下,总存在几个有bug的样例。这种测试方法没有任何指向性,效率太低。
第二单元
测试思想:黑盒测试:随机样例 + 边界条件样例 + 不同分支样例 + 性能测试样例
第二单元测试每个样例的时间大大增加,逼迫采用其他的测试方法。在第二单元中我发现很多随机样例都是重复测试,很多特殊的组合情况和边界条件很难被测试到。所以我在第二单元的测试中加入一些人工构造的边界条件样例和不同分支的样例。
第三单元
测试思想:黑盒测试:随机样例+边界条件样例+不同分支样例+性能测试样例,白盒测试:基于JML的单元测试+面向功能的系统组件测试
第三单元官方给出了全面的JML模型代码用来规格化描述软件系统的输入和输出行为。实际上这也为我们测试软件提供了一个很好的思路:在软件规格的基础上针对每一个软件基础模块进行单元测试。不同于针对软件整体的黑盒测试,白盒测试能更有效的检测软件的实现是否与设计相符。其次,第三次作业的软件系统已经足够复杂,单靠全局的黑盒测试并不能充分地测试到软件的所有分支情况。所以针对各个组件的测试是十分有必要的。例如在对图类的测试中,通过JUnit进行多个图样例的测试,就能够确定图类的功能是否完备,简洁高效。
第四单元
测试思想:黑盒测试:随机样例+边界条件样例+不同分支样例,白盒测试:面向功能的系统组件测试。面向设计视图的测试。
UML给出软件测试的全新思路:针对设计的不同视角进行测试。我在第四单元作业中针对各大模型图类的状态进行了测试(在公测中有一个点爆出状态原子性被破坏之后才意识到进行这种测试)。虽然之后的测试没有测出什么问题,但是这种全新的思路给出了从软件设计的角度对软件进行测试的方向。
面向对象的方法思维:面向对象是一种很有价值的编程模型,在很多场景下都适用。其符合人在日常生活中的认知规律。从面向对象方法思维下,设计软件时首先想到的是系统内部各个组件独立的行为以及组件相互之间的交互,在处理复杂问题时采用这种思维方法效率更高,构建的的软件可维护性和可拓展性更强。
封装解耦的习惯:每个单元多次作业的增量式开发,让我体会到系统组件之间解耦,系统组件内部封装的重要性。如果系统的组件都耦合在一起,给系统添加新功能的难度会很大。可以说是这种作业形式逼我将封装解耦作为编程的习惯。
软件架构能力的提升:在学习这门课程之前,我对软件系统的架构只有模糊大概的认识。在经过半年的训练之后,我对软件架构有系统的认识,软件架构的设计也成为我构建系统的必要步骤。
善于利用现有的成熟工具链:在很多领域有成熟的工具链,在面向对象课程中使用了jprofiler
、JMLTestNG
、StarUML
等工具,为软件构建提供了各种便利,极大的提高了生产力。熟练使用现有的各种工具是一位优秀的工程师的必备素养。
编程能力提高:经过3个月的训练,我的编程能力得到极大的提高,积累了许多编程的经验。现在编程时只要逻辑确定下来就能写出代码来,不会再犹豫实现上的细节问题。
课上实验:本学期OO的课上实验都是在理论课结束后立马进行,没有给同学缓冲的时间,导致课上实验效果不佳。我认为课程组应该调整课上实验的时间,并在课上实验之前下发一些资料作为理论课程的补充。这样可以让同学在进行课上实验时有更大的收获。
教程材料:希望课程组能提供完整的理论课资料,供学有余力的同学提前学习后面的内容,结构化地呈现出OO课程的框架,让同学们对OO课程整体有更加清晰的认识。
完善更新通知机制:编程作业的要求和限制进行更改时,有一个统一的通知渠道,而不是散乱在问题贴的回答中。
完善代码风格评价机制:在互测时发现很多人的代码很难读懂,只有checkstyle
不能保证软件的可读性和规范性。希望官方能增加一些手段保证软件的可读性和规范性。
最后感谢课程组数个月的辛勤付出。面向对象这门课伴随着我走出很远,让我受益良多。
手机扫一扫
移动阅读更方便
你可能感兴趣的文章