--- # 教案:《高级程序设计》第9周——工程架构:从"写代码"到"造系统" | 项目 | 内容 | |------|------| | **课程名称** | 高级程序设计 | | **周次** | 第9周 | | **主题** | 工程架构——从"写代码"到"造系统" | | **学时** | 2学时(90分钟) | | **授课对象** | 具备Python基础、已完成Java面向对象特性学习的学生 | | **教学环境** | JDK 17+、IntelliJ IDEA、Maven(模板) | | **前情提要** | 本课程原计划使用JavaFX GUI,后根据教学反馈转向CLI + MVC + 爬虫工程化 | --- ## 教学调整说明:为什么选择CLI而不是GUI? > **原计划**:JavaFX桌面应用 → **新计划**:CLI命令行应用 | 维度 | GUI (JavaFX) | CLI (命令行) | |------|--------------|-------------| | **学习重心** | 布局、控件、事件监听 | 架构、分层、命令路由 | | **学生痛点** | "窗口点击"与后端能力无关 | 真正锻炼工程思维 | | **AI辅助** | AI生成FXML,学生看不懂 | AI辅助重构架构 | | **工程化** | 脱离真实后端开发场景 | 模拟真实服务器/大数据开发 | | **核心转型** | "视觉装饰"优先 | "逻辑架构"优先 | **决策理由**: 1. **985学生需要的是工程思维**,不是拖控件 2. **接口抽象**是弱项,CLI + MVC更能暴露这个问题 3. **彩色终端**足够酷炫,且代码量可控 **更深层的教育价值**: > 在GUI框架中,架构已被框架强制划定,学生只是"遵守规矩";而CLI世界里没有任何框架告诉你模型在哪、视图在哪——**当外部约束消失,内部的工程纪律才真正建立**。这正是本节课要传递的核心精神。 --- ## 一、教学目标 | 目标维度 | 具体描述 | |----------|----------| | **知识掌握** | 理解MVC架构的职责划分及其演化脉络;掌握Maven项目结构与pom.xml基础;理解Command模式的路由原理。 | | **工程实践** | 能搭建规范的Maven项目包结构;能实现基于Scanner的控制台交互;能用Command接口实现可扩展的命令路由;能识别架构中的"越权行为"。 | | **思维转型** | 从"一个类写全部"转向"分层解耦";从"修改现有代码"转向"新增类实现功能";从"满足功能"转向"代码的工程洁癖"。 | | **工具应用** | 利用AI辅助审查MVC职责越权;让AI扮演"架构审计师"检查分层是否清晰;理解AI生成代码中的架构缺陷。 | --- ## 二、教学重点与难点 | 项目 | 内容 | 突破方法 | |------|------|----------| | **重点** | MVC三层职责划分、CLI交互实现、Command接口解耦、代码中的工程细节(常量、输出归属) | 以"新增命令需要改什么"为切入点,展示Command模式的优势;通过现场"代码找茬"强化细节意识 | | **难点** | Controller不写业务逻辑、Command接口的多态实现、共享数据模型的设计缺陷识别 | 现场演示:增加一个命令只需新建类,无需修改Controller;暴露`List
`共享引用的问题并预告解决方案 | --- ## 三、教学过程设计(90分钟) | 环节 | 时间 | 教学内容 | 师生活动 | AI协同点 | |------|------|----------|----------|----------| | **1. 痛点引入:从脚本到工程的鸿沟** | 10' | 展示"意大利面"式爬虫代码,演示改一处需要动全身 | **教师演示**:现场展示一段混乱代码,让学生找问题 | 用AI分析代码耦合度 | | **2. CLI vs GUI:架构选择的思考** | 10' | 对比两种方案的优缺点,解释为什么CLI更适合培养工程思维 | **教师讲解**:用对比表格说明选择CLI的理由 | — | | **3. MVC分层设计** | 20' | 讲解Model/View/Controller三层职责,用"餐厅类比"强化理解,随后批判类比局限性 | **教师讲解**:配合架构图讲解三层交互,引导学生寻找类比破绽 | 用AI生成MVC职责对照表 | | **4. Command模式:可扩展的命令路由** | 15' | 引入Command接口,解释"一个命令就是一个类" | **类比**:Command像酒店的服务部门,Controller是前台 | 让AI解释Command模式的多态原理 | | **5. Maven模板与环境** | 5' | 直接使用提供的Maven模板,讲解目录结构 | **教师演示**:解压模板 → IDEA打开 → 运行 | — | | **6. 三层代码落地** | 20' | **Model**:Article实体
**View**:ConsoleView(ANSI常量)
**Command接口**+实现
**Controller**:Map路由 | **教师演示**:分步写出代码,刻意埋入1~2个"越权细节"让学生找茬 | 学生用AI做"架构审计" | | **7. 架构反思与展望** | 5' | 指出当前`List
`共享引用的问题,预告W10策略模式与仓库层 | **师生互动**:你发现这个设计有什么风险? | 让AI分析共享可变状态的危害 | | **8. 实践任务:空壳程序** | 5' | 搭建完整包结构,实现CLI循环 | 学生现场编码,教师巡视 | 完成后用AI检查包结构 | | **9. 总结与过渡** | 5' | 本周实现了"骨架+命令可扩展",下周填入"灵魂"——解析器,并解决数据安全问题 | 总结Command模式优势,预告策略模式 | — | --- ## 四、核心教学内容脚本 ### 4.1 痛点引入:从脚本到工程的鸿沟(10分钟) **教师口播**: > "同学们,前8周我们学的是Java语法,从变量到类,从继承到接口。但有一个问题:代码写完之后,怎么组织?" > > "来看这段代码——这是某个同学写的'爬虫',他一个人完成了一个'完整'的项目。" **展示"脚本式"代码**: ```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()); } } } ``` **提问引导**: 1. "如果我想把标题保存到文件,要改哪里?" 2. "如果我想支持另一个网站,它的HTML结构不一样,要怎么办?" 3. "如果我想让输出变成彩色,要改哪里?" **痛点提炼**: > "看到了吗?才60行代码,已经'牵一发而动全身'了。这就是一个'脚本'的宿命——功能全混在一起,改一个小需求,整个文件都要翻。" > > "这周我们要解决:**怎么让代码'改起来不疼'?**" --- ### 4.2 CLI vs GUI:架构选择的思考(10分钟) **教师口播**: > "既然要写一个'完整'的爬虫应用,我们有两个选择:图形界面(GUI)或命令行界面(CLI)。为什么我推荐CLI而不是GUI?" **对比表格** | 维度 | GUI (JavaFX) | CLI (命令行) | |------|--------------|-------------| | **代码量** | FXML + Controller + CSS,大量模板代码 | 纯Java,代码量可控 | | **学习重心** | 布局、控件、事件监听 | 架构、分层、命令路由 | | **后端能力** | 几乎无关 | 模拟真实服务器开发 | | **可测试性** | 难(需要UI测试框架) | 易(直接测试Command类) | | **工程思维** | 弱(关注视觉) | 强(关注逻辑) | **核心观点**: > **CLI更需要MVC!** GUI有现成的事件系统(点击按钮→触发事件),而CLI只有字符流。**没有架构,分分钟写成脚本**。MVC在CLI里是"刚需",不是"装饰"。 > > **更深一层**:在GUI里,框架已经硬塞给你一套架构,你只是在填空;但在CLI里,所有结构都必须由你亲手搭建。**当外部约束消失,内部的工程纪律才真正开始建立**——这才是本节课的真正目的。 **CLI也能很酷**: - ANSI彩色输出(红/绿/黄/蓝) - 表格展示数据 - 进度条动画 - 模拟真实大数据开发场景 --- ### 4.3 MVC分层设计(20分钟) #### 4.3.1 MVC的起源与演进 **教师口播**: > "MVC不是新东西,它是1970年代为桌面应用设计的架构思想。但它的核心——'职责分离'——在任何软件里都适用。" | 年代 | 场景 | MVC的角色 | |------|------|----------| | 1970s | Smalltalk-72 GUI | 最早的用户界面架构 | | 1990s | Web开发 (Struts) | 后端模板引擎 | | 2000s | ASP.NET MVC | 现代Web框架 | | 2020s | CLI + API | 解耦业务逻辑与表现层 | #### 4.3.2 从GUI到CLI的映射 | GUI组件 | CLI对应 | 说明 | |--------|--------|------| | 窗口/按钮 | 命令行输入 | **View = 用户交互** | | 数据模型 | Article实体类 | **Model = 数据结构** | | 事件监听 | Command路由 | **Controller = 调度** | #### 4.3.3 MVC三层职责 **架构图示**: ``` ┌─────────────────────────────────────────┐ │ 入口 │ │ (main方法) │ └─────────────────┬───────────────────────┘ │ ▼ ┌─────────────────────────────────────────┐ │ Controller │ │ - 接收命令(crawl, help, exit) │ │ - 分发给对应的Command │ │ 【口诀】:Controller不管"怎么做", │ │ 只管"派给谁" │ └─────────┬───────────────┬───────────────┘ │ │ ▼ ▼ ┌─────────────────┐ ┌─────────────────┐ │ Model │ │ View │ │ - 数据实体 │ │ - 输入解析 │ │ - 业务逻辑 │ │ - 输出格式化 │ │ 【口诀】: │ │ 【口诀】: │ │ Model管"数据" │ │ View管"呈现" │ └─────────────────┘ └─────────────────┘ ``` **三层职责详解** | 层级 | 职责 | 典型代码 | 禁止做什么 | |------|------|----------|------------| | **Model** | 数据结构 + 业务逻辑 | `class Article { String title; String content; }` | 不能有`System.out.println`,不能有`Scanner` | | **View** | 接收用户输入 + 格式化输出 | `class ConsoleView { String readInput(); void print(String); }` | 不能写爬虫逻辑,只做"传声筒" | | **Controller** | 协调调度 | `class CrawlerController { void handle(String cmd) { ... } }` | 不能直接写业务细节,委托给Command | #### 4.3.4 类比强化:"餐厅类比" > "把MVC想象成一家餐厅: > - **Model是后厨**:只管做菜(数据加工),不管谁来吃、怎么端 > - **View是服务员**:只管端菜和收钱(输入输出),不管菜怎么做 > - **Controller是前台**:只管把顾客的点单传给后厨,把做好的菜端给顾客 > > 如果后厨开始管'谁来吃饭',这餐厅就乱了。" #### 4.3.5 对"餐厅类比"的批判性思考(关键!) **教师导引**: > "刚才的类比好理解吗?很好。但任何一个类比都有它的边界,如果把它当成真理,就会出问题。现在我们来给这个类比'找茬'。" **提问学生**: 1. "后厨真的完全不知道客人是谁吗?如果客人有忌口(比如不吃香菜),这个信息需不需要传到后厨?" 2. "服务员只是端菜吗?在真实餐厅里,服务员经常向后厨反馈'客人觉得今天的菜咸了',这属于View→Model的反向影响吗?" 3. "在这个类比里,我们把前台(Controller)和后厨(Model)的关系说成单向的。但实际上,后厨做完了菜,需要通知前台'菜好了',这不就是**观察者模式**吗?" **点明本质**: > "实际MVC的数据流向常常是**双向**的:Controller调用Model的方法改变数据,Model变化后又通知View更新显示。只不过在本次CLI项目中,我们暂时使用'请求-响应'的单向简化模型——用户输入命令,系统处理,然后立即输出结果。这个简化版够用,但你要知道完整的MVC是更动态的。随着系统复杂,Model层需要一个专门的'仓库类'来管理数据,并通知视图刷新——这正是W10我们将要深入的内容。" #### 4.3.6 MVC的数据流向(本课程简化版) ``` CLI用户输入 ↓ View(解析命令字符串) ↓ Controller(找到对应Command) ↓ Command.execute()(执行业务逻辑) ↓ Model(Article数据,目前暂存于List) ↓ View(display()展示数据) ↓ CLI终端显示 ``` --- ### 4.4 Command模式:可扩展的命令路由(15分钟) **教师口播**: > "现在引入一个设计模式——Command(命令)模式。它的核心思想是:**一个命令就是一个类**。" #### 4.4.1 为什么需要Command模式? **演示:增加一个命令的代价(switch-case版)** ```java // 现状代码 switch (cmd) { case "crawl": handleCrawl(); break; case "help": showHelp(); break; // 如果要增加 list 命令? // 1. 加 case "list" // 2. 加 handleList() 方法 // 3. 可能还要改其他地方... } ``` **提问**: - "如果我想增加10个命令,这个类要改多少次?" - "如果我不小心删了一个case,整个程序还能跑吗?" **痛点提炼**: > "每加一个功能,就要在这个类里戳一个洞。**这就是'肥控制器'陷阱**——所有的逻辑都堆在Controller里,它变成了新的'意大利面'。" #### 4.4.2 Command模式的四个要素 | 要素 | 角色 | 示例 | |------|------|------| | **Command接口** | 抽象的"订单" | `Command` 接口 | | **ConcreteCommand** | 具体的订单 | `HelpCommand`、`CrawlCommand` | | **Invoker** | 接单的前台 | `CrawlerController` | | **Receiver** | 执行者 | `ConsoleView`、`ArticleRepository` | #### 4.4.3 Command接口定义 ```java // src/main/java/com/crawler/command/Command.java package com.crawler.command; import com.crawler.model.Article; import java.util.List; public interface Command { String getName(); // 命令名,如 "crawl" void execute(String[] args, List
articles); // 执行逻辑 } ``` #### 4.4.4 Controller的变革(从switch到Map) ```java // 修改后的Controller public class CrawlerController { private Map commands; // 用Map存命令 private ConsoleView view; // 持有View以输出错误 public CrawlerController(ConsoleView view, List
articles) { this.view = view; this.commands = new HashMap<>(); // 增加命令无需改Controller代码,只需在这里注册 commands.put("crawl", new CrawlCommand(view)); commands.put("help", new HelpCommand(view)); commands.put("list", new ListCommand(view)); commands.put("exit", new ExitCommand(view)); } public void handle(String input) { if (input.isEmpty()) return; String[] parts = input.split("\\s+"); String cmd = parts[0].toLowerCase(); Command command = commands.get(cmd); if (command == null) { view.printError("Unknown command: " + cmd); // 通过View输出,而非直接System.out return; } // 执行命令,传入参数和文章列表 command.execute(parts, articles); } } ``` **对比表格** | 维度 | switch-case | Command模式 | |------|-------------|-------------| | 增加命令 | 要改Controller | 新建一个类 | | 多态体验 | 无 | execute()的多态调用 | | 可测试性 | 难 | 每个Command可单独测试 | | 代码量 | 少 | 多,但更清晰 | **类比强化**: > "Command模式就像**酒店的客房服务**:每个服务(清理、送餐、按摩)都是一个独立的部门。前台(Controller)只负责接电话,然后把请求'派发'给对应的部门。部门自己知道怎么干活,不需要前台教。" > > "如果想新增一个服务,前台只需要'登记'一下,不需要把现有部门重新装修。" --- ### 4.5 Maven模板与环境(5分钟) **教师口播**: > "这周我们不发愁pom.xml配置。我已经把 Maven 模板准备好了,你们只需要解压、打开、运行。" **模板使用流程**: ``` 1. 解压 [my-crawler-template.zip] 2. 用 IDEA 打开文件夹 3. 右键 pom.xml → Maven → Reload Project 4. 运行 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 ``` --- ### 4.6 代码落地(20分钟) #### 4.6.1 Model层:Article实体 ```java // src/main/java/com/crawler/model/Article.java package com.crawler.model; public class Article { private String title; private String url; private String content; public Article(String title, String url, String content) { this.title = title; this.url = url; this.content = content; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } public String getContent() { return content; } public void setContent(String content) { this.content = content; } @Override public String toString() { return "Article{title='" + title + "', url='" + url + "'}"; } } ``` #### 4.6.2 View层:ANSI常量集中管理(工程细节!) ```java // src/main/java/com/crawler/view/ConsoleView.java package com.crawler.view; import com.crawler.model.Article; import java.util.List; import java.util.Scanner; public class ConsoleView { // ANSI颜色常量——集中管理,避免散落各处 private static final String ANSI_GREEN = "\033[32m"; private static final String ANSI_RED = "\033[31m"; private static final String ANSI_CYAN = "\033[36m"; private static final String ANSI_RESET = "\033[0m"; private Scanner scanner = new Scanner(System.in); public String readLine() { System.out.print("crawler> "); return scanner.nextLine().trim(); } public void print(String msg) { System.out.println(msg); } public void printSuccess(String msg) { print(ANSI_GREEN + msg + ANSI_RESET); } public void printError(String msg) { print(ANSI_RED + msg + ANSI_RESET); } public void printInfo(String msg) { print(ANSI_CYAN + msg + ANSI_RESET); } // 展示文章列表 public void display(List
articles) { if (articles.isEmpty()) { printInfo("No articles yet. Use 'crawl ' first."); return; } print("+----------+--------------------------------+"); print("| Title | URL |"); print("+----------+--------------------------------+"); for (Article a : articles) { String title = a.getTitle(); if (title.length() > 10) title = title.substring(0, 10) + ".."; String url = a.getUrl(); if (url.length() > 30) url = url.substring(0, 27) + "..."; print("| " + String.format("%-10s", title) + " | " + url + " |"); } print("+----------+--------------------------------+"); printInfo("Total: " + articles.size() + " articles"); } } ``` **教师提示**: > "注意:所有ANSI转义码都被定义为`private static final`常量。如果把`\033[32m`散落在项目各处,一旦想调整颜色,就得满世界去改——这正是我们之前痛批的'意大利面'。**这就是工程细节**。" #### 4.6.3 Command接口与四个实现(全部通过View输出) ```java // Command.java public interface Command { String getName(); void execute(String[] args, List
articles); } // 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"); } } // ListCommand.java public class ListCommand implements Command { private ConsoleView view; public ListCommand(ConsoleView v) { this.view = v; } public String getName() { return "list"; } public void execute(String[] args, List
articles) { view.display(articles); } } // 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]); } } // 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!"); // 全部输出都通过View,绝不让System.out直接出现在这里 System.exit(0); } } ``` **故意埋设的"找茬点"**: > "我在刚才的代码里有没有隐藏违反MVC原则的地方?`CrawlCommand`的存根里,`view.printInfo("Stub: Would crawl " + args[1]);` —— 这个字符串拼接算是"业务逻辑"吗?留给大家用AI架构审计时讨论。 #### 4.6.4 Controller:Map路由(全部通过View输出) ```java // src/main/java/com/crawler/controller/CrawlerController.java package com.crawler.controller; import com.crawler.command.*; import com.crawler.model.Article; import com.crawler.view.ConsoleView; import java.util.HashMap; import java.util.List; import java.util.Map; public class CrawlerController { private Map commands = new HashMap<>(); private ConsoleView view; // 持有View private List
articles; public CrawlerController(ConsoleView view, List
articles) { this.view = view; this.articles = 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) { if (input.isEmpty()) return; String[] parts = input.split("\\s+"); String cmdName = parts[0].toLowerCase(); Command cmd = commands.get(cmdName); if (cmd == null) { view.printError("Unknown command: " + cmdName); // 错误信息也走View! return; } cmd.execute(parts, articles); } } ``` #### 4.6.5 main方法:组装 ```java // src/main/java/com/crawler/App.java package com.crawler; import com.crawler.controller.CrawlerController; import com.crawler.model.Article; import com.crawler.view.ConsoleView; import java.util.ArrayList; import java.util.List; public class App { public static void main(String[] args) { ConsoleView view = new ConsoleView(); List
articles = new ArrayList<>(); CrawlerController controller = new CrawlerController(view, articles); view.printSuccess("Welcome to CLI Crawler!"); view.printInfo("Type 'help' for commands."); while (true) { controller.handle(view.readLine()); } } } ``` #### 4.6.6 架构反思与展望:共享List
的隐患(关键!) **教师口播**: > "现在这个架构已经可用了。但请大家审视一下:我们所有的Command都直接拿到了`List
`的引用。换句话说,任何一个命令都可以随意增、删、改这个列表。" > > "这就好像一家酒店,所有服务员、厨师、清洁工都能随意进出保险箱——**数据结构完全裸奔了**。" **提问**: - "如果CrawlCommand不小心写错了代码,把一个null塞进articles,HelpCommand会不会受影响?" - "如果未来我们要在添加文章时也写入日志文件,现在的设计能优雅实现吗?还是得在所有Command里分别加日志代码?" **预告解决方案**: > "下周,我们将引入**策略模式**和一个真正的**Model仓库层(ArticleRepository)**。这个仓库会把`List`封装起来,对外只提供`add()`、`getAll()`等安全接口。任何命令想修改数据,都必须通过仓库。这就是从'数据结构'到'模型层'的进化——我们W9先搭骨架,W10给它装上盔甲。" --- ### 4.7 实践任务(5分钟) **任务要求**: 1. 使用Maven模板创建项目 2. 实现完整包结构(model/view/command/controller) 3. 实现4个Command:help/list/crawl/exit 4. `list`命令能展示已抓取的文章 5. 运行并测试循环 6. **代码找茬(额外加分)**:找出你自己代码中是否存在`System.out`直接调用、硬编码ANSI字符串等"越权行为" **验收标准**: - [x] Maven编译通过 - [x] Command接口和4个实现分离在不同文件 - [x] Controller里没有switch-case - [x] 新增命令只需新建类,不改Controller - [x] list命令能正确显示空列表 - [x] 所有输出均通过ConsoleView完成,无直接System.out.println(main除外) - [x] ANSI颜色码集中定义为View常量 --- ## 五、课后作业 ### 5.1 必做任务 1. **完善Article**:增加`author`、`publishDate`字段 2. **★ HistoryCommand(强制作业)**: - 实现`history`命令,记录用户输入过的所有命令 - 使用`List`存储历史(复习W8集合) - 示例输出: ``` crawler> history 1. help 2. list 3. crawl https://example.com ``` 3. **AI架构审计**:将类名和方法名发给AI,指令: > "作为Java架构审计师,请检查我的MVC三层划分是否存在越权行为?Model层是否包含输入输出代码?View层是否越权写了业务逻辑?有没有地方直接使用了System.out或硬编码ANSI码?" ### 5.2 选做任务 1. **命令别名**:给`crawl`增加别名`c`,`help`增加别名`h` 2. **URL验证**:检查URL格式是否以http://或https://开头 3. **暗色主题**:实现不同的配色方案(利用View中的ANSI常量,只需修改一处即可) 4. **思考并回答**:分析`List
`共享引用的潜在风险,写一段200字的小结 ### 5.3 思考题 1. **Command vs switch-case**:增加10个命令,哪种方式代码改动量更小? 2. **如果不用Command接口,直接用Map存命令类行不行?** 接口的意义是什么? 3. **Controller里的`commands.put()`能否减少?** 提示:思考"注册机制" 4. **为什么ExitCommand里的`view.printSuccess("Bye!")`比直接`System.out.println`更"MVC"?** 提示:回忆View的职责 --- ## 六、AI协同升级 ### 架构审计师任务(必做) **学生执行步骤**: 1. 列出项目中所有类名(不含方法实现) 2. 将类名列表发给AI 3. 输入指令: > "作为Java架构审计师,请检查我的MVC三层划分是否清晰。Model层是否包含了不应该有的代码(Scanner/System.out)?View层是否越权写了业务逻辑?请指出任何一处直接使用System.out.println的地方,并建议如何改正。" **预期AI输出**: - 指出哪一层有越权行为 - 建议如何整改 - 评价整体架构健康度 ### 进阶AI探究(选做) > "假设我的Command接口中execute方法接收了一个`List
`参数,请分析这种设计在工程上有什么隐患,并给出重构建议。" --- ## 七、教学反思与调整记录 | 日期 | 事项 | 调整内容 | |------|------|----------| | 2026-04-28 | 首次编写 | 基于CLI+MVC重构 | | 2026-04-30 | 教授反馈 | 引入Command模式、提供Maven模板、升级AI协同比 | | 2026-04-30 | 逻辑重排 | 按"问题→选择→架构→模式"顺序重写 | | 2026-05-01 | v2 vs V3合并 | 融合深度改进:增加教育哲学、批判性思考、ANSI常量、共享List隐患、故意埋坑 | --- ## 附录1:Maven模板说明 > 老师提供`my-crawler-template.zip`压缩包,包含: > - pom.xml(含Jsoup依赖) > - 空的src/main/java结构 > - .gitignore ## 附录2:常见问题速查 | 问题 | 解答 | |------|------| | IDEA不识别pom.xml | 右键 pom.xml → Maven → Reload Project | | 中文乱码 | Settings → Editor → File Encodings → UTF-8 | | 包名大小写 | 包名全小写,类名首字母大写 | | Command找不到 | 检查是否 implements Command,是否 @Override getName() | | 命令不生效 | 检查 commands.put() 是否注册了该命令 | | 输出颜色乱码 | IDEA控制台需支持ANSI,Windows下建议使用Windows Terminal或调整设置 | | 我的System.out为什么被老师说越权 | View层才是与用户交互的唯一出口,所有输出都应通过View,这样将来改成GUI或日志时只需改View | ## 附录3:教学逻辑说明 | 顺序 | 内容 | 设计理由 | |------|------|----------| | 1 | 痛点引入 | 从问题出发,让学生感受"为什么需要架构" | | 2 | CLI vs GUI | 解释技术选型,建立"工程思维 > 视觉装饰"的认知 | | 3 | MVC分层 | 核心架构概念,理解职责分离,通过类比及批判加深理解 | | 4 | Command模式 | 具体实现方式,解决"肥控制器"问题 | | 5 | Maven | 工具链支持 | | 6 | 代码落地 | 实践验证,刻意植入细节规范,训练工程洁癖 | | 7 | 架构反思 | 暴露共享可变状态隐患,为W10策略模式+仓库层做铺垫 | | 8 | 实践任务 | 现场编码验证 | | 9 | 总结 | 强化认知,预告下周 | --- ## 版本说明 - **v1**:首次编写,CLI+MVC基础框架 - **v2**:按"问题→选择→架构→模式"逻辑重排 - **v3 (本版)**:融合v2结构 + V3深度改进,包含: - 更深的CLI教育哲学 - 餐厅类比批判性思考 - ANSI常量集中管理工程细节 - 全部输出走View - 共享List架构隐患反思 - 故意埋坑让学生找茬 - W10铺垫(策略模式+仓库层)