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