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