You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
14 KiB
14 KiB
高级程序设计 · 第9周
工程架构:从"写代码"到"造系统"
CLI + MVC + Command模式实战
📌 本周导航
- 痛点引入:脚本的宿命
- CLI vs GUI:为什么选命令行?
- MVC分层:职责分离的艺术
- Command模式:可扩展的路由
- Maven模板:工程化第一步
- 代码落地:从接口到实现
- 架构反思:共享数据的隐患
- 实践任务 + 课后作业
1️⃣ 痛点引入:从脚本到工程的鸿沟
这是一段“意大利面”爬虫
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 三层职责
┌─────────────────────────────────────────┐
│ 入口 │
│ (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 模式?
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 接口定义
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
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 实体
public class Article {
private String title;
private String url;
private String content;
// 构造器、getter/setter、toString
}
📦 只存放数据,没有任何输入输出代码
View:ConsoleView(ANSI常量集中管理)
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)
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(存根,下周填坑)
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
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 组装
// 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 的隐患
当前问题
- 所有 Command 都直接拿到
List<Article>引用 - 任何一个命令都可以随意增、删、改列表
- 数据完全“裸奔”
🚨 就像酒店所有员工都能进保险箱
提问
- 如果
CrawlCommand不小心把null塞进列表,ListCommand会怎样? - 如果我们要在添加文章时写日志,现在的设计能优雅实现吗?
预告解决方案(W10)
- 策略模式 + 仓库层(ArticleRepository)
- 封装
List,对外只暴露add()、getAll()等安全接口
W9 搭骨架,W10 装上盔甲
8️⃣ 实践任务(现场5分钟)
必做项
- 使用 Maven 模板创建项目
- 实现完整包结构(model/view/command/controller)
- 实现 4 个 Command:help / list / crawl / exit
list能展示已抓取的文章(目前存根即可)- 运行并测试循环
额外加分:代码找茬
- 检查是否仍有
System.out直接调用 - 检查 ANSI 码是否硬编码在多个地方
验收标准
- Maven 编译通过
- Command 接口和 4 个实现在不同文件
- Controller 里没有 switch-case
- 新增命令只需新建类,不改 Controller
- list 能正确显示空列表
- 所有输出均通过
ConsoleView - ANSI 颜色码集中定义为常量
9️⃣ 课后作业
必做
- 完善 Article:增加
author、publishDate字段 - ★ HistoryCommand:记录用户输入过的所有命令(用
List<String>) - AI 架构审计:将类名发给 AI,指令:
“作为Java架构审计师,请检查我的MVC三层划分是否存在越权行为?”
选做
- 命令别名(c 代替 crawl)
- URL 格式验证
- 暗色主题(修改一处常量)
- 思考题:分析
List<Article>共享引用的风险(200字小结)
🤖 AI 协同升级
架构审计师任务(必做)
步骤:
- 列出所有类名(不含方法实现)
- 发给 AI
- 指令:“检查 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 才是唯一输出出口 |
谢谢!
课件已上传,模板在课程群
保持工程洁癖,下周见!
