面向对象程序设计第四单元总结(UML系列)
2019面向对象程序设计第四单元总结
前言
本单元是面向对象程序设计课程的最后一个单元了,本单元是和UML模型相关,也就是说,我们需要正确理解UML模型的基础上,对构建出的UML模型进行解析,但是我们实际上要理解的是UML建模的理念,即
-
类模型定义系统的解决方案:使用“这些类”来实现相应的需求
-
状态模型定义类的行为机制:“这个类”将按照这样的行为逻辑运行
-
交互模型定义类之间的协作机制:“这些类”在一起完成“这个业务”
在此基础上,对UML的模型就有了一个整体的把握,编码起来就不会很困难了。
1.作业架构分析
1.1 需求分析
本单元作业要求对于给定的UML模型(由starUML建立),需要能够正确地将其中的UMLElement进行归类,并根据规则建立起联系,在第二次作业中,如果模型不符合有效性检查,则输出错误信息,否则,输出正确的查询结果。
1.2 第一次作业
第一次作业其实如果理解了UML模型的话,建立起架构其实不是很困难,就是定义和我们Java中的类相似的数据结构,类中包含方法,方法中包含参数,然后父类、子类以及接口之间要建立起相应的关系。
类图
第一次作业的架构思路还是比较清晰的,针对所有的UMLClass,构建一个MyClassContainer类,保存所有Class和Interface,然后再构建MyClass 和 MyInterface类,构建可以自己定义数据结构的自定义类和自定义接口,用来保存方法、属性以及它们的父类。并且在类里面建立MyOperation类,用来保存其中的参数。对于数据的保存还是比较简单的,本次作业的重点在于对于继承关系的处理,本质上是图的搜索,对于类则只需逐级往上寻找即可,对于接口则按接口的父类实现图来完成。
类:
public void setFatherInfo() {
if (father == null) {
topFather = this;
isUpdateFather = true;
return;
}
MyClass nowFather = father;
while (nowFather != null) {
attributeList.putAll(nowFather.attributeList); //继承所有父类的属性
assoList.addAll(nowFather.assoList);//继承所有父类的关联
assoSet.addAll(nowFather.assoSet);
updateName2Id(nowFather.atName2Id);
atId2Name.putAll(nowFather.atId2Name);
interList.addAll(nowFather.interList);//继承所有接口,并更新id和name
if (nowFather.isUpdateFather) {//如果此时的某一上层父类已经更新,则直接继承此父类所有属性即可,无需继续寻找父类
topFather = nowFather.topFather;
isUpdateFather = true;
return;
}
topFather = nowFather;
nowFather = nowFather.father;
}
isUpdateFather = true;
}
接口:
private void setInterFaceForInterFaces(MyClass a) {
Queue<String> visit = new PriorityQueue<>(a.getInterList());
while (!visit.isEmpty()) {//bfs算法
String nowVisit = visit.poll();
a.addInterface(nowVisit);
for (String i : interfaceIdList.get(nowVisit).getFather()) {
if (!visit.contains(i)) {
visit.offer(i);
}
}
}
}
不得不说,这次作业的要求,体现了分层设计的思想,很完美的体现了面向对象的思维。
1.3 第二次作业
第二次作业比第一次作业稍微复杂了一些,因为加入了顺序图和状态图,但思路和第一次作业是一致的,增加的有效性检查部分,实质上也是对图的遍历与搜索,整体思路还是很清晰的。
类图
本次作业的架构和第一次作业一致,针对StateMachine,构建StateMachineContainer类,针对Sequence,构建SequenceContainer类,然后就是里面的子元素State和LifeLine,分类保存即可。本次作业的难点依旧是根据给定的模型,抽象出一个图,然后剩下的就是关于图的计算问题,对于R001类检查,检查重名即可,对于R002类,用dfs检查有无环即可,对于R003类,也是在遍历图的过程中检查有无重名即可。
UML002:
public void addAssoendName(String name) { //增加元素过程中寻找重复的名字
if (name == null) {
return;
}
checkUml002(name);
assoendNameList.add(name);
}
private void checkUml002(String name) {
if (assoendNameList.contains(name) || atName2Id.containsKey(name)) {
MyClassContainer.addUml002(
new AttributeClassInformation(name, this.name));
}
}
UML008:
public ArrayList<UmlClass> checkCycle() { // dfs
ArrayList<UmlClass> out = new ArrayList<>();
out.add(origin);
MyClass nowFather = this.father;
while (nowFather != null) {
out.add(nowFather.origin);
nowFather = nowFather.father;
if (nowFather == this) {
return out;
}
}
return null;
}
public void checkCycle(MyInterface next) {
if (next.id.equals(this.id)) {
if (!dirty) {
dirty = true;
} else {
visit.add(next.id);
trace.add(next.origin);
out.addAll(trace);
return;
}
} else if (visit.contains(next.id)) {
visit.add(next.id);
trace.add(next.origin);
return;
}
trace.add(next.origin);
visit.add(next.id);
for (MyInterface i : next.father) {
checkCycle(i);
visit.remove(i.id);
trace.remove(i.origin);
}
}
UML009:
public void addFather(MyInterface father) { //在遍历过程中寻找重复继承
if (!isDupExtend && this.father.contains(father)) {
isDupExtend = true;
MyClassContainer.addUml009(origin);
}
this.father.add(father);
}
对于状态图的后继也类似,都是图的遍历过程。
2.各单元架构设计与oo方法理解演进
第一单元采用的是组合模式,Factor组成Term,Term组成Poly,Poly组成表达式。因此可以根据这一点实现三个类,相应的输入解析也可以分层进行。本单元的重点是构造抽象层次,统一归一化处理,也算是面向对象思想的一个初步的体现,尽管不是很明显,另一个就是要减少耦合,尽量让类独立出来。
第二单元对于电梯系列,是生产者/消费者的模型,以及发布/订阅者模型的综合,还有单例模式,本单元的架构设计较为简单,重要的是要根据算法特性来实现自己的架构,即电梯、调度器以及发布需求的主类,三个对象通过多线程模式进行交互,这个单元我也学习到了一些设计模式,在编写程序的时候其实也可以思考思考可以抽象成哪种模式,然后构建类、接口来实现需求。
第三单元是规格作业,也就是契约式设计的思想,这个单元对于架构来说也就是设计一个能方便图的计算的架构而已,具体也就是增加仅限于图计算的一个类,然后去调用这个类即可,本单元的重点是契约式设计,尽管在现实的应用很少,但是这种方法实现者和调用者的合作来完成一次方法的完美调用的思想,对于测试还是有很大的帮助的。
第四单元是UML模型,说实话,其实我们的每次作业的设计其实都是模型化设计,针对诸多不同类型的对象构造层次和关系,最终形成一个模型,对象之间的交互,是顺序图,类的状态变化,是状态图,模型化设计的思想让人在设计时自然考虑了类的功能和方法,在实现的时候就能大大降低难度,本单元的架构也是分层设计的思想,具体已经在架构设计里分析过,这里就不重复提了。
总的来说,架构设计是重中之重,一个好的设计在应对变化的需求的时候可以得心应手,大大减小工作量,否则就需要重构,耗费时间和精力。以及要善于利用设计模式来实现需求。
3.各单元测试理解与实践演进
第一单元的测试就是简单的输入输出,只需构造相应的测试样例即可,在构造测试样例的时候,要构造完整的,覆盖所有情况的方面其实很困难,对于输入正确的数据,正确性实现较为简单,因为逻辑就那样,如果格式错误的话就会比较麻烦,需要考虑各种可能的输入错误的情况,但是验证输出的正确性,如果输入长度较长的话也会比较麻烦,这个时候可以借助其他语言的特性,使用和评测机一样的方法来验证正确性,比如python语言等,通过计算表达式的值来验证正确性。
第二单元的测试就更加复杂了,多线程的测试,出错的位置可能有很多,线程安全问题和逻辑问题都可能出现大大增加了测试的复杂度,而且输出内容非常多,输入时间难以控制,说实话这个单元其实更适合看代码来发现自己逻辑上的bug,总之第二单元的测试还是很复杂的。
第三单元的测试引入了Junit,通过自己构造测试代码,来观察实现的正确性,还可以有覆盖率的反馈,在编写test代码的时候直接根据规格一模一样写,可以达到很好的验证正确性的效果,其他性能方面的问题,也只能借助其他语言来批量生成测试用例来观察效率。
最后一个单元的测试,其实更适合Junit来实现,只要各个模块的代码实现正确,那么整个程序的功能也会是正确的,Junit工具一定要好好利用。
总的来说,还是要针对那种实现较为复杂的代码进行重点测试,以及根据需求来思考可能错误的地方,来构造样例。
4.本学期课程收获
这学期在15次作业的压力下,收获还是很大的。
在语言方面,大体上掌握了java语言的特性以及语法,java是一门面向对象的语言,一切皆是对象,这个我觉得离不开IDEA的强大功能,所以选择一个好的IDE还是很重要的,数据封装的思想、访问控制以及继承、接口实现有了更深层次的理解,以及要减少耦合,让各个类功能独立。
在思想方面,本课程强调面向对象的思维,在本学期的作业中,收获最大的就是从以前只会写面向过程式的程序到现在也能够写面向对象式的程序了,从第一单元的表达式求导,先是对java语言有一个初步的理解,第三次作业的时候将数据分层为Poly/Term/Factor构造一个表达式树,其实那个时候我就有一点对面向对象的模糊理解了,一个表达式也能抽象出来这么多的数据结构,当时我还觉得很新奇。第二单元是多线程,也就是要恰当地使用wait/notify以及锁来控制进程的同步与互斥,在这个单元,我收获了关于如何保证线程安全的方法,通过锁来实现,即访问临界资源的时候需要通过一个锁来保护这个资源,这一点和os的思想类似,同时也使用了生产者/消费者或者说是工厂模式,也对设计模式有了个初步的理解。第三单元是规格系列,规格系列采用了契约式设计的思想,虽然现在可能只是我们根据规格撰写,方法编写者和方法调用者都有各自的权利与义务,来共同保证方法的正确执行,这一点对于测试、以及合作写工程的时候都是有很大的帮助的。最后一个单元则是和UML模型相关,从作业中我也学会了模型化设计的基本要素和精髓。
本课程的精髓在于架构意识、数据结构与算法,其中架构意识对于完成这种增量性作业来说还是很重要的,如果架构好,就无需重构,我个人认为我第四单元的架构是最好的,只需增加新功能即可,并且类之间的耦合度很低,在其他单元编写代码的过程中也很少重构。数据结构和算法次之,但是架构也是决定算法实现难度的一个重要的因素,因此架构在一开始设计的时候一定要考虑充分,如果架构不好,那么就会很难实现新的需求或者需求的改变。
5.对课程的改进建议
首先第一点,oo很重要的一个方面就是架构设计,但是第一单元由于表达式求导的特性其实同样可以用面向过程式的设计来解答出来,还不能很好地体现oo的思想,就拿第一单元的作业为例,完全可以不按照Poly/Term/Factor分层的套路来,直接按照求导上数据方面的特性来即可。现在的指导书里好像只是强调了架构的重要性,但是并没有告诉我们怎么实践它们,如果以后能在指导书里提示这一点的话,我觉得可以让同学们上手的更快一些。
第二点是关于设计模式,设计模式其实对于理解面向对象思维也是有很大的帮助的,我的想法是以后在作业中以及理论课上是否也可以穿插一些对于设计模式的介绍,比如在每个单元总结课的时候,给同学们讲一下本单元的作业体现了什么样的设计模式,也是很有启发和帮助的。
第三点是关于互测屋人数,由于oo作业覆盖的时间长度很长,而且还有其他课程需要应付,一个人看7份代码实际上工作量是比较大的,个人建议可以改成4-5人的互测屋,现在的话其实很多同学到后面直接放弃互测环节了,或者干脆提交几个自己生成的数据就不管了,就等着运气好抓到bug,如果互测屋人数减少的话我认为是可以改善这一点的。