OO第四单元博客作业
OO第四单元博客
总结本单元的架构设计
第一次作业
第十三次作业只需要解析UML类图,因此根据查询需求我的架构如下:
1.建立自己的Class类,此类中有:
private final String name;
private final HashMap<String, Map<Visibility, Integer>> opvis;
private final HashMap<String, List<Operation>> opers;
private final HashMap<String, List<UmlAttribute>> attrs;
private int operationCount;
private int attributeCount;
private final List<AttributeClassInformation> informationNotHidden;
private Class father;
private Class topfather;
private final List<Class> assoClass;
private final List<Class> impInterface;
其中name-------对应此UMLClass的名字
opvis---------对应类的操作可见性查询操作(CLASS_OPERATION_VISIBILITY),是一个key为methodname,value为一个可见性映射到出现次数的一个Map的HashMap
opers-----------对应此Class下从一个operation name映射到List<Operation>的HashMap,此处由于同一个name下的Operation可以出现多个,因此使用List进行保存。
attrs-----------与opers同理。
informationNotHidden---------对应的是不满足信息隐藏原则的AttributeClassInformation(CLASS_INFO_HIDDEN)
father---------对应直接的父类,父节点
topfather---------对应顶层父类
assoClass--------对应有关联关系的Class
assoClass---------对应实现的接口。
PS:由于Interface是Class的特殊形式,因此在我的设计中,Interface同样是一个Class,但是区别是Interface支持多继承,而Class只支持单继承,因此如果是UMLClass,那么继承类保存在father中,实现接口保存在impInterface中,但对于UMLInterface,其多继承的接口直接保存在impInterface中。
2.建立自己的Operation类
private final String name;
private final Visibility visibility;
private String returnParam;
private boolean typevalid;
private final List<String> paramInformationList;
用一个List<String>来保存此Operation下的所有参数信息
3.建立MyUmlInteraction
由于UML类图中,众多元素有着父子关系,但是考虑到子类可能在解析时比父类先加入,因此会造成信息的丢失,就采用两次遍历的思路。
第一次遍历:
获取顶层的一些父类(UML_CLASS,UML_INTERFACE,UML_OPERATION),构造相应的对象,根据id存入map中方便下一步的查找建立关系。
第二次遍历:
遍历其他类型,按照UML对应的规则建立相应的关系。
private final HashMap<String, Class> id2class;
private final HashMap<String, List<Class>> name2class;
private final HashMap<String, Class> id2interface;
private final HashMap<String, UmlElement> umlElementHashMap;
private final HashMap<String, Operation> operationHashMap;
根据这种架构,我们在初始化完成的时候就已经将要查询的东西存储到了相应的对象中,再进行查询操作的时候我们只需要先根据name2class来判断一下类是否存在,是否重复,如果不重复,直接取出Class对象调用相应的接口即可返回答案。
第二次作业
加入状态图和时序图的查询
状态图:
1.加入StateMachine模型:
管理所有的状态
private final HashMap<String, List<State>> states;
private int stateCount;
private final String id;
states------同样是name到所有同名State的Map,用于根据name查找对应的状态。
2.加入State:
private final HashMap<State, List<Transition>> nextStates;
nextStates是一个由目标状态映射到对应的状态转换(可能多个)的一个HashMap,通过这个HashMap,我们就建立起了状态之间的联系,建立起了一张图。
3.加入Transition:管理状态转换的具体信息
private final List<UmlEvent> triggers;
经过上述类型的定义,我们就建立起了一个完整的状态图模型。
状态图中需要注意的是getSubsequentStateCount方法要查询的是所有的直接和间接后继,我们使用bfs进行遍历计数,代码如下:
int result = 0;
for (String id : states.keySet()) {
states.get(id).init();
}
Queue<State> bfs = new LinkedList<>();
State src = stateMachine.getStates().get(s1).get(0);
bfs.add(src);
while (!bfs.isEmpty()) {
State now = bfs.remove();
for (State next : now.getNextStates().keySet()) {
if (!next.isVisit()) {
next.visit();
bfs.add(next);
result++;
}
}
}
return result;
其余方法直接都是保存好的,直接通过接口查询即可。
时序图:
1.建立Interaction模型:
private final HashMap<String, List<LifeLine>> lifelines;
private int count;
管理所有的LifeLine
2.建立LifeLine:根据需求,只需要统计各种message交互的次数
private int inCount;
private int outsynchCall;
private int outcreateMess;
private int outReplay;
private int outdeleMess;
private int outasynCall;
private int outasynSig;
在查询时,直接调用相应的接口即可
第三次作业
加入了有效性检查
R001:
在Class中加入一个
HashMap<String, Integer> assoName
这个HashMap记录了本Class中每一个name的关联对端出现的次数,在构造的时候碰见关联关系的时候直接对这个Map进行增操作,在顶层直接遍历查询即可。
R002:
这块是本次作业的难点,需要对Class和Interface都进行检查
对Class的检查相对容易,一步一步向上找父亲,判断是否相同即可。
public boolean checkCircular(Class thisclass) {
HashSet<UmlClass> circular = new HashSet<>();
Class ptr = thisclass.getFather();
while (ptr != null) {
if (ptr == thisclass) {
return true;
}
if (!circular.add((UmlClass) ptr.getUmlClassOrInterface())) {
return false;
}
ptr = ptr.getFather();
}
return false;
}
对Interface的检查相对来说比较困难,相当于在一个有向图中找环,这里使用dfs算法。
public void findCycle(Class thisInter, List<Class> result) {
int j = trace.indexOf(thisInter);
if (j != -1) {
while (j < trace.size()) {
if (!result.contains(trace.get(j))) {
result.add(trace.get(j));
}
j++;
}
return;
}
trace.add(thisInter);
for (Class next : thisInter.getImpInterface()) {
searched.add(next);
findCycle(next, result);
}
trace.remove(trace.size() - 1);
}
R003:
dfs遍历一遍即可,判断是否重复访问。
R004:
这里可以直接照搬CLASS_IMPLEMENT_INTERFACE_LIST操作,也就是在查到每一个Interface的时候都检查一下当前已经遍历过的Interface中是否存在即可。
R005、R006、R007:
均较为容易,在构造方法的时候进行检查记录即可。
R008:
在Transition中加上一个
private String guard;
对类图中的初始状态进行检查即可
总结自己在四个单元中架构设计及OO方法理解的演进
第一单元
第一单元是唯一一次重构的,从第一次作业的面向过程的架构设计:
首先,将一个读入的多项式按照+-号分成若干项,再将项用*分开称为若干因子。而由于第一次作业的输入限制。因子只会是
a*x**b
这个形式,因此,求导就变得十分容易。而且每一个项利用*合并之后仍然满足
a*x**b
因此,我们要做的就变成了:
- 在每一项内部将因子对应相乘,化简成最简形式。
- 将这个项求导
- 将项求导后的结果相+-
再到完全的面向对象设计:
参考第二次作业给出的提示,给出层次化的处理策略:
- 在人处理表达式的时候,我们往往不会首先关注每一个项内部的具体内容,而是忽略具体内容,在表达式层先弄清楚项的加减结构。
- 然后提取出每一项的内容分别看出分出因子,不管因子的具体内容
- 对因子进行求导
- 最后根据运算结构选择适合的组合方式,组合成为最终的导数
因此,将每一种运算符和每一种类型的因子分别建立类,考虑到运算符都至多是二元的,因此考虑使用二叉树进行组织。
在这个过程中,建立了面向对象的基本概念,二叉树递归的理解更加深入,而且是第一次尝到了面向对象的优势和甜头,第二道第三次作业完成的比较轻松。
第二单元
根据实现需要,我将程序分为4个线程,分别为Main,InputThread,Scheduler和Elevator线程。
下面逐一进行介绍:
Main:主线程,负责各个进程的创建,初始化,并在其他进程结束后退出。
InputThread:输入处理线程,从输入中读入Request分为PersonRequest和ElevatorRequest。根据读入请求的类型,分别进行不同的操作。
- PersonRequest:根据换乘的实现需要,我将PersonRequest包装为LiftRequest,并加入全局的waitQueue中。
PS:LiftRequest继承自PersonRequest,添加了的字段为finalFloor,以及三个bool类型,canMoveOnA,canMoveOnB,canMoveOnC。
private final int finalFloor;
private boolean canMoveOnA = true;
private boolean canMoveOnB = true;
private boolean canMoveOnC = true;
这时,fromfloor和tofloor就表示的是这次乘电梯的起止楼层,finalfloor在读入的时候就已经设置完成,表示的是经过若干次换乘,最终的目标楼层,由此,就可以在每次乘梯时改变from和to,并根据final判断请求是否完成。
- ElevatorRequest:加电梯指令,也需要根据请求,创建Elevator进程,开启进程。
Scheduler:从waitQueue中读入请求,并根据请求放入每个Elevator维护的一个processingQueue中,供电梯执行。在我的架构中,只要将某个LiftRequest加入到某个processingQueue中,电梯就一定会完成这个请求的一次乘坐(运送到tofloor)。scheduler的具体设计见下文。
Elevator:负责模拟电梯的运行,负责将自己的processingQueue的请求做完,直到processingQueue空为止。
第二单元在荣老师的提示下,直接采用了InputThread,Scheduler和Elevator的面向对象思路,充分将任务解耦,每个线程只负责自己的一块,此单元作业没有重构。
第三单元
第三单元面向对象的思想已经有JML给出提示,读懂JML,按照JML实现即可,这一单元的难点是复杂度控制
缓存策略
面对quary方法每次都要将图遍历进行计算,我们可以把要查询的内容当作图本身的属性,从而只需要在某些操作后对这些属性进行修改。
例如:qbs在JML中是遍历产生的,但是JML只是规定了结果符合的规则,并不要求我们一定要遍历实现。因此我将blocknum作为一个字段存放在图中,在ap,ar的时候对blocknum进行相应的修改即可。
与此类似的例子有很多,例如第二次作业中的ageMean,ageVar等,这种方法重点是理解图的工作过程,找到什么时候这些字段需要改变,否则会漏写,答案肯定是错误的。
并查集
并查集是处理是一种简洁的数据结构,主要用于解决一些元素分组的问题,它管理了一系列不相交的集合,并支持合并与查询。
并查集与离散二中学过的分割是类似的,将某些具有相同性质的元素划分成一个集合,这与划分连通分量相匹配。因此,使用并查集来管理连通分量的查询(isCircle)可以使查询直接到O(logn)。
堆优化的Dijkstra
一般的Dijkstra算法使O(n2)的复杂度,主要是因为每次操作后需要找到当前dist最短的元素作为下一个确定最短路径的节点。而如果要用数组存放的话找最短需要O(n)的遍历。但是使用Java内置的PriorityQueue可以降低这个操作的复杂度。Java内置的PriorityQueue内部采用堆来维护节点之间的大小关系,可以O(logn)的复杂度直接找到最小,将O(n2)的Dijkstra优化成为了O(nlogn).有效降低了复杂度。
第四单元
第四单元的对象由UML中定义的类型给出,同样比较容易,但是我们根据需求,对应的建立了自己的UML类,在这个过程中上层调用下层的查询接口直接返回结果,也充分体现了面向对象的思想。
这一单元的难点是有向图的各种操作,包括基本的bfs,dfs,找环等,具体见第一部分。
总结自己在四个单元中测试理解与实践的演进
第一单元
第一单元使用python脚本测试,跟python的表达式计算库进行比对,利用管道对java进行输入输出控制,并对结果进行对比,但是我的评测机的问题是,在第三单元的时候无法自动生成有强度的错误格式数据,此时就是只能人工分类测试,但是由于当时偷懒了,导致这块出现了一个小bug。
第二单元
第二单元很可惜的是没有搭建自动评测机。
第三单元
1.使用Junit测试
Junit测试是针对Java程序的最小功能单元测试的方法,也即针对单个Java方法的测试。
JUnit是一个开源的Java语言的单元测试框架,我们可以非常简单地组织测试代码,并随时运行它们,Junit就会给出成功的测试和失败的测试,还可以生成测试报告。
在测试的时候需要根据JML中声明(signal)的多种情况全面的覆盖输入数据,并且在每一种情况下的边界情况下尽量考虑。
优势:
-
Junit编写及其简单,集成在IDEA中,可以在写完一个方法后随时进行测试。
-
对于较为简单的方法,Junit手写测试可以确保所写代码基本覆盖JML中声明的所有情况,主要可以避免一些基本的逻辑错误。
-
JML是针对方法的测试,而且在出错后会生成错误报告,可以快速确定bug,方便debug。
劣势:
-
在我的测试中,Junit的输入数据都是靠手动构造,这样的话数据肯定不够强,只能把基本的情况测一下,测试的强度无法保证。
-
Junit的输出也是根据输入手动计算的,这样的话如果对JML的理解本身就有问题的话,很容易导致手写的标准输出也是错的,强度很低。
2.使用JML语言的应用工具链
这一块我做作业的时候没有使用,昨晚才听人说在blog上看见,感觉很厉害,这些工具可以根据JML给的规格自动生成各种数据并进行正确性检查。这样就保证了测试的强度,解决了我手写Junit的问题。由于我也没用过,这里就放出链接大伙可以试试
https://blog.csdn.net/denglili2090/article/details/101237257
3.使用数据生成器+对拍器
我主要使用的是这种方法,在完成了所有的类和方法并用Junit简单做了测试之后,就可以使用一个数据生成器生成大批量数据,在检验正确性的时候这个数据可以很大,由于这个单元指令都很整齐,构造随机生成器并不复杂。我这里使用C++来构造的,用python会更加方便。
一个简单的例子
#include<iostream>
#include<stdlib.h>
#include<time.h>
using namespace std;
int person_id[123456];
int Group_id[10];
int getPersonid(){
return rand()%10000;
}
int getValue(){
return rand()%1000;
}
int getAge(){
return rand()%200;
}
char *randstr(char *str, const int len)
{
int i;
for (i = 0; i < len; ++i)
{
switch ((rand() % 3))
{
case 1:
str[i] = 'A' + rand() % 26;
break;
case 2:
str[i] = 'a' + rand() % 26;
break;
default:
str[i] = '0' + rand() % 10;
break;
}
}
str[++i] = '\0';
return str;
}
char name[20];
int main(){
srand((unsigned)time(NULL));
int num_ap = 1000;
int cnt_id = 0;
for(int i=0;i<num_ap;i++){
int newid = getPersonid();
person_id[cnt_id++] = newid;
cout<<"ap "<<newid<<" "<<randstr(name,10)<<" "<<getAge()<<"\n";
}
cnt_id = 0;
for(int i=0;i<9;i++){
int newid = getPersonid();
Group_id[cnt_id++] = newid;
cout<<"ag "<<newid<<"\n";
}
for(int i=0;i<1000;i++){
int index_personid = rand()%num_ap;
int index_groupid = rand()%9;
cout<<"atg "<<person_id[index_personid]<<" "<<Group_id[index_groupid]<<"\n";
}
for(int i=0;i<1100;i++){
int index_personid1 = rand()%num_ap;
int index_personid2 = rand()%num_ap;
cout<<"ar "<<person_id[index_personid1]<<" "<<person_id[index_personid2]<<" "<<getValue()<<"\n";
}
for(int i=0;i<1500;i++){
cout<<"qbs"<<"\n";
}
for(int i=0;i<333;i++){
int index_personid1 = rand()%num_ap;
int index_personid2 = rand()%num_ap;
cout<<"qci "<<person_id[index_personid1]<<" "<<person_id[index_personid2]<<"\n";
}
}
生成之后就可拉上几个小伙伴进行对拍了,这样的话找的人不是特别拉胯的话都是可以找出bug的。
优势:
- 这样测试,测试强度比较强。
劣势:
- 需要找同学来对拍,没有标准输出,只能比较,理论上是不能保证正确性的。
- 无法自己测试,需要找人,而且在多个人出问题时不好判断谁是对的。
改进:
使用python的一个图论库NetworkX,将此作为标准输出。
第四单元
在网上找一些UML图,通过随机生成生成一些测试指令,跟小伙伴进行对拍。
总结自己的课程收获
1.首先,在这门课中收获了java的基本编程技能,对java语言基本可以熟练运用了。虽然荣老师说这不是OO课的工作,但是要完成如此繁重的任务,只能逼自己自学java语言了,现在看来成效显著。learn by coding
2.其次,在这门课中算法水平又有所长进,第一单元的递归下降,第三第四单元的各种图算法在自己绝望的反复debug中真的是掌握的很扎实了。
3.入门了多线程,可以基本理解并发情况下编程的特点,并可以写出不错的多线程程序。
4.掌握了较大规模软件开发的大概过程(虽然也不是很大),由原来的oj测试小程序到现在的独立开发。每个单元都在重复设计-----实现-------测试的三步走策略,开发能力在潜移默化中得到了很大的提高。
5.这门课最大的收获是学习了一些有关面向对象设计的思想,以及一些设计模式。包括但不限于:如何抽象一个类,如何抽象出继承关系,单例模式,工厂模式,JML
建模语言,UML
建模语言等。这些设计思想在进行代码的架构设计中非常有用,一个好的设计不仅可以让你快速写完代码,还能让代码有良好的扩展性,应对新的功能。也许架构能力的培养就是在一次次的推倒重构中进行的。
立足于自己的体会给课程提三个具体改进建议
1.首先,实验课感觉内容很好,但是形式上有些鸡肋。上机的环境很宽松,可以随便交流(这可能不是问题),感觉这样的话可以把上机的内容作为训练一样的内容发放下去供同学们日常学习。而且感觉上机的编程题不能看到评测结果这块还是蛮影响体验的,建议实验课对编程题能开放一下评测结果,并且在每次上机后由助教给出此次上机一些答案的解析。这样能方便同学们学习。
2.希望在暑假预习课程的时候可以像当时计组一样对java的基本语言和基本数据结构做一个梳理和教学,如果直接上来就是java编程的话可能程度不太好的同学会产生消极的心理。虽说自学是每个程序猿所必需的,但毕竟预习课程是在暑假,而且是面向全体同学的,程度有好有坏,希望能尽量让过渡平滑一点。
3.希望老师不要在课上说什么OO课只有第一单元,第二单元有难度,后面就是一马平川了。我和周围的同学实际感受下来都没有觉得后面两个单元很轻松,反而后两个单元,尤其是JML的第二次作业大量的同学分数很低。希望在后面不要让同学们放松警惕了。😃
4.checkstyle感觉还是有些问题,比如说checkstyle可以通过将{},()上移的方法减少行数,至少我周围很多同学都在使用将{上移的方法进行格式修改,但是这样的修改会极大的破坏代码的可读性和可修改性,感觉这个修改方式是和咱们checkstyle的初衷相悖的,希望能修改一下checkstyle的检查规则。