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

高级程序设计 · 第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 三层职责

!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 模式?

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 执行者 ConsoleViewArticleRepository

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分钟)

必做项

  1. 使用 Maven 模板创建项目
  2. 实现完整包结构(model/view/command/controller)
  3. 实现 4 个 Command:help / list / crawl / exit
  4. list 能展示已抓取的文章(目前存根即可)
  5. 运行并测试循环

额外加分:代码找茬

  • 检查是否仍有 System.out 直接调用
  • 检查 ANSI 码是否硬编码在多个地方

验收标准

  • Maven 编译通过
  • Command 接口和 4 个实现在不同文件
  • Controller 里没有 switch-case
  • 新增命令只需新建类,不改 Controller
  • list 能正确显示空列表
  • 所有输出均通过 ConsoleView
  • ANSI 颜色码集中定义为常量

9️⃣ 课后作业

必做

  1. 完善 Article:增加 authorpublishDate 字段
  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 才是唯一输出出口

谢谢!

课件已上传,模板在课程群

保持工程洁癖,下周见!