2022-OO-Unit1

2022-OO-Unit1

mashiroly

1. 引子

​ 在供参考的博客要求中,可以体会到课程组呼吁同学们“将关注点置于已完成的代码”,分析其结构与问题。本文当然不会缺失这一部分,但本文更愿意将重点放在“设计”,缕清这一个月来认识“面向对象”的思路变化。

2. 设计

先简述迭代过程。
hw1:在题目要求上增加可处理多层括号;
hw2:在题目要求上增加可处理三角函数嵌套因子;
hw3:更新自定义函数嵌套。

2.1 递归下降与统一表示

​ 递进式的三次作业中,架构的整体思路是一致的,即归纳为“递归下降”与“统一表示”。hw1已经确定了思路,也因此没有经历大规模重构。因此,本小节不强调三次作业题目的区别,大部分直接对hw3的题目表述进行分析。
​ 对个人而言,这也产生了新的问题,眼高手低意味着第一周工作量极大。

2.1.1 递归下降:从形式化表述到结构化数据
flowchart TB A[Expr] --> B[Term] B -->C[Factor] C --- D[Num] C --- E[Pow] C --- F[ExprFactor] F -.ONE.-> A C --- G[Tri] C --- H[SelfFunc] C --- I[Sum] G -.TWO.-> C H -.THREE.-> C

​ 上图为根据形式化表述简化的表达式结构。根据形式化表述,Expr、Term与Factor天然存在层次关系与递归定义。我们根据上图的方式建立句法分析类Lexer、解析类Parser,将表达式的解析划分为三层,按字读取、正则表达式辅助、顺序地逐层解析、返回下一层的结果。

​ 在Factor一层,我们遇到了ExprFactor、Tri和SelfFunc三种涉及递归的Factor,具体实现用一句话概括,就是“左括号递归,右括号返回”。实现ExprFactor到Expr的递归,我们就可以处理多层括号嵌套。实现Tri和SelfFunc到Factor的递归(下一步往往是再次把解析任务交给ExprFactor),我们就可以处理这两种函数的嵌套。

​ 这时,我们会遇到一个问题:现在我们的确能将表达式解析成树状结构了,但我们最终目的是对表达式展开化简——所谓“返回下一层的结果”,返回的是什么呢?为了构建起树状结构,为了父节点能够”看见“子节点,这个”结果“理应是对象本身,并把子节点对象加入父节点对象的属性。但是,子节点对象所表示的数据以何种形式交给它的父节点对象呢?另一种表述是,最底层的子节点对象(叶结点)究竟以什么形式存储数据呢?

​ 这个问题直接催生了“统一表示”的思路,并使得设计和实现紧密关联。

2.1.2 统一表示:化程序员所见为程序之所见

​ 对于程序员而言,不论是4,还是(x+1),还是(sin((+-x**2-1)**3))**4-+-3*cos(0),甚至是不合法的++--+123,都是人脑可理解、可计算的“算式”,这是因为人脑完全理解“算式”的计算顺序、计算规则,并能够完全看见“算式”的全部信息并同时利用。

​ 但对程序而言,句法分析类Lexer只能看见当前解析到的位置,对象只能看见对象本身,可以访问子节点对象(因为已经包含在属性中)。如果不能做好数据的管理,那么所谓“对象协同”仅止于构建一棵树。

​ 站在程序员的角度,既然不论何种“算式”都共用同一套计算规则,我们就应该建立一套完备的统一表示,让各级对象的数据可以交互。统一表示成什么因人而异,重要的是有其思想。在我的设计中,建立Basic类:

public class Basic {
    private BigInteger coef = BigInteger.ONE;
    private BigInteger pow = BigInteger.ZERO;
    private HashMap<Factor, BigInteger> sin = new HashMap<>();
    private HashMap<Factor, BigInteger> cos = new HashMap<>();
}

