[{"data":1,"prerenderedAt":1546},["ShallowReactive",2],{"post:\u002Fposts\u002Fswdesign\u002Fsw-design-notes-25":3,"posts:ordered":1252},{"id":4,"title":5,"body":6,"category":1235,"cover":1236,"date":1237,"description":1238,"extension":1239,"featured":1240,"meta":1241,"navigation":246,"path":1242,"seo":1243,"series":1244,"seriesOrder":1245,"stem":1246,"tags":1247,"__hash__":1251},"posts\u002Fposts\u002FswDesign\u002Fsw-design-notes-25.md","软件分析与设计笔记 25：代码重构：坏味道与手法",{"type":7,"value":8,"toc":1210},"minimark",[9,16,25,30,33,36,108,118,127,131,136,139,145,152,158,162,165,171,175,184,191,195,198,220,223,226,304,314,318,325,332,416,430,436,443,496,500,503,510,513,528,576,582,586,589,596,631,634,640,644,650,654,660,666,672,744,750,753,757,761,768,781,784,794,798,801,815,824,828,831,851,861,870,874,894,901,904,931,934,1021,1024,1206],[10,11,12],"blockquote",{},[13,14,15],"p",{},"这是我在学习软件分析与设计时整理的系列笔记。概念部分是我按自己的理解写的，示例与图示都是自己编、自己画的，重构手法的名称参考的是公开的重构目录类资料。理解有偏差的地方欢迎指出。",[10,17,18],{},[13,19,20,24],{},[21,22,23],"strong",{},"贯穿案例","：片区家电维修派单系统。24 篇讲怎么判断设计好坏，这一篇讲发现不好之后怎么动手。",[26,27,29],"h2",{"id":28},"_0-一段能跑但不敢改的代码","0 一段「能跑但不敢改」的代码",[13,31,32],{},"有这么一类代码：功能正常、测试通过、线上没出过问题 —— 但没人愿意改它。",[13,34,35],{},"本例里有个现成的候选：客服端的派单逻辑。",[37,38,43],"pre",{"className":39,"code":40,"language":41,"meta":42,"style":42},"language-java shiki shiki-themes github-light github-dark","public void dispatch(String orderNo, String engineerId, String operatorId,\n                     String remark, boolean urgent, boolean needPhoto,\n                     String photoUrl, int priority) {\n    \u002F\u002F 280 行\n}\n","java","",[44,45,46,66,83,95,102],"code",{"__ignoreMap":42},[47,48,51,55,58,62],"span",{"class":49,"line":50},"line",1,[47,52,54],{"class":53},"szBVR","public",[47,56,57],{"class":53}," void",[47,59,61],{"class":60},"sScJk"," dispatch",[47,63,65],{"class":64},"sVt8B","(String orderNo, String engineerId, String operatorId,\n",[47,67,69,72,75,78,80],{"class":49,"line":68},2,[47,70,71],{"class":64},"                     String remark, ",[47,73,74],{"class":53},"boolean",[47,76,77],{"class":64}," urgent, ",[47,79,74],{"class":53},[47,81,82],{"class":64}," needPhoto,\n",[47,84,86,89,92],{"class":49,"line":85},3,[47,87,88],{"class":64},"                     String photoUrl, ",[47,90,91],{"class":53},"int",[47,93,94],{"class":64}," priority) {\n",[47,96,98],{"class":49,"line":97},4,[47,99,101],{"class":100},"sJ8bj","    \u002F\u002F 280 行\n",[47,103,105],{"class":49,"line":104},5,[47,106,107],{"class":64},"}\n",[13,109,110,111,114,115],{},"改之前要看半小时；改完之后不确定有没有影响别的地方；于是每次新需求都绕着它走，在外面再加一个 ",[44,112,113],{},"if","。",[21,116,117],{},"这种代码不是坏了，而是正在变坏。",[13,119,120,123,124],{},[21,121,122],{},"重构（refactoring）"," 就是针对这种状态的：",[21,125,126],{},"在不改变外部行为的前提下，改善内部结构。",[26,128,130],{"id":129},"_1-什么是重构","1 什么是重构",[132,133,135],"h3",{"id":134},"_11-两个关键词","1.1 两个关键词",[13,137,138],{},"这个定义里有两个关键词。",[13,140,141,144],{},[21,142,143],{},"「不改变外部行为」"," —— 这一点决定了重构和「改需求」「修 bug」是不同的事。重构完成后，系统的输入输出不应该有任何变化，测试应该全部照旧通过。",[13,146,147,148,151],{},"本例里这条特别关键：如果重构派单逻辑时顺手「优化」了派单规则（比如改成优先派给昨天没派过的人），那就不是重构，而是",[21,149,150],{},"改需求"," —— 它必须经过业务确认、要更新需求文档、要重新验收。",[13,153,154,157],{},[21,155,156],{},"「改善内部结构」"," —— 改善的标准就是笔记 24 讲的：内聚更高、耦合更低、更容易理解和修改。",[132,159,161],{"id":160},"_12-为什么给手法起名字","1.2 为什么给手法起名字",[13,163,164],{},"重构手法的名称大多很直白，比如「提取方法」「上移方法」「下移方法」。它们不是新东西，只是给「把代码动一下但不改行为」这件事起了名字 —— 有了名字，团队之间才能说「这里做个 Pull Up」而不用解释半天。",[13,166,167,170],{},[21,168,169],{},"命名本身就是一种协作工具","。它让「我觉得这里该改」变成「这里该做 Extract Method」，后者可以被讨论、被反对、被执行。",[26,172,174],{"id":173},"_2-四种坏味道与对应手法","2 四种坏味道与对应手法",[13,176,177,180,181],{},[21,178,179],{},"坏味道（code smell）"," 指的是代码里那种「不用运行就能感觉不对劲」的特征。它不等于 bug：",[21,182,183],{},"坏味道是提示，不是错误。",[13,185,186],{},[187,188],"img",{"alt":189,"src":190},"四种常见的代码坏味道与对应的重构手法：长参数列表、依恋情结、重复代码、被拒绝的馈赠，每种都用派单宝案例里的具体代码举例并给出对应手法；底部给出重构的前提与时机","\u002Fimages\u002Fsw-design-notes-25-refactoring-smells.svg",[132,192,194],{"id":193},"_21-长参数列表long-parameter-list","2.1 长参数列表（long parameter list）",[13,196,197],{},"一个方法要传七八个参数，调用方每次都要数顺序。",[13,199,200,201,204,205,208,209,212,213,208,216,219],{},"本例开头那个 ",[44,202,203],{},"dispatch()"," 就是现成的例子：八个参数，其中 ",[44,206,207],{},"needPhoto"," 和 ",[44,210,211],{},"photoUrl"," 是一对（有图才有 URL），",[44,214,215],{},"urgent",[44,217,218],{},"priority"," 又重复表达同一件事。",[13,221,222],{},"手法：把这几个参数打包成一个对象（Introduce Parameter Object），或者把一部分逻辑挪回它数据所属的类（Extract Class）。",[13,224,225],{},"改完是这样：",[37,227,229],{"className":39,"code":228,"language":41,"meta":42,"style":42},"public void dispatch(DispatchRequest request) { ... }\n\nclass DispatchRequest {\n    String orderNo;\n    String engineerId;\n    String operatorId;\n    String remark;\n    boolean urgent;\n    String photoUrl;   \u002F\u002F 有图才有值\n}\n",[44,230,231,242,248,259,264,269,275,281,290,299],{"__ignoreMap":42},[47,232,233,235,237,239],{"class":49,"line":50},[47,234,54],{"class":53},[47,236,57],{"class":53},[47,238,61],{"class":60},[47,240,241],{"class":64},"(DispatchRequest request) { ... }\n",[47,243,244],{"class":49,"line":68},[47,245,247],{"emptyLinePlaceholder":246},true,"\n",[47,249,250,253,256],{"class":49,"line":85},[47,251,252],{"class":53},"class",[47,254,255],{"class":60}," DispatchRequest",[47,257,258],{"class":64}," {\n",[47,260,261],{"class":49,"line":97},[47,262,263],{"class":64},"    String orderNo;\n",[47,265,266],{"class":49,"line":104},[47,267,268],{"class":64},"    String engineerId;\n",[47,270,272],{"class":49,"line":271},6,[47,273,274],{"class":64},"    String operatorId;\n",[47,276,278],{"class":49,"line":277},7,[47,279,280],{"class":64},"    String remark;\n",[47,282,284,287],{"class":49,"line":283},8,[47,285,286],{"class":53},"    boolean",[47,288,289],{"class":64}," urgent;\n",[47,291,293,296],{"class":49,"line":292},9,[47,294,295],{"class":64},"    String photoUrl;   ",[47,297,298],{"class":100},"\u002F\u002F 有图才有值\n",[47,300,302],{"class":49,"line":301},10,[47,303,107],{"class":64},[13,305,306,309,310,313],{},[21,307,308],{},"参数对象还有个额外好处：它能承载校验。","「有图必须有 URL」这条规则现在可以写进 ",[44,311,312],{},"DispatchRequest"," 的构造函数里，而不是散在每个调用方。",[132,315,317],{"id":316},"_22-依恋情结feature-envy","2.2 依恋情结（feature envy）",[13,319,320,321,324],{},"某个方法对",[21,322,323],{},"别的类","的数据比对自己类的数据更感兴趣，反复调用对方的取值方法。",[13,326,327,328,331],{},"本例里一个典型：如果 ",[44,329,330],{},":RequestPartsController"," 里写了这样一段 ——",[37,333,335],{"className":39,"code":334,"language":41,"meta":42,"style":42},"\u002F\u002F 在控制器里\nboolean canRequest = order.getStatus().equals(\"IN_PROGRESS\")\n                  && order.getEngineerId().equals(engineer.getId())\n                  && stock.getOnHandQty() >= qty;\n",[44,336,337,342,374,397],{"__ignoreMap":42},[47,338,339],{"class":49,"line":50},[47,340,341],{"class":100},"\u002F\u002F 在控制器里\n",[47,343,344,346,349,352,355,358,361,364,367,371],{"class":49,"line":68},[47,345,74],{"class":53},[47,347,348],{"class":64}," canRequest ",[47,350,351],{"class":53},"=",[47,353,354],{"class":64}," order.",[47,356,357],{"class":60},"getStatus",[47,359,360],{"class":64},"().",[47,362,363],{"class":60},"equals",[47,365,366],{"class":64},"(",[47,368,370],{"class":369},"sZZnC","\"IN_PROGRESS\"",[47,372,373],{"class":64},")\n",[47,375,376,379,381,384,386,388,391,394],{"class":49,"line":85},[47,377,378],{"class":53},"                  &&",[47,380,354],{"class":64},[47,382,383],{"class":60},"getEngineerId",[47,385,360],{"class":64},[47,387,363],{"class":60},[47,389,390],{"class":64},"(engineer.",[47,392,393],{"class":60},"getId",[47,395,396],{"class":64},"())\n",[47,398,399,401,404,407,410,413],{"class":49,"line":97},[47,400,378],{"class":53},[47,402,403],{"class":64}," stock.",[47,405,406],{"class":60},"getOnHandQty",[47,408,409],{"class":64},"() ",[47,411,412],{"class":53},">=",[47,414,415],{"class":64}," qty;\n",[13,417,418,419,422,423,426,427],{},"它连续取了 ",[44,420,421],{},"order"," 的三个方法、",[44,424,425],{},"stock"," 的一个方法，而自己一个字段都没用 —— ",[21,428,429],{},"这就是依恋情结。",[13,431,432,433],{},"手法：把方法搬到它真正依赖的那个类里（Move Method）。这条直接对应笔记 20 的原则：",[21,434,435],{},"行为应该由持有状态的对象提供。",[13,437,438,439,442],{},"改法是让 ",[44,440,441],{},"WorkOrder"," 自己回答：",[37,444,446],{"className":39,"code":445,"language":41,"meta":42,"style":42},"\u002F\u002F 在 WorkOrder 里\nboolean canRequestParts(String engineerId) {\n    return status == IN_PROGRESS && this.engineerId.equals(engineerId);\n}\n",[44,447,448,453,463,492],{"__ignoreMap":42},[47,449,450],{"class":49,"line":50},[47,451,452],{"class":100},"\u002F\u002F 在 WorkOrder 里\n",[47,454,455,457,460],{"class":49,"line":68},[47,456,74],{"class":53},[47,458,459],{"class":60}," canRequestParts",[47,461,462],{"class":64},"(String engineerId) {\n",[47,464,465,468,471,474,477,480,484,487,489],{"class":49,"line":85},[47,466,467],{"class":53},"    return",[47,469,470],{"class":64}," status ",[47,472,473],{"class":53},"==",[47,475,476],{"class":64}," IN_PROGRESS ",[47,478,479],{"class":53},"&&",[47,481,483],{"class":482},"sj4cs"," this",[47,485,486],{"class":64},".engineerId.",[47,488,363],{"class":60},[47,490,491],{"class":64},"(engineerId);\n",[47,493,494],{"class":49,"line":97},[47,495,107],{"class":64},[132,497,499],{"id":498},"_23-重复代码duplicate-code","2.3 重复代码（duplicate code）",[13,501,502],{},"同一段逻辑出现在两三个类里，改动时只改了其中一处 —— 那没改的地方就永久不一致了。",[13,504,505,506,509],{},"本例里一个真实的风险点：",[21,507,508],{},"「工单状态是否允许某操作」这件事","，在客服端、工程师 App、仓管端各写了一遍。",[13,511,512],{},"手法：相同的部分提到父类（Pull Up），或抽成独立方法（Extract Method）。",[13,514,515,516,527],{},"但要小心 —— 24 篇 §4.2 已经提醒过：",[21,517,518,519,522,523,526],{},"「校验工单状态」在派单里要求 ",[44,520,521],{},"CREATED","、在申请配件里要求 ",[44,524,525],{},"IN_PROGRESS","，它们看起来一样其实是两个规则","。所以这里该抽的不是「一个通用校验方法」，而是「状态机的判定能力」：",[37,529,531],{"className":39,"code":530,"language":41,"meta":42,"style":42},"\u002F\u002F 在 WorkOrder 里（不是抽成一个通用工具类）\nboolean canDispatch()  { return status == CREATED; }\nboolean canRequestParts() { return status == IN_PROGRESS; }\n",[44,532,533,538,558],{"__ignoreMap":42},[47,534,535],{"class":49,"line":50},[47,536,537],{"class":100},"\u002F\u002F 在 WorkOrder 里（不是抽成一个通用工具类）\n",[47,539,540,542,545,548,551,553,555],{"class":49,"line":68},[47,541,74],{"class":53},[47,543,544],{"class":60}," canDispatch",[47,546,547],{"class":64},"()  { ",[47,549,550],{"class":53},"return",[47,552,470],{"class":64},[47,554,473],{"class":53},[47,556,557],{"class":64}," CREATED; }\n",[47,559,560,562,564,567,569,571,573],{"class":49,"line":85},[47,561,74],{"class":53},[47,563,459],{"class":60},[47,565,566],{"class":64},"() { ",[47,568,550],{"class":53},[47,570,470],{"class":64},[47,572,473],{"class":53},[47,574,575],{"class":64}," IN_PROGRESS; }\n",[13,577,578,581],{},[21,579,580],{},"共同点抽出来的是「能力归属」，不是「代码形状」","。这两者的差别，就是「好重构」和「制造耦合」的差别。",[132,583,585],{"id":584},"_24-被拒绝的馈赠refused-bequest","2.4 被拒绝的馈赠（refused bequest）",[13,587,588],{},"子类继承了一堆自己用不上的父类成员，只能空着或者抛异常。",[13,590,591,592,595],{},"这正是 ",[21,593,594],{},"27 篇考试里那道重构题","考的坏味道。两个真题例子都是这个形态：",[597,598,599,618],"ul",{},[600,601,602,605,606,609,610,613,614,617],"li",{},[44,603,604],{},"SubContractedProject extends Project"," —— ",[44,607,608],{},"addMember()"," 被重写成空、",[44,611,612],{},"removeMember()"," 直接 ",[44,615,616],{},"return null","；",[600,619,620,605,623,626,627,630],{},[44,621,622],{},"OutsourcedEngineer extends Engineer",[44,624,625],{},"accept()","、",[44,628,629],{},"requestParts()"," 同样被重写成空（27 篇 §10.2 举的例子）。",[13,632,633],{},"手法：把用不上的成员下推到真正需要它的子类（Pull Down），或者干脆改用组合。",[13,635,636,639],{},[21,637,638],{},"判断只需要看一件事：子类有没有把父类的方法重写成空、或直接 return null","。有，就是被拒绝的馈赠。",[132,641,643],{"id":642},"_25-记住习惯比记住清单更重要","2.5 记住习惯比记住清单更重要",[13,645,646,647,114],{},"这四个只是最常见的一批。真正有用的不是记住全部，而是",[21,648,649],{},"养成看到这类特征就停下来想一秒的习惯",[26,651,653],{"id":652},"_3-为什么动手前必须先有测试","3 为什么动手前必须先有测试",[13,655,656,657],{},"重构的风险只有一个，但很实在，通常叫回归风险（regression risk）：",[21,658,659],{},"你可能会在「改善结构」的同时不小心改变了行为，而且自己没发现。",[13,661,662,663],{},"如果有一组能快速运行的测试，这个过程就变得安全：改一步、跑一次测试、通过了继续，不通过就回退。这就是为什么笔记 14 讲的单元测试在这里格外重要 —— ",[21,664,665],{},"它不是重构的额外负担，而是重构的前提条件。",[13,667,668,669,671],{},"本例里重构那个 ",[44,670,203],{}," 之前，最该先有的测试是这五条：",[673,674,675,688],"table",{},[676,677,678],"thead",{},[679,680,681,685],"tr",{},[682,683,684],"th",{},"测什么",[682,686,687],{},"为什么先测它",[689,690,691,706,714,722,736],"tbody",{},[679,692,693,703],{},[694,695,696,697,699,700],"td",{},"派单后工单状态从 ",[44,698,521],{}," 变 ",[44,701,702],{},"ASSIGNED",[694,704,705],{},"这是核心行为，不能变",[679,707,708,711],{},[694,709,710],{},"同一工单不能派给两个人",[694,712,713],{},"并发\u002F重复的边界",[679,715,716,719],{},[694,717,718],{},"工程师当天满 5 单时拒绝派单",[694,720,721],{},"业务规则，容易被「优化」掉",[679,723,724,733],{},[694,725,726,727,729,730,732],{},"有 ",[44,728,211],{}," 时 ",[44,731,207],{}," 必须为 true",[694,734,735],{},"参数之间的关系",[679,737,738,741],{},[694,739,740],{},"紧急单的优先级高于普通单",[694,742,743],{},"排序规则",[13,745,746,749],{},[21,747,748],{},"这五条测试写出来之后，重构才有意义"," —— 因为每改一步都能验证「行为没变」。",[13,751,752],{},"没有测试时，重构的唯一保障是你的细心程度。而细心程度不能作为工程保障。",[26,754,756],{"id":755},"_4-什么时候重构","4 什么时候重构",[132,758,760],{"id":759},"_41-三次法则","4.1 三次法则",[13,762,763,764,767],{},"常见的判断依据是",[21,765,766],{},"三次法则（Rule of Three）","：",[597,769,770,773,776],{},[600,771,772],{},"第一次遇到这段逻辑，先照常写；",[600,774,775],{},"第二次遇到相似的东西，先记下来，但忍住不动；",[600,777,778],{},[21,779,780],{},"第三次遇到时，再动手抽象。",[13,782,783],{},"这条规则的意义在于克制：过早抽象和重复代码一样有害 —— 那时你还没弄清它们真正的共同点。",[13,785,786,787,790,791],{},"本例里可以验证这一点：第一次做「派单」时需要校验状态，第二次做「申请配件」时又需要 —— 如果这时就抽一个通用的 ",[44,788,789],{},"validateStatus()","，那第三次（「回填工单」）时你会发现它要求的状态是第三个值，于是只能给它加参数。",[21,792,793],{},"等到第三次再抽，你才知道该抽的是「状态机」而不是「校验方法」。",[132,795,797],{"id":796},"_42-另外两种时机","4.2 另外两种时机",[13,799,800],{},"另外还有两种时机：",[597,802,803,809],{},[600,804,805,808],{},[21,806,807],{},"顺手重构。"," 每次改需求时，顺手把要碰的那一小块整理干净。成本低、效果好；",[600,810,811,814],{},[21,812,813],{},"专门的重构迭代。"," 当积累的结构问题已经影响开发速度时，专门留出时间。这种情况通常说明前面顺手重构做得太少。",[13,816,817,818,820,821,823],{},"本例里两者都会出现：改派单规则时顺手把那段 ",[44,819,113],{}," 抽成方法（顺手重构）；而那个 280 行的 ",[44,822,203],{}," 要专门排一个迭代 —— 因为它需要先补测试。",[26,825,827],{"id":826},"_5-重构和前面那些设计模型的关系","5 重构和前面那些设计模型的关系",[13,829,830],{},"回到这个系列的主线。重构不是「写代码时候的单独技艺」，它和前面对应得很清楚：",[597,832,833,839,845],{},[600,834,835,838],{},[21,836,837],{},"类图是目标。"," 重构就是让代码重新回到类图所描述的结构上。发现代码和类图脱节了，要么改代码，要么改图（说明当初的设计判断需要修正）；",[600,840,841,844],{},[21,842,843],{},"职责划分是标准。"," 依恋情结之所以是坏味道，正是因为当初的职责划错了位置 —— 22 篇那个「控制器里写数量校验」的例子就是同一个问题；",[600,846,847,850],{},[21,848,849],{},"测试是护栏。"," 没有测试，重构就变成赌博。",[13,852,853,854,857,858],{},"还有一个相关的说法叫",[21,855,856],{},"技术债（technical debt）","：为了赶进度而接受的结构妥协，会像债务一样产生「利息」—— 每次新需求都要多绕一点路。重构是还本金的方式。",[21,859,860],{},"债务本身不一定坏，坏的是不还。",[13,862,863,864,866,867],{},"本例里那笔「技术债」可以量化：因为 ",[44,865,203],{}," 太长，每次改派单规则平均要两天（一天读代码、一天改+测）。如果重构一次花三天，后面还有五次派单相关需求，那就省下七天。",[21,868,869],{},"这就是「利息」的具体形状。",[26,871,873],{"id":872},"_6-常见的三个顾虑","6 常见的三个顾虑",[597,875,876,882,888],{},[600,877,878,881],{},[21,879,880],{},"「没有时间重构。」"," 顺手重构的成本其实很低，代价是不做的话会持续累积。上面那个「两天」的账就是回答；",[600,883,884,887],{},[21,885,886],{},"「重构会引入 bug。」"," 在测试保护下的小步重构，风险比「在外面再加一层判断」低得多；",[600,889,890,893],{},[21,891,892],{},"「不如重写。」"," 重写的风险是很难完全复制现有系统的隐含行为 —— 那些行为往往没人记得，但用户依赖它们。本例里「紧急单优先」这条规则就没写在需求文档里，只存在于代码里；重写的人很可能漏掉。多数情况下，渐进重构更稳。",[13,895,896,897,900],{},"第三条的解法是：",[21,898,899],{},"重写之前先给老系统补测试","，让那些隐含行为变成显式断言。补不出来，就说明你还不知道它做了什么 —— 那更不该重写。",[26,902,903],{"id":903},"本节自测",[905,906,907,910,913,919,922,925,928],"ol",{},[600,908,909],{},"重构的定义里，「不改变外部行为」这一条为什么重要？举出本例中一个「以为是重构其实是改需求」的例子。",[600,911,912],{},"依恋情结的对策为什么是把方法搬走，而不是把数据搬过来？",[600,914,915,916,918],{},"为什么重构之前必须有测试？重构那段 ",[44,917,203],{}," 之前，最该先补哪几条测试？",[600,920,921],{},"三次法则为什么会建议「第二次先忍住不动」？本例中第二次就抽象会犯什么错？",[600,923,924],{},"技术债这个说法里，「利息」指的是什么？怎么用本例量化它？",[600,926,927],{},"「重复代码」这一节里，为什么不该抽成一个「通用校验方法」，而该抽成「状态机判定」？",[600,929,930],{},"四个坏味道里，哪个是考场上最常出现的那一个？判断依据是什么？",[132,932,933],{"id":933},"参考答案",[905,935,936,946,960,967,973,983,1004],{},[600,937,938,939,942,943,945],{},"因为不改变行为才是重构，改变行为就是",[21,940,941],{},"改需求或修 bug","，要走业务确认与验收。本例：重构 ",[44,944,203],{}," 时顺手把派单规则改成「优先派给昨天没派过的人」—— 那是改需求，不是重构（§1.1）。",[600,947,948,949,952,953,955,956,959],{},"因为",[21,950,951],{},"行为应该由持有状态的对象提供","。把数据搬过来会让控制器持有业务数据，职责更乱；搬方法则是让 ",[44,954,441],{}," 自己回答 ",[44,957,958],{},"canRequestParts()","（§2.2）。",[600,961,962,963,966],{},"因为重构的唯一风险是",[21,964,965],{},"在改善结构时不小心改变行为而不自知","，测试是唯一的验证手段。本例最该先补五条：状态从 CREATED 变 ASSIGNED、同一工单不能派两人、满 5 单拒绝、有 photoUrl 时 needPhoto 必须为 true、紧急单优先（§3）。",[600,968,948,969,972],{},[21,970,971],{},"那时你还没弄清它们真正的共同点","。本例第二次就抽象会做出「通用 validateStatus()」，到第三次（回填工单要求第三个状态值）只能给它加参数 —— 于是它再也不是共用逻辑（§4.1）。",[600,974,975,976,979,980,982],{},"「利息」是",[21,977,978],{},"每次新需求都要多绕的那点路","。本例：因为 ",[44,981,203],{}," 太长，每次改派单规则要两天（一天读、一天改测）；重构花三天，后面还有五次需求，就省下七天（§5）。",[600,984,985,986,522,988,605,990,993,994,996,997,1000,1001,1003],{},"因为「校验工单状态」在派单里要求 ",[44,987,521],{},[44,989,525],{},[21,991,992],{},"看起来一样其实是两个规则","，抽成通用方法后一遇新需求就要加参数。该抽的是「状态机判定能力」，让 ",[44,995,441],{}," 提供 ",[44,998,999],{},"canDispatch()"," 与 ",[44,1002,958],{},"（§2.3）。",[600,1005,1006,1009,1010,1013,1014,1000,1017,1020],{},[21,1007,1008],{},"被拒绝的馈赠（refused bequest）","。判断只需要看一件事：",[21,1011,1012],{},"子类有没有把父类的方法重写成空、或直接 return null","。两道真题（",[44,1015,1016],{},"SubContractedProject",[44,1018,1019],{},"OutsourcedEngineer","）都是这个形态（§2.4）。",[26,1022,1023],{"id":1023},"术语表",[673,1025,1026,1039],{},[676,1027,1028],{},[679,1029,1030,1033,1036],{},[682,1031,1032],{},"术语",[682,1034,1035],{},"中文",[682,1037,1038],{},"一句话解释",[689,1040,1041,1052,1063,1074,1085,1096,1107,1118,1129,1140,1151,1162,1173,1184,1195],{},[679,1042,1043,1046,1049],{},[694,1044,1045],{},"refactoring",[694,1047,1048],{},"重构",[694,1050,1051],{},"不改变外部行为，改善内部结构",[679,1053,1054,1057,1060],{},[694,1055,1056],{},"code smell",[694,1058,1059],{},"坏味道",[694,1061,1062],{},"代码里提示结构问题的特征，不等于 bug",[679,1064,1065,1068,1071],{},[694,1066,1067],{},"long parameter list",[694,1069,1070],{},"长参数列表",[694,1072,1073],{},"方法参数过多，调用方易出错",[679,1075,1076,1079,1082],{},[694,1077,1078],{},"feature envy",[694,1080,1081],{},"依恋情结",[694,1083,1084],{},"方法对别的类的数据更感兴趣",[679,1086,1087,1090,1093],{},[694,1088,1089],{},"duplicate code",[694,1091,1092],{},"重复代码",[694,1094,1095],{},"同一逻辑出现在多处，改动容易漏",[679,1097,1098,1101,1104],{},[694,1099,1100],{},"refused bequest",[694,1102,1103],{},"被拒绝的馈赠",[694,1105,1106],{},"子类继承了用不上的父类成员",[679,1108,1109,1112,1115],{},[694,1110,1111],{},"extract class",[694,1113,1114],{},"提取类",[694,1116,1117],{},"把一部分职责拆成新的类",[679,1119,1120,1123,1126],{},[694,1121,1122],{},"extract method",[694,1124,1125],{},"提取方法",[694,1127,1128],{},"把一段逻辑抽成独立方法",[679,1130,1131,1134,1137],{},[694,1132,1133],{},"introduce parameter object",[694,1135,1136],{},"引入参数对象",[694,1138,1139],{},"把多个参数打包成一个对象",[679,1141,1142,1145,1148],{},[694,1143,1144],{},"move method",[694,1146,1147],{},"移动方法",[694,1149,1150],{},"把方法搬到它依赖的数据所在的类",[679,1152,1153,1156,1159],{},[694,1154,1155],{},"pull up",[694,1157,1158],{},"上移",[694,1160,1161],{},"把子类中相同的成员提到父类",[679,1163,1164,1167,1170],{},[694,1165,1166],{},"pull down",[694,1168,1169],{},"下移",[694,1171,1172],{},"把父类中用不上的成员推到子类",[679,1174,1175,1178,1181],{},[694,1176,1177],{},"rule of three",[694,1179,1180],{},"三次法则",[694,1182,1183],{},"同样的重复出现第三次再抽象",[679,1185,1186,1189,1192],{},[694,1187,1188],{},"regression risk",[694,1190,1191],{},"回归风险",[694,1193,1194],{},"改动导致原有功能被弄坏的可能",[679,1196,1197,1200,1203],{},[694,1198,1199],{},"technical debt",[694,1201,1202],{},"技术债",[694,1204,1205],{},"结构妥协带来的长期额外成本",[1207,1208,1209],"style",{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}",{"title":42,"searchDepth":85,"depth":85,"links":1211},[1212,1213,1217,1224,1225,1229,1230,1231,1234],{"id":28,"depth":68,"text":29},{"id":129,"depth":68,"text":130,"children":1214},[1215,1216],{"id":134,"depth":85,"text":135},{"id":160,"depth":85,"text":161},{"id":173,"depth":68,"text":174,"children":1218},[1219,1220,1221,1222,1223],{"id":193,"depth":85,"text":194},{"id":316,"depth":85,"text":317},{"id":498,"depth":85,"text":499},{"id":584,"depth":85,"text":585},{"id":642,"depth":85,"text":643},{"id":652,"depth":68,"text":653},{"id":755,"depth":68,"text":756,"children":1226},[1227,1228],{"id":759,"depth":85,"text":760},{"id":796,"depth":85,"text":797},{"id":826,"depth":68,"text":827},{"id":872,"depth":68,"text":873},{"id":903,"depth":68,"text":903,"children":1232},[1233],{"id":933,"depth":85,"text":933},{"id":1023,"depth":68,"text":1023},"学习",null,"2026-11-16","重构不改变外部行为、只改善内部结构：四种常见坏味道与对应手法、为什么动手之前必须先有测试、什么时候该重构，以及重构和前面那些设计模型是什么关系。","md",false,{},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-25",{"title":5,"description":1238},"软件分析与设计笔记",25,"posts\u002FswDesign\u002Fsw-design-notes-25",[1048,1248,1249,1250],"代码质量","软件设计","学习笔记","dPga7ZQBthsQP9WWr-GNUioazY4-5CZOZO3XyN2qYNw",[1253,1259,1265,1271,1272,1278,1284,1290,1296,1302,1308,1314,1320,1326,1332,1338,1344,1350,1356,1361,1366,1371,1376,1381,1386,1391,1396,1401,1406,1412,1416,1420,1424,1428,1432,1436,1440,1444,1448,1452,1456,1462,1466,1472,1476,1480,1484,1488,1492,1496,1500,1505,1509,1513,1518,1523,1529,1533,1538,1542],{"path":1254,"title":1255,"description":1256,"date":1257,"series":1244,"seriesOrder":1258},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-28","软件分析与设计笔记 28：draw.io 作图手册：五张图的符号位置、落笔顺序与自检","不模拟软件界面，只讲清三件事：每个 UML 符号在 draw.io 里搜什么词、一张图该按什么顺序落笔、画完拿什么标准自查。覆盖用例图、活动图、顺序图、类图、界面草图五种图纸，末尾附「最容易画反的十二笔」总检表。","2026-11-19",28,{"path":1260,"title":1261,"description":1262,"date":1263,"series":1244,"seriesOrder":1264},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-27","软件分析与设计笔记 27：Paper 1 应试：从读案例到交卷的一次完整演算","拿一份完整的案例，从头到尾走一遍：怎么读案例列角色、怎么在四个小问之间分配时间、每一张图从哪一句话推出来、两张表怎么填、交卷前拿什么清单自查。附 C25 真题与样卷的题型分值对照。","2026-11-18",27,{"path":1266,"title":1267,"description":1268,"date":1269,"series":1244,"seriesOrder":1270},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-26","软件分析与设计笔记 26：总结：把整条链路串起来","系列收尾：从需求到代码的完整链路回顾、每一环的产出与验收方式、六条贯穿全系列的原则，以及一份可以直接拿去做项目自查的清单。","2026-11-17",26,{"path":1242,"title":5,"description":1238,"date":1237,"series":1244,"seriesOrder":1245},{"path":1273,"title":1274,"description":1275,"date":1276,"series":1244,"seriesOrder":1277},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-24","软件分析与设计笔记 24：设计原则：内聚、耦合与多态","好设计怎么衡量：内聚与耦合分别在说什么、一次从「差设计」到「多态重设计」的完整对照、抽象类与接口的选择依据，以及复用为什么会从优势变成风险。","2026-11-15",24,{"path":1279,"title":1280,"description":1281,"date":1282,"series":1244,"seriesOrder":1283},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-23","软件分析与设计笔记 23：设计建模：从对象模型到能实现的方案","分析模型能用但还不能直接开工：对象模型怎么映射到关系模型、继承映射的三种方案怎么选、数据访问层与 Data Broker 解决什么问题，以及三层架构与 MVC 的分工。","2026-11-14",23,{"path":1285,"title":1286,"description":1287,"date":1288,"series":1244,"seriesOrder":1289},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-22","软件分析与设计笔记 22：分析建模：边界、实体与控制对象","不经过分析直接写代码会踩什么坑、三种分析对象各管一段、怎么用顺序图完成一次用例实现，以及从顺序图导出类的属性、操作与关系的方法。","2026-11-13",22,{"path":1291,"title":1292,"description":1293,"date":1294,"series":1244,"seriesOrder":1295},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-21","软件分析与设计笔记 21：UML：类图与顺序图","为什么需要一种共同的画法：类符号三格怎么画、五种关系的符号与设计含义、每种关系落到 Java 是什么写法，顺序图的三要素怎么摆；以及这三种最容易找不到的符号在 draw.io 里各搜什么词。","2026-11-12",21,{"path":1297,"title":1298,"description":1299,"date":1300,"series":1244,"seriesOrder":1301},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-20","软件分析与设计笔记 20：面向对象概念：对象、关系与多态","面向对象不是「用某个语言写代码」：对象与类的区别、封装真正在保护什么、五种关系（依赖、关联、聚合、组合、继承）各自何时用，以及多态为什么能减少条件判断。","2026-11-11",20,{"path":1303,"title":1304,"description":1305,"date":1306,"series":1244,"seriesOrder":1307},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-19","软件分析与设计笔记 19：规范化与逻辑数据模型","一次改地址引发的数据不一致，讲清规范化的真正动机；UNF 到 3NF 每一步在解决什么问题，派生字段为什么不能存，优化为什么和规范化方向相反，以及逻辑数据模型在 3NF 之上补了什么。","2026-11-10",19,{"path":1309,"title":1310,"description":1311,"date":1312,"series":1244,"seriesOrder":1313},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-18","软件分析与设计笔记 18：数据库分析：实体、关系与数据字典","为什么不能拿到需求就动手建表：实体与关系怎么判定、基数为什么必须由业务方确认、数据命名要遵守什么规则，以及数据字典四列里最容易被空着的那一列。","2026-11-09",18,{"path":1315,"title":1316,"description":1317,"date":1318,"series":1244,"seriesOrder":1319},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-17","软件分析与设计笔记 17：用生成式 AI 做设计与开发：能用的场景与要防的坑","生成式 AI 在工作流里真正站得住的三个场景 —— 造示例数据、解释陌生代码、起草初稿；它为什么会复现社会偏见、为什么会编得像真的，以及哪些内容绝不能直接采用。","2026-11-08",17,{"path":1321,"title":1322,"description":1323,"date":1324,"series":1244,"seriesOrder":1325},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-16","软件分析与设计笔记 16：从 persona 到 URS：一份交付物的完整骨架","把前面十几篇串成一份能交出去的东西：用户需求规格说明书的九个部分、每一章来自哪个环节、由谁确认，以及面向决策者的提案该讲清哪三件事。","2026-11-07",16,{"path":1327,"title":1328,"description":1329,"date":1330,"series":1244,"seriesOrder":1331},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-15","软件分析与设计笔记 15：非功能需求：把「快、稳、安全」变成数字","为什么「系统要快」不是一条需求：非功能需求怎么量化，决定性能的用户量、交易量、数据量三张表怎么估，安全需求要写清什么，以及可靠性的「不可用」该怎么定义。","2026-11-06",15,{"path":1333,"title":1334,"description":1335,"date":1336,"series":1244,"seriesOrder":1337},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-14","软件分析与设计笔记 14：软件测试：层级、方法与等价类划分","「我们测过了」这句话问题在哪：为什么静态评审和动态测试缺一不可，五个测试层级各自验证什么，白盒与黑盒的差别，以及用等价类划分和边界值把无限输入变成有限用例的方法。","2026-11-05",14,{"path":1339,"title":1340,"description":1341,"date":1342,"series":1244,"seriesOrder":1343},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-13","软件分析与设计笔记 13：需求获取：五种调查方法与一场访谈","需求从哪里来：五种事实调查方法各自适合回答什么、代价是什么，访谈从准备到记录的全过程，问卷设计里最容易犯的错，以及访谈记录必须写下来的那几项信息。","2026-11-04",13,{"path":1345,"title":1346,"description":1347,"date":1348,"series":1244,"seriesOrder":1349},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-12","软件分析与设计笔记 12：前端设计：把流程变成屏幕","界面设计不是「画得好看」：从活动图走到屏幕的三步，CRUD 任务流怎么当检查表，界面必须回答的三个问题，信息架构与五种导航类型，视觉一致性与留白，以及移动端与 Web 端的差别。","2026-11-03",12,{"path":1351,"title":1352,"description":1353,"date":1354,"series":1244,"seriesOrder":1355},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-11","软件分析与设计笔记 11：Agentic AI 与业务流程自动化","AI agent 到底是什么：五项能力与四个核心组件，什么场景该用定时任务、什么该用程序、什么才值得用 agent，两种实现路径的差别，必须提前防住的四类失败模式，以及设计方法从提示词到编排的演进。","2026-11-02",11,{"path":1357,"title":1358,"description":1359,"date":1360,"series":1244,"seriesOrder":301},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-10","软件分析与设计笔记 10：精炼用例模型：include、extend 与泛化","三种精炼用例关系的判断标准：include 什么时候用、它和 extend 的区别到底在哪、参与者泛化与用例泛化各解决什么问题，以及精炼到什么程度就该停手。","2026-11-01",{"path":1362,"title":1363,"description":1364,"date":1365,"series":1244,"seriesOrder":292},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-09","软件分析与设计笔记 09：功能需求（二）：从流程到业务实体","活动图画完之后，怎么从流程里筛出真正的业务实体：三条判断标准、哪些名词其实不该建实体、属性从哪里来，以及领域模型和后面的数据模型分别负责什么。","2026-10-31",{"path":1367,"title":1368,"description":1369,"date":1370,"series":1244,"seriesOrder":283},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-08","软件分析与设计笔记 08：功能需求（一）：用例模型与活动图","从用户故事到用例的翻译过程：什么才算「好」的参与者，系统边界怎么划，用例描述必须写清哪四件事，活动图怎么把分支、并行与泳道画清楚，以及这两个图在 draw.io 里各用哪个形状、最容易画错哪一笔。","2026-10-30",{"path":1372,"title":1373,"description":1374,"date":1375,"series":1244,"seriesOrder":277},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-07","软件分析与设计笔记 07：用户故事：把需求写成可验收的一句话","用户故事的三段结构，怎么从用户画像和数据地图里把它提取出来，一条故事多大算合适、时序为什么也是需求的一部分，以及验收标准怎么写才算真的可验收。","2026-10-29",{"path":1377,"title":1378,"description":1379,"date":1380,"series":1244,"seriesOrder":271},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-06","软件分析与设计笔记 06：范围界定（数据侧）：用数据校验你的判断","访谈结论和数字对不上时该怎么办：分析的四个层次分别在回答什么问题、为什么不能跳级、数据准备与搜集怎么做成一张应用地图，以及数据最容易在哪几个地方骗你。","2026-10-28",{"path":1382,"title":1383,"description":1384,"date":1385,"series":1244,"seriesOrder":104},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-05","软件分析与设计笔记 05：范围界定（用户侧）：用户画像与客户旅程图","为什么「所有用户」是一个陷阱，项目边界该怎么划，用户画像要写哪五个要素、最容易写成什么废纸，客户旅程图怎么把阶段、想法和情绪画在同一张纸上，以及怎么从这两样东西一步步推到可验证的需求。","2026-10-27",{"path":1387,"title":1388,"description":1389,"date":1390,"series":1244,"seriesOrder":97},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-04","软件分析与设计笔记 04：接手陌生案例的第一步：三问与一张表","拿到一个完全陌生的业务案例，怎么从零开始不跑偏：只问 Who、What、Why 三个问题，把「问题—事实—假设—理由」记成一张四列表，说清为什么假设必须写明理由，以及带着问题进访谈为什么比空手去有效得多。","2026-10-26",{"path":1392,"title":1393,"description":1394,"date":1395,"series":1244,"seriesOrder":85},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-03","软件分析与设计笔记 03：Agile 与 Scrum：迭代、角色与产出物","从「三周做完交付，客户说这不是我要的」讲起，说清瀑布式与迭代式的根本差别，Scrum 的三个角色和三种产出物，一个 sprint 里四次固定活动各自在解决什么问题，以及敏捷里最常见的几个误解。","2026-10-25",{"path":1397,"title":1398,"description":1399,"date":1400,"series":1244,"seriesOrder":68},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-02","软件分析与设计笔记 02：设计思维、用户驱动与数据驱动","从「客服要一个能一次看完所有排期的界面」这个翻车案例讲起，说清设计思维到底是什么、用户驱动与数据驱动各自能看见什么又看不见什么、为什么真正的落点是把两者合起来的 data-informed design，以及访谈中怎么问出有用的信息。","2026-10-24",{"path":1402,"title":1403,"description":1404,"date":1405,"series":1244,"seriesOrder":50},"\u002Fposts\u002Fswdesign\u002Fsw-design-notes-01","软件分析与设计笔记 01：课程地图与从需求到设计的主干","这一篇不写具体方法，先把整条链路铺开：软件分析与设计到底要解决什么问题，从需求到设计分成哪两段，中间有哪十几个环节，一份完整的交付物里应该有什么，以及后面 27 篇笔记怎么排、新人该按什么顺序读。","2026-10-23",{"path":1407,"title":1408,"description":1409,"date":1410,"series":1411,"seriesOrder":50},"\u002Fposts\u002Fjavaspring\u002Fspring-notes-01","Spring 学习笔记 01：IoC、依赖注入与第一个 Spring Boot 应用","从「手写 new 出来的三层结构」的痛点讲起，说清 IoC 容器、Bean 与依赖注入是怎么回事，再用 Maven 与 Spring Initializr 跑起第一个能返回 JSON 的应用。","2026-10-15","Spring 学习笔记",{"path":1413,"title":1414,"description":1415,"date":1410,"series":1411,"seriesOrder":68},"\u002Fposts\u002Fjavaspring\u002Fspring-notes-02","Spring 学习笔记 02：Spring Data JPA 基础——实体映射与 Repository","用 ORM 把类映射到表：@Entity、@Id、@GeneratedValue 怎么用，CrudRepository 三层接口怎么选，H2 与 MySQL 怎么配，以及 ddl-auto 到底该写哪个值。",{"path":1417,"title":1418,"description":1419,"date":1410,"series":1411,"seriesOrder":85},"\u002Fposts\u002Fjavaspring\u002Fspring-notes-03","Spring 学习笔记 03：JPA 查询——派生方法、JPQL 与分页排序","Repository 自带的方法只够按主键查，这篇讲三种自己造查询的方式：方法名派生、@Query 写 JPQL、原生 SQL，以及 Pageable 分页排序和投影返回。",{"path":1421,"title":1422,"description":1423,"date":1410,"series":1411,"seriesOrder":97},"\u002Fposts\u002Fjavaspring\u002Fspring-notes-04","Spring 学习笔记 04：JPA 关联映射与仓储测试","一对多、多对多怎么映射，谁负责维护外键，FetchType 与 CascadeType 该怎么选，顺带用 @DataJpaTest 与 AssertJ 把仓储层测起来。",{"path":1425,"title":1426,"description":1427,"date":1410,"series":1411,"seriesOrder":104},"\u002Fposts\u002Fjavaspring\u002Fspring-notes-05","Spring 学习笔记 05：Spring MVC 与 Thymeleaf 入门","一次页面请求在 Spring MVC 里走了哪几步，@Controller 怎么把数据交给模板，@PathVariable 与 @RequestParam 怎么取参数，以及控制器为什么必须是无状态的。",{"path":1429,"title":1430,"description":1431,"date":1410,"series":1411,"seriesOrder":271},"\u002Fposts\u002Fjavaspring\u002Fspring-notes-06","Spring 学习笔记 06：表单绑定与服务端校验","把表单字段绑到对象上，用 Bean Validation 注解做服务端校验，把错误信息显示回用户刚填的那一页，再补上自定义校验注解与全局异常处理。",{"path":1433,"title":1434,"description":1435,"date":1410,"series":1411,"seriesOrder":277},"\u002Fposts\u002Fjavaspring\u002Fspring-notes-07","Spring 学习笔记 07：Thymeleaf 片段复用与国际化","用 th:fragment 与 th:insert\u002Fth:replace 把导航栏、页脚抽成片段，页面套用统一布局，再用 messages.properties 与 LocaleResolver 做中英文切换。",{"path":1437,"title":1438,"description":1439,"date":1410,"series":1411,"seriesOrder":283},"\u002Fposts\u002Fjavaspring\u002Fspring-notes-08","Spring 学习笔记 08：HTTP Session 与会话状态管理","HTTP 无状态带来的问题，Cookie 与 Session 怎么分工，Spring 里读写会话的几种方式，以及会话固定、超时、多实例共享这些必须想清楚的事。",{"path":1441,"title":1442,"description":1443,"date":1410,"series":1411,"seriesOrder":292},"\u002Fposts\u002Fjavaspring\u002Fspring-notes-09","Spring 学习笔记 09：拦截器 HandlerInterceptor","登录检查、权限判断、请求耗时统计这类横切逻辑该放哪：HandlerInterceptor 的三个回调各在什么时机执行，怎么注册与排除路径，以及它和过滤器的分工。",{"path":1445,"title":1446,"description":1447,"date":1410,"series":1411,"seriesOrder":301},"\u002Fposts\u002Fjavaspring\u002Fspring-notes-10","Spring 学习笔记 10：Service 层与事务","一组数据库操作要么全成功要么全失败，靠 @Transactional 怎么做到；传播行为、隔离级别、回滚规则怎么选，以及自调用失效、乐观锁悲观锁这些实战里的坑。",{"path":1449,"title":1450,"description":1451,"date":1410,"series":1411,"seriesOrder":1355},"\u002Fposts\u002Fjavaspring\u002Fspring-notes-11","Spring 学习笔记 11：REST API 基础——把 Service 暴露成接口","同一套 Service 怎么同时服务页面和客户端：@RestController 的路由与方法语义、状态码怎么给、DTO 怎么定，以及 MockMvc 端到端和切片测试的两种写法与各自的坑。",{"path":1453,"title":1454,"description":1455,"date":1410,"series":1411,"seriesOrder":1349},"\u002Fposts\u002Fjavaspring\u002Fspring-notes-12","Spring 学习笔记 12：REST 进阶——版本、更新、跨域与文档化","接口能跑通之后要处理的几件事：URL 与响应结构怎么定、版本怎么演进、更新怎么做到可安全重试、CORS 怎么配、HATEOAS 与 Spring Data REST 该不该用。",{"path":1457,"title":1458,"description":1459,"date":1460,"series":1461,"seriesOrder":97},"\u002Fposts\u002Fjava\u002Fjava-functionalprogramming-notes","Java 函数式编程笔记：Lambda、函数式接口与 Streams","学习 Java 函数式编程时的个人笔记：Lambda 表达式的五步简写与方法引用、常见函数式接口、Stream 流水线与惰性求值，以及创建流、中间操作、终止操作与收集器的用法。示例代码均为自写，运行结果来自本地实测。","2026-10-13","Java 学习笔记",{"path":1463,"title":1464,"description":1465,"date":1460,"series":1461,"seriesOrder":85},"\u002Fposts\u002Fjava\u002Fjava-generics-notes","Java 泛型笔记：泛型方法、有界类型与泛型类","学习 Java 泛型时的个人笔记：从重复的重载方法与多态尝试讲起，再到泛型方法与类型推断、有界泛型类型、Comparable 与 Number 边界，以及泛型类在集合中的用法。示例代码均为自写，运行结果来自本地实测。",{"path":1467,"title":1468,"description":1469,"date":1470,"series":1471,"seriesOrder":97},"\u002Fposts\u002Fmysql\u002Fmysql-learning-04","MySQL 学习笔记 04：数据操纵（增删改）","MySQL 学习笔记第四篇：INSERT 插入（含批量插入与 INSERT ... SELECT）、UPDATE 更新、DELETE 删除，以及各自的坑。","2026-10-10","SQL 学习笔记",{"path":1473,"title":1474,"description":1475,"date":1470,"series":1471,"seriesOrder":104},"\u002Fposts\u002Fmysql\u002Fmysql-learning-05","MySQL 学习笔记 05：DDL 概览","MySQL 学习笔记第五篇：数据定义语言概览——Schema、Table、View、Index、Constraints 这几类对象分别对应哪些 CREATE \u002F ALTER \u002F DROP 命令。",{"path":1477,"title":1478,"description":1479,"date":1470,"series":1471,"seriesOrder":271},"\u002Fposts\u002Fmysql\u002Fmysql-learning-06","MySQL 学习笔记 06：CREATE TABLE 与数据类型","MySQL 学习笔记第六篇：CREATE TABLE 的完整语法、常用数据类型的选型建议，以及外键的两种定义方式。",{"path":1481,"title":1482,"description":1483,"date":1470,"series":1471,"seriesOrder":277},"\u002Fposts\u002Fmysql\u002Fmysql-learning-07","MySQL 学习笔记 07：索引","MySQL 学习笔记第七篇：索引的作用与 B+ 树结构、单列与复合索引的创建、索引的代价与适用场景。",{"path":1485,"title":1486,"description":1487,"date":1470,"series":1471,"seriesOrder":283},"\u002Fposts\u002Fmysql\u002Fmysql-learning-08","MySQL 学习笔记 08：DROP 与 ALTER","MySQL 学习笔记第八篇：DROP 删除数据库对象、ALTER 修改表结构，以及常见的坑。",{"path":1489,"title":1490,"description":1491,"date":1470,"series":1471,"seriesOrder":292},"\u002Fposts\u002Fmysql\u002Fmysql-learning-09","MySQL 学习笔记 09：约束与数据完整性","MySQL 学习笔记第九篇：SQL 约束——NOT NULL、CHECK、PRIMARY KEY、FOREIGN KEY 等如何保证数据完整性，以及它们的对比。",{"path":1493,"title":1494,"description":1495,"date":1470,"series":1471,"seriesOrder":301},"\u002Fposts\u002Fmysql\u002Fmysql-learning-10","MySQL 学习笔记 10：视图","MySQL 学习笔记第十篇：视图（Views）的定义与特性、和表的对比、创建与使用，以及可更新视图的限制。",{"path":1497,"title":1498,"description":1499,"date":1470,"series":1471,"seriesOrder":1355},"\u002Fposts\u002Fmysql\u002Fmysql-learning-11","MySQL 学习笔记 11：存储过程","MySQL 学习笔记第十一篇：存储过程（Stored Procedures）的概念、创建与调用、带参数的写法，以及优缺点。",{"path":1501,"title":1502,"description":1503,"date":1504,"series":1471,"seriesOrder":50},"\u002Fposts\u002Fmysql\u002Fmysql-learning-01","MySQL 学习笔记 01：MySQL 基础知识","MySQL 学习笔记第一篇：MySQL Server 与 Workbench 的安装，数据库与 DBMS 的基本概念，关系模型、ACID 事务、常见数据类型，以及后续的学习路线。","2026-10-09",{"path":1506,"title":1507,"description":1508,"date":1504,"series":1471,"seriesOrder":68},"\u002Fposts\u002Fmysql\u002Fmysql-learning-02","MySQL 学习笔记 02：SELECT 查询与过滤","MySQL 学习笔记第二篇：SELECT 的基本语法与逻辑执行顺序，WHERE 过滤（比较、LIKE、IN、BETWEEN、NULL）、各种 JOIN、UNION 与子查询。",{"path":1510,"title":1511,"description":1512,"date":1504,"series":1471,"seriesOrder":85},"\u002Fposts\u002Fmysql\u002Fmysql-learning-03","MySQL 学习笔记 03：聚合、分组与输出控制","MySQL 学习笔记第三篇：COUNT\u002FSUM\u002FAVG 等聚合函数、GROUP BY 分组、HAVING 过滤、ORDER BY 与 LIMIT，以及一条 SELECT 的完整逻辑执行顺序。",{"path":1514,"title":1515,"description":1516,"date":1517,"series":1461,"seriesOrder":68},"\u002Fposts\u002Fjava\u002Fjava-oop-notes","Java 面向对象笔记：类、继承、多态与异常","学习 Java 面向对象编程时的个人笔记：类与对象、封装与访问修饰符、继承与多态、集合、抽象类与接口、异常处理。示例代码均为自写，运行结果来自本地实测。","2026-10-06",{"path":1519,"title":1520,"description":1521,"date":1522,"series":1461,"seriesOrder":50},"\u002Fposts\u002Fjava\u002Fjava-basics-notes","Java 基础语法笔记：从环境安装到方法与数组","学习 Java 基础语法时的个人笔记：JDK 安装、输入输出、变量与运算符、条件与循环、字符串、数组与方法。示例代码均为自写，运行结果来自本地实测。","2026-10-05",{"path":1524,"title":1525,"description":1526,"date":1527,"series":1528,"seriesOrder":50},"\u002Fposts\u002Fgit\u002Fgit-install-and-basics","Git 安装与基本使用","从装好 Git 到跑通日常提交流程，把最常用的几条命令和它们背后的三个区域讲清楚。","2026-10-04","Git 入门",{"path":1530,"title":1531,"description":1532,"date":1527,"series":1528,"seriesOrder":68},"\u002Fposts\u002Fgit\u002Flocal-remote-repository","本地远程仓库的部署","用自己的机器当 Git 服务器：建一个裸仓库、走 SSH 连上去，把本地提交推上去。",{"path":1534,"title":1535,"description":1536,"date":1527,"series":1537,"seriesOrder":50},"\u002Fposts\u002Fguide\u002Fguide-file-layout","文件都放在哪","文章、图片、封面分别放在仓库的什么位置，哪些目录可以放手改、哪些碰了会出问题。","博客使用指南",{"path":1539,"title":1540,"description":1541,"date":1527,"series":1537,"seriesOrder":68},"\u002Fposts\u002Fguide\u002Fguide-frontmatter","frontmatter 每个字段是什么意思","文章开头那段被横线包住的字段，逐个说明作用、默认行为和写错的后果。",{"path":1543,"title":1544,"description":1545,"date":1527,"series":1537,"seriesOrder":85},"\u002Fposts\u002Fguide\u002Fguide-markdown","正文怎么写 —— Markdown 与排版效果","标题、链接、图片、代码、表格分别怎么写，以及本站对它们做了哪些自定义处理。",1793268147834]