[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"site-settings":3,"\u002Farticles\u002Fdesign-pattern-26":26,"\u002Farticles\u002Fdesign-pattern-26\u002Frelated":43,"\u002Farticles\u002Fdesign-pattern-26\u002Fadjacent":72,"\u002Fcategories":84,"render-design-pattern-26":110},{"code":4,"message":5,"data":6},0,"ok",{"author_city":7,"author_years":8,"site_tagline":9,"site_description":10,"skill_one_desc":11,"author_status":12,"skill_two_title":13,"about_hero_image":9,"skill_one_title":14,"icp":15,"author_role":16,"og_default_image":9,"closing_quote_date":17,"toolkit":18,"skill_two_desc":19,"author_bio":9,"site_footer":9,"site_url":20,"author_tagline":21,"weekly_thought":9,"tagline_line_one":22,"author_bio_two":9,"closing_quote":23,"site_name":24,"tagline_line_two":25,"home_hero_image":9},"西安","10","","花开空白 - 个人博客","熟悉Java、Python、Go等后端语言。","努力工作中","前端开发","后端开发","陕ICP备2021009682号-1","开发者","2022年·秋","Java,JavaScript,TypeScript,Python,Go","熟悉Js、Ts语言以及Vue、uniapp等框架。","https:\u002F\u002Firule.cn","很高兴遇见你。","把复杂的事，","把每次一发布，都当成一次和陌生人的握手。","花开空白","写成简单的故事。",{"code":4,"message":5,"data":27},{"id":28,"slug":29,"title":30,"summary":31,"coverUrl":9,"categoryId":32,"status":33,"topFlag":4,"viewCount":34,"likeCount":4,"wordCount":35,"readingMinutes":36,"publishedAt":37,"contentMd":38,"contentHtml":39,"tags":40,"metaTitle":9,"metaDescription":9,"keywords":9,"canonicalUrl":9,"robots":41,"ogImage":9,"faq":42,"aiSummary":42},"2113935519934058498","design-pattern-26","设计模式修炼之路 ㉖ MVC 模式——改界面不动业务，改业务不动界面","比 GoF 早十五年、依赖方向定生死、业务逻辑永远住 Model、换视图零成本、MVC→MVP→MVVM 进化树","2113897628365230081",1,2596,8085,21,"2025-02-16T16:19:17","> J2EE 模式第一站。\n\nGoF 二十三式集齐，龙珠在手。可回头看看真实的企业应用：请求从浏览器涌进来，一个页面背后是数据存取、业务规则、界面渲染、请求分发……二十三个模式各管一段，谁来给它们排兵布阵？从 ㉖ 起，本系列进入最后一片大陆：**J2EE 企业应用模式**——八种模式，讲的就是\"分层协作\"这件事。第一站，也是最响亮的一站：MVC。\n\n先看痛点。最早的 JSP 页面（人称 Model 1 时代）长这样：页面就是应用，应用就是页面——一个方法里，参数解析、查数据库、业务规则、拼 HTML 四件事糊成一团。想改个按钮配色？通读两百行业务逻辑。想改条促销规则？在同一坨代码里翻 HTML。**四件事有四个变化方向，糊在一起，每次变化都是全量阅读。**\n\nMVC 开出的药方只有一句话：**按\"谁变化\"切分，而不是按\"做什么\"堆砌。**\n\n## 一、MVC 模式是什么\n\nMVC 不是 GoF 的孩子——它比 GoF 早出生十五年。1979 年，挪威工程师 Trygve Reenskaug 在 Xerox PARC 访学期间，为 Smalltalk-79 提出了这套结构（最初叫 Thing-Model-View-Editor，后简化）。GoF 管一个类怎么设计，MVC 管**整个应用怎么分层**——它是架构模式，粒量更大，但药理相通。\n\n三个角色：\n\n\u003Ctable>\u003Cthead>\u003Ctr>\u003Cth>角色\u003C\u002Fth>\u003Cth>职责\u003C\u002Fth>\u003Cth>认识谁\u003C\u002Fth>\u003Cth>本文对应\u003C\u002Fth>\u003C\u002Ftr>\u003C\u002Fthead>\u003Ctbody>\u003Ctr>\u003Ctd>Model（模型）\u003C\u002Ftd>\u003Ctd>数据 + 业务规则，应用的灵魂\u003C\u002Ftd>\u003Ctd>**谁都不认识**\u003C\u002Ftd>\u003Ctd>Catalog + Product\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>View（视图）\u003C\u002Ftd>\u003Ctd>把模型数据渲染成用户能看的样子\u003C\u002Ftd>\u003Ctd>只认识模型的数据\u003C\u002Ftd>\u003Ctd>ConsoleView \u002F HtmlView\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>Controller（控制器）\u003C\u002Ftd>\u003Ctd>接收请求、翻译、调模型、选视图\u003C\u002Ftd>\u003Ctd>认识另外两位\u003C\u002Ftd>\u003Ctd>ProductController\u003C\u002Ftd>\u003C\u002Ftr>\u003C\u002Ftbody>\u003C\u002Ftable>\n\n职责边界一句话说清：**Model 不知道自己被谁看着，View 不关心数据对不对，Controller 两头都不懂行——只管翻译转发。**\n\n注意那张表中间一列——**依赖方向**才是 MVC 的灵魂，比\"三个角色\"本身重要得多：Model 不依赖任何人（所以能独立测试、独立演化）；View 只依赖 Model 的数据（所以换个皮不伤筋骨）；Controller 依赖两位（所以它是粘合剂，也是唯一知道全局的）。所有\"这段代码该放哪\"的争论，答案都藏在依赖方向里。\n\n## 二、优点与缺点\n\n**优点：**\n\n1. **变化隔离**：界面天天改、规则偶尔改、参数解析很少改——三个变化速度不同的东西装进三个房间，各自折腾互不惊扰。\n2. **多视图复用**：同一份 Model，控制台、HTML、JSON 接口、APP 页面各挂各的 View——本文后半段当场演示。\n3. **Model 可独立测试**：业务规则住进 Model 后，测试它不需要启动任何界面、不需要 mock 任何渲染器——直接调方法、断言异常。\n4. **并行开发**：接口约定好，一个人写 View、一个人写 Model，Controller 最后联调——前端后端分工的鼻祖。\n\n**缺点：**\n\n1. **类数量膨胀**：一个页面拆成三个（组）类，demo 级应用写 MVC 是自虐。\n2. **Controller 易发福**：路由、参数解析、视图选择全在它身上，一不留神就长成\"上帝控制器\"；更常见的伴生病是**贫血模型**——Model 只剩 getter\u002Fsetter，业务逻辑全被 Controller 吸走，MVC 名存实亡。\n3. **职责划分靠自觉**：`if (delta > price * 0.2)` 写在 Controller 里编译器不报错——MVC 的边界是纪律问题，没有语言机制强制。\n4. **简单场景过度设计**：只有一个静态页的应用、纯计算无界面的程序，没有 V 可言。\n\n## 三、什么时候用\n\n判断标准：**有界面（或多种输出格式），且业务逻辑值得独立于界面演化。**\n\n- **典型 Web 应用**：页面样式随产品经理的心情天天变，业务规则随法务的意见偶尔变——变化速度不同，就该分房住\n- **多渠道输出**：同一数据要出 Web 页、APP 接口、小程序、开放 API——一份 Model 挂一排 View\n- **桌面应用**：MVC 的出生场景——数据变了，多个打开的窗口同时刷新\n\n反面对照：**纯后端服务**——没有 View，分层交给 ㉙ DAO 和服务层模式；**一次性脚本、demo**——别自虐；**Model 和 View 天生耦合的应用**（比如游戏渲染引擎），强行 MVC 只会制造搬运代码。\n\n## 四、实现方式：商品目录\n\n### 4.1 反面教材：一个类包揽一切\n\nJSP Model 1 时代的画风：\n\n```java\npublic class BadPage {\n\n    private static final Map\u003CString, Product> DB = new LinkedHashMap\u003C>();\n    \u002F\u002F static 初始化块灌三条商品数据，略\n\n    \u002F\u002F \"页面\"：一个方法从请求干到 HTML\n    public static String raisePrice(String id, String deltaText) {\n        int delta;\n        try {\n            delta = Integer.parseInt(deltaText);            \u002F\u002F 本该是控制器的活：参数解析\n        } catch (NumberFormatException e) {\n            return \"\u003Cp style='color:red'>参数必须是数字\u003C\u002Fp>\";  \u002F\u002F 本该是视图的活：错误展示\n        }\n        Product product = DB.get(id);                        \u002F\u002F 本该是模型的活：数据存取\n        if (product == null) {\n            return \"\u003Cp style='color:red'>商品不存在：\" + id + \"\u003C\u002Fp>\";\n        }\n        if (delta > product.getPrice() * 0.2) {              \u002F\u002F 本该是模型的活：业务规则\n            return \"\u003Cp style='color:red'>单次调价不能超过原价的 20%\u003C\u002Fp>\";\n        }\n        product.setPrice(product.getPrice() + delta);        \u002F\u002F 本该是模型的活：状态变更\n        StringBuilder html = new StringBuilder(\"\u003Ch1>商品列表\u003C\u002Fh1>\u003Cul>\");   \u002F\u002F 本该是视图的活：渲染\n        for (Product p : DB.values()) {\n            html.append(\"\u003Cli>\").append(p.getName())\n                .append(\" \").append(p.getPrice()).append(\"元\u003C\u002Fli>\");\n        }\n        return html.append(\"\u003C\u002Ful>\").toString();\n    }\n}\n```\n\n跑起来是能跑的：\n\n```\n[请求] 调价 P101 +50（50 > 199×20%，被业务规则拒绝）→\n\u003Cp style='color:red'>单次调价不能超过原价的 20%\u003C\u002Fp>\n[请求] 调价 P103 +200（200 ≤ 1899×20%，通过）→\n\u003Ch1>商品列表\u003C\u002Fh1>\u003Cul>\u003Cli>机械键盘 199元\u003C\u002Fli>\u003Cli>无线鼠标 89元\u003C\u002Fli>\u003Cli>4K 显示器 2099元\u003C\u002Fli>\u003C\u002Ful>\n```\n\n但它是一道四合一的乱炖：改样式要通读业务规则，改规则要在 HTML 里找，想加个控制台输出？整个方法复制一遍。最痛的是**测试**——想验证\"20% 上限\"这条规则，断言里得写 `assertTrue(result.contains(\"color:red\"))`——**业务规则的测试竟然要解析 HTML**。\n\n### 4.2 正解：Model、View、Controller 三分\n\n**Model：数据 + 业务规则，谁都不认识。**\n\n```java\npublic class Catalog {\n\n    private final Map\u003CString, Product> products = new LinkedHashMap\u003C>();\n\n    public Catalog() { \u002F* 灌三条商品数据，略 *\u002F }\n\n    public List\u003CProduct> findAll() {\n        return List.copyOf(products.values());\n    }\n\n    public Product find(String id) {\n        Product product = products.get(id);\n        if (product == null) {\n            throw new IllegalArgumentException(\"商品不存在：\" + id);\n        }\n        return product;\n    }\n\n    \u002F** 业务规则住在模型家：单次调价不能超过原价的 20% *\u002F\n    public void raisePrice(String id, int delta) {\n        Product product = find(id);\n        if (delta \u003C= 0) {\n            throw new IllegalArgumentException(\"调价幅度必须为正数\");\n        }\n        if (delta > product.getPrice() * 0.2) {\n            throw new IllegalArgumentException(\"单次调价不能超过原价的 20%\");\n        }\n        product.setPrice(product.getPrice() + delta);\n    }\n}\n```\n\n**View：只管\"模型数据长什么样\"。**\n\n```java\npublic interface ProductView {\n    String render(List\u003CProduct> products);\n    String renderError(String message);\n}\n\npublic class ConsoleView implements ProductView {\n    @Override\n    public String render(List\u003CProduct> products) {\n        StringBuilder sb = new StringBuilder(\"------ 商品列表（控制台版）------\");\n        for (Product p : products) {\n            sb.append(String.format(\"%n  %s  %-8s %5d 元\", p.getId(), p.getName(), p.getPrice()));\n        }\n        return sb.toString();\n    }\n\n    @Override\n    public String renderError(String message) {\n        return \"  × \" + message;\n    }\n}\n```\n\nHtmlView 同款接口，输出换成 `\u003Ch1>` + `\u003Cul>` 的拼装——同一个 Model 的另一张皮。\n\n**Controller：请求的接待员，只做翻译转发。**\n\n```java\npublic class ProductController {\n\n    private final Catalog catalog;     \u002F\u002F Model\n    private final ProductView view;    \u002F\u002F View\n\n    public ProductController(Catalog catalog, ProductView view) {\n        this.catalog = catalog;\n        this.view = view;\n    }\n\n    \u002F** 模拟 HTTP 请求入口：\"list\" \u002F \"raise P101 30\" *\u002F\n    public String dispatch(String request) {\n        String[] parts = request.trim().split(\"\\\\s+\");\n        switch (parts[0]) {\n            case \"list\" -> { return view.render(catalog.findAll()); }\n            case \"raise\" -> {\n                try {\n                    catalog.raisePrice(parts[1], Integer.parseInt(parts[2]));\n                    return view.render(catalog.findAll());\n                } catch (IllegalArgumentException | ArrayIndexOutOfBoundsException e) {\n                    return view.renderError(e.getMessage());\n                }\n            }\n            default -> { return view.renderError(\"未知请求：\" + request); }\n        }\n    }\n}\n```\n\n**收钱。** 先看 Model 独立测试——这是反面教材永远给不了的待遇：\n\n```java\nCatalog catalog = new Catalog();\ntry {\n    catalog.raisePrice(\"P101\", 999);   \u002F\u002F 999 > 199×20%\n} catch (IllegalArgumentException e) {\n    System.out.println(\"[Model 独立测试] \" + e.getMessage() + \"（零 UI 参与，直接断言异常）\");\n}\n```\n\n再组装上线，跑完整流程：\n\n```java\nProductController controller = new ProductController(catalog, new ConsoleView());\n\nSystem.out.println(controller.dispatch(\"list\"));\nSystem.out.println(controller.dispatch(\"raise P101 30\"));     \u002F\u002F 通过\nSystem.out.println(controller.dispatch(\"raise P102 999\"));    \u002F\u002F 拒绝\n\n\u002F\u002F 换视图：同一个 Model、同一个 Controller，只把 View 换成 HtmlView\nProductController htmlController = new ProductController(catalog, new HtmlView());\nSystem.out.println(htmlController.dispatch(\"list\"));\n```\n\n运行输出：\n\n```\n========== 正解：MVC 三层各司其职 ==========\n[Model 独立测试] 单次调价不能超过原价的 20%（零 UI 参与，直接断言异常）\n\n[请求] \"list\"\n------ 商品列表（控制台版）------\n  P101  机械键盘       199 元\n  P102  无线鼠标        89 元\n  P103  4K 显示器    1899 元\n\n[请求] \"raise P101 30\"（30 ≤ 199×20%，通过）\n------ 商品列表（控制台版）------\n  P101  机械键盘       229 元\n  P102  无线鼠标        89 元\n  P103  4K 显示器    1899 元\n\n[请求] \"raise P102 999\"（999 > 89×20%，拒绝）\n  × 单次调价不能超过原价的 20%\n\n—— 换视图：同一个 Model、同一个 Controller，只把 View 换成 HtmlView ——\n[请求] \"list\"（HTML 版）\n\u003Ch1>商品列表\u003C\u002Fh1>\u003Cul>\u003Cli>机械键盘 229元\u003C\u002Fli>\u003Cli>无线鼠标 89元\u003C\u002Fli>\u003Cli>4K 显示器 1899元\u003C\u002Fli>\u003C\u002Ful>\n```\n\n最后一组对照是 MVC 的含金量时刻：ConsoleView 换 HtmlView，**Catalog 一行没改、ProductController 一行没改**——只换了一个构造器参数。反过来，\"20% 上限\"改成\"10%\"，只动 Catalog 一个方法，两个视图连知道都不需要知道。\n\n### 4.3 三处细节\n\n**细节一：View 认识的是模型的数据，不是模型本身。** 本文 ProductView 的接口签名是 `render(List\u003CProduct>)`——View 依赖 List 和 Product，**不依赖 Catalog 这个类**。这不是巧合是刻意：View 需要\"渲染什么\"，不需要\"谁来提供\"。将来 Catalog 换成 RemoteCatalog、ElasticSearchCatalog，只要吐出来的还是 List\\\u003CProduct\\>，所有视图无动于衷。依赖越窄，变化越贵不了。\n\n**细节二：View 是被注入的策略。** Controller 构造器接收 ProductView，换视图等于换策略——⑮ 策略模式的形制。Spring MVC 把这一步做成了流水线：Controller 返回逻辑视图名 + Model 数据（ModelAndView），ViewResolver 按名字解析出真视图（JSP、Thymeleaf、Freemarker、JSON 序列化器）——同一个 Controller 方法，浏览器请求返回 HTML 页、Ajax 请求返回 JSON，内容协商全自动。本文手动的 `new HtmlView(...)`，在 Spring 里叫 ViewResolver 链。\n\n**细节三：Controller 里出现业务判断就该报警。** `if (delta > product.getPrice() * 0.2)` 这行代码放进 Controller，功能上一模一样，编译器毫无意见——但它意味着这条规则只有走这个 Controller 的请求才能享受，第二个入口（APP 接口、定时任务）进来就得复制一遍。判断标准很简单：**这段逻辑换个入口还成立吗？成立就住 Model；只有这个入口成立才是 Controller 的事**。贫血模型的病因，十有八九是这条判断没做。\n\n## 五、在框架里找它\n\n1. **Spring MVC**：J2EE 模式目录的集大成者。@Controller 对应 Controller，@Service + @Repository 对应 Model（业务与数据分层），Thymeleaf\u002FJSP 对应 View；DispatcherServlet 统一接客（它是 ㉚ 前端控制器模式的主角，此处先按下不表）。你每天写的三层注解，就是 MVC 的实体化。\n\n2. **Struts**：J2EE 时代的 MVC 旗手。Action 类是 Controller，ActionForm\u002FJavaBean 是 Model，JSP 是 View——它的兴衰说明 MVC 结构本身不挑实现技术，挑的是实现得好不好用。\n\n3. **iOS Cocoa MVC**：苹果官方文档直接教 MVC——UIViewController + UIView + Model。移动端与桌面端才是 MVC 的出生地：数据变了，所有开着的窗口同时刷新，靠的就是 Model 广播事件。\n\n4. **前端框架**：React 组件的 `state → render()` 单向数据流，是\"Model 变 → View 跟着变\"的函数式转译；Vue、Angular 则走得更远——第六节的 MVVM。\n\n## 六、五十年进化树：MVC → MVP → MVVM\n\nMVC 从 1979 年活到今天，没死，但换了四次形态。四个代际的差别只有一个问题：**View 怎么知道该更新了？**\n\n\u003Ctable>\u003Cthead>\u003Ctr>\u003Cth>代际\u003C\u002Fth>\u003Cth>View 怎么更新\u003C\u002Fth>\u003Cth>代表\u003C\u002Fth>\u003Cth>适合\u003C\u002Fth>\u003C\u002Ftr>\u003C\u002Fthead>\u003Ctbody>\u003Ctr>\u003Ctd>经典 MVC（1979）\u003C\u002Ftd>\u003Ctd>View 订阅 Model，Model 变了主动广播\u003C\u002Ftd>\u003Ctd>Smalltalk\u003C\u002Ftd>\u003Ctd>常驻内存的桌面应用\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>Web MVC \u002F Model 2（1998）\u003C\u002Ftd>\u003Ctd>View 无法常驻；Controller 把 Model 数据喂给 View，渲染完就销毁\u003C\u002Ftd>\u003Ctd>JSP Model 2、Struts、Spring MVC\u003C\u002Ftd>\u003Ctd>HTTP 请求-响应\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>MVP\u003C\u002Ftd>\u003Ctd>Presenter 持有 View 接口，一步一指挥；View 完全被动\u003C\u002Ftd>\u003Ctd>WinForms、早期 Android\u003C\u002Ftd>\u003Ctd>要单测 View 逻辑\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>MVVM\u003C\u002Ftd>\u003Ctd>数据绑定：View 声明式绑定 ViewModel 的属性，双向同步\u003C\u002Ftd>\u003Ctd>WPF、Vue、Angular\u003C\u002Ftd>\u003Ctd>声明式 UI\u003C\u002Ftd>\u003C\u002Ftr>\u003C\u002Ftbody>\u003C\u002Ftable>\n\n看出主旋律了吗？**View 越来越被动，越来越声明式。** 经典 MVC 里 View 还要自己订阅、自己监听；Web MVC 里 View 变成一性性的渲染模板；MVP 里 View 退化成一个接口实现；MVVM 里 View 干脆只声明\"我绑定谁\"——**人写的命令式代码步步后退，框架的声明式机制步步前进**。这和 ⑭ 模板方法到 ⑮ 策略的演进一脉相承：好莱坞原则——别打电话给我们，我们会打给你（Don't call us, we'll call you）。\n\n顺带一提，前端面试爱考的\"双向绑定\"，本质是 MVVM 在 View 与 ViewModel 之间装了两条单向数据流：用户输入 → 更新 ViewModel（表单监听），ViewModel 变化 → 刷新 View（订阅通知）。Vue 的 v-model、Angular 的 ngModel，拆开看还是 ⑯ 观察者那套老手艺。\n\n## 写在最后\n\n- MVC 是架构模式，1979 年出生，比 GoF 早十五年——管的是整个应用的分层，不是一个类的设计\n- 三分职责：Model 管数据和规则，View 管呈现，Controller 管翻译转发\n- 依赖方向定生死：Model 谁都不认识——业务逻辑永远住 Model，Controller 里出现业务判断就该报警\n- 换视图零成本、Model 独立测试，是 MVC 兑现的两张支票\n- 五十年进化只有一个主旋律：View 越来越被动，声明式步步进逼\n\n㉖ 的 Controller 直接持有 Catalog、直接调用，丝滑顺畅——因为它们在同一个进程里。可 EJB 时代的真实画风是：业务层住在另一台服务器上。表示层每次调用业务，都要 JNDI 查找、建网络连接、翻译远程异常、失败重试……Controller 迟早被这些\"远程破事\"淹没。谁来替表示层把这些脏活挡在门外？——业务代表模式，下一篇见。\n","\u003Cblockquote>\n \u003Cp>J2EE 模式第一站。\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Cp>GoF 二十三式集齐，龙珠在手。可回头看看真实的企业应用：请求从浏览器涌进来，一个页面背后是数据存取、业务规则、界面渲染、请求分发……二十三个模式各管一段，谁来给它们排兵布阵？从 ㉖ 起，本系列进入最后一片大陆：\u003Cstrong>J2EE 企业应用模式\u003C\u002Fstrong>——八种模式，讲的就是\"分层协作\"这件事。第一站，也是最响亮的一站：MVC。\u003C\u002Fp>\n\u003Cp>先看痛点。最早的 JSP 页面（人称 Model 1 时代）长这样：页面就是应用，应用就是页面——一个方法里，参数解析、查数据库、业务规则、拼 HTML 四件事糊成一团。想改个按钮配色？通读两百行业务逻辑。想改条促销规则？在同一坨代码里翻 HTML。\u003Cstrong>四件事有四个变化方向，糊在一起，每次变化都是全量阅读。\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Cp>MVC 开出的药方只有一句话：\u003Cstrong>按\"谁变化\"切分，而不是按\"做什么\"堆砌。\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Ch2>一、MVC 模式是什么\u003C\u002Fh2>\n\u003Cp>MVC 不是 GoF 的孩子——它比 GoF 早出生十五年。1979 年，挪威工程师 Trygve Reenskaug 在 Xerox PARC 访学期间，为 Smalltalk-79 提出了这套结构（最初叫 Thing-Model-View-Editor，后简化）。GoF 管一个类怎么设计，MVC 管\u003Cstrong>整个应用怎么分层\u003C\u002Fstrong>——它是架构模式，粒量更大，但药理相通。\u003C\u002Fp>\n\u003Cp>三个角色：\u003C\u002Fp>\n\u003Ctable>\n \u003Cthead>\n  \u003Ctr>\n   \u003Cth>角色\u003C\u002Fth>\n   \u003Cth>职责\u003C\u002Fth>\n   \u003Cth>认识谁\u003C\u002Fth>\n   \u003Cth>本文对应\u003C\u002Fth>\n  \u003C\u002Ftr>\n \u003C\u002Fthead>\n \u003Ctbody>\n  \u003Ctr>\n   \u003Ctd>Model（模型）\u003C\u002Ftd>\n   \u003Ctd>数据 + 业务规则，应用的灵魂\u003C\u002Ftd>\n   \u003Ctd>**谁都不认识**\u003C\u002Ftd>\n   \u003Ctd>Catalog + Product\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>View（视图）\u003C\u002Ftd>\n   \u003Ctd>把模型数据渲染成用户能看的样子\u003C\u002Ftd>\n   \u003Ctd>只认识模型的数据\u003C\u002Ftd>\n   \u003Ctd>ConsoleView \u002F HtmlView\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>Controller（控制器）\u003C\u002Ftd>\n   \u003Ctd>接收请求、翻译、调模型、选视图\u003C\u002Ftd>\n   \u003Ctd>认识另外两位\u003C\u002Ftd>\n   \u003Ctd>ProductController\u003C\u002Ftd>\n  \u003C\u002Ftr>\n \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>职责边界一句话说清：\u003Cstrong>Model 不知道自己被谁看着，View 不关心数据对不对，Controller 两头都不懂行——只管翻译转发。\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Cp>注意那张表中间一列——\u003Cstrong>依赖方向\u003C\u002Fstrong>才是 MVC 的灵魂，比\"三个角色\"本身重要得多：Model 不依赖任何人（所以能独立测试、独立演化）；View 只依赖 Model 的数据（所以换个皮不伤筋骨）；Controller 依赖两位（所以它是粘合剂，也是唯一知道全局的）。所有\"这段代码该放哪\"的争论，答案都藏在依赖方向里。\u003C\u002Fp>\n\u003Ch2>二、优点与缺点\u003C\u002Fh2>\n\u003Cp>\u003Cstrong>优点：\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Col>\n \u003Cli>\u003Cstrong>变化隔离\u003C\u002Fstrong>：界面天天改、规则偶尔改、参数解析很少改——三个变化速度不同的东西装进三个房间，各自折腾互不惊扰。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>多视图复用\u003C\u002Fstrong>：同一份 Model，控制台、HTML、JSON 接口、APP 页面各挂各的 View——本文后半段当场演示。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>Model 可独立测试\u003C\u002Fstrong>：业务规则住进 Model 后，测试它不需要启动任何界面、不需要 mock 任何渲染器——直接调方法、断言异常。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>并行开发\u003C\u002Fstrong>：接口约定好，一个人写 View、一个人写 Model，Controller 最后联调——前端后端分工的鼻祖。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>\u003Cstrong>缺点：\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Col>\n \u003Cli>\u003Cstrong>类数量膨胀\u003C\u002Fstrong>：一个页面拆成三个（组）类，demo 级应用写 MVC 是自虐。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>Controller 易发福\u003C\u002Fstrong>：路由、参数解析、视图选择全在它身上，一不留神就长成\"上帝控制器\"；更常见的伴生病是\u003Cstrong>贫血模型\u003C\u002Fstrong>——Model 只剩 getter\u002Fsetter，业务逻辑全被 Controller 吸走，MVC 名存实亡。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>职责划分靠自觉\u003C\u002Fstrong>：\u003Ccode>if (delta &gt; price * 0.2)\u003C\u002Fcode> 写在 Controller 里编译器不报错——MVC 的边界是纪律问题，没有语言机制强制。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>简单场景过度设计\u003C\u002Fstrong>：只有一个静态页的应用、纯计算无界面的程序，没有 V 可言。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2>三、什么时候用\u003C\u002Fh2>\n\u003Cp>判断标准：\u003Cstrong>有界面（或多种输出格式），且业务逻辑值得独立于界面演化。\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Cul>\n \u003Cli>\u003Cstrong>典型 Web 应用\u003C\u002Fstrong>：页面样式随产品经理的心情天天变，业务规则随法务的意见偶尔变——变化速度不同，就该分房住\u003C\u002Fli>\n \u003Cli>\u003Cstrong>多渠道输出\u003C\u002Fstrong>：同一数据要出 Web 页、APP 接口、小程序、开放 API——一份 Model 挂一排 View\u003C\u002Fli>\n \u003Cli>\u003Cstrong>桌面应用\u003C\u002Fstrong>：MVC 的出生场景——数据变了，多个打开的窗口同时刷新\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>反面对照：\u003Cstrong>纯后端服务\u003C\u002Fstrong>——没有 View，分层交给 ㉙ DAO 和服务层模式；\u003Cstrong>一次性脚本、demo\u003C\u002Fstrong>——别自虐；\u003Cstrong>Model 和 View 天生耦合的应用\u003C\u002Fstrong>（比如游戏渲染引擎），强行 MVC 只会制造搬运代码。\u003C\u002Fp>\n\u003Ch2>四、实现方式：商品目录\u003C\u002Fh2>\n\u003Ch3>4.1 反面教材：一个类包揽一切\u003C\u002Fh3>\n\u003Cp>JSP Model 1 时代的画风：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">public class BadPage {\n\n    private static final Map&lt;String, Product&gt; DB = new LinkedHashMap&lt;&gt;();\n    \u002F\u002F static 初始化块灌三条商品数据，略\n\n    \u002F\u002F \"页面\"：一个方法从请求干到 HTML\n    public static String raisePrice(String id, String deltaText) {\n        int delta;\n        try {\n            delta = Integer.parseInt(deltaText);            \u002F\u002F 本该是控制器的活：参数解析\n        } catch (NumberFormatException e) {\n            return \"&lt;p style='color:red'&gt;参数必须是数字&lt;\u002Fp&gt;\";  \u002F\u002F 本该是视图的活：错误展示\n        }\n        Product product = DB.get(id);                        \u002F\u002F 本该是模型的活：数据存取\n        if (product == null) {\n            return \"&lt;p style='color:red'&gt;商品不存在：\" + id + \"&lt;\u002Fp&gt;\";\n        }\n        if (delta &gt; product.getPrice() * 0.2) {              \u002F\u002F 本该是模型的活：业务规则\n            return \"&lt;p style='color:red'&gt;单次调价不能超过原价的 20%&lt;\u002Fp&gt;\";\n        }\n        product.setPrice(product.getPrice() + delta);        \u002F\u002F 本该是模型的活：状态变更\n        StringBuilder html = new StringBuilder(\"&lt;h1&gt;商品列表&lt;\u002Fh1&gt;&lt;ul&gt;\");   \u002F\u002F 本该是视图的活：渲染\n        for (Product p : DB.values()) {\n            html.append(\"&lt;li&gt;\").append(p.getName())\n                .append(\" \").append(p.getPrice()).append(\"元&lt;\u002Fli&gt;\");\n        }\n        return html.append(\"&lt;\u002Ful&gt;\").toString();\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>跑起来是能跑的：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>[请求] 调价 P101 +50（50 &gt; 199×20%，被业务规则拒绝）→\n&lt;p style='color:red'&gt;单次调价不能超过原价的 20%&lt;\u002Fp&gt;\n[请求] 调价 P103 +200（200 ≤ 1899×20%，通过）→\n&lt;h1&gt;商品列表&lt;\u002Fh1&gt;&lt;ul&gt;&lt;li&gt;机械键盘 199元&lt;\u002Fli&gt;&lt;li&gt;无线鼠标 89元&lt;\u002Fli&gt;&lt;li&gt;4K 显示器 2099元&lt;\u002Fli&gt;&lt;\u002Ful&gt;\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>但它是一道四合一的乱炖：改样式要通读业务规则，改规则要在 HTML 里找，想加个控制台输出？整个方法复制一遍。最痛的是\u003Cstrong>测试\u003C\u002Fstrong>——想验证\"20% 上限\"这条规则，断言里得写 \u003Ccode>assertTrue(result.contains(\"color:red\"))\u003C\u002Fcode>——\u003Cstrong>业务规则的测试竟然要解析 HTML\u003C\u002Fstrong>。\u003C\u002Fp>\n\u003Ch3>4.2 正解：Model、View、Controller 三分\u003C\u002Fh3>\n\u003Cp>\u003Cstrong>Model：数据 + 业务规则，谁都不认识。\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">public class Catalog {\n\n    private final Map&lt;String, Product&gt; products = new LinkedHashMap&lt;&gt;();\n\n    public Catalog() { \u002F* 灌三条商品数据，略 *\u002F }\n\n    public List&lt;Product&gt; findAll() {\n        return List.copyOf(products.values());\n    }\n\n    public Product find(String id) {\n        Product product = products.get(id);\n        if (product == null) {\n            throw new IllegalArgumentException(\"商品不存在：\" + id);\n        }\n        return product;\n    }\n\n    \u002F** 业务规则住在模型家：单次调价不能超过原价的 20% *\u002F\n    public void raisePrice(String id, int delta) {\n        Product product = find(id);\n        if (delta &lt;= 0) {\n            throw new IllegalArgumentException(\"调价幅度必须为正数\");\n        }\n        if (delta &gt; product.getPrice() * 0.2) {\n            throw new IllegalArgumentException(\"单次调价不能超过原价的 20%\");\n        }\n        product.setPrice(product.getPrice() + delta);\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Cstrong>View：只管\"模型数据长什么样\"。\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">public interface ProductView {\n    String render(List&lt;Product&gt; products);\n    String renderError(String message);\n}\n\npublic class ConsoleView implements ProductView {\n    @Override\n    public String render(List&lt;Product&gt; products) {\n        StringBuilder sb = new StringBuilder(\"------ 商品列表（控制台版）------\");\n        for (Product p : products) {\n            sb.append(String.format(\"%n  %s  %-8s %5d 元\", p.getId(), p.getName(), p.getPrice()));\n        }\n        return sb.toString();\n    }\n\n    @Override\n    public String renderError(String message) {\n        return \"  × \" + message;\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>HtmlView 同款接口，输出换成 \u003Ccode>&lt;h1&gt;\u003C\u002Fcode> + \u003Ccode>&lt;ul&gt;\u003C\u002Fcode> 的拼装——同一个 Model 的另一张皮。\u003C\u002Fp>\n\u003Cp>\u003Cstrong>Controller：请求的接待员，只做翻译转发。\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">public class ProductController {\n\n    private final Catalog catalog;     \u002F\u002F Model\n    private final ProductView view;    \u002F\u002F View\n\n    public ProductController(Catalog catalog, ProductView view) {\n        this.catalog = catalog;\n        this.view = view;\n    }\n\n    \u002F** 模拟 HTTP 请求入口：\"list\" \u002F \"raise P101 30\" *\u002F\n    public String dispatch(String request) {\n        String[] parts = request.trim().split(\"\\\\s+\");\n        switch (parts[0]) {\n            case \"list\" -&gt; { return view.render(catalog.findAll()); }\n            case \"raise\" -&gt; {\n                try {\n                    catalog.raisePrice(parts[1], Integer.parseInt(parts[2]));\n                    return view.render(catalog.findAll());\n                } catch (IllegalArgumentException | ArrayIndexOutOfBoundsException e) {\n                    return view.renderError(e.getMessage());\n                }\n            }\n            default -&gt; { return view.renderError(\"未知请求：\" + request); }\n        }\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Cstrong>收钱。\u003C\u002Fstrong> 先看 Model 独立测试——这是反面教材永远给不了的待遇：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">Catalog catalog = new Catalog();\ntry {\n    catalog.raisePrice(\"P101\", 999);   \u002F\u002F 999 &gt; 199×20%\n} catch (IllegalArgumentException e) {\n    System.out.println(\"[Model 独立测试] \" + e.getMessage() + \"（零 UI 参与，直接断言异常）\");\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>再组装上线，跑完整流程：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">ProductController controller = new ProductController(catalog, new ConsoleView());\n\nSystem.out.println(controller.dispatch(\"list\"));\nSystem.out.println(controller.dispatch(\"raise P101 30\"));     \u002F\u002F 通过\nSystem.out.println(controller.dispatch(\"raise P102 999\"));    \u002F\u002F 拒绝\n\n\u002F\u002F 换视图：同一个 Model、同一个 Controller，只把 View 换成 HtmlView\nProductController htmlController = new ProductController(catalog, new HtmlView());\nSystem.out.println(htmlController.dispatch(\"list\"));\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>运行输出：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>========== 正解：MVC 三层各司其职 ==========\n[Model 独立测试] 单次调价不能超过原价的 20%（零 UI 参与，直接断言异常）\n\n[请求] \"list\"\n------ 商品列表（控制台版）------\n  P101  机械键盘       199 元\n  P102  无线鼠标        89 元\n  P103  4K 显示器    1899 元\n\n[请求] \"raise P101 30\"（30 ≤ 199×20%，通过）\n------ 商品列表（控制台版）------\n  P101  机械键盘       229 元\n  P102  无线鼠标        89 元\n  P103  4K 显示器    1899 元\n\n[请求] \"raise P102 999\"（999 &gt; 89×20%，拒绝）\n  × 单次调价不能超过原价的 20%\n\n—— 换视图：同一个 Model、同一个 Controller，只把 View 换成 HtmlView ——\n[请求] \"list\"（HTML 版）\n&lt;h1&gt;商品列表&lt;\u002Fh1&gt;&lt;ul&gt;&lt;li&gt;机械键盘 229元&lt;\u002Fli&gt;&lt;li&gt;无线鼠标 89元&lt;\u002Fli&gt;&lt;li&gt;4K 显示器 1899元&lt;\u002Fli&gt;&lt;\u002Ful&gt;\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>最后一组对照是 MVC 的含金量时刻：ConsoleView 换 HtmlView，\u003Cstrong>Catalog 一行没改、ProductController 一行没改\u003C\u002Fstrong>——只换了一个构造器参数。反过来，\"20% 上限\"改成\"10%\"，只动 Catalog 一个方法，两个视图连知道都不需要知道。\u003C\u002Fp>\n\u003Ch3>4.3 三处细节\u003C\u002Fh3>\n\u003Cp>\u003Cstrong>细节一：View 认识的是模型的数据，不是模型本身。\u003C\u002Fstrong> 本文 ProductView 的接口签名是 \u003Ccode>render(List&lt;Product&gt;)\u003C\u002Fcode>——View 依赖 List 和 Product，\u003Cstrong>不依赖 Catalog 这个类\u003C\u002Fstrong>。这不是巧合是刻意：View 需要\"渲染什么\"，不需要\"谁来提供\"。将来 Catalog 换成 RemoteCatalog、ElasticSearchCatalog，只要吐出来的还是 List&lt;Product&gt;，所有视图无动于衷。依赖越窄，变化越贵不了。\u003C\u002Fp>\n\u003Cp>\u003Cstrong>细节二：View 是被注入的策略。\u003C\u002Fstrong> Controller 构造器接收 ProductView，换视图等于换策略——⑮ 策略模式的形制。Spring MVC 把这一步做成了流水线：Controller 返回逻辑视图名 + Model 数据（ModelAndView），ViewResolver 按名字解析出真视图（JSP、Thymeleaf、Freemarker、JSON 序列化器）——同一个 Controller 方法，浏览器请求返回 HTML 页、Ajax 请求返回 JSON，内容协商全自动。本文手动的 \u003Ccode>new HtmlView(...)\u003C\u002Fcode>，在 Spring 里叫 ViewResolver 链。\u003C\u002Fp>\n\u003Cp>\u003Cstrong>细节三：Controller 里出现业务判断就该报警。\u003C\u002Fstrong> \u003Ccode>if (delta &gt; product.getPrice() * 0.2)\u003C\u002Fcode> 这行代码放进 Controller，功能上一模一样，编译器毫无意见——但它意味着这条规则只有走这个 Controller 的请求才能享受，第二个入口（APP 接口、定时任务）进来就得复制一遍。判断标准很简单：\u003Cstrong>这段逻辑换个入口还成立吗？成立就住 Model；只有这个入口成立才是 Controller 的事\u003C\u002Fstrong>。贫血模型的病因，十有八九是这条判断没做。\u003C\u002Fp>\n\u003Ch2>五、在框架里找它\u003C\u002Fh2>\n\u003Col>\n \u003Cli>\n  \u003Cp>\u003Cstrong>Spring MVC\u003C\u002Fstrong>：J2EE 模式目录的集大成者。@Controller 对应 Controller，@Service + @Repository 对应 Model（业务与数据分层），Thymeleaf\u002FJSP 对应 View；DispatcherServlet 统一接客（它是 ㉚ 前端控制器模式的主角，此处先按下不表）。你每天写的三层注解，就是 MVC 的实体化。\u003C\u002Fp>\u003C\u002Fli>\n \u003Cli>\n  \u003Cp>\u003Cstrong>Struts\u003C\u002Fstrong>：J2EE 时代的 MVC 旗手。Action 类是 Controller，ActionForm\u002FJavaBean 是 Model，JSP 是 View——它的兴衰说明 MVC 结构本身不挑实现技术，挑的是实现得好不好用。\u003C\u002Fp>\u003C\u002Fli>\n \u003Cli>\n  \u003Cp>\u003Cstrong>iOS Cocoa MVC\u003C\u002Fstrong>：苹果官方文档直接教 MVC——UIViewController + UIView + Model。移动端与桌面端才是 MVC 的出生地：数据变了，所有开着的窗口同时刷新，靠的就是 Model 广播事件。\u003C\u002Fp>\u003C\u002Fli>\n \u003Cli>\n  \u003Cp>\u003Cstrong>前端框架\u003C\u002Fstrong>：React 组件的 \u003Ccode>state → render()\u003C\u002Fcode> 单向数据流，是\"Model 变 → View 跟着变\"的函数式转译；Vue、Angular 则走得更远——第六节的 MVVM。\u003C\u002Fp>\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2>六、五十年进化树：MVC → MVP → MVVM\u003C\u002Fh2>\n\u003Cp>MVC 从 1979 年活到今天，没死，但换了四次形态。四个代际的差别只有一个问题：\u003Cstrong>View 怎么知道该更新了？\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Ctable>\n \u003Cthead>\n  \u003Ctr>\n   \u003Cth>代际\u003C\u002Fth>\n   \u003Cth>View 怎么更新\u003C\u002Fth>\n   \u003Cth>代表\u003C\u002Fth>\n   \u003Cth>适合\u003C\u002Fth>\n  \u003C\u002Ftr>\n \u003C\u002Fthead>\n \u003Ctbody>\n  \u003Ctr>\n   \u003Ctd>经典 MVC（1979）\u003C\u002Ftd>\n   \u003Ctd>View 订阅 Model，Model 变了主动广播\u003C\u002Ftd>\n   \u003Ctd>Smalltalk\u003C\u002Ftd>\n   \u003Ctd>常驻内存的桌面应用\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>Web MVC \u002F Model 2（1998）\u003C\u002Ftd>\n   \u003Ctd>View 无法常驻；Controller 把 Model 数据喂给 View，渲染完就销毁\u003C\u002Ftd>\n   \u003Ctd>JSP Model 2、Struts、Spring MVC\u003C\u002Ftd>\n   \u003Ctd>HTTP 请求-响应\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>MVP\u003C\u002Ftd>\n   \u003Ctd>Presenter 持有 View 接口，一步一指挥；View 完全被动\u003C\u002Ftd>\n   \u003Ctd>WinForms、早期 Android\u003C\u002Ftd>\n   \u003Ctd>要单测 View 逻辑\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>MVVM\u003C\u002Ftd>\n   \u003Ctd>数据绑定：View 声明式绑定 ViewModel 的属性，双向同步\u003C\u002Ftd>\n   \u003Ctd>WPF、Vue、Angular\u003C\u002Ftd>\n   \u003Ctd>声明式 UI\u003C\u002Ftd>\n  \u003C\u002Ftr>\n \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>看出主旋律了吗？\u003Cstrong>View 越来越被动，越来越声明式。\u003C\u002Fstrong> 经典 MVC 里 View 还要自己订阅、自己监听；Web MVC 里 View 变成一性性的渲染模板；MVP 里 View 退化成一个接口实现；MVVM 里 View 干脆只声明\"我绑定谁\"——\u003Cstrong>人写的命令式代码步步后退，框架的声明式机制步步前进\u003C\u002Fstrong>。这和 ⑭ 模板方法到 ⑮ 策略的演进一脉相承：好莱坞原则——别打电话给我们，我们会打给你（Don't call us, we'll call you）。\u003C\u002Fp>\n\u003Cp>顺带一提，前端面试爱考的\"双向绑定\"，本质是 MVVM 在 View 与 ViewModel 之间装了两条单向数据流：用户输入 → 更新 ViewModel（表单监听），ViewModel 变化 → 刷新 View（订阅通知）。Vue 的 v-model、Angular 的 ngModel，拆开看还是 ⑯ 观察者那套老手艺。\u003C\u002Fp>\n\u003Ch2>写在最后\u003C\u002Fh2>\n\u003Cul>\n \u003Cli>MVC 是架构模式，1979 年出生，比 GoF 早十五年——管的是整个应用的分层，不是一个类的设计\u003C\u002Fli>\n \u003Cli>三分职责：Model 管数据和规则，View 管呈现，Controller 管翻译转发\u003C\u002Fli>\n \u003Cli>依赖方向定生死：Model 谁都不认识——业务逻辑永远住 Model，Controller 里出现业务判断就该报警\u003C\u002Fli>\n \u003Cli>换视图零成本、Model 独立测试，是 MVC 兑现的两张支票\u003C\u002Fli>\n \u003Cli>五十年进化只有一个主旋律：View 越来越被动，声明式步步进逼\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>㉖ 的 Controller 直接持有 Catalog、直接调用，丝滑顺畅——因为它们在同一个进程里。可 EJB 时代的真实画风是：业务层住在另一台服务器上。表示层每次调用业务，都要 JNDI 查找、建网络连接、翻译远程异常、失败重试……Controller 迟早被这些\"远程破事\"淹没。谁来替表示层把这些脏活挡在门外？——业务代表模式，下一篇见。\u003C\u002Fp>",[],"index,follow",null,{"code":4,"message":5,"data":44},[45,54,63],{"id":46,"slug":47,"title":48,"summary":49,"coverUrl":9,"categoryId":32,"status":33,"topFlag":4,"viewCount":50,"likeCount":4,"wordCount":51,"readingMinutes":52,"publishedAt":53},"2113935138768293890","design-pattern-30","设计模式修炼之路 ㉚ 前端控制器模式——所有请求，先过我这道门","横切关注点写一次全站无死角、抄漏一处校验就是一个洞、门神必须薄、路由分散声明集中执行、与 MVC 的 C 对账",2755,5825,15,"2025-05-12T18:40:38",{"id":55,"slug":56,"title":57,"summary":58,"coverUrl":9,"categoryId":32,"status":33,"topFlag":4,"viewCount":59,"likeCount":4,"wordCount":60,"readingMinutes":61,"publishedAt":62},"2113907939247919106","design-pattern-01","设计模式修炼之路 ① 初识设计模式——先把地图摊开","这是系列的第一篇，没有一行代码：设计模式是什么、模式分哪四大家族、六大原则长什么样。先把地图摊开，路从下一篇开始走。",3268,1903,5,"2023-09-10T12:19:35",{"id":64,"slug":65,"title":66,"summary":67,"coverUrl":9,"categoryId":32,"status":33,"topFlag":4,"viewCount":68,"likeCount":4,"wordCount":69,"readingMinutes":70,"publishedAt":71},"2113934922031828994","design-pattern-27","设计模式修炼之路 ㉗ 业务代表模式——替表示层挡住远程的脏活","替表示层挡住远程脏活、查找+重试+异常翻译一肩挑、重试风暴、外观\u002F代理\u002F业务代表三兄弟对账、Feign 转世",1811,6147,16,"2025-03-25T21:44:13",{"code":4,"message":5,"data":73},{"prev":74,"next":83},{"id":75,"slug":76,"title":77,"summary":78,"coverUrl":9,"categoryId":32,"status":33,"topFlag":4,"viewCount":79,"likeCount":4,"wordCount":80,"readingMinutes":81,"publishedAt":82},"2113934683157827585","design-pattern-25","设计模式修炼之路 ㉕ 解释器模式——让程序读懂规则","GoF 收官、规则即数据零发版、文法即代码、递归下降解析器、短路求值白送、解释 vs 编译 vs 配置表",3389,8660,22,"2025-01-19T13:11:38",{"id":64,"slug":65,"title":66,"summary":67,"coverUrl":9,"categoryId":32,"status":33,"topFlag":4,"viewCount":68,"likeCount":4,"wordCount":69,"readingMinutes":70,"publishedAt":71},{"code":4,"message":5,"data":85},[86,92,97,103],{"id":87,"name":88,"slug":89,"description":90,"sortOrder":4,"articleCount":91},"1946000000000000001","antlr4","tech","编程与技术笔记","13",{"id":32,"name":93,"slug":94,"description":95,"sortOrder":4,"articleCount":96},"设计模式","design-pattern","设计模式系列","34",{"id":98,"name":99,"slug":100,"description":101,"sortOrder":33,"articleCount":102},"1946000000000000002","随笔","essay","生活与思考","0",{"id":104,"name":105,"slug":106,"description":107,"sortOrder":108,"articleCount":109},"2105200093764698113","工具","tools","好用的工具推荐",2,"1",{"html":111,"toc":112},"\u003Cblockquote>\n \u003Cp>J2EE 模式第一站。\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Cp>GoF 二十三式集齐，龙珠在手。可回头看看真实的企业应用：请求从浏览器涌进来，一个页面背后是数据存取、业务规则、界面渲染、请求分发……二十三个模式各管一段，谁来给它们排兵布阵？从 ㉖ 起，本系列进入最后一片大陆：\u003Cstrong>J2EE 企业应用模式\u003C\u002Fstrong>——八种模式，讲的就是\"分层协作\"这件事。第一站，也是最响亮的一站：MVC。\u003C\u002Fp>\n\u003Cp>先看痛点。最早的 JSP 页面（人称 Model 1 时代）长这样：页面就是应用，应用就是页面——一个方法里，参数解析、查数据库、业务规则、拼 HTML 四件事糊成一团。想改个按钮配色？通读两百行业务逻辑。想改条促销规则？在同一坨代码里翻 HTML。\u003Cstrong>四件事有四个变化方向，糊在一起，每次变化都是全量阅读。\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Cp>MVC 开出的药方只有一句话：\u003Cstrong>按\"谁变化\"切分，而不是按\"做什么\"堆砌。\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Ch2 id=\"toc-1\">一、MVC 模式是什么\u003C\u002Fh2>\n\u003Cp>MVC 不是 GoF 的孩子——它比 GoF 早出生十五年。1979 年，挪威工程师 Trygve Reenskaug 在 Xerox PARC 访学期间，为 Smalltalk-79 提出了这套结构（最初叫 Thing-Model-View-Editor，后简化）。GoF 管一个类怎么设计，MVC 管\u003Cstrong>整个应用怎么分层\u003C\u002Fstrong>——它是架构模式，粒量更大，但药理相通。\u003C\u002Fp>\n\u003Cp>三个角色：\u003C\u002Fp>\n\u003Ctable>\n \u003Cthead>\n  \u003Ctr>\n   \u003Cth>角色\u003C\u002Fth>\n   \u003Cth>职责\u003C\u002Fth>\n   \u003Cth>认识谁\u003C\u002Fth>\n   \u003Cth>本文对应\u003C\u002Fth>\n  \u003C\u002Ftr>\n \u003C\u002Fthead>\n \u003Ctbody>\n  \u003Ctr>\n   \u003Ctd>Model（模型）\u003C\u002Ftd>\n   \u003Ctd>数据 + 业务规则，应用的灵魂\u003C\u002Ftd>\n   \u003Ctd>**谁都不认识**\u003C\u002Ftd>\n   \u003Ctd>Catalog + Product\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>View（视图）\u003C\u002Ftd>\n   \u003Ctd>把模型数据渲染成用户能看的样子\u003C\u002Ftd>\n   \u003Ctd>只认识模型的数据\u003C\u002Ftd>\n   \u003Ctd>ConsoleView \u002F HtmlView\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>Controller（控制器）\u003C\u002Ftd>\n   \u003Ctd>接收请求、翻译、调模型、选视图\u003C\u002Ftd>\n   \u003Ctd>认识另外两位\u003C\u002Ftd>\n   \u003Ctd>ProductController\u003C\u002Ftd>\n  \u003C\u002Ftr>\n \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>职责边界一句话说清：\u003Cstrong>Model 不知道自己被谁看着，View 不关心数据对不对，Controller 两头都不懂行——只管翻译转发。\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Cp>注意那张表中间一列——\u003Cstrong>依赖方向\u003C\u002Fstrong>才是 MVC 的灵魂，比\"三个角色\"本身重要得多：Model 不依赖任何人（所以能独立测试、独立演化）；View 只依赖 Model 的数据（所以换个皮不伤筋骨）；Controller 依赖两位（所以它是粘合剂，也是唯一知道全局的）。所有\"这段代码该放哪\"的争论，答案都藏在依赖方向里。\u003C\u002Fp>\n\u003Ch2 id=\"toc-2\">二、优点与缺点\u003C\u002Fh2>\n\u003Cp>\u003Cstrong>优点：\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Col>\n \u003Cli>\u003Cstrong>变化隔离\u003C\u002Fstrong>：界面天天改、规则偶尔改、参数解析很少改——三个变化速度不同的东西装进三个房间，各自折腾互不惊扰。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>多视图复用\u003C\u002Fstrong>：同一份 Model，控制台、HTML、JSON 接口、APP 页面各挂各的 View——本文后半段当场演示。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>Model 可独立测试\u003C\u002Fstrong>：业务规则住进 Model 后，测试它不需要启动任何界面、不需要 mock 任何渲染器——直接调方法、断言异常。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>并行开发\u003C\u002Fstrong>：接口约定好，一个人写 View、一个人写 Model，Controller 最后联调——前端后端分工的鼻祖。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>\u003Cstrong>缺点：\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Col>\n \u003Cli>\u003Cstrong>类数量膨胀\u003C\u002Fstrong>：一个页面拆成三个（组）类，demo 级应用写 MVC 是自虐。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>Controller 易发福\u003C\u002Fstrong>：路由、参数解析、视图选择全在它身上，一不留神就长成\"上帝控制器\"；更常见的伴生病是\u003Cstrong>贫血模型\u003C\u002Fstrong>——Model 只剩 getter\u002Fsetter，业务逻辑全被 Controller 吸走，MVC 名存实亡。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>职责划分靠自觉\u003C\u002Fstrong>：\u003Ccode>if (delta &gt; price * 0.2)\u003C\u002Fcode> 写在 Controller 里编译器不报错——MVC 的边界是纪律问题，没有语言机制强制。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>简单场景过度设计\u003C\u002Fstrong>：只有一个静态页的应用、纯计算无界面的程序，没有 V 可言。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2 id=\"toc-3\">三、什么时候用\u003C\u002Fh2>\n\u003Cp>判断标准：\u003Cstrong>有界面（或多种输出格式），且业务逻辑值得独立于界面演化。\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Cul>\n \u003Cli>\u003Cstrong>典型 Web 应用\u003C\u002Fstrong>：页面样式随产品经理的心情天天变，业务规则随法务的意见偶尔变——变化速度不同，就该分房住\u003C\u002Fli>\n \u003Cli>\u003Cstrong>多渠道输出\u003C\u002Fstrong>：同一数据要出 Web 页、APP 接口、小程序、开放 API——一份 Model 挂一排 View\u003C\u002Fli>\n \u003Cli>\u003Cstrong>桌面应用\u003C\u002Fstrong>：MVC 的出生场景——数据变了，多个打开的窗口同时刷新\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>反面对照：\u003Cstrong>纯后端服务\u003C\u002Fstrong>——没有 View，分层交给 ㉙ DAO 和服务层模式；\u003Cstrong>一次性脚本、demo\u003C\u002Fstrong>——别自虐；\u003Cstrong>Model 和 View 天生耦合的应用\u003C\u002Fstrong>（比如游戏渲染引擎），强行 MVC 只会制造搬运代码。\u003C\u002Fp>\n\u003Ch2 id=\"toc-4\">四、实现方式：商品目录\u003C\u002Fh2>\n\u003Ch3 id=\"toc-5\">4.1 反面教材：一个类包揽一切\u003C\u002Fh3>\n\u003Cp>JSP Model 1 时代的画风：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> class\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> BadPage\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    private\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> static\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> final\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> Map&#x3C;\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">String\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">Product\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">> DB \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> new\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> LinkedHashMap&#x3C;>();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">    \u002F\u002F static 初始化块灌三条商品数据，略\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">    \u002F\u002F \"页面\"：一个方法从请求干到 HTML\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> static\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> String \u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">raisePrice\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(String \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">id\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, String \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">deltaText\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        int\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> delta;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        try\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">            delta \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> Integer.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">parseInt\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(deltaText);            \u003C\u002Fspan>\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F\u002F 本该是控制器的活：参数解析\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        } \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">catch\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (NumberFormatException \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">e\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">            return\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"&#x3C;p style='color:red'>参数必须是数字&#x3C;\u002Fp>\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">;  \u003C\u002Fspan>\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F\u002F 本该是视图的活：错误展示\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        Product product \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> DB.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">get\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(id);                        \u003C\u002Fspan>\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F\u002F 本该是模型的活：数据存取\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        if\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (product \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">==\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\"> null\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">            return\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"&#x3C;p style='color:red'>商品不存在：\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> id \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"&#x3C;\u002Fp>\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        if\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (delta \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">>\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> product.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getPrice\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">*\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0.2\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {              \u003C\u002Fspan>\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F\u002F 本该是模型的活：业务规则\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">            return\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"&#x3C;p style='color:red'>单次调价不能超过原价的 20%&#x3C;\u002Fp>\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        product.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">setPrice\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(product.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getPrice\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> delta);        \u003C\u002Fspan>\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F\u002F 本该是模型的活：状态变更\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        StringBuilder html \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> StringBuilder\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"&#x3C;h1>商品列表&#x3C;\u002Fh1>&#x3C;ul>\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);   \u003C\u002Fspan>\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F\u002F 本该是视图的活：渲染\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        for\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (Product p \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">:\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> DB.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">values\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">()) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">            html.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">append\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"&#x3C;li>\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">).\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">append\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(p.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getName\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">())\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">                .\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">append\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\" \"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">).\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">append\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(p.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getPrice\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">()).\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">append\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"元&#x3C;\u002Fli>\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        return\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> html.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">append\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"&#x3C;\u002Ful>\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">).\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">toString\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>跑起来是能跑的：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan>[请求] 调价 P101 +50（50 > 199×20%，被业务规则拒绝）→\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>&#x3C;p style='color:red'>单次调价不能超过原价的 20%&#x3C;\u002Fp>\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>[请求] 调价 P103 +200（200 ≤ 1899×20%，通过）→\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>&#x3C;h1>商品列表&#x3C;\u002Fh1>&#x3C;ul>&#x3C;li>机械键盘 199元&#x3C;\u002Fli>&#x3C;li>无线鼠标 89元&#x3C;\u002Fli>&#x3C;li>4K 显示器 2099元&#x3C;\u002Fli>&#x3C;\u002Ful>\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>但它是一道四合一的乱炖：改样式要通读业务规则，改规则要在 HTML 里找，想加个控制台输出？整个方法复制一遍。最痛的是\u003Cstrong>测试\u003C\u002Fstrong>——想验证\"20% 上限\"这条规则，断言里得写 \u003Ccode>assertTrue(result.contains(\"color:red\"))\u003C\u002Fcode>——\u003Cstrong>业务规则的测试竟然要解析 HTML\u003C\u002Fstrong>。\u003C\u002Fp>\n\u003Ch3 id=\"toc-6\">4.2 正解：Model、View、Controller 三分\u003C\u002Fh3>\n\u003Cp>\u003Cstrong>Model：数据 + 业务规则，谁都不认识。\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> class\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> Catalog\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    private\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> final\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> Map&#x3C;\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">String\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">Product\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">> products \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> new\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> LinkedHashMap&#x3C;>();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> Catalog\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() { \u003C\u002Fspan>\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F* 灌三条商品数据，略 *\u002F\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> List&#x3C;\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">Product\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">> \u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">findAll\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        return\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> List.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">copyOf\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(products.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">values\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">());\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> Product \u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">find\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(String \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">id\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        Product product \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> products.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">get\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(id);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        if\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (product \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">==\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\"> null\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">            throw\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> IllegalArgumentException\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"商品不存在：\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> id);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        return\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> product;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">    \u002F** 业务规则住在模型家：单次调价不能超过原价的 20% *\u002F\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> void\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> raisePrice\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(String \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">id\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">int\u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\"> delta\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        Product product \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> find\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(id);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        if\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (delta \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">&#x3C;=\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">            throw\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> IllegalArgumentException\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"调价幅度必须为正数\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        if\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (delta \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">>\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> product.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getPrice\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">*\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0.2\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">            throw\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> IllegalArgumentException\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"单次调价不能超过原价的 20%\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        product.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">setPrice\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(product.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getPrice\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> delta);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Cstrong>View：只管\"模型数据长什么样\"。\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> interface\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> ProductView\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    String \u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">render\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(List&#x3C;\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">Product\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">> \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">products\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    String \u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">renderError\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(String \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">message\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> class\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> ConsoleView\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> implements\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> ProductView\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    @\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">Override\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> String \u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">render\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(List&#x3C;\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">Product\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">> \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">products\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        StringBuilder sb \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> StringBuilder\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"------ 商品列表（控制台版）------\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        for\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (Product p \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">:\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> products) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">            sb.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">append\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(String.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">format\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"%n  %s  %-8s %5d 元\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, p.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getId\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(), p.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getName\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(), p.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getPrice\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">()));\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        return\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> sb.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">toString\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    @\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">Override\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> String \u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">renderError\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(String \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">message\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        return\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"  × \"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> message;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>HtmlView 同款接口，输出换成 \u003Ccode>&lt;h1&gt;\u003C\u002Fcode> + \u003Ccode>&lt;ul&gt;\u003C\u002Fcode> 的拼装——同一个 Model 的另一张皮。\u003C\u002Fp>\n\u003Cp>\u003Cstrong>Controller：请求的接待员，只做翻译转发。\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> class\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> ProductController\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    private\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> final\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> Catalog catalog;     \u003C\u002Fspan>\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F\u002F Model\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    private\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> final\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ProductView view;    \u003C\u002Fspan>\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F\u002F View\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> ProductController\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(Catalog \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">catalog\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, ProductView \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">view\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">        this\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">.catalog \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> catalog;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">        this\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">.view \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> view;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">    \u002F** 模拟 HTTP 请求入口：\"list\" \u002F \"raise P101 30\" *\u002F\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> String \u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">dispatch\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(String \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">request\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        String\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">[] parts \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> request.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">trim\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">().\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">split\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\\\\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">s+\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        switch\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (parts[\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">0\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">]) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">            case\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"list\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> ->\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> { \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">return\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> view.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">render\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(catalog.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">findAll\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">()); }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">            case\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"raise\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> ->\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">                try\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    catalog.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">raisePrice\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(parts[\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">1\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">], Integer.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">parseInt\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(parts[\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">2\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">]));\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">                    return\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> view.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">render\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(catalog.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">findAll\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">());\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">                } \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">catch\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (IllegalArgumentException | ArrayIndexOutOfBoundsException \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">e\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">                    return\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> view.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">renderError\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(e.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getMessage\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">());\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">                }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">            }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">            default\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> ->\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> { \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">return\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> view.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">renderError\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"未知请求：\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> request); }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Cstrong>收钱。\u003C\u002Fstrong> 先看 Model 独立测试——这是反面教材永远给不了的待遇：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">Catalog catalog \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> Catalog\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">try\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    catalog.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">raisePrice\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"P101\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">999\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);   \u003C\u002Fspan>\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F\u002F 999 > 199×20%\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">} \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">catch\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (IllegalArgumentException \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">e\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"[Model 独立测试] \"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getMessage\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"（零 UI 参与，直接断言异常）\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>再组装上线，跑完整流程：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">ProductController controller \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> ProductController\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(catalog, \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> ConsoleView\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">());\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(controller.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">dispatch\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"list\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">));\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(controller.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">dispatch\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"raise P101 30\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">));     \u003C\u002Fspan>\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F\u002F 通过\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(controller.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">dispatch\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"raise P102 999\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">));    \u003C\u002Fspan>\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F\u002F 拒绝\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F\u002F 换视图：同一个 Model、同一个 Controller，只把 View 换成 HtmlView\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">ProductController htmlController \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> ProductController\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(catalog, \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> HtmlView\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">());\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(htmlController.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">dispatch\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"list\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">));\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>运行输出：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan>========== 正解：MVC 三层各司其职 ==========\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>[Model 独立测试] 单次调价不能超过原价的 20%（零 UI 参与，直接断言异常）\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>[请求] \"list\"\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>------ 商品列表（控制台版）------\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>  P101  机械键盘       199 元\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>  P102  无线鼠标        89 元\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>  P103  4K 显示器    1899 元\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>[请求] \"raise P101 30\"（30 ≤ 199×20%，通过）\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>------ 商品列表（控制台版）------\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>  P101  机械键盘       229 元\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>  P102  无线鼠标        89 元\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>  P103  4K 显示器    1899 元\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>[请求] \"raise P102 999\"（999 > 89×20%，拒绝）\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>  × 单次调价不能超过原价的 20%\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>—— 换视图：同一个 Model、同一个 Controller，只把 View 换成 HtmlView ——\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>[请求] \"list\"（HTML 版）\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>&#x3C;h1>商品列表&#x3C;\u002Fh1>&#x3C;ul>&#x3C;li>机械键盘 229元&#x3C;\u002Fli>&#x3C;li>无线鼠标 89元&#x3C;\u002Fli>&#x3C;li>4K 显示器 1899元&#x3C;\u002Fli>&#x3C;\u002Ful>\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>最后一组对照是 MVC 的含金量时刻：ConsoleView 换 HtmlView，\u003Cstrong>Catalog 一行没改、ProductController 一行没改\u003C\u002Fstrong>——只换了一个构造器参数。反过来，\"20% 上限\"改成\"10%\"，只动 Catalog 一个方法，两个视图连知道都不需要知道。\u003C\u002Fp>\n\u003Ch3 id=\"toc-7\">4.3 三处细节\u003C\u002Fh3>\n\u003Cp>\u003Cstrong>细节一：View 认识的是模型的数据，不是模型本身。\u003C\u002Fstrong> 本文 ProductView 的接口签名是 \u003Ccode>render(List&lt;Product&gt;)\u003C\u002Fcode>——View 依赖 List 和 Product，\u003Cstrong>不依赖 Catalog 这个类\u003C\u002Fstrong>。这不是巧合是刻意：View 需要\"渲染什么\"，不需要\"谁来提供\"。将来 Catalog 换成 RemoteCatalog、ElasticSearchCatalog，只要吐出来的还是 List&lt;Product&gt;，所有视图无动于衷。依赖越窄，变化越贵不了。\u003C\u002Fp>\n\u003Cp>\u003Cstrong>细节二：View 是被注入的策略。\u003C\u002Fstrong> Controller 构造器接收 ProductView，换视图等于换策略——⑮ 策略模式的形制。Spring MVC 把这一步做成了流水线：Controller 返回逻辑视图名 + Model 数据（ModelAndView），ViewResolver 按名字解析出真视图（JSP、Thymeleaf、Freemarker、JSON 序列化器）——同一个 Controller 方法，浏览器请求返回 HTML 页、Ajax 请求返回 JSON，内容协商全自动。本文手动的 \u003Ccode>new HtmlView(...)\u003C\u002Fcode>，在 Spring 里叫 ViewResolver 链。\u003C\u002Fp>\n\u003Cp>\u003Cstrong>细节三：Controller 里出现业务判断就该报警。\u003C\u002Fstrong> \u003Ccode>if (delta &gt; product.getPrice() * 0.2)\u003C\u002Fcode> 这行代码放进 Controller，功能上一模一样，编译器毫无意见——但它意味着这条规则只有走这个 Controller 的请求才能享受，第二个入口（APP 接口、定时任务）进来就得复制一遍。判断标准很简单：\u003Cstrong>这段逻辑换个入口还成立吗？成立就住 Model；只有这个入口成立才是 Controller 的事\u003C\u002Fstrong>。贫血模型的病因，十有八九是这条判断没做。\u003C\u002Fp>\n\u003Ch2 id=\"toc-8\">五、在框架里找它\u003C\u002Fh2>\n\u003Col>\n \u003Cli>\n  \u003Cp>\u003Cstrong>Spring MVC\u003C\u002Fstrong>：J2EE 模式目录的集大成者。@Controller 对应 Controller，@Service + @Repository 对应 Model（业务与数据分层），Thymeleaf\u002FJSP 对应 View；DispatcherServlet 统一接客（它是 ㉚ 前端控制器模式的主角，此处先按下不表）。你每天写的三层注解，就是 MVC 的实体化。\u003C\u002Fp>\u003C\u002Fli>\n \u003Cli>\n  \u003Cp>\u003Cstrong>Struts\u003C\u002Fstrong>：J2EE 时代的 MVC 旗手。Action 类是 Controller，ActionForm\u002FJavaBean 是 Model，JSP 是 View——它的兴衰说明 MVC 结构本身不挑实现技术，挑的是实现得好不好用。\u003C\u002Fp>\u003C\u002Fli>\n \u003Cli>\n  \u003Cp>\u003Cstrong>iOS Cocoa MVC\u003C\u002Fstrong>：苹果官方文档直接教 MVC——UIViewController + UIView + Model。移动端与桌面端才是 MVC 的出生地：数据变了，所有开着的窗口同时刷新，靠的就是 Model 广播事件。\u003C\u002Fp>\u003C\u002Fli>\n \u003Cli>\n  \u003Cp>\u003Cstrong>前端框架\u003C\u002Fstrong>：React 组件的 \u003Ccode>state → render()\u003C\u002Fcode> 单向数据流，是\"Model 变 → View 跟着变\"的函数式转译；Vue、Angular 则走得更远——第六节的 MVVM。\u003C\u002Fp>\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2 id=\"toc-9\">六、五十年进化树：MVC → MVP → MVVM\u003C\u002Fh2>\n\u003Cp>MVC 从 1979 年活到今天，没死，但换了四次形态。四个代际的差别只有一个问题：\u003Cstrong>View 怎么知道该更新了？\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Ctable>\n \u003Cthead>\n  \u003Ctr>\n   \u003Cth>代际\u003C\u002Fth>\n   \u003Cth>View 怎么更新\u003C\u002Fth>\n   \u003Cth>代表\u003C\u002Fth>\n   \u003Cth>适合\u003C\u002Fth>\n  \u003C\u002Ftr>\n \u003C\u002Fthead>\n \u003Ctbody>\n  \u003Ctr>\n   \u003Ctd>经典 MVC（1979）\u003C\u002Ftd>\n   \u003Ctd>View 订阅 Model，Model 变了主动广播\u003C\u002Ftd>\n   \u003Ctd>Smalltalk\u003C\u002Ftd>\n   \u003Ctd>常驻内存的桌面应用\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>Web MVC \u002F Model 2（1998）\u003C\u002Ftd>\n   \u003Ctd>View 无法常驻；Controller 把 Model 数据喂给 View，渲染完就销毁\u003C\u002Ftd>\n   \u003Ctd>JSP Model 2、Struts、Spring MVC\u003C\u002Ftd>\n   \u003Ctd>HTTP 请求-响应\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>MVP\u003C\u002Ftd>\n   \u003Ctd>Presenter 持有 View 接口，一步一指挥；View 完全被动\u003C\u002Ftd>\n   \u003Ctd>WinForms、早期 Android\u003C\u002Ftd>\n   \u003Ctd>要单测 View 逻辑\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>MVVM\u003C\u002Ftd>\n   \u003Ctd>数据绑定：View 声明式绑定 ViewModel 的属性，双向同步\u003C\u002Ftd>\n   \u003Ctd>WPF、Vue、Angular\u003C\u002Ftd>\n   \u003Ctd>声明式 UI\u003C\u002Ftd>\n  \u003C\u002Ftr>\n \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>看出主旋律了吗？\u003Cstrong>View 越来越被动，越来越声明式。\u003C\u002Fstrong> 经典 MVC 里 View 还要自己订阅、自己监听；Web MVC 里 View 变成一性性的渲染模板；MVP 里 View 退化成一个接口实现；MVVM 里 View 干脆只声明\"我绑定谁\"——\u003Cstrong>人写的命令式代码步步后退，框架的声明式机制步步前进\u003C\u002Fstrong>。这和 ⑭ 模板方法到 ⑮ 策略的演进一脉相承：好莱坞原则——别打电话给我们，我们会打给你（Don't call us, we'll call you）。\u003C\u002Fp>\n\u003Cp>顺带一提，前端面试爱考的\"双向绑定\"，本质是 MVVM 在 View 与 ViewModel 之间装了两条单向数据流：用户输入 → 更新 ViewModel（表单监听），ViewModel 变化 → 刷新 View（订阅通知）。Vue 的 v-model、Angular 的 ngModel，拆开看还是 ⑯ 观察者那套老手艺。\u003C\u002Fp>\n\u003Ch2 id=\"toc-10\">写在最后\u003C\u002Fh2>\n\u003Cul>\n \u003Cli>MVC 是架构模式，1979 年出生，比 GoF 早十五年——管的是整个应用的分层，不是一个类的设计\u003C\u002Fli>\n \u003Cli>三分职责：Model 管数据和规则，View 管呈现，Controller 管翻译转发\u003C\u002Fli>\n \u003Cli>依赖方向定生死：Model 谁都不认识——业务逻辑永远住 Model，Controller 里出现业务判断就该报警\u003C\u002Fli>\n \u003Cli>换视图零成本、Model 独立测试，是 MVC 兑现的两张支票\u003C\u002Fli>\n \u003Cli>五十年进化只有一个主旋律：View 越来越被动，声明式步步进逼\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>㉖ 的 Controller 直接持有 Catalog、直接调用，丝滑顺畅——因为它们在同一个进程里。可 EJB 时代的真实画风是：业务层住在另一台服务器上。表示层每次调用业务，都要 JNDI 查找、建网络连接、翻译远程异常、失败重试……Controller 迟早被这些\"远程破事\"淹没。谁来替表示层把这些脏活挡在门外？——业务代表模式，下一篇见。\u003C\u002Fp>",[113,116,119,122,125,129,132,135,138,141],{"id":114,"text":115,"level":108},"toc-1","一、MVC 模式是什么",{"id":117,"text":118,"level":108},"toc-2","二、优点与缺点",{"id":120,"text":121,"level":108},"toc-3","三、什么时候用",{"id":123,"text":124,"level":108},"toc-4","四、实现方式：商品目录",{"id":126,"text":127,"level":128},"toc-5","4.1 反面教材：一个类包揽一切",3,{"id":130,"text":131,"level":128},"toc-6","4.2 正解：Model、View、Controller 三分",{"id":133,"text":134,"level":128},"toc-7","4.3 三处细节",{"id":136,"text":137,"level":108},"toc-8","五、在框架里找它",{"id":139,"text":140,"level":108},"toc-9","六、五十年进化树：MVC → MVP → MVVM",{"id":142,"text":143,"level":108},"toc-10","写在最后"]