1 changed files with 0 additions and 530 deletions
@ -1,530 +0,0 @@ |
|||||
## 高级程序设计 · 第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<Article> articles); |
|
||||
} |
|
||||
``` |
|
||||
|
|
||||
--- |
|
||||
|
|
||||
## Controller 的变革:从 switch 到 Map |
|
||||
|
|
||||
```java |
|
||||
public class CrawlerController { |
|
||||
private Map<String, Command> commands = new HashMap<>(); |
|
||||
|
|
||||
public CrawlerController(ConsoleView view, List<Article> 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<Article> 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<Article> articles) { |
|
||||
view.printInfo("Commands: crawl <url>, 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<Article> articles) { |
|
||||
if (args.length < 2) { |
|
||||
view.printError("Usage: crawl <url>"); |
|
||||
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<Article> articles) { |
|
||||
view.printSuccess("Bye!"); |
|
||||
System.exit(0); |
|
||||
} |
|
||||
} |
|
||||
``` |
|
||||
|
|
||||
> ✅ 所有输出都通过 View → 将来改 GUI 只需换 View 实现 |
|
||||
|
|
||||
--- |
|
||||
|
|
||||
## Controller + main 组装 |
|
||||
|
|
||||
```java |
|
||||
// Controller 中持有 Map<String,Command> |
|
||||
// App.java 中: |
|
||||
ConsoleView view = new ConsoleView(); |
|
||||
List<Article> articles = new ArrayList<>(); |
|
||||
CrawlerController controller = new CrawlerController(view, articles); |
|
||||
view.printSuccess("Welcome to CLI Crawler!"); |
|
||||
while (true) { |
|
||||
controller.handle(view.readLine()); |
|
||||
} |
|
||||
``` |
|
||||
|
|
||||
> 🔁 完成交互循环 |
|
||||
|
|
||||
--- |
|
||||
|
|
||||
## 7️⃣ 架构反思:共享 List<Article> 的隐患 |
|
||||
|
|
||||
### 当前问题 |
|
||||
|
|
||||
- 所有 Command 都直接拿到 `List<Article>` 引用 |
|
||||
- 任何一个命令都可以随意增、删、改列表 |
|
||||
- 数据完全“裸奔” |
|
||||
|
|
||||
> 🚨 就像酒店所有员工都能进保险箱 |
|
||||
|
|
||||
--- |
|
||||
|
|
||||
## 提问 |
|
||||
|
|
||||
- 如果 `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<String>`) |
|
||||
3. **AI 架构审计**:将类名发给 AI,指令: |
|
||||
> “作为Java架构审计师,请检查我的MVC三层划分是否存在越权行为?” |
|
||||
|
|
||||
### 选做 |
|
||||
|
|
||||
- 命令别名(c 代替 crawl) |
|
||||
- URL 格式验证 |
|
||||
- 暗色主题(修改一处常量) |
|
||||
- 思考题:分析 `List<Article>` 共享引用的风险(200字小结) |
|
||||
|
|
||||
--- |
|
||||
|
|
||||
## 🤖 AI 协同升级 |
|
||||
|
|
||||
### 架构审计师任务(必做) |
|
||||
|
|
||||
**步骤**: |
|
||||
1. 列出所有类名(不含方法实现) |
|
||||
2. 发给 AI |
|
||||
3. 指令:“检查 MVC 分层是否清晰,是否有越权行为” |
|
||||
|
|
||||
### 进阶探究(选做) |
|
||||
|
|
||||
> “假设我的 Command 接口中 execute 方法接收了一个 `List<Article>` 参数,请分析这种设计在工程上有什么隐患,并给出重构建议。” |
|
||||
|
|
||||
--- |
|
||||
|
|
||||
## 📚 总结与过渡 |
|
||||
|
|
||||
### 本周成果 |
|
||||
|
|
||||
- ✅ 工程化包结构 |
|
||||
- ✅ MVC 分层清晰 |
|
||||
- ✅ Command 模式实现可扩展路由 |
|
||||
- ✅ 所有输出走 View,常量集中管理 |
|
||||
|
|
||||
### 下周预告 |
|
||||
|
|
||||
- **策略模式**:封装爬取算法 |
|
||||
- **仓库层(Repository)**:武装 `List<Article>`,解决共享隐患 |
|
||||
|
|
||||
> 🚀 从“写代码”到“造系统”,踏出坚实第一步! |
|
||||
|
|
||||
--- |
|
||||
|
|
||||
## Q&A |
|
||||
|
|
||||
### 常见问题 |
|
||||
|
|
||||
| 问题 | 解答 | |
|
||||
|------|------| |
|
||||
| IDEA 不识别 pom.xml | 右键 → Maven → Reload Project | |
|
||||
| 中文乱码 | Settings → File Encodings → UTF-8 | |
|
||||
| 输出颜色乱码 | Windows 建议使用 Windows Terminal | |
|
||||
| 我的 System.out 被批评 | View 才是唯一输出出口 | |
|
||||
|
|
||||
--- |
|
||||
|
|
||||
## 谢谢! |
|
||||
|
|
||||
### 课件已上传,模板在课程群 |
|
||||
|
|
||||
**保持工程洁癖,下周见!** |
|
||||
Loading…
Reference in new issue