名词 | 解释 |
---|---|
CogC | 每个非抽象方法的认知复杂性,嵌套的控制结构越多,认知复杂性就越高 |
ev(G) | 每个非抽象方法的基本复杂性,用图论度量方法的控制流结构,范围1-v(G) |
iv(G) | 方法的设计复杂性,与对其他方法的调用之间的相互联系有关 |
v(G) | 每个非抽象方法的圈复杂度,即if‘s, while‘s, for‘s, do‘s, switch cases, catches, conditional expressions, &&‘s , ||‘s的数量 |
类(共9个) | 说明 | 功能 | Source Code Lines(709) |
---|---|---|---|
MainClass | 主类 | 输入和输出 | 21 |
Expression | 表达式处理函数类 | 处理表达式 | 256 |
Operator | 基本运算符类 | 运算符类的父类 | 84 |
Add | Operator子类,加减法类 | 生成和化简加减法表达式 | 173 |
Mult | Operator子类,乘法类 | 生成和化简乘法表达式 | 80 |
Power | Operator子类,指数类 | 生成指数表达式 | 48 |
Term | 常量/变量类 | 常量和变量 | 30 |
OperatorFactory | 运算符类工厂类 | 工厂模式生成运算符类 | 17 |
SignedOpe | (Add内)带正负性的Operator类 | 用于Add内的表达式化简 | (25) |
method(2) | Lines | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|---|
main(String[]) | 11 | 0 | 1 | 1 | 1 |
process(String) | 7 | 2 | 1 | 3 | 3 |
method(9) | Lines | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|---|
getIterm(String,String) | 26 | 12 | 5 | 7 | 8 |
getMultCoe(Operator) | 22 | 8 | 5 | 5 | 6 |
getMultIndex(Operator) | 22 | 8 | 5 | 4 | 6 |
getOpePriority(String) | 14 | 5 | 5 | 5 | 6 |
getSymbol(String) | 49 | 19 | 12 | 19 | 22 |
getTermArray(String) | 24 | 6 | 3 | 3 | 4 |
getTree(String) | 59 | 46 | 12 | 14 | 17 |
setMultCoe(Operator,BigInteger) | 15 | 5 | 4 | 4 | 4 |
simplify(Operator) | 12 | 5 | 3 | 4 | 6 |
分析:可以看出,主要是getTree(生成表达式树)、getSymbol(分解出项的前置符号)和getIterm(分解出不带符号的项)这3个方法的度量数值偏高。这三个方法的CogC偏高的原因是原因是在这两个方法中运用了较多的if判断语句,造成方法的认知复杂;getTree的v(G)偏高的原因是在方法中调用了较多Expression类下的static方法,而getSymbol和getIterm的v(G)偏高的原因是使用了较多的正则表达式匹配方法和String.equals方法。
property(7) | 类型 | 说明 |
---|---|---|
aa | Operator | 左子树 |
bb | Operator | 右子树 |
type | String | 表明类型,如Single,Add,Mult |
term | term | Single型包含的数据 |
isSingle | boolean | 是否为Single |
certain | boolean | isSingle & term是常量 |
positive | boolean | 表明子类型,如Add分为加法和减法 |
method(14) | Lines | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|---|
addSingle(Term) | 6 | 0 | 1 | 1 | 1 |
getaa() | 2 | 0 | 1 | 1 | 1 |
getbb() | 2 | 0 | 1 | 1 | 1 |
getDerivative() | 10 | 3 | 3 | 1 | 3 |
getNew(Operator,Operator,boolean) | 2 | 0 | 1 | 1 | 1 |
getNumberOpe(BigInteger) | 4 | 0 | 1 | 1 | 1 |
getResult(Operator) | 2 | 0 | 1 | 1 | 1 |
getType() | 2 | 0 | 1 | 1 | 1 |
isCertain() | 2 | 0 | 1 | 1 | 1 |
isPositive() | 2 | 0 | 1 | 1 | 1 |
isSingle() | 2 | 0 | 1 | 1 | 1 |
Operator(Operator,Operator,boolean) | 16 | 7 | 1 | 4 | 7 |
setType(String) | 2 | 0 | 1 | 1 | 1 |
toString() | 7 | 2 | 2 | 2 | 2 |
method(11) | Lines | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|---|
Add(Operator,Operator,boolean) | 3 | 0 | 1 | 1 | 1 |
getAddArray(Operator,boolean) | 29 | 13 | 3 | 7 | 7 |
getDerivative() | 14 | 5 | 3 | 3 | 3 |
getNew(Operator,Operator,boolean) | 3 | 0 | 1 | 1 | 1 |
getResult(Operator) | 55 | 27 | 5 | 12 | 13 |
toString() | 42 | 22 | 1 | 12 | 12 |
SignedOpe.getOperator() | 2 | 0 | 1 | 1 | 1 |
SignedOpe.getPositive() | 2 | 0 | 1 | 1 | 1 |
SignedOpe.setOperator(Operator) | 2 | 0 | 1 | 1 | 1 |
SignedOpe.setPositive(boolean) | 2 | 0 | 1 | 1 | 1 |
SignedOpe.SignedOpe(Operator,boolean) | 3 | 0 | 1 | 1 | 1 |
分析:Add的度量数值偏高,主要体现在getResult(化简)和toString(字符串输出)上。这是因为两个方法内都涉及大量的if判断。同时,使用BigInteger的方法和String.equals方法也使这两个方法的v(G)值偏高。
method(6) | Lines | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|---|
getDerivative() | 9 | 2 | 2 | 2 | 2 |
getMultArray(Operator) | 10 | 4 | 2 | 3 | 3 |
getNew(Operator,Operator,boolean) | 3 | 0 | 1 | 1 | 1 |
getResult(Operator) | 25 | 7 | 3 | 5 | 6 |
Mult(Operator,Operator,boolean) | 3 | 0 | 1 | 1 | 1 |
toString() | 20 | 7 | 1 | 6 | 7 |
method(5) | Lines | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|---|
getDerivative() | 14 | 4 | 2 | 3 | 3 |
getNew(Operator,Operator) | 3 | 0 | 1 | 1 | 1 |
getResult(Operator) | 5 | 1 | 2 | 2 | 2 |
Power(Operator,Operator) | 3 | 0 | 1 | 1 | 1 |
toString() | 14 | 3 | 1 | 2 | 3 |
property(3) | 类型 | 说明 |
---|---|---|
type | int | 0-常量 1-变量 |
number | BigInteger | 常量的值 |
name | String | 变量名 |
method(4) | Lines | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|---|
addTerm(String) | 4 | 2 | 2 | 1 | 2 |
isCertain() | 2 | 0 | 1 | 1 | 1 |
Term(int,BigInteger,String) | 7 | 0 | 1 | 1 | 1 |
toString() | 7 | 2 | 2 | 2 | 2 |
method(1) | Lines | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|---|
getOperator(Operator,Operator,String) | 14 | 4 | 5 | 5 | 5 |
Average | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|
Homework1 | 4.44 | 2.31 | 3.17 | 3.63 |
在第一次作业中,我试图采用面向对象的思想来架构总体框架。我的总体构思是以表达式树作为核心算法,并采用工厂模式去构造表达树。该构思的主要难点在于表达式树的构建以及化简。
由于本人对面向对象的理解的不成熟和思考上的狭隘,在此次作业中大量的代码仍然是面向过程的产物,在架构上也出现很多的缺陷。
由上面的数据不难发现,该作业的代码主要集中于Expression、Operator、Add和Mult类。原因:虽然Operator作为符号类的父类,但Operator中存储了太多的属性导致代码多。另外Expression.getTree(生成表达式树)、Add.getResult(加减法表达式化简)、Mult.getResult(乘法表达式化简)的方法构思上的不足,导致代码冗长。
另外,在类的内聚和耦合方面,类与类之间的耦合程度过大,类的内聚程度不够,容易导致意想不到的bug,以及对bug的定位和修复带来了很大的困难。比如,在Expression中整合了过多的方法,造成代码繁杂和难以维护;Add和Mult的getResult(表达式化简)等方法,对于Expression类中的方法调用依赖性过多,在定位bug过程中,常常在多个类中反复跳转,阻碍重重。
总之,此次作业的初步采用了面向对象方法来设计,但在方法代码的合理划分和分配、类的内聚和耦合方法还存在很大的改进空间。
类(共12个) | 描述 | 描述 | Source Code Lines(1978) |
---|---|---|---|
MainClass | 主类 | 输入和输出 | 85 |
Express | 表达式处理函数类 | 处理表达式 | 363 |
Operator | 基本运算符类 | 运算符类的父类 | 83 |
Add | Operator子类,加减法类 | 生成和化简加减法表达式 | 230 |
Mult | Operator子类,乘法类 | 生成和化简乘法表达式 | 232 |
Power | Operator子类,指数类 | 生成指数表达式 | 56 |
Sin | Operator子类,Sin类 | 生成Sin(x) | 41 |
Cos | Operator子类,Cos类 | 生成Cos(x) | 41 |
Term | 常量/变量类 | 常量和变量 | 30 |
OpeFac | 运算符类工厂 | 工厂模式生成运算符类 | 24 |
SignedOpe | (Add内)带正负性的Operator类 | 用于Add内的表达式化简 | (23) |
Poly | 项解析类 | 描述项的特征,用于Mult内化简 | 81 |
method | Lines | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|---|
process(String) | 16 | 0 | 1 | 1 | 1 |
main(String[]) | 22 | 6 | 1 | 7 | 7 |
formatCheck(String) | 38 | 7 | 6 | 3 | 6 |
property(1) | 类型 | 说明 |
---|---|---|
canInt | boolean | 是否能中断 |
method | Lines | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|---|
getBigInteger(Operator) | 8 | 2 | 2 | 2 | 2 |
getBraP(ArrayList,int,String) | 56 | 27 | 12 | 15 | 19 |
getIterm(String,String) | 16 | 6 | 2 | 5 | 6 |
getOpePri(String) | 17 | 7 | 6 | 7 | 8 |
getPrint(Operator) | 20 | 7 | 3 | 5 | 5 |
getSin(ArrayList,String,int,int) | 35 | 25 | 7 | 10 | 12 |
getSinP(ArrayList,int) | 23 | 10 | 5 | 5 | 7 |
getSymbol(String,boolean) | 32 | 11 | 8 | 8 | 10 |
getTermArray(String) | 25 | 6 | 3 | 3 | 4 |
getTree(String,boolean) | 57 | 40 | 7 | 18 | 19 |
interrupt() | 6 | 1 | 2 | 1 | 2 |
removeBr(String) | 32 | 19 | 7 | 10 | 13 |
simplify(Operator) | 19 | 10 | 4 | 7 | 8 |
分析:相比第一次作业,Express的方法变得更多,方法的复杂性也都有所上升。这主要是因为增加了更多的新功能。
property(7) | 类型 | 说明 |
---|---|---|
aa | Operator | 左子树 |
bb | Operator | 右子树 |
type | String | 表明类型,如Single,Add,Mult |
term | term | Single型包含的数据 |
isSingle | boolean | 是否为Single |
certain | boolean | isSingle & term是常量 |
positive | boolean | 表明子类型,如Add分为加法和减法 |
method | Lines | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|---|
addSingle(Term) | 5 | 0 | 1 | 1 | 1 |
getaa() | 2 | 0 | 1 | 1 | 1 |
getbb() | 2 | 0 | 1 | 1 | 1 |
getDerivative() | 7 | 2 | 2 | 1 | 2 |
getNew(Operator,Operator,boolean,String,boolean) | 3 | 0 | 1 | 1 | 1 |
getNumberOpe(BigInteger) | 3 | 0 | 1 | 1 | 1 |
getResult(Operator) | 2 | 0 | 1 | 1 | 1 |
getType() | 2 | 0 | 1 | 1 | 1 |
isCertain() | 2 | 0 | 1 | 1 | 1 |
isPositive() | 2 | 0 | 1 | 1 | 1 |
isSingle() | 2 | 0 | 1 | 1 | 1 |
Operator(Operator,Operator,boolean,String,boolean) | 18 | 7 | 1 | 4 | 7 |
setCertain(boolean) | 2 | 0 | 1 | 1 | 1 |
toString() | 7 | 2 | 2 | 2 | 2 |
method | Lines | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|---|
Add(Operator,Operator,boolean) | 2 | 0 | 1 | 1 | 1 |
getAddArray(Operator,boolean) | 32 | 17 | 3 | 10 | 10 |
getDerivative() | 16 | 5 | 2 | 2 | 3 |
getNew(Operator,Operator,boolean) | 51 | 25 | 12 | 17 | 19 |
getResult(Operator) | 57 | 27 | 5 | 12 | 13 |
toString() | 42 | 22 | 1 | 12 | 12 |
SignedOpe.getOperator() | 2 | 0 | 1 | 1 | 1 |
SignedOpe.getPositive() | 2 | 0 | 1 | 1 | 1 |
SignedOpe.setOperator(Operator) | 2 | 0 | 1 | 1 | 1 |
SignedOpe.setPositive(boolean) | 2 | 0 | 1 | 1 | 1 |
SignedOpe.SignedOpe(Operator,boolean) | 3 | 0 | 1 | 1 | 1 |
分析:相比上次的Add,主要是getNew(产生新Add并化简)的度量值增加,这是因为getNew分担了部分getResult(化简)的功能。设计原因:化简的代码过多,全放在getResult中显得过于冗杂。而getNew和getResult配套使用,外界仅能调用getNew,而调用getNew的同时也调用了getResult,故因而可以把部分getResult中的化简代码放在getNew,使得代码分布不那么过分集中。
值得注意的是,运算符类工厂在产生新的运算符时,调用的是运算符的getNew()方法,然而在getNew()中调用了getResult()的方法。在getResult()中产生新的Add时,若直接调用运算符工厂去生成新的Add,则会调至无限递归死循环而爆栈,因此在getResult()中只能使用Add的默认构造器。
method | Lines | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|---|
getDerivative() | 22 | 6 | 2 | 2 | 4 |
getMultArray(Operator) | 9 | 4 | 2 | 3 | 3 |
getNew(Operator,Operator,boolean) | 23 | 8 | 7 | 8 | 9 |
getPoly(Operator) | 36 | 9 | 5 | 6 | 6 |
getResult(Operator) | 28 | 6 | 2 | 3 | 5 |
getSinglePoly(Operator) | 43 | 15 | 2 | 12 | 14 |
isPolyQualified(Operator) | 31 | 20 | 8 | 8 | 10 |
Mult(Operator,Operator,boolean) | 2 | 0 | 1 | 1 | 1 |
toString() | 27 | 9 | 1 | 6 | 9 |
分析:getNew()的复杂性提高的原因同Add.getNew()。
method | Lines | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|---|
getDerivative() | 17 | 7 | 2 | 5 | 5 |
getNew(Operator,Operator) | 9 | 3 | 1 | 6 | 6 |
getResult(Operator) | 5 | 1 | 2 | 2 | 2 |
Power(Operator,Operator) | 2 | 0 | 1 | 1 | 1 |
toString() | 14 | 3 | 1 | 2 | 3 |
method | Lines | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|---|
getDerivative() | 17 | 4 | 2 | 2 | 4 |
getNew(Operator) | 4 | 0 | 1 | 1 | 1 |
getResult(Operator) | 2 | 0 | 1 | 1 | 1 |
Sin(Operator) | 2 | 0 | 1 | 1 | 1 |
toString() | 9 | 2 | 1 | 2 | 2 |
method | Lines | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|---|
Cos(Operator) | 2 | 0 | 1 | 1 | 1 |
getDerivative() | 17 | 4 | 2 | 2 | 4 |
getNew(Operator) | 4 | 0 | 1 | 1 | 1 |
getResult(Operator) | 2 | 0 | 1 | 1 | 1 |
toString() | 9 | 2 | 1 | 2 | 2 |
property(3) | 类型 | 说明 |
---|---|---|
type | int | 0-常量 1-变量 |
number | BigInteger | 常量的值 |
name | String | 变量名 |
method | Lines | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|---|
addTerm(String) | 4 | 2 | 2 | 1 | 2 |
isCertain() | 2 | 0 | 1 | 1 | 1 |
Term(int,BigInteger,String) | 7 | 0 | 1 | 1 | 1 |
toString() | 7 | 2 | 2 | 2 | 2 |
method | Lines | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|---|
getOpe(Operator,Operator,String) | 20 | 6 | 7 | 7 | 7 |
property(5) | 类型 | 说明 |
---|---|---|
coe | BigInteger | 系数 |
xxIndex | BigInteger | x的指数 |
sinIndex | BigInteger | sin的指数 |
cosIndex | BigInteger | cos的指数 |
qualified | boolean | 是否能参与化简 |
method | Lines | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|---|
equals(Poly) | 10 | 3 | 2 | 5 | 6 |
getCoe() | 2 | 0 | 1 | 1 | 1 |
getCosIndex() | 2 | 0 | 1 | 1 | 1 |
getPolyOpe() | 37 | 12 | 2 | 9 | 9 |
getSinIndex() | 2 | 0 | 1 | 1 | 1 |
getxIndex() | 2 | 0 | 1 | 1 | 1 |
isQualified() | 2 | 0 | 1 | 1 | 1 |
Poly(BigInteger,BigInteger,BigInteger,BigInteger) | 5 | 0 | 1 | 1 | 1 |
setQualified(boolean) | 2 | 0 | 1 | 1 | 1 |
说明:getPolyOpe(根据Poly生成相应的Operator)的复杂度高的原因是在该方法中调用了较多OpeFac(表达式类工厂)的方法。
相比第一次作业,第二次作业增加了sin(x)函数和cos(x)函数,以及表达式因子。
相比第二次作业,第三次作业增加了对sin函数和cos函数内含表达式因子的运算,以及输入格式判断。
第二次作业开始,对第一次作业的某些类的架构进行了部分重构,总体架构保持不变。
首先我针对内聚耦合问题做了一定的优化。在第一次作业,Add类和Mult类的getResult(表达式化简)方法过度依赖Express类中的方法,即我将只会在Add类和Mult类中用到的函数错误地整合到了Express中,导致架构的混乱。所以在第二次作业的小重构中,我将Express类中的方法进行了进一步的拆分和整合,新增了Poly类并移植了部分函数,从而加强了各个类的内聚程度,降低了类与类之间的耦合程度。
另外,对于面向过程方法的代码过长的问题,我也做了一定的优化。我提取了Express.getTree(生成表达式树)中的多处用到的相似代码,并将其整合为一个static方法,优化了代码的运行逻辑,增强了简洁性。
第三次作业开始,对第二次作业的代码未做较大修改,仅仅增量开发了sin、cos函数和表达式格式检查。
经过一定优化后的代码,相比第一次作业来说,虽然代码量增加了,但逻辑性更加清楚,类的功能更加明确。但是,老问题依然存在,类的内聚和耦合性还存在欠缺,代码的冗杂性和可读性都有不足之处。
输入和输出
主要方法 | 说明 |
---|---|
main | 主程序,承担输入与输出功能 |
process | 对输入表达式进行必要处理,如去除空白符 |
formatCheck | 对输入表达式进行初步格式检查 |
处理表达式
主要方法 | 说明 |
---|---|
getTree | 由表达式字符串生成表达式树 |
simplify | 对表达式树进行化简 |
生成和化简表达式
主要方法 | 说明 |
---|---|
getNew | 生成新的表达式类并化简 |
getResult | 表达式化简 |
toString | 字符串输出 |
getDerivative | 求导,并以字符串输出 |
Mult.getResult流程与Add.getResult类似
第一次作业在强测和互测中,共发现2个问题。
第一个问题是设计上的一个bug,具体为不支持x前带有多个符号,若表达式中出现如“+-x”的形式,则表达式树生成时会变成“x”。产生原因是在生成表达树的过程中,忽略了x作为表达式第一项时,可以带多个前置符号的问题。
解决方法:修改代码。
第二个问题是运行超时问题。产生原因是在Express、Add、Mult类的toString方法中,调用了过多的toString方法。
解决方法:在一个方法中,只调用一次同一元素的toString,并设置String变量将其存下来,避免多次递归调用造成程序运行超时,甚至爆栈。
下面对比两次调用的OO度量:
Add.toString() | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|
只调用一次toString | 22 | 1 | 12 | 12 |
多次调用toString | 22 | 1 | 12 | 12 |
可以看到,两者在OO度量上没有变化。
但在程序运行时间上,当表达式变长时,两者的时间差便以倍数增长。
第二次作业在互测中发现一个bug,为BigInteger的报错。
报错信息:
Exception in thread "main" java.lang.NumberFormatException: For input string: "(7)*(7)"
产生原因:化简过程中,对于表达式树的化简不彻底。当输入为(7-sin(x)+sin(x))*(7-sin(x)+sin(x))时,经过程序的化简,得到的结果是(7)*(7),并且得出这个表达式为确定的常数,于是 new BigInteger((7)*(7)) 时产生了上述报错信息。
解决方法:
在Express中添加了一个static方法getBigInteger(),在该方法中首先判断输入的字符串是否满足正则表达式"^\d+$",若满足,才生成BigInteger。
在强测和互测中均未发现bug。
根据自己测试程序的样例,能够找到他人的部分bug。
另外,根据读他人代码,可以针对性的对其存疑部分的代码功能进行特定的测试,以检测正确性。如在第一次作业互评中,见到一位同学根据有限状态机来做表达式的输入,于是我通过阅读他的代码,成功发现了他的一个bug:当输入x * 2时,他的程序不会读入x后的乘法项。
自动评测机由Python编写,由Python的第三方库sympy支持表达式的求导和运算。根据指导书给出的规则,自动随机生成相应的测试样例并自动评测。
本人在总体架构上没有进行大的重构,只在第一次作业到第二次作业过渡的过程中,对Express、Add、Mult类进行了部分功能重构。
相比第一次作业,后面的作业增加了一个新的Poly类,该类整合了Express、Add和Mult类中对表达式项分析的功能。
method | v(G) | method | v(G) |
---|---|---|---|
getDerivative | 2 | getDerivative | 4 |
getMultArray | 3 | getMultArray | 3 |
getNew | 1 | getNew | 9 |
getPoly | 6 | ||
getResult | 6 | getResult | 5 |
getSinglePoly | 14 | ||
isPolyQualified | 10 | ||
Mult | 1 | Mult | 1 |
toString() | 7 | toString | 9 |
表格左边为第一次作业,右边为第三次作业。观察表格可以得出,第三次作业增加的getPoly、getSinglePoly和isPolyQualified方法,使得Mult类的v(G)值大幅增加。但是相应的,Poly类的内聚性很强。在Add类中也要调用Poly类中的相关方法,这些方法原先存在于Mult类中,如此一来便减小了Add类和Mult类的耦合。同时,代码的可读性和简便性也因Poly类而增强。
本单元的作业,让我初步切实体会到了面向对象方法的特性:良好的可拓展性。面向对象的灵活性是以前我熟悉的c语言面向过程方法不可比拟的,因此我认为面向对象确实是非常重要的思考方法。
与此同时,我也认识到了面向对象方法对程序代码架构的依赖性。若程序架构不行,再好的面向对象思想都是无用功。而且,当类与类直接的耦合度过大的时候,面向对象的多个类反而增加了debug的难度,不得不说这几次作业中debug过程着实令我非常痛苦。
但痛苦只是暂时的,完成这个困难的作业并且通过强测互测的成就感,还是挺强的。同时作业对我理解面向对象的帮助还是挺大的,对于架构设计的思考也是不可或缺的学习历程。
原文:https://www.cnblogs.com/Junly7/p/14587171.html