## 高级程序设计 · 第9周 #### 工程架构:从"写代码"到"造系统" ##### CLI + MVC + Command模式实战 --- ### 📌 本周导航 - 痛点引入:脚本的宿命 - CLI vs GUI:为什么选命令行? - MVC分层:职责分离的艺术 - Command模式:可扩展的路由 - Maven模板:工程化第一步 - 代码落地:从接口到实现 - 架构反思:共享数据的隐患 - 实践任务 + 课后作业 --- ### 1️⃣ 痛点引入:从脚本到工程的鸿沟 #### 这是一段“意大利面”爬虫 ```java public class Crawler { public static void main(String[] args) { System.out.print("请输入URL: "); Scanner scanner = new Scanner(System.in); String url = scanner.nextLine(); List titles = new ArrayList(); try { Document doc = Jsoup.connect(url).get(); Elements elements = doc.select(".post-title"); for (Element e : elements) { String title = e.text(); System.out.println("标题: " + title); titles.add(title); } } catch (Exception ex) { System.out.println("出错啦: " + ex.getMessage()); } } } ``` --- ### 脚本的三大痛点 | 需求 | 需要改哪里? | |------|--------------| | 保存标题到文件 | 改 main 内部逻辑 | | 支持不同网站结构 | 全部重写解析代码 | | 彩色输出 | 一个一个改 print | > 😫 **牵一发而动全身 → 改起来疼** ### 本周目标:**让代码“改起来不疼”** --- ## 2️⃣ CLI vs GUI:架构选择的思考 ### 图形界面 vs 命令行 | 维度 | GUI (JavaFX) | CLI (命令行) | |------|--------------|-------------| | 学习重心 | 布局、控件、事件 | **架构、分层、路由** | | 后端能力 | 弱 | 模拟真实服务器 | | 工程思维 | 弱(关注视觉) | **强(关注逻辑)** | | 可测试性 | 难 | 易 | --- ## 核心观点 > **CLI 更需要 MVC!** - GUI 有现成事件系统,框架强塞给你一套架构 - CLI 只有字符流 → **没有架构,分分钟写成脚本** > 🎯 **当外部约束消失,内部的工程纪律才真正开始建立** ### CLI 也能很酷 - ANSI 彩色输出 - 表格展示数据 - 模拟大数据/后端开发 --- ## 3️⃣ MVC 分层设计 ### MVC 的起源与演进 | 年代 | 场景 | MVC的角色 | |------|------|----------| | 1970s | Smalltalk-72 GUI | 最早的用户界面架构 | | 1990s | Web开发 (Struts) | 后端模板引擎 | | 2000s | ASP.NET MVC | 现代Web框架 | | 2020s | CLI + API | 解耦业务逻辑与表现层 | **核心不变:职责分离** --- ## MVC 三层职责 ![[mvc.png]] ``` ┌─────────────────────────────────────────┐ │ 入口 │ │ (main方法) │ └─────────────────┬───────────────────────┘ ▼ ┌─────────────────────────────────────────┐ │ Controller │ │ 只管"派给谁",不管"怎么做" │ └─────────┬───────────────┬───────────────┘ ▼ ▼ ┌─────────────────┐ ┌─────────────────┐ │ Model │ │ View │ │ 管"数据" │ │ 管"呈现" │ │ + 业务逻辑 │ │ + 输入输出 │ └─────────────────┘ └─────────────────┘ ``` --- ## 三层“禁止做什么” | 层级 | 禁止行为 | | -------------- | -------------------------------------- | | **Model** | 不能有 `System.out.println`,不能有 `Scanner` | | **View** | 不能写爬虫逻辑,只做“传声筒” | | **Controller** | 不能直接写业务细节,委托给 Command | > 🔴 **越权就是架构腐败的开始** --- ## 🍽️ 餐厅类比(帮助理解) - **Model = 后厨**:只管做菜,不管谁来吃、怎么端 - **View = 服务员**:只管端菜和收钱,不管菜怎么做 - **Controller = 前台**:接单 → 派给后厨 → 叫服务员上菜 --- ## 🤔 对类比的批判性思考(关键!) > 任何类比都有边界,不要当成真理 | 场景 | 暴露的问题 | |------|------------| | 客人有忌口(不吃香菜) | 信息需要传到后厨 → Model 可能需要知道 meta 信息 | | 服务员反馈“今天的菜咸了” | View → Model 反向影响 | | 后厨做完菜通知前台 | **观察者模式**,数据流可能是双向的 | **本课程简化模型**:请求-响应,单向流 --- ## MVC 数据流向(本课程简化版) ``` CLI用户输入 ↓ View(解析命令字符串) ↓ Controller(找到对应Command) ↓ Command.execute()(执行业务逻辑) ↓ Model(Article数据,暂存于List) ↓ View(display()展示数据) ↓ CLI终端显示 ``` --- ## 4️⃣ Command 模式:可扩展的命令路由 ### 为什么需要 Command 模式? ```java switch (cmd) { case "crawl": handleCrawl(); break; case "help": showHelp(); break; // 如果要增加 list 命令? // 1. 加 case "list" // 2. 加 handleList() 方法 // 3. 可能还要改其他地方... } ``` > 每加一个功能,就要在这个类里戳一个洞 → **肥控制器陷阱** --- ## Command 模式的四个要素 | 要素 | 角色 | 示例 | |------|------|------| | Command接口 | 抽象的“订单” | `Command` | | ConcreteCommand | 具体的订单 | `HelpCommand` | | Invoker | 接单的前台 | `CrawlerController` | | Receiver | 执行者 | `ConsoleView`、`ArticleRepository` | --- ## Command 接口定义 ```java package com.crawler.command; import com.crawler.model.Article; import java.util.List; public interface Command { String getName(); void execute(String[] args, List
articles); } ``` --- ## Controller 的变革:从 switch 到 Map ```java public class CrawlerController { private Map commands = new HashMap<>(); public CrawlerController(ConsoleView view, List
articles) { commands.put("help", new HelpCommand(view)); commands.put("list", new ListCommand(view)); commands.put("crawl", new CrawlCommand(view)); commands.put("exit", new ExitCommand(view)); } public void handle(String input) { // 解析命令 → 从 Map 取 Command → 调用 execute } } ``` > **增加新命令:只需新建类,Controller 零改动!** --- ## 对比:switch-case vs Command | 维度 | switch-case | Command模式 | |------|-------------|-------------| | 增加命令 | 要改 Controller | 新建一个类 | | 多态体验 | 无 | `execute()` 多态 | | 可测试性 | 难 | 每个 Command 单独测试 | | 代码量 | 少 | 多,但更清晰 | > 🏨 **类比:酒店客房服务,前台只负责派单** --- ## 5️⃣ Maven 模板与环境(5分钟) ### 直接使用模板,不折腾配置 ``` my-crawler-template.zip ↓ 解压 + IDEA打开 ↓ 右键 pom.xml → Maven → Reload Project ↓ 运行 App.java ``` ### 标准目录结构 ``` src/main/java/com/crawler/ ├── model/Article.java ├── view/ConsoleView.java ├── command/ │ ├── Command.java │ ├── CrawlCommand.java │ ├── HelpCommand.java │ ├── ListCommand.java │ └── ExitCommand.java └── controller/CrawlerController.java ``` --- ## 6️⃣ 代码落地(分步实现) ### Model:Article 实体 ```java public class Article { private String title; private String url; private String content; // 构造器、getter/setter、toString } ``` > 📦 只存放数据,没有任何输入输出代码 --- ## View:ConsoleView(ANSI常量集中管理) ```java public class ConsoleView { private static final String ANSI_GREEN = "\033[32m"; private static final String ANSI_RED = "\033[31m"; // ... 其他常量 public void printSuccess(String msg) { System.out.println(ANSI_GREEN + msg + ANSI_RESET); } public void printError(String msg) { ... } public void display(List
articles) { ... } } ``` > ✨ **所有颜色码集中定义 → 改主题只需改一处** --- ## Command 实现示例(HelpCommand) ```java public class HelpCommand implements Command { private ConsoleView view; public HelpCommand(ConsoleView v) { this.view = v; } public String getName() { return "help"; } public void execute(String[] args, List
articles) { view.printInfo("Commands: crawl , list, help, exit"); } } ``` > ⚠️ 全部输出通过 `view`,绝不让 `System.out` 直接出现在这里 --- ## CrawlCommand(存根,下周填坑) ```java public class CrawlCommand implements Command { private ConsoleView view; public CrawlCommand(ConsoleView v) { this.view = v; } public String getName() { return "crawl"; } public void execute(String[] args, List
articles) { if (args.length < 2) { view.printError("Usage: crawl "); return; } view.printInfo("Stub: Would crawl " + args[1]); } } ``` > 🔍 **找茬点**:这里拼接字符串算是“业务逻辑”吗?留给大家用 AI 审计。 --- ## ExitCommand ```java public class ExitCommand implements Command { private ConsoleView view; public ExitCommand(ConsoleView v) { this.view = v; } public String getName() { return "exit"; } public void execute(String[] args, List
articles) { view.printSuccess("Bye!"); System.exit(0); } } ``` > ✅ 所有输出都通过 View → 将来改 GUI 只需换 View 实现 --- ## Controller + main 组装 ```java // Controller 中持有 Map // App.java 中: ConsoleView view = new ConsoleView(); List
articles = new ArrayList<>(); CrawlerController controller = new CrawlerController(view, articles); view.printSuccess("Welcome to CLI Crawler!"); while (true) { controller.handle(view.readLine()); } ``` > 🔁 完成交互循环 --- ## 7️⃣ 架构反思:共享 List
的隐患 ### 当前问题 - 所有 Command 都直接拿到 `List
` 引用 - 任何一个命令都可以随意增、删、改列表 - 数据完全“裸奔” > 🚨 就像酒店所有员工都能进保险箱 --- ## 提问 - 如果 `CrawlCommand` 不小心把 `null` 塞进列表,`ListCommand` 会怎样? - 如果我们要在添加文章时写日志,现在的设计能优雅实现吗? ### 预告解决方案(W10) - **策略模式** + **仓库层(ArticleRepository)** - 封装 `List`,对外只暴露 `add()`、`getAll()` 等安全接口 > W9 搭骨架,W10 装上盔甲 --- ## 8️⃣ 实践任务(现场5分钟) ### 必做项 1. 使用 Maven 模板创建项目 2. 实现完整包结构(model/view/command/controller) 3. 实现 4 个 Command:help / list / crawl / exit 4. `list` 能展示已抓取的文章(目前存根即可) 5. 运行并测试循环 ### 额外加分:代码找茬 - 检查是否仍有 `System.out` 直接调用 - 检查 ANSI 码是否硬编码在多个地方 --- ## 验收标准 - [x] Maven 编译通过 - [x] Command 接口和 4 个实现在不同文件 - [x] Controller 里没有 switch-case - [x] 新增命令只需新建类,不改 Controller - [x] list 能正确显示空列表 - [x] 所有输出均通过 `ConsoleView` - [x] ANSI 颜色码集中定义为常量 --- ## 9️⃣ 课后作业 ### 必做 1. **完善 Article**:增加 `author`、`publishDate` 字段 2. **★ HistoryCommand**:记录用户输入过的所有命令(用 `List`) 3. **AI 架构审计**:将类名发给 AI,指令: > “作为Java架构审计师,请检查我的MVC三层划分是否存在越权行为?” ### 选做 - 命令别名(c 代替 crawl) - URL 格式验证 - 暗色主题(修改一处常量) - 思考题:分析 `List
` 共享引用的风险(200字小结) --- ## 🤖 AI 协同升级 ### 架构审计师任务(必做) **步骤**: 1. 列出所有类名(不含方法实现) 2. 发给 AI 3. 指令:“检查 MVC 分层是否清晰,是否有越权行为” ### 进阶探究(选做) > “假设我的 Command 接口中 execute 方法接收了一个 `List
` 参数,请分析这种设计在工程上有什么隐患,并给出重构建议。” --- ## 📚 总结与过渡 ### 本周成果 - ✅ 工程化包结构 - ✅ MVC 分层清晰 - ✅ Command 模式实现可扩展路由 - ✅ 所有输出走 View,常量集中管理 ### 下周预告 - **策略模式**:封装爬取算法 - **仓库层(Repository)**:武装 `List
`,解决共享隐患 > 🚀 从“写代码”到“造系统”,踏出坚实第一步! --- ## Q&A ### 常见问题 | 问题 | 解答 | |------|------| | IDEA 不识别 pom.xml | 右键 → Maven → Reload Project | | 中文乱码 | Settings → File Encodings → UTF-8 | | 输出颜色乱码 | Windows 建议使用 Windows Terminal | | 我的 System.out 被批评 | View 才是唯一输出出口 | --- ## 谢谢! ### 课件已上传,模板在课程群 **保持工程洁癖,下周见!**