2022面向对象设计与构造第四单元总结
2022面向对象设计与构造第四单元总结
一、第四单元架构设计
1.1 第一次作业
在开始动手写本单元作业之前,我查看了一些往届的博客,也有留意微信群里的讨论,发现不少人都是通过自己设置的一些My**类,来在代码中重现UML图的层次结构,方便之后的各种查询操作。但我经过仔细考虑之后,决定不采用这种方法,一方面是因为这种方法过于耗时,让我本不足的期末复习时间雪上加霜,另一方面,这种方法也不见得能显著提高查询效率。
我最初的设计十分简单粗暴,就是直接将传入的elements直接保存在一个elements容器,当需要查找特定元素时,例如查找一个类下的所有操作,就遍历整个elements容器,寻找parentId为class.Id的operation。这样做的效率非常地低,最坏情况下,也就是对于getClassOperationCouplingDegree()
函数,时间复杂度达到了惊人的O(n^3),显然不能满足2s的CPU时间要求。
为了提高效率,我决定在构造函数中为不同类型的元素分组,即建立一系列的classes、opreations等属性,这样的话,查询的时候就只需要在对应的容器中查询,而不需要遍历所有元素。经过测试,这种优化方法基本能满足时间要求。
但这种优化并没有降低时间复杂度,只是在同一时间复杂度的基础上降低了系数而已。考虑到测评CPU时间普遍长于本机测试CPU时间,为了保险起见,我对代码又做了进一步优化。为了实现对特定元素的快速定位,我利用hashmap的O(1)查询的特点,在构造时对一些不同类型的元素实现了映射,具体映射如下所示:
private final ArrayList<UmlClass> classes = new ArrayList<>();
private final ArrayList<UmlGeneralization> generalizations = new ArrayList<>();
private final ArrayList<UmlAttribute> attributes = new ArrayList<>();
private final HashMap<String, ArrayList<UmlOperation>> class2operations = new HashMap<>();
private final HashMap<String, ArrayList<UmlParameter>> operation2parameters = new HashMap<>();
private final HashMap<String, String> class2father = new HashMap<>();
private final HashMap<String, ArrayList<String>> class2generalFathers = new HashMap<>();
private final HashMap<String, String> interface2name = new HashMap<>();
其中绝大部分string类型的key值都是对应元素ID。
通过这种优化,我的代码效率得到极大的提升,最高时间复杂度降为了O(n^2)。
第二、三次作业
第二、三次作业基本上是对第一次作业架构的延续,唯一需要解决的问题是,由于代码行数过多,即使我将一些private方法搬到了工具类里面,MyImplementation类还是远远超出了500行的限制。为此,我将所有的属性、方法都拆分搬运到了三个工具类里面,于是乎,MyImplementation类就变成了这个样子:
public class MyImplementation implements UserApi {
private final ClassOperation classOperation = new ClassOperation();
private final StateOperation stateOperation = new StateOperation();
private final SequenceOperation sequenceOperation = new SequenceOperation();
public MyImplementation(UmlElement[] elements) {
ArrayList<UmlElement> elements1 = new ArrayList<>(Arrays.asList(elements));
for (UmlElement element : elements1) {
if (element instanceof UmlClass) {
classOperation.handleClass(element);
}
else if (element instanceof UmlInterface) {
classOperation.handleInterface(element);
}
else if (element instanceof UmlGeneralization) {
classOperation.handleGeneralization(element);
}
else if (element instanceof UmlOperation) {
classOperation.handleOperation(element);
}
else if (element instanceof UmlParameter) {
classOperation.handleParameter(element);
}
/*@ 以下else-if语句省略 */
}
}
@Override
public void checkForAllRules() throws PreCheckRuleException {
checkForUml001();
checkForUml002();
checkForUml003();
checkForUml004();
checkForUml005();
checkForUml006();
checkForUml007();
checkForUml008();
checkForUml009();
}
@Override
public void checkForUml001() throws UmlRule001Exception {
classOperation.checkForUml001();
}
@Override
public void checkForUml002() throws UmlRule002Exception {
classOperation.checkForUml002();
}
@Override
public void checkForUml003() throws UmlRule003Exception {
classOperation.checkForUml003();
}
/*@ 以下checkForUML00X方法省略 */
@Override
public int getClassCount() {
return classOperation.getClassCount();
}
@Override
public int getClassSubClassCount(String className) throws ClassNotFoundException,
ClassDuplicatedException {
return classOperation.getClassSubClassCount(className);
}
@Override
public int getClassOperationCount(String className) throws ClassNotFoundException,
ClassDuplicatedException {
return classOperation.getClassOperationCount(className);
}
/*@ 以下查询方法省略 */
二、架构设计思维及OO方法理解的演进
我在这四个单元架构设计思维的演进,其实可以集中体现为我写代码时定义的类和方法的数量变化。
在第一单元中,虽然经过pre的训练,我对面向对象已经有了一个基本的认识和理解,但由于面向对象编程经验的缺乏,我的架构设计和代码编写还带着很大的盲目性。借用我在第一次博客中的总结:
当时第一节OO课临下课时,老师的PPT放出题目简介,我一下子就感受到了作业的难度。指导书发布之后,我盯着指导书反反复复看了一个多小时,边看边思考架构和一些细节问题,但越想脑子越乱,陷于细节的泥潭中无法脱身。于是我索性放弃无谓的纠缠,先把表达式的预处理方法写好。写好之后,先前混杂着连续加减和空白字符的混乱表达式一下子变得干净清爽,我也得以继续集中注意力思考架构问题。
考虑到最多只存在一层括号,我很快想到,如果把表达式拆到括号在左右两端时,就可以将括号去掉。要达到这个目的,首先通过加减号将表达式拆成一个个项,再通过乘号将项拆成一个个因子。但我很快就发现了一个问题:如何区分括号中的加号和括号外的加号?仔细思考之后,我采用了一个比较取巧的方法:遍历字符串,将最外层的加号换成“@”号,减号换成“#”号。这样就能通过正则表达式将表达式拆开,拆乘号同理。
接下去就比较顺利了。先把表达式一直向下拆直到常数和x,然后在各类中实现calculate()方法,将底层运算结果向上传递,上层综合下层结果进行计算,最后在最上层用字符串化方法输出。虽然之后遇到了一些奇奇怪怪的小bug,但最后都算是比较顺利地解决了。
面对一个较为复杂的问题时,我还难以在一个全局的角度对架构进行整体的思考,因此不得不采用走一步看一步的策略。这种策略造成的后果是:架构较为混乱,各类之间分工不甚明确。此外,因为我当时方法封装思想也较为薄弱,使得我在很多方法中都实现了过多的逻辑,一个方法完成了几个方法的工作。
在第二单元中,我开始在动笔之前有意识地思考整体架构和需要建立的类。但因为我的OO思维仍然不够成熟,在我最开始的设计中,只实现了主类、请求类和电梯类,后来经过周四实验课的提示,我才将调度器类加入架构中。不过,和第一单元对比起来,不管是对各类的分工,还是类中方法的设置,我都有了长足的进步。虽然我在第二单元的得分远不如第一单元,但我对第二单元架构的满意度远超第一单元。
在第三第四单元,作业的整体架构已被官方包限制,加之第四单元临近期末,我也无心在架构上花太多心思,所以基本都是直接实现接口方法。不过,虽然宏观结构并无太大操作空间,微观结构还是可以自由发挥的。比如在第三单元中,为了对dijkstra算法进行优化,我建立并维护了一个小根堆类,还建立了一些其它的辅助类,实现了不错的局部架构。此外,在第三单元,我第一次对IDEA的警告进行了清零,对Java语法的掌握进一步熟练。
三、测试理解与实践的演进
在第一单元中,我根据形式化描述递归生成测试数据,并基于python sympy库进行对拍,代码如下:
import random
import sys
sys.setrecursionlimit(1000000) # 例如这里设置为一百万
OP = ['', '+', '-']
TRIGO = ["sin", "cos"]
NUM_MAX_LEN = 8 # 常数最大长度
TERM_MAX_SCALE = 3 # 一个项最多含有多少个因子
EXPR_MAX_SCALE = 3 # 一个表达式最多含有多少个项
SUB_EXPR_MAX_SCALE = 3 # 一个括号里的表达式最多含有多少个项
BRACKET_MAX_LEN = 3 # 括号嵌套层数
TRIGO_BRACKET_LEN = 3 # 三角函数嵌套层数
# 指数
def new_power():
power_str = ""
if random.randint(0, 1) > 0:
power_str += "**"
power_str += OP[random.randint(0, 1)]
power_str += str(random.randint(0, 4))
# print(power_str)
return power_str
# 常数因子
def new_num():
while 1:
num_str = ""
num_str += OP[random.randint(0, 2)]
type = random.randint(0,5)
if type == 0:
return num_str + "0"
elif type > 0 and type <=4:
num_len = random.randint(1, NUM_MAX_LEN//3)
else:
num_len = random.randint(NUM_MAX_LEN//2, NUM_MAX_LEN)
for i in range(num_len):
num_str += str(random.randint(0, 9))
if len(num_str) > 1 and num_str[0]!='0' and num_str[1]!='0':
break
if len(num_str) <= 1:
break
if len(num_str)==2 and num_str[1]=='0':
break
# print(num_str)
return num_str
# 幂函数
def new_var():
var_str = "x"
var_str += new_power()
# print(var_str)
return var_str
def new_powerFunct(trigo_cnt):
if trigo_cnt == 0:
type = random.randint(1, 3)
else:
type = random.randint(0, 3)
if type == 0 :
return new_trigo(trigo_cnt - 1)
elif type == 1:
return new_var()
elif type == 2:
return new_num()
else:
return "(" + new_expr(random.randint(1, EXPR_MAX_SCALE), random.randint(0, BRACKET_MAX_LEN)) + ")" +new_power()
def new_trigo(trigo_cnt):
str_trigo = TRIGO[random.randint(0,1)]
str_trigo += "("
str_trigo += new_powerFunct(trigo_cnt);
str_trigo += ")"
str_trigo += new_power();
return str_trigo
# 因子
def new_factor(bracket_cnt, trigo_cnt):
if bracket_cnt == 0:
factor_type = random.randint(1, 3)
else:
factor_type = random.randint(0, 3)
if factor_type == 0:
return "(" + new_expr(random.randint(1, SUB_EXPR_MAX_SCALE), bracket_cnt - 1) + ")" + new_power()
elif factor_type == 1:
return new_num()
elif factor_type == 2:
return new_var()
else:
return new_trigo(trigo_cnt - 1)
# 项
def new_term(factor_cnt=1, bracket_cnt=1, trigo_cnt = 1):
term_str = ""
if factor_cnt == 1:
term_str += OP[random.randint(0, 2)]
term_str += new_factor(bracket_cnt, trigo_cnt)
else:
factor_cnt -= 1
term_str += new_term(factor_cnt, bracket_cnt, trigo_cnt)
term_str += "*"
term_str += new_factor(bracket_cnt, trigo_cnt)
# print(term_str)
return term_str
# 表达式
def new_expr(term_cnt=1, bracket_cnt=1, trigo_cnt=1):
expr_str = ""
if term_cnt == 1:
expr_str += OP[random.randint(0, 2)]
else:
term_cnt -= 1
expr_str += new_expr(term_cnt, bracket_cnt,trigo_cnt)
expr_str += OP[random.randint(1, 2)]
expr_str += new_term(random.randint(1, TERM_MAX_SCALE), bracket_cnt, trigo_cnt)
# print(expr_str)
return expr_str
f=open('C:/Users/Dell/Desktop/sj.txt','w')
lst=[]
if __name__ == "__main__":
for i in range(5000):
while 1:
expr = new_expr(random.randint(1, EXPR_MAX_SCALE), random.randint(0, BRACKET_MAX_LEN), random.randint(1, TRIGO_BRACKET_LEN))
if len(expr) <= 100:
break
expr=expr+'\n'
lst.append(expr)
f.write(expr)
在第二单元中,因为当时个人的一些原因,空闲时间不是很充足,所以做的测试很少,这也导致了我在第二单元大量的失分。
在第三单元中,我基本的测试策略就是 单元测试 + 情况覆盖
- 单元测试:使用JUnit工具,对每一个类建立一个测试类,然后再在测试类内编写各方法对应的测试方法。
- 情况覆盖:由于本单元需要实现的接口都由JML描述,所以在编写测试方法时,只需要覆盖各方法的JML中写明的各种情况即可。比如对于Network接口中的storeEmojiId方法,它的JML描述为:
/*@ public normal_behavior
@ requires !(\exists int i; 0 <= i && i < emojiIdList.length; emojiIdList[i] == id);
@ assignable emojiIdList, emojiHeatList;
@ ensures (\exists int i; 0 <= i && i < emojiIdList.length; emojiIdList[i] == id && emojiHeatList[i] == 0);
@ ensures emojiIdList.length == \old(emojiIdList.length) + 1 &&
@ emojiHeatList.length == \old(emojiHeatList.length) + 1;
@ ensures (\forall int i; 0 <= i && i < \old(emojiIdList.length);
@ (\exists int j; 0 <= j && j < emojiIdList.length; emojiIdList[j] == \old(emojiIdList[i]) &&
@ emojiHeatList[j] == \old(emojiHeatList[i])));
@ also
@ public exceptional_behavior
@ signals (EqualEmojiIdException e) (\exists int i; 0 <= i && i < emojiIdList.length;
@ emojiIdList[i] == id);
@*/
public void storeEmojiId(int id) throws EqualEmojiIdException;
由JML可知,这个方法有两种执行的可能,一种是emojiIdList不存在输入的id,此时将id加入emojiIdList,并将对应的emojiHeatList置零。另一种是emojiIdList存在输入的id,此时则会抛出EqualEmojiIdException异常。基于上面的复习,我编写了以下的测试方法:
@org.junit.jupiter.api.Test
void storeEmojiIdTest() throws Exception {
network.storeEmojiId(1);
System.out.println(network.containsEmojiId(1));
network.storeEmojiId(1);
}
前两行是为了测试第一种情况,第三行是为了测试第二种情况。若方法实现无误,则测试方法执行之后应该会先输出一个true,然后再抛出一个EqualEmojiIdException异常。
在第四单元中,因为mdj数据较难生成,所以也只是简单的测试了一下各个方法的基本正确性。
四、课程收获
这个学期的OO课程给我带来了非常多的收获,这些收获不仅是在面向对象编程方法与技巧上,更在编程的整体思维上。下面是我的分点总结:
-
面向对象编程方法与技巧:
- 面向对象语言(Java)特性:类、接口、属性、方法、构造、继承、实现、多态、向下(上)转型、引用、重写、重载……
- 设计模式:工厂模式、单例模式……
- 内聚耦合性分析:圈复杂度、基本圈复杂度……
- 多线程编程:同步块、锁、wait-notify、线程间通信……
- 面向对象建模语言:JML、UML……
- 其他:异常处理机制、Java垃圾回收机制、正则表达式、向下递归……
-
编程的整体思维:
我所理解的编程的整体思维,就是指对一个复杂问题的分析、抽象、架构设计和代码实现能力。这些能力的重要程度是超过具体知识本身的。在OO课程一次次的磨炼中,我的这些能力相比于学期开始之前已经有了很大的提高,这些能力也必将在我未来的学习和工作中发挥更大的作用。
五、改进建议
- 希望周一的作业发布时间能提早一些,把3点半到7点的那段时间利用起来。
- 希望测评界面像计组或者C语言那样,给一个整体的是否通过的标识,而不是现在这种只给出每个点是否通过。
- 希望OO网站将作业和实验放在一级栏目里,方便打开网站直接进入。