ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基本路径覆盖法实战:控制流图、圈复杂度与独立路径用例设计

基本路径覆盖法实战:控制流图、圈复杂度与独立路径用例设计 做测试这行十来年我经手过不少被测试用例评审卡住的时刻。最常见的一种场景是开发写完一个函数你拿过来盯着看条件分支绕来绕去写了十几个用例跑完覆盖率报告一看分支覆盖率还是卡在 76%剩下那 24% 死活找不到该补哪条数据。这种时候能救命的就是基本路径覆盖法。它不靠灵感不靠拍脑袋而是把代码翻译成一张图算出一个数再机械地把路径一条条推出来——推导过程是可以被人复现和核对的这才是它最值钱的地方。我用它来处理过支付金额计算、风控规则判定、工单状态流转、协议解析这类分支密集的模块。适合谁来学我建议三类人重点看一是正在做软件测试作业、准备各种测试大赛的同学基本路径测试几乎是指定考点圈复杂度和独立路径怎么推是必答题二是转岗做测试一两年、还在靠经验堆用例的同行这个方法能帮你把漏测变成可计算三是准备软件测试面试的人面试官问这个函数要写几条用例你答得出 V(G) 的数值和推导依据跟答看情况吧完全不是一个层次。下面我按实际干活时的顺序讲——先想清楚它的定位再画图、算复杂度、推路径最后落到一张能直接用的用例表上中间把我踩过的坑一并交代。1. 先把基本路径覆盖法摆到正确的位置1.1 它到底解决的是什么问题一句话说明白基本路径覆盖法要保证程序中每条独立路径至少被执行一次。所谓独立路径是指在控制流图里引入至少一条此前所有路径都没走过的新边的那条路径。所有独立路径的线性组合能够覆盖图中每一条边。这句话听起来抽象但它的工程含义很实在只要把独立路径走全了代码里每一行可执行语句、每一个分支出口都至少被碰过一次不会存在从来没被执行到的死角。为什么我要强调可计算这三个字因为等价类划分、边界值分析这类黑盒方法输入域的划分是依赖人的判断的。同样一个年龄字段有人划成 0-17、18-60、61-120有人划成未成年、成年、老年边界取 18 还是 17 常常要吵。而基本路径覆盖法面对的是代码本身判定节点数几个、边有几条谁数都是同一个结果V(G) 算出来是 5 就是 5不存在争议空间。对需要向评审、向甲方、向导师解释为什么这些用例就够了的场合这种确定性非常关键。它也有明显的边界。基本路径覆盖不保证条件组合的完全覆盖。一个包含a b的判定节点在你从 A 到 B 的路径视图里只是一个菱形但实际执行时 a 真 b 真、a 真 b 假、a 假短路是三种不同的执行轨迹。后面我会专门讲这个坑这也是绝大多数人第一次用它时最容易翻车的地方。1.2 与语句覆盖、判定覆盖、条件覆盖的层级关系很多人搞不清这几种覆盖到底谁强谁弱我用一张表把关系理清楚这是面试和作业里都高频出现的对照点。覆盖准则覆盖目标强度用例数量级语句覆盖每行可执行语句至少执行一次最弱少判定覆盖分支覆盖每个判定的真、假两个出口都执行中等中条件覆盖每个复合判定中的每个原子条件取真取假各一次中等中判定/条件覆盖判定出口与原子条件同时满足较强较多条件组合覆盖每个判定中所有原子条件的真值组合都覆盖强多基本路径覆盖每条独立路径至少执行一次强约等于 V(G)从表里能看出一个规律越往下约束越细用例数增长越快。基本路径覆盖的用例数量通常等于圈复杂度 V(G)这个数一般介于判定覆盖和条件组合覆盖之间。它在实践中的甜点位置就在这里——比判定覆盖多了一个组合顺序的维度因为路径是带顺序的节点序列又不至于像条件组合那样指数膨胀。注意不要记成基本路径覆盖一定强于条件组合覆盖。判定内部的原子条件真值组合基本路径覆盖是不管的。两者是不同维度的覆盖不是简单的包含关系。我个人的用法是核心业务逻辑用基本路径覆盖把骨架撑起来然后针对每个包含、||的复合判定单独补一小组条件组合用例做补丁。这样既控制了用例总量又堵住了短路求值留下的缝。1.3 为什么它在测试大赛和面试里总是被点名全国各类软件测试赛事里白盒测试环节基本都会给一段代码要求画出控制流图、计算圈复杂度、列出独立路径、设计测试用例。这四步就是标准流程一步都省不掉。原因是它可评分路径数对不对、V(G) 算得对不对、用例输入能不能对上路径全是客观判分点评委不用去争论这个边界该不该取。面试也是同理。面试官丢一个函数过来问你要设计几条用例他想听的不是一个数字而是你推导的过程。你说这个函数有 3 个判定节点圈复杂度是 4所以至少要 4 条独立路径但其中一条因为条件互斥不可达实际补一条相邻路径替代——这段话的信息量比背十条八股文都大。2. 从代码到控制流图翻译规则与踩坑记录2.1 控制流图里只有两种元素别画复杂了控制流图Control Flow GraphCFG只有两种东西节点和边。节点代表一条或多条顺序执行的语句块边代表控制转移的方向。判定节点含条件的节点有两条或两条以上出边分别标注真和假处理节点只有一条出边。一个容易被忽略的点连续的赋值语句应当合并成一个节点不要一条语句画一个圈。我见过不少同学把int a 1; int b 2; int c a b;画成三个节点结果边数虚高圈复杂度算出来离谱。正确的做法是只要中间没有分支跳转一律合并。这样做的好处是图更紧凑V(G) 的结果也更稳定。还有入口和出口的处理。严格定义下控制流图应当有一个统一入口节点和一个统一出口节点。函数里有多个return时可以让所有 return 语句都连向同一个出口节点。多出来的这两条边虽然看起来是凑数但它恰恰保证了 E - N 2 这个公式成立——公式的推导前提就是图是连通的、有唯一入口出口。2.2 常见语句结构的画法对照不同的语句结构画法不同我按实际遇到频率排序整理成一张对照表直接照着套就行。语句结构节点处理方式边数变化判定节点计数顺序语句多条合并为一个节点1 条入 1 条出0if 无 else条件一个判定节点真分支一个处理节点增加 2 条边1if-else条件一个判定节点两个分支各一个节点汇合处补一个节点增加 4 条边1if-else if-else 链每个 else if 都是一个独立判定节点逐级递增每个 else if 各 1while 循环条件判定节点 循环体节点回边指向判定增加 3 条边1for 循环初始化合并入前节点条件为判定节点步进合并入循环体同 while1switch-case每个 case 视为一个判定出口分支数决定边数case 数 - 1try-catchcatch 块视为一个特殊分支正常流不经过它增加 2 条边可视为 1提前 return直接连出口节点增加 1 条边0这张表是我自己总结的用熟了以后画图速度能快一倍以上。特别提醒else if链——很多人只算一个判定节点其实if (a) ... else if (b) ... else if (c) ...里 b 和 c 的判断都是独立的判定节点每个都要计数。这是圈复杂度算错的头号原因。2.3 复合判定怎么处理这里有个大坑这是我最想强调的一点。if (a 0 b 0)这样的复合条件在标准的基本路径覆盖流程里通常被当作单个判定节点处理。这样处理是 McCabe 原始方法的规定图更简洁路径推导更利索。但你要清楚这样做的代价当成单节点后a0 b0这个菱形只区分整体真和整体假而a5, b-1和a-1, b5是两条不同的原子条件组合却都走整体假这条边。基本路径覆盖不会要求你为这两种情况各写用例。怎么办我的做法是两步走第一步绘图时按单节点处理算出基础路径集把主干逻辑的用例先定下来。第二步针对每个包含逻辑运算符的判定单独列出它的原子条件和短路顺序补充条件组合用例。比如a 0 b 0我会额外补三组数据(正, 正)、(正, 非正)、(非正, 任意)——最后一组是验证短路确保 a 为假时 b 根本不会被求值如果 b 是一个有副作用的函数调用这一点可能引发真实缺陷。提示如果被测代码的复合条件里含有函数调用、属性访问这类可能有副作用或可能抛异常的表达式短路求值的顺序必须单独设计用例基本路径覆盖管不到这一层。3. 圈复杂度那个能帮你砍函数的数字3.1 三种算法结果必须一致圈复杂度 V(G) 有三种等价的计算方式我用同一个控制流图验算给你看这三种方法一定要都能算出来因为实际工作中不同场合用哪个更方便差别很大。V(G) E - N 2 E 是边数N 是节点数 V(G) P 1 P 是判定节点数 V(G) 封闭区域数 1 数图中被边围起来的区域加外部区域三个公式算出来的值必须完全相同。如果不同说明你的图有问题——大概率是边漏画了、节点重复了、或者图不连通。为什么判定节点数加一就等于路径数直觉解释是每增加一个判定节点就相当于多了一次二选一的机会独立路径的数量就多一条。起始的时候没有任何判定只有一条路径从入口直通出口所以是 0 1 1。每加一个判定就加一条。我在带新人的时候经常让他们用三种方法交叉验算一旦出现不一致图的错误基本立刻暴露。这个习惯能省下大量返工时间。3.2 McCabe 给出的复杂度分级参考算出来的数字本身没有意义有意义的是它对应的风险等级。McCabe 当年给过一个经验划分我在项目里一直沿用虽然不同团队标准略有出入但量级是通用的。圈复杂度范围风险等级处理建议1 - 10低结构简单用例设计直接做代码可读性可接受11 - 20中需要仔细拆解路径建议同时补条件组合用例21 - 50高可维护性明显下降建议重构拆分函数后再测试大于 50极高基本无法完整测试重构是第一优先级我踩过一次典型的坑一个订单金额计算函数圈复杂度算下来是 34。当时我硬着头皮推了 35 条路径写用例写到怀疑人生而且推完之后自己都不敢确认有没有漏。后来跟开发沟通把优惠计算、税费计算、运费计算拆成三个函数每个 V(G) 都在 8 以内测试用例总数反而从 35 条降到了 19 条覆盖率还更高。这件事让我形成一个固定动作动手推路径之前先算 V(G)超过 15 就先去找开发聊拆分。测试的成本不是线性增长的是指数级增长的。3.3 循环为什么只按一次算这是作业里最常被扣分的点必须讲清楚。while循环理论上可以执行 0 次、1 次、2 次……无穷多次如果按实际执行次数算路径路径数是无穷的方法就没法用了。基本路径覆盖的处理方式是把循环拉直循环体只在图中画一次回边只画一条。这样循环结构在图上就是一个普通的判定节点贡献 1 个 V(G)。测试用例设计时循环相关的路径通常取两个数据点——零次循环不进循环体和一次循环进循环体执行一次。那循环的边界问题谁管交给边界值分析。比如循环条件是i 10我会额外补 9 次、10 次、11 次这几个数据点。基本路径覆盖负责结构的完整性边界值负责边界附近的正确性两者配合才能把循环测透。4. 独立路径推导基线加翻转的机械流程4.1 第一条基线路径怎么挑基线路径也叫主路径的选择没有硬性规定但有条经验优先选节点最多、覆盖最广的那条路径。这样后续每翻转一个判定节点得到的路径跟基线的差异最小容易验证是否引入新边。具体操作是从入口出发一路选最主要的那个分支走到出口。我一般会选正常业务流程那条因为它最能反映代码的主体逻辑。选定后把这条路径的节点序列完整写下来标上边。基线路径必须是一条真实可达的路径——每一步的边都真实存在不能凭空造。初学时有人会把图上所有节点串一遍当基线这是错的那样得到的不是有效路径。4.2 翻转一个判定节点得到一条新路径得到基线之后每次只改变一个判定节点的出口方向保持其余部分与基线一致就得到一条新的独立路径。重复这个过程直到所有判定节点都被翻转过一次。最终路径总数恰好是 P 1也就是 V(G)。判断一条路径是否独立的标准是它至少包含一条此前所有路径都没走过的边。如果一条新路径的所有边在前面的路径里都出现过那它就不算独立路径说明你翻转的方式有问题或者这个判定节点的两条分支在图上已被其他路径覆盖。实际操作中会出现一种情况翻转某个判定节点后得到的路径不可达因为该分支的前置条件与该分支内部的逻辑冲突。这时候要换一条替代路径比如翻转另一个判定节点或者组合翻转两个节点。这种情况在业务代码里并不罕见遇到时不要慌把它记录下来在用例说明里写清楚该分支不可达即可——这本身就是一条有价值的测试结论。4.3 独立路径的线性无关到底是什么意思教材上会写独立路径是线性无关的路径集合这个说法来自图论里的路径向量空间。每条路径可以表示为一个向量向量的分量对应图中的每一条边走过该边记 1没走过记 0。独立路径的意思是这些向量线性无关其最大数量恰好是 E - N 2。实际工作中不需要真的去解线性方程组你只需要记住一件事每条独立路径至少要引入一条新边。这个判据比线性无关更直观检查起来也快。我审别人的路径推导时就用这一条一条条核对新增边几分钟就能看完。5. 完整实操三角形类型判断函数的全套推导光讲规则没意思我拿一个经典函数从头到尾走一遍。这段代码在各种测试作业和大赛里出现过无数次因为它分支够多、结构够典型又不涉及业务细节。5.1 被测代码与需求说明int triangleType(int a, int b, int c) { if (a 0 || b 0 || c 0) { return 0; // 边长非法 } if (a b c || a c b || b c a) { return 0; // 三边构不成三角形 } if (a b b c) { return 1; // 等边三角形 } if (a b || b c || a c) { return 2; // 等腰三角形 } return 3; // 一般三角形 }需求很简单输入三个整数边长返回 0 表示非法输入或构不成三角形1 表示等边2 表示等腰3 表示一般三角形。返回值是整数编码而不是枚举这是后面会造成一点小麻烦的地方先记着。5.2 画图、算复杂度、列路径按前面讲的规则画图。四个 if 各作为一个判定节点两个return 0合并指向同一出口其余 return 也连出口。节点清单判定 N1a0||b0||c0、处理 N2return 0、判定 N3abc||...、判定 N4abbc、处理 N5return 1、判定 N6ab||bc||ac、处理 N7return 2、处理 N8return 3、出口 N9。总共 N 9。边清单N1 → N2真、N1 → N3假N2 → N9N3 → N2真、N3 → N4假N4 → N5真、N4 → N6假N5 → N9N6 → N7真、N6 → N8假N7 → N9N8 → N9E 2 1 2 2 1 2 1 1 12 条边。三种方法验算V(G) E - N 2 12 - 9 2 5V(G) P 1 4 1 5数封闭区域图中有 4 个独立的环状区域加外部区域 5三者完全一致说明图没画错。接下来推独立路径。选节点最多的那条作基线P1基线N1 → N3 → N4 → N6 → N8 → N9对应一般三角形。然后逐个翻转判定节点P2翻转 N1走真分支 → N1 → N2 → N9。边长非法P3翻转 N3走真分支 → N1 → N3 → N2 → N9。构不成三角形P4翻转 N4走真分支 → N1 → N3 → N4 → N5 → N9。等边P5翻转 N6走真分支 → N1 → N3 → N4 → N6 → N7 → N9。等腰五条路径每条都引入了至少一条新边P2 引入 N1→N2 和 N2→N9P3 引入 N3→N2P4 引入 N4→N5 和 N5→N9P5 引入 N6→N7 和 N7→N9。全部满足独立性判据路径集合完整。5.3 把路径翻译成可执行的测试用例路径推出来了接下来是选具体数据。这里必须结合等价类和边界值因为路径只告诉你要走哪条路不告诉你输入什么数。我按这个思路填了一张表。用例编号覆盖路径输入 a, b, c预期返回选择依据TC-01P13, 4, 53一般三角形代表值TC-02P21, 2, 00边长下边界取 0 触发非法TC-03P2-1, 5, 50负数边界验证左侧边界TC-04P31, 2, 30恰好两边之和等于第三边经典边界TC-05P31, 1, 100远小于第三边等价类补强TC-06P43, 3, 31等边代表值TC-07P4100, 100, 1001大值等边验证无溢出问题TC-08P53, 3, 52等腰代表值且非等边TC-09P55, 3, 32交换位置验证边顺序无关性TC-10P12147483647, 2147483647, 12大数相加溢出探测注意 TC-10 这条它覆盖的还是 P1 之外的路径实际上走的是 P5但它存在的意义不是补路径而是探测整数溢出。三个 int 相加可能溢出这是等价类和边界值思维的贡献路径法本身发现不了。5.4 一个实测中发现的返回值陷阱在我第一次跑这套用例的时候遇到一个尴尬情况TC-02、TC-03、TC-04、TC-05 这四条用例预期返回都是 0。全覆盖通过之后我依然无法回答一个问题——程序到底是走了 N1→N2 这条路还是走了 N3→N2 这条路这就是前面提到的返回值编码问题。两个不同的缺陷位置可能返回同一个值用例通过了但你不知道它从哪条路过来的。解决办法有三个一是让开发在两条 return 前打不同的日志测试时开日志跑一遍人工确认路径。二是用覆盖率工具JaCoCo、Coverage.py、gcov 都行看分支覆盖报告从报告上直接读哪条边被走过。三是更彻底的做法推动开发把返回值拆开非法边长返回 -1构不成三角形返回 -2语义清晰测试也能自证。我通常三管齐下日志用于调试期快速定位覆盖率工具用于最终确认推动语义拆分是长期治理。这件事给我的教训是基本路径覆盖推出来的路径跟用例能不能验证这条路径是两件事。设计用例的时候必须多问一句跑完之后我怎么知道它走的是这条路。6. 常见问题速查与排查技巧6.1 问题速查表现象可能原因排查动作三种方法算出的 V(G) 不一致图漏边、节点未合并、图不连通逐个节点核对入边出边检查是否有孤立节点翻转判定节点后路径不可达分支条件与实际逻辑互斥记录为不可达路径改用其他判定节点翻转路径数对上了但覆盖率不达标复合条件短路未被覆盖补条件组合用例别指望路径法覆盖原子条件用例跑通了但无法确认路径多个分支返回同一结果加日志、用覆盖率工具、推动返回值语义化else if链算出的 V(G) 偏小漏算了后续判定节点每个 else if 单独计数函数 V(G) 超过 30 且还在涨函数职责过多先谈重构拆分别硬测switch 分支 V(G) 对不上case 数量与判定数换算错误有 default 时 P case 数无 default 时 P case 数 - 1这张表里的每一条我都在真实项目里遇到过尤其是第三条和第六条出现频率极高。6.2 覆盖率工具怎么选我给你三个场景工具选择不必纠结按技术栈走就行。我列一下自己用过且稳定的组合。Java 项目JaCoCo 是首选跟 Maven、Gradle 集成都很顺报告里直接给出每个方法的分支覆盖率还能配合 SonarQube 看圈复杂度的历史趋势。Cobertura 是老牌工具但现在新项目我基本不用了。Python 项目coverage.py 做行覆盖和分支覆盖--branch参数打开分支覆盖。要单独看圈复杂度用 radon 这个库radon cc your_module.py -a一条命令给出每个函数的复杂度评级A 到 F 六个档我一般把 F 档的函数直接列进重构清单。C 和 C 项目gcov 加 lcov编译时加--coverage参数跑完用例生成.gcda文件用 lcov 转成 HTML 报告每个分支的命中次数一目了然。嵌入式场景我会用 gcov 的交叉编译版本配合目标板上的文件回传。工具的作用不是替代推导而是验证推导。我自己推完路径、写完用例之后一定会跑一遍覆盖率报告做交叉核对。如果报告显示某条边命中次数是 0要么是我的路径推错了要么是用例数据选错了两种情况都必须查清楚。6.3 几条带血的实操心得第一条心得先算数再画图最后推路径。很多人上来就画图画完发现 V(G) 是 28进退两难。反过来的顺序是先扫一眼代码数一数判定节点数量估出 V(G) 的量级超过 15 就去找开发聊拆分谈完了再画图。这个顺序调整能省下大量无效劳动。第二条心得路径编号一定要留着跟用例一一对应。我见过团队里用例写得挺好但没人知道每条用例对应哪条路径半年后需求变更代码加了个分支所有人都不知道该补哪条用例。我的做法是在用例管理工具里给每条用例打标签比如PATH-P3代码一改对着标签查漏效率完全不一样。第三条心得不可达路径要写进测试报告。翻转判定节点后得到一条不可达路径这不是失败这是一个发现——它说明代码里存在永远走不到的分支可能是开发写错的死代码也可能是条件设计冗余。把这类发现单独列一节评审时会非常有说服力。第四条心得别拿基本路径覆盖去测数据密集型逻辑。如果一个函数的主体是几十行的数值计算只有两三个 if那它的问题不在路径上在数值精度和边界上。这种情况老老实实用等价类和边界值硬套基本路径覆盖属于用力用错地方。第五条心得复合条件的短路顺序要单独写用例尤其是表达式含副作用时。我遇到过一个真实缺陷if (user ! null user.getRole().equals(admin))在某次重构后顺序被调换导致空指针异常。这类问题路径覆盖和分支覆盖都发现不了只能靠针对短路顺序设计的专项用例。7. 和其他方法怎么配合以及面试里怎么答7.1 我的标准组合拳单独用基本路径覆盖效果只能算及格。我在实际项目里的组合是这样的第一步用基本路径覆盖把函数的结构骨架摸清楚得到 V(G) 条主干路径这是用例的柱石。第二步对每个判定节点里的复合条件用条件组合覆盖补短路用例数量控制在每个判定不超过三组。第三步对每个输入参数用等价类划分和边界值分析补充数据取值重点照顾 0、负数、最大最小值、空值、超长字符串这些。第四步对整个业务流程用场景法串起来做成端到端的链路用例。这四步下来用例总数大概是纯路径法的两到三倍但覆盖深度完全不是一个级别。而且分工明确路径法负责代码结构完整性条件组合负责原子条件正确性等价类边界值负责数据边界正确性场景法负责业务流转正确性。四条线各管一摊谁都不越界评审的时候也容易解释。7.2 和接口测试、AI 生成用例的衔接现在很多团队在做接口测试用例的自动生成还有用 AI 根据 PRD 直接产出功能用例。基本路径覆盖在这类流程里依然有用而且用法很有意思。我把控制流图的路径表整理成结构化文本作为提示的上下文喂给模型让它针对每条路径生成具体的输入数据组合和断言。这样做比直接丢一段 PRD 给模型效果稳定得多因为路径表已经把结构信息固化了模型只需要做数据填充不需要猜代码逻辑。实测下来生成的用例命中率比纯文本描述高出不少人工复核的工作量能砍掉一半以上。接口测试同理。把服务端的方法调用链画成控制流图或者在网关层把请求路由的分支画出来同样能算出 V(G) 并推出路径。区别只是画图的层级从语句级升到了服务级思路完全一致。7.3 面试答题的完整模板最后说面试。被问到给你这个函数你打算设计多少条测试用例的时候我建议按这个顺序答先说方法选择。判断这是白盒场景函数分支清晰适合用基本路径覆盖法确定主干用例数量再用等价类和边界值补充数据取值。再说推导过程。控制流图上有几个判定节点节点数多少、边数多少用 E - N 2 和 P 1 两种方式交叉验证得到圈复杂度。然后给结论。圈复杂度是几独立路径就是几条其中某条因为条件互斥不可达实际有效路径是几条不可达那条要单独记录。最后补一句覆盖局限。说明基本路径覆盖不覆盖原子条件的真值组合所以针对判定里的复合条件会额外补条件组合用例并解释为什么——短路求值的顺序可能隐藏缺陷。这四段话说完基本就把面试官想听的都覆盖了。你要能把第五节的三角形例子当场手推一遍那基本就稳了。额外提一句如果面试官问一个测试用例最多要检查几项这跟基本路径覆盖没有直接关系但关联很深。我的答案是一条用例最好只验证一条路径的一个输出结果因为多输出断言会让失败归因变得困难。路径法的思路天然支持这一点——一条路径一个用例失败时立刻知道是哪条路出了问题。7.4 关于这项工作能干多久的一点私人体会常有人问软件测试这行能干到多少岁我自己的看法是工具会换、框架会换但把复杂逻辑拆成可验证的有限集合这个能力是越老越值钱的。基本路径覆盖法诞生几十年了工具从手写报告变成了 SonarQube 一键出图内核一点没变。新人拼的是工具熟练度老手拼的是拆解能力和判断力——什么时候该推路径什么时候该去推动重构什么时候该说这函数测不了必须拆这些判断没有捷径只能靠一个个项目堆出来。最后分享一个我一直在用的小习惯每次写完一套用例花五分钟倒过来问自己一句如果我要故意让这些用例漏掉一个缺陷我会怎么改代码。这个问题经常能问出新的用例来。基本路径覆盖给了你一个有限的路径集合但代码里的坑从来不只在路径上返回值、异常、并发、副作用都是它够不着的地方。把这套方法当起点别当终点。
返回列表