4.1 Model2模式在书店站点的落地


4.1 Model2模式在书店站点的落地

本节摘要:Model2 是 MVC 思想在 JSP 时代的标准落地形态:Servlet 当控制器接住所有请求,业务逻辑住进模型层,页面退守为纯视图。本节用青梧书肆的下单流程做解剖标本,展示改造前后数据流的本质变化——页面从"自己拉数据"变成"等控制器推数据"。本节是第 4 章手术的总纲,后面两站的分包规范与事务演练都在这个骨架上进行。

把页面里的业务逻辑请出去

先看病灶。改造前的下单页面是标准的 Model1 形态,一页到底:

<%-- 改造前:下单页面,查询、校验、写入、渲染一肩挑 --%> <% String bookId = request.getParameter("bookId"); Connection conn = DriverManager.getConnection(url, user, pwd); PreparedStatement ps = conn.prepareStatement("select stock from book where id=?"); ps.setString(1, bookId); ResultSet rs = ps.executeQuery(); if (!rs.next() || rs.getInt("stock") < 1) { out.println("库存不足"); return; } // ……校验余额、写订单表、扣库存,几十行后…… %> <p>下单成功,订单号:<%= orderNo %></p>

这段代码单看每一行都没错,合在一起就是维护噩梦:查询语句写死在视图里没法复用,业务规则改一条要通读全页,想给"下单"写个单元测试更是无从下手——逻辑焊死在 HTTP 页面里,离开容器就跑不起来。

Model2 的处方是三段分家。同样的下单流程,改造后请求先进控制器:

// OrderController:只管流程,不碰 SQL protected void doPost(HttpServletRequest req, HttpServletResponse resp) { String bookId = req.getParameter("bookId"); try { Order order = orderService.place(bookId, req.getRemoteUser()); req.setAttribute("order", order); // 数据放上托盘 req.getRequestDispatcher("/order/done.jsp") .forward(req, resp); // 转发给视图 } catch (BizException e) { req.setAttribute("error", e.getMessage()); req.getRequestDispatcher("/order/fail.jsp").forward(req, resp); } }

页面瘦身后只剩收数据、画界面:

<%-- 改造后:订单完成页,只有展示 --%> <p>下单成功,订单号:${order.orderNo}</p>

两版结构并排画出来,差异一目了然。

图4-1 Model1 与 Model2 的结构对照

图4-1 Model1 与 Model2 的结构对照

数据流改向:从拉到推

结构对照之外,还有一个更隐蔽也更深刻的变化:数据流方向反了。Model1 时代页面是主动的,自己查库、自己算数、自己决定展示什么——拉模式。Model2 之后页面变成被动的,控制器把算好的数据放进 request 作用域,页面只管取出来画——推模式。

这个翻转把 2.2 节的作用域知识变成了日常工具。控制器往 request 作用域放数据、页面取数据的约定,是控制器与视图之间的接口契约:页面需要什么,控制器就必须放什么,名字对得上、类型说得清。青梧书肆的改造里,我们把每个控制器递给视图的数据整理成一张"托盘清单"写在类注释里,谁改谁更新——这份清单后来成了交接文档里被翻得最勤的一页。

跳转方式也顺带定型:展示型跳转(控制器到视图)用转发,request 托盘里的数据直达终点;写操作完成后的跳转(下单完成去结果页)用重定向防刷新重复提交——2.3 节的口诀在这里每天都要用。

案例:一次复用需求逼出重构

背景:公司决定给青梧书肆配一个手机端入口,第一版需求很小:查看订单状态。如果沿用 Model1,唯一的选择是把订单查询逻辑从页面里抄一份到新的 API 页面——两处代码从第一天起就会漂移。

操作:把下单与查询里的业务逻辑(校验规则、订单状态机、库存扣减)从页面里整体搬到订单服务类,页面只留转发与展示。手机端 API 控制器与 Web 控制器并列,调用同一个服务。

结果:同一套业务逻辑服务两个入口,后续"订单超时自动取消"这类规则改动只改服务层一处,两个入口同时生效。

解读:这个案例揭示了 Model2 的真实价值顺序——很多人以为分层是为了"好看",其实第一收益是复用,第二收益才是可测试性。服务层能脱离容器写单元测试后,团队第一次敢在周五下午改下单逻辑了,这对维护期的意义怎么估都不过分。

变式:如果站点注定只有一个入口、生命周期只剩一年,全面 Model2 改造的投入未必划算——按流程改造最复杂的两三个页面即可,其余保持现状。改造的深度跟着站点的剩余寿命走,这是维护期工程的基本定价原则。

⚠️ 常见坑:改造后的纯净视图很容易被"就加一行判断"攻破——第一行业务逻辑回到页面的那一刻,分层就开始溃烂。青梧书肆的对策是把"页面里不许出现查询与业务判断"写进评审清单,机器扫百分号括号数量,超过阈值直接打回。

推模式的两个边界情形

落地推模式后有两个边界情形值得预演。其一,页面需要"不在本次请求里"的数据怎么办——比如订单完成页顺带展示会员等级。正路是把依赖数据一次性装配进请求作用域,控制器多调一次服务;退而求其次才从会话作用域取,但取什么要写成显式契约,避免页面偷偷依赖某个没人记得的会话属性。其二,数据装配失败怎么办——托盘里的对象为空时,页面按 5.1 节的宽容语义会安静输出空白,故障被藏起来。对策在 5.1 节给过:装配失败在控制器层就拦住走错误分支,别让半截托盘流到视图。两个情形合成一条总纪律:视图收到的托盘必须是"装配完整、失败即短路"的,视图永远不负责拼数据。

手术顺序的选择

多页面要动 Model2 手术时,顺序影响成败。青梧书肆的顺序是"先难后易":先拿下订单流程这个最难的标本(4.1 开篇的手术),把模式、分包、事务的坑一次性踩完;再用沉淀下来的模板改购物车、会员资料这些中等复杂度的流程;最后批量处理纯展示页——它们几乎只剩"把脚本查询搬进服务层"的体力活。这个顺序的依据是:模式的坑集中在复杂流程里,先啃硬骨头,后面全是重复劳动;反过来先易后难,团队会在简单页面浪费新鲜感,到硬骨头面前又缺经验。执行时的配套动作是每完成一类页面就更新一份"手术模板"——控制器骨架、分包样例、事务块,下一个页面照模板填空。十几个页面下来,模板迭代了四版,最后一版的每一段注释都是前几版的教训。

本节要点回顾

  • Model2 三段分家:控制器管流程、模型管业务、视图管展示,下单流程是检验分层的试金石;
  • 数据流从拉变推:控制器把数据放进 request 作用域,页面被动接收,作用域成为两者间的接口契约;
  • 跳转纪律日常化:展示走转发、写后走重定向,2.3 节的口诀成为工程惯例;
  • 复用是第一收益:多入口共享同一服务层,才是分层的头号回报,可测试性紧随其后。

骨架立起来了,但"控制器、服务、视图"要真正成为一个团队的工作界面,还得靠分包与命名规范固定下来——下一站进入施工现场管理。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U