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课程一次次的磨炼中,我的这些能力相比于学期开始之前已经有了很大的提高,这些能力也必将在我未来的学习和工作中发挥更大的作用。

五、改进建议

  1. 希望周一的作业发布时间能提早一些,把3点半到7点的那段时间利用起来。
  2. 希望测评界面像计组或者C语言那样,给一个整体的是否通过的标识,而不是现在这种只给出每个点是否通过。
  3. 希望OO网站将作业和实验放在一级栏目里,方便打开网站直接进入。
posted @ 2022-06-20 19:11  20231026  阅读(27)  评论(1编辑  收藏  举报