完备地将树中处于叶结点Factor的数据表示为$$coefx**pow\Pi(\sin(Factor)a*cos(Factor)b)$$,例如:1,x**2,sin(x)**4.如需拓展其他函数,只需修改Basic类即可。

Basic类不是叶结点Factor(1+x)显然需要两个Basic对象才能表示,因此需要容器basics管理Basic对象,并将容器作为表示该数据的对象的属性。现在,我们可以将2.1.1节图中最下一排(从Num到Tri)全部建类,唯一的属性是basics。结合抽象层次关系,我们让这些类统一实现Factor接口,运用归一化的思想,建立归一化访问,无差别引用下层数据。对于SelfFunc和Sum,实践中感觉使用Factor接口比较困难,因此按照2.3.4节的方法当作Expr处理。

在我的设计中,basics是HashMap<Basic, BigInteger>,其中BigInteger是Basic作为一个因子整体的幂次。如(2*x)**2 -> <{2,1,…},2>

​ 我们终于解决了叶结点Factor如何存储数据的问题,从最底层向上返回的对象终于可以将数据交给上层参与计算了。但统一表示还能更新一步:不论是Term还是Expr也能用basics的形式表示,因此将Expr和Term也实现Factor接口。显然Expr和ExprFactor类是一致的,我们统一为Expr类。自此,任何一级合法表达式都能用basics表示,只需在类内填补构建、计算、优化的方法了。

2.2 搭建架构

classDiagram class Parser{ -lexer -funclist +Parser(String str, SelfFunclist funclist) +parseExpr() +parseTerm() +parseFactor() } class Lexer{ -cutToken -input -details -num -funcName +Lexer(String str) +reWriteINput +getCurToken() +getDetails() +getNum() +getFuncName() +next() } Parser <.. Lexer Parser <.. SelfFunclist class SelfFunclist{ -funcs +addFunc() +getFuncs() } SelfFunclist *-- SelfFunc class SelfFunc{ -funcList -name -expression -vars +define(String str) +setList() +reference() +getExpression() +getName() } class Sum{ -iter -cnt -start -end +Sum() +reference(String str) } class Factor{ <<interface>> +getBasics() +toString() +getExp() +setExp() +equals() } Factor<|..Num Num:-basics Num:-exp Num:+Num(BigInteger coef) Factor<|..Pow Pow:-basics Pow:-exp Pow:+Pow(BigInteger pow) Factor<|..Sin Sin:-basics Sin:-exp Sin:+Sin(Factor factor, BigInterger exp) Factor<|..Cos Cos:-basics Cos:-exp Cos:+Cos(Factor factor, BigInterger exp) Factor<|..Expr Expr:-basics Expr:-terms Expr:-exp Expr:+getTerms() Expr:+addTerm(Term term) Expr:+addBasics(Term term) Factor<|..Term Term:-basics Term:-factors Term:-exp Term:+addFactor(Factor factor) Term:multBasics(Factor factor) Term:Merge(HashMap basics) Basic ..>Factor Sum ..>Expr SelfFunc ..>Expr class Basic{ -coef -pow -cos -sin +Basic() +Basic(Basic) +multCoef() +addCoef() +addPow() +addSin() +addCos() +merges() +equals() +toString() }

​ 整体思路已经在2.1两小节清晰表述。Parser类和Lexer类协作,提取当前字符(串)的数据,并将数据分配给对应的对象;所有实现Factor接口的类都具有属性Basic,只有读到叶结点时,才会调用Basic中的方法存储数据并返回。Term的方法实现了Factor之间的乘法与幂次,合并同类项,保证交给Expr的Basic幂次为1 且已合并同类项(出于HashMap容器的要求)。Expr的方法实现了Term之间的加法及合并同类项。最后通过toString直接将顶层Expr的basics属性输出。

2.3 难点问题

2.3.1 连续符号

​ 连续符号对我而言是hw1的一道坎。最初采用符号计数器,但由于正则表达式的设计,遇到形如+-(...)无法处理。换用思路:遇到负号则递归,返回时取相反。实现过程中发现很难确定什么时候返回,bug频出。最终采用了直接改写原串为+1*-1*(...)的形式,并控制pos回调,顺利解决。

2.3.2 幂次

​ 对幂次的处理可能是最纠结的一步:到底给Term处理还是给Expr处理?最终,由于幂次与乘法的相似,将其交给Term,保证返回给Expr时整体的exp一定是1,更体现对象的内聚,也算多了一种debug的方案。

2.3.3 解析自定义函数与求和函数

​ 由于感到困难(偷懒),并没有对自定义函数与求和函数重新建树处理,而是利用对括号建栈解析、字符串替换得到新的Expr,重新建Parser、传入函数表解析,这样返回的仍是Expr,作为Factor,交给上级处理。

3. 度量分析

3.1 类的内聚与耦合

OCavg:平均操作复杂度。

OMax:最大操作复杂度。

WMC:加权操作复杂度。

class OCavg Ocmax WMC
expression.Basic 2.533333333333333 10.0 38.0
expression.Cos 1.3333333333333333 3.0 8.0
expression.Expr 1.5 4.0 15.0
expression.Num 1.3333333333333333 3.0 8.0
expression.Pow 1.3333333333333333 3.0 8.0
expression.SelfFunc 2.142857142857143 6.0 15.0
expression.SelfFuncList 1.0 1.0 3.0
expression.Sin 1.3333333333333333 3.0 8.0
expression.Sum 1.6666666666666667 3.0 5.0
expression.Term 2.4444444444444446 9.0 22.0
MainClass 1.5 2.0 3.0
parser.Lexer 2.8 11.0 28.0
parser.Parser 3.6 11.0 18.0
Total 184.0
Average 1.978494623655914 5.0 13.142857142857142

两个解析类的复杂度较高,这是递归下降本身的因素。而数据类中Term复杂度极高,归因为乘法和幂次依赖多次遍历,未能找到更好的方案。

3.2 方法的复杂度

CogC:认知复杂度。

ev(G):基本圈复杂度。

iv(G):设计复杂度。

v(G):圈复杂度。

选择复杂度最高的方法列出。

method CogC ev(G) iv(G) v(G)
expression.Basic.addCos(Factor, BigInteger) 9.0 4.0 5.0 5.0
expression.Basic.addSin(Factor, BigInteger) 9.0 4.0 5.0 5.0
expression.Basic.equals(Object) 4.0 3.0 5.0 7.0
expression.Term.Merge(HashMap, HashMap) 8.0 4.0 5.0 5.0
expression.SelfFunc.reference(String) 8.0 1.0 7.0 7.0
expression.Term.multBasics(Factor) 24.0 1.0 9.0 9.0
expression.Basic.toString() 18.0 2.0 10.0 10.0
parser.Lexer.next() 13.0 2.0 10.0 11.0
Total 146.0 117.0 172.0 191.0
Average 1.5698924731182795 1.2580645161290323 1.8494623655913978 2.053763440860215

就平均复杂度而言,各方法还算比较简单。复杂度最高的不出意料仍是multBasics。此外,由于没用单独的优化类,优化完全通过toString内部的分支实现,分支复杂。

4. 测试与bug分析

​ 测试主要采用自动评测与手动构造边界数据结合。由于没用自己动手实现评测机,这里描述构造和互测出的bug。

  1. 连续符号,如2.3.1所述;
  2. 自定义函数和求和函数”字符串替换实现,遗漏了需要加括号的情况。

​ 总之,bug均出自较“面向过程”的步骤,所在模块复杂度均较高,企图采用trick避免对结构的设计。在之后的作业中应直面问题,训练的目的在“学会“而非”答题“。

posted @ 2022-03-26 15:58  mashiroly  阅读(130)  评论(2编辑  收藏  举报