diff --git a/W10/CrawlerMain2/.gitignore b/W10/CrawlerMain2/.gitignore deleted file mode 100644 index f68d109..0000000 --- a/W10/CrawlerMain2/.gitignore +++ /dev/null @@ -1,29 +0,0 @@ -### IntelliJ IDEA ### -out/ -!**/src/main/**/out/ -!**/src/test/**/out/ - -### Eclipse ### -.apt_generated -.classpath -.factorypath -.project -.settings -.springBeans -.sts4-cache -bin/ -!**/src/main/**/bin/ -!**/src/test/**/bin/ - -### NetBeans ### -/nbproject/private/ -/nbbuild/ -/dist/ -/nbdist/ -/.nb-gradle/ - -### VS Code ### -.vscode/ - -### Mac OS ### -.DS_Store \ No newline at end of file diff --git a/W10/CrawlerMain2/.idea/libraries/jcommon_1_0_24.xml b/W10/CrawlerMain2/.idea/libraries/jcommon_1_0_24.xml deleted file mode 100644 index cef0a8d..0000000 --- a/W10/CrawlerMain2/.idea/libraries/jcommon_1_0_24.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/W10/CrawlerMain2/.idea/libraries/jfreechart_1_5_3.xml b/W10/CrawlerMain2/.idea/libraries/jfreechart_1_5_3.xml deleted file mode 100644 index 6fdf9d7..0000000 --- a/W10/CrawlerMain2/.idea/libraries/jfreechart_1_5_3.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/W10/CrawlerMain2/.idea/libraries/jsoup_1_17_2.xml b/W10/CrawlerMain2/.idea/libraries/jsoup_1_17_2.xml deleted file mode 100644 index 90ce41d..0000000 --- a/W10/CrawlerMain2/.idea/libraries/jsoup_1_17_2.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/W10/CrawlerMain2/.idea/libraries/kumo_core_1_12.xml b/W10/CrawlerMain2/.idea/libraries/kumo_core_1_12.xml deleted file mode 100644 index c74069d..0000000 --- a/W10/CrawlerMain2/.idea/libraries/kumo_core_1_12.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/W10/CrawlerMain2/.idea/libraries/logback_classic_1_4_11.xml b/W10/CrawlerMain2/.idea/libraries/logback_classic_1_4_11.xml deleted file mode 100644 index 54a73cf..0000000 --- a/W10/CrawlerMain2/.idea/libraries/logback_classic_1_4_11.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/W10/CrawlerMain2/.idea/libraries/logback_core_1_4_11.xml b/W10/CrawlerMain2/.idea/libraries/logback_core_1_4_11.xml deleted file mode 100644 index fbdb3a1..0000000 --- a/W10/CrawlerMain2/.idea/libraries/logback_core_1_4_11.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/W10/CrawlerMain2/.idea/libraries/slf4j_api_2_0_9.xml b/W10/CrawlerMain2/.idea/libraries/slf4j_api_2_0_9.xml deleted file mode 100644 index 7c49634..0000000 --- a/W10/CrawlerMain2/.idea/libraries/slf4j_api_2_0_9.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/W10/CrawlerMain2/.idea/misc.xml b/W10/CrawlerMain2/.idea/misc.xml deleted file mode 100644 index 3653b1f..0000000 --- a/W10/CrawlerMain2/.idea/misc.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/W10/CrawlerMain2/CrawlerMain2.iml b/W10/CrawlerMain2/CrawlerMain2.iml deleted file mode 100644 index e0317a0..0000000 --- a/W10/CrawlerMain2/CrawlerMain2.iml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/W10/CrawlerMain2/src/CrawlerMain.java b/W10/CrawlerMain2/src/CrawlerMain.java deleted file mode 100644 index a6aafec..0000000 --- a/W10/CrawlerMain2/src/CrawlerMain.java +++ /dev/null @@ -1,299 +0,0 @@ -import org.jsoup.Jsoup; -import org.jsoup.nodes.Document; -import org.jsoup.nodes.Element; -import org.jsoup.select.Elements; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.File; -import java.io.FileWriter; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -// 1. 抽象策略接口(去掉public,因为文件名是CrawlerMain.java) -interface Crawler { - List startCrawl(); -} - -// 2. 抽象模板父类 -abstract class BaseCrawler implements Crawler { - protected final String baseUrl; - protected static final Logger logger = LoggerFactory.getLogger(BaseCrawler.class); - - protected BaseCrawler(String baseUrl) { - this.baseUrl = baseUrl; - } - - protected Document getPage(String url) throws Exception { - logger.info("正在请求页面:{}", url); - return Jsoup.connect(url) - .userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64)") - .timeout(15000) - .get(); - } - - @Override - public abstract List startCrawl(); -} - -// 3. 实体类 -class Movie { - private final String title; - private final String rating; - - public Movie(String title, String rating) { - this.title = title; - this.rating = rating; - } - - public String getTitle() { return title; } - public double getRatingDouble() { return Double.parseDouble(rating); } - public String getRating() { return rating; } - - @Override - public String toString() { - return "电影:《" + title + "》 | 评分:" + rating; - } -} - -class Hero { - private final String name; - public Hero(String name) { this.name = name; } - public String getName() { return name; } - @Override - public String toString() { return "英雄:" + name; } -} - -class Weather { - private final String province; - private final String city; - private final String temperature; - private final String condition; - - public Weather(String province, String city, String temperature, String condition) { - this.province = province; - this.city = city; - this.temperature = temperature; - this.condition = condition; - } - - public String getProvince() { return province; } - public String getCity() { return city; } - public String getTemperature() { return temperature; } - public String getCondition() { return condition; } - - @Override - public String toString() { - return "省份:" + province + " | 城市:" + city + " | 天气:" + condition + " | 温度:" + temperature; - } -} - -// 4. 具体策略类 -class MovieCrawler extends BaseCrawler { - private static final Logger logger = LoggerFactory.getLogger(MovieCrawler.class); - - public MovieCrawler() { - super("https://movie.douban.com/top250"); - } - - @Override - public List startCrawl() { - List list = new ArrayList<>(); - logger.info("开始爬取豆瓣电影Top250"); - try { - for (int i = 0; i < 250; i += 25) { - Document doc = getPage(baseUrl + "?start=" + i); - Elements items = doc.select(".item"); - for (Element e : items) { - String title = e.select(".title").first().text().split("/")[0].trim(); - String rating = e.select(".rating_num").text(); - list.add(new Movie(title, rating)); - } - Thread.sleep(1000); - } - logger.info("豆瓣电影爬取完成,共{}条数据", list.size()); - } catch (Exception e) { - logger.error("电影爬取失败", e); - } - return list; - } -} - -class HeroCrawler extends BaseCrawler { - private static final Logger logger = LoggerFactory.getLogger(HeroCrawler.class); - - public HeroCrawler() { - super("https://pvp.qq.com/web201605/herolist.shtml"); - } - - @Override - public List startCrawl() { - List list = new ArrayList<>(); - logger.info("开始爬取王者荣耀英雄数据"); - try { - Document doc = getPage(baseUrl); - Elements heros = doc.select("ul.herolist li a"); - for (Element h : heros) { - String name = h.text().trim(); - if (!name.isEmpty()) { - list.add(new Hero(name)); - } - } - logger.info("英雄爬取完成,共{}条数据", list.size()); - } catch (Exception e) { - logger.error("英雄爬取失败", e); - } - return list; - } -} - -class WeatherCrawler extends BaseCrawler { - private static final Logger logger = LoggerFactory.getLogger(WeatherCrawler.class); - - private static final String[][] cities = { - {"北京","北京","101010100"},{"上海","上海","101020100"},{"天津","天津","101030100"},{"重庆","重庆","101040100"}, - {"河北","石家庄","101090101"},{"山西","太原","101100101"},{"辽宁","沈阳","101070101"},{"吉林","长春","101060101"}, - {"黑龙江","哈尔滨","101050101"},{"江苏","南京","101190101"},{"浙江","杭州","101210101"},{"安徽","合肥","101220101"}, - {"福建","福州","101230101"},{"江西","南昌","101240101"},{"山东","济南","101120101"},{"河南","郑州","101180101"}, - {"湖北","武汉","101200101"},{"湖南","长沙","101250101"},{"广东","广州","101280101"},{"海南","海口","101310101"}, - {"四川","成都","101270101"},{"贵州","贵阳","101260101"},{"云南","昆明","101290101"},{"陕西","西安","101110101"}, - {"甘肃","兰州","101160101"},{"青海","西宁","101150101"},{"内蒙古","呼和浩特","101080101"},{"广西","南宁","101300101"}, - {"西藏","拉萨","101140101"},{"宁夏","银川","101170101"},{"新疆","乌鲁木齐","101130101"}, - {"香港","香港","101320101"},{"澳门","澳门","101330101"},{"台湾","台北","101340101"} - }; - - public WeatherCrawler() { - super("https://www.weather.com.cn/weather/"); - } - - @Override - public List startCrawl() { - List list = new ArrayList<>(); - logger.info("开始爬取全国天气数据"); - try { - for (String[] city : cities) { - String province = city[0]; - String cityName = city[1]; - String code = city[2]; - Document doc = getPage(baseUrl + code + ".shtml"); - Element today = doc.select("ul.t li").first(); - if (today != null) { - String temp = today.select(".tem").text(); - String wea = today.select(".wea").text(); - list.add(new Weather(province, cityName, temp, wea)); - } - Thread.sleep(500); - } - logger.info("天气数据爬取完成,共{}条数据", list.size()); - } catch (Exception e) { - logger.error("天气爬取失败", e); - } - return list; - } -} - -// 5. 策略上下文Context -class CrawlerContext { - private Crawler crawlerStrategy; - private static final Logger logger = LoggerFactory.getLogger(CrawlerContext.class); - - public void setCrawlerStrategy(Crawler crawlerStrategy) { - this.crawlerStrategy = crawlerStrategy; - } - - public List executeCrawl() { - if (crawlerStrategy == null) { - logger.error("未设置爬取策略"); - return new ArrayList<>(); - } - return crawlerStrategy.startCrawl(); - } -} - -// 6. 工具类 -final class DataUtil { - private static final String PATH = "D:\\Java爬虫\\"; - private static final Logger logger = LoggerFactory.getLogger(DataUtil.class); - - private DataUtil() {} - - public static void initFolder() { - File dir = new File(PATH); - if (!dir.exists()) { - boolean created = dir.mkdirs(); - if (created) { - logger.info("创建目录:{}", PATH); - } - } - } - - public static void saveText(String fileName, String content) throws IOException { - if (content == null || content.isBlank()) { - logger.warn("保存文件内容为空,跳过:{}", fileName); - return; - } - try (FileWriter fw = new FileWriter(PATH + fileName)) { - fw.write(content); - } - logger.info("文件保存成功:{}", fileName); - } - - public static void addAll(String fileName, List dataList) throws IOException { - if (dataList == null || dataList.isEmpty()) { - logger.warn("批量数据为空,跳过保存:{}", fileName); - return; - } - StringBuilder sb = new StringBuilder(); - dataList.forEach(item -> sb.append(item).append("\r\n")); - saveText(fileName, sb.toString()); - } - - public static void analyzeData(List movieList, List heroList) { - if (movieList == null || heroList == null) { - logger.error("分析数据列表为空"); - return; - } - logger.info("===== 执行数据分析 ====="); - double sum = 0; - for (Movie movie : movieList) { - sum += movie.getRatingDouble(); - } - double avg = sum / movieList.size(); - System.out.println("电影平均评分:" + String.format("%.2f", avg)); - System.out.println("8.5分以上电影数量:" + movieList.stream().filter(m -> m.getRatingDouble() >= 8.5).count()); - System.out.println("英雄总数量:" + heroList.size()); - logger.info("数据分析结束"); - } -} - -// 7. 主程序(必须和文件名一致,public) -public class CrawlerMain { - private static final Logger logger = LoggerFactory.getLogger(CrawlerMain.class); - - public static void main(String[] args) { - logger.info("===== 爬虫程序启动 ====="); - CrawlerContext context = new CrawlerContext(); - - context.setCrawlerStrategy(new MovieCrawler()); - List movieList = (List) context.executeCrawl(); - - context.setCrawlerStrategy(new HeroCrawler()); - List heroList = (List) context.executeCrawl(); - - context.setCrawlerStrategy(new WeatherCrawler()); - List weatherList = (List) context.executeCrawl(); - - try { - DataUtil.initFolder(); - DataUtil.addAll("电影数据.txt", movieList); - DataUtil.addAll("英雄数据.txt", heroList); - DataUtil.addAll("天气数据.txt", weatherList); - DataUtil.analyzeData(movieList, heroList); - logger.info("===== 全部任务执行完成 ====="); - System.out.println("✅ 数据已全部保存至 D:\\Java爬虫"); - } catch (Exception e) { - logger.error("程序运行异常", e); - } - } -} \ No newline at end of file diff --git a/W10/CrawlerMain2/src/logback.xml b/W10/CrawlerMain2/src/logback.xml deleted file mode 100644 index 0564736..0000000 --- a/W10/CrawlerMain2/src/logback.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - %d{HH:mm:ss} %-5level - %msg%n - - - - - - - \ No newline at end of file diff --git a/W10/CrawlerMain2/.idea/.gitignore b/W10/java-cli/.idea/.gitignore similarity index 100% rename from W10/CrawlerMain2/.idea/.gitignore rename to W10/java-cli/.idea/.gitignore diff --git a/W10/java-cli/.idea/compiler.xml b/W10/java-cli/.idea/compiler.xml new file mode 100644 index 0000000..ff21975 --- /dev/null +++ b/W10/java-cli/.idea/compiler.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/W10/java-cli/.idea/jarRepositories.xml b/W10/java-cli/.idea/jarRepositories.xml new file mode 100644 index 0000000..712ab9d --- /dev/null +++ b/W10/java-cli/.idea/jarRepositories.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/W10/java-cli/.idea/java-cli.iml b/W10/java-cli/.idea/java-cli.iml new file mode 100644 index 0000000..d6ebd48 --- /dev/null +++ b/W10/java-cli/.idea/java-cli.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/W10/java-cli/.idea/misc.xml b/W10/java-cli/.idea/misc.xml new file mode 100644 index 0000000..dbdc8e1 --- /dev/null +++ b/W10/java-cli/.idea/misc.xml @@ -0,0 +1,14 @@ + + + + + + + + + + \ No newline at end of file diff --git a/W10/CrawlerMain2/.idea/modules.xml b/W10/java-cli/.idea/modules.xml similarity index 54% rename from W10/CrawlerMain2/.idea/modules.xml rename to W10/java-cli/.idea/modules.xml index 8824534..2a96444 100644 --- a/W10/CrawlerMain2/.idea/modules.xml +++ b/W10/java-cli/.idea/modules.xml @@ -2,7 +2,7 @@ - + \ No newline at end of file diff --git a/W10/java-cli/java-cli/.gitignore b/W10/java-cli/java-cli/.gitignore new file mode 100644 index 0000000..0ebcf1a --- /dev/null +++ b/W10/java-cli/java-cli/.gitignore @@ -0,0 +1,4 @@ +*.jar +*.jar +*.class +*.log \ No newline at end of file diff --git a/W10/java-cli/java-cli/README.md b/W10/java-cli/java-cli/README.md new file mode 100644 index 0000000..3ea02ec --- /dev/null +++ b/W10/java-cli/java-cli/README.md @@ -0,0 +1,17 @@ +# DataCollect 教学项目 — 最小可运行版本 + +这是一个最小可用的 Java CLI 演示工程,目标:打印帮助信息以验证运行环境。 + +构建: +```bash +mvn -q package +``` + +运行(示例): +```bash +java -jar target/datacollect-cli-0.1.0-jar-with-dependencies.jar --help +``` + +项目结构(最小): +- `src/main/java/com/example/datacollect/Main.java` — CLI 入口,打印帮助 +- `pom.xml` — Maven 构建配置,生成可执行 jar diff --git a/W10/java-cli/java-cli/pom.xml b/W10/java-cli/java-cli/pom.xml new file mode 100644 index 0000000..01bc611 --- /dev/null +++ b/W10/java-cli/java-cli/pom.xml @@ -0,0 +1,45 @@ + + 4.0.0 + com.example + datacollect-cli + 0.1.0 + + 11 + 11 + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + + org.apache.maven.plugins + maven-assembly-plugin + 3.3.0 + + + + com.example.datacollect.Main + + + + jar-with-dependencies + + + + + make-assembly + package + + single + + + + + + + diff --git a/W10/java-cli/java-cli/src/main/java/com/example/datacollect/Main.java b/W10/java-cli/java-cli/src/main/java/com/example/datacollect/Main.java new file mode 100644 index 0000000..f3a2268 --- /dev/null +++ b/W10/java-cli/java-cli/src/main/java/com/example/datacollect/Main.java @@ -0,0 +1,32 @@ +package com.example.datacollect; + +import com.example.datacollect.controller.CrawlerController; +import com.example.datacollect.repository.ArticleRepository; +import com.example.datacollect.strategy.StrategyFactory; +import com.example.datacollect.view.ConsoleView; +import java.util.ArrayList; +import java.util.List; + +public class Main { + // 全局命令历史列表 + private static final List commandHistory = new ArrayList<>(); + + public static void main(String[] args) { + ConsoleView view = new ConsoleView(); + ArticleRepository repository = new ArticleRepository(); + StrategyFactory factory = new StrategyFactory(); + CrawlerController controller = new CrawlerController(view, repository, factory); + + view.printSuccess("Welcome to CLI Crawler (w10)! Type help for commands."); + + while (true) { + String input = view.readLine(); + commandHistory.add(input); + controller.handle(input); + } + } + + public static List getCommandHistory() { + return commandHistory; + } +} \ No newline at end of file diff --git a/W10/java-cli/java-cli/src/main/java/com/example/datacollect/command/AnalyzeCommand.java b/W10/java-cli/java-cli/src/main/java/com/example/datacollect/command/AnalyzeCommand.java new file mode 100644 index 0000000..48b4790 --- /dev/null +++ b/W10/java-cli/java-cli/src/main/java/com/example/datacollect/command/AnalyzeCommand.java @@ -0,0 +1,58 @@ +package com.example.datacollect.command; + +import com.example.datacollect.model.Article; +import com.example.datacollect.repository.ArticleRepository; +import com.example.datacollect.strategy.CrawlStrategy; +import com.example.datacollect.strategy.StrategyFactory; +import com.example.datacollect.view.ConsoleView; +import java.util.List; +import java.util.Optional; + +public class AnalyzeCommand implements Command { + private final ConsoleView view; + private final StrategyFactory strategyFactory; + + public AnalyzeCommand(ConsoleView view, StrategyFactory strategyFactory) { + this.view = view; + this.strategyFactory = strategyFactory; + } + + @Override + public String getName() { + // 命令名:控制台输入 analyze 即可调用 + return "analyze"; + } + + @Override + public void execute(String[] args, ArticleRepository repository) { + // 校验参数:必须传入URL + if (args.length < 2) { + view.printError("Usage: analyze "); + return; + } + + String url = args[1]; + view.printInfo("Analyzing: " + url); + + // 根据URL匹配对应爬取策略 + Optional strategyOpt = strategyFactory.getStrategy(url); + if (strategyOpt.isEmpty()) { + view.printError("Unsupported website: " + url); + return; + } + + // 执行爬取解析(复用原有逻辑) + List
articleList = strategyOpt.get().crawl(url); + + // 仅统计,不调用仓库存储 + int articleCount = articleList.size(); + int totalContentLen = articleList.stream() + .mapToInt(art -> art.getContent() == null ? 0 : art.getContent().length()) + .sum(); + + // 控制台输出统计结果 + view.printInfo("===== Analysis Result ====="); + view.printInfo("Total articles: " + articleCount); + view.printInfo("Total content length: " + totalContentLen + " chars"); + } +} \ No newline at end of file diff --git a/W10/java-cli/java-cli/src/main/java/com/example/datacollect/command/Command.java b/W10/java-cli/java-cli/src/main/java/com/example/datacollect/command/Command.java new file mode 100644 index 0000000..5156410 --- /dev/null +++ b/W10/java-cli/java-cli/src/main/java/com/example/datacollect/command/Command.java @@ -0,0 +1,9 @@ +package com.example.datacollect.command; + +import com.example.datacollect.repository.ArticleRepository; + +public interface Command { + String getName(); + // 原来的参数是 List
,现在改成 ArticleRepository + void execute(String[] args, ArticleRepository repository); +} \ No newline at end of file diff --git a/W10/java-cli/java-cli/src/main/java/com/example/datacollect/command/CrawlCommand.java b/W10/java-cli/java-cli/src/main/java/com/example/datacollect/command/CrawlCommand.java new file mode 100644 index 0000000..d8657df --- /dev/null +++ b/W10/java-cli/java-cli/src/main/java/com/example/datacollect/command/CrawlCommand.java @@ -0,0 +1,52 @@ +package com.example.datacollect.command; + +import com.example.datacollect.model.Article; +import com.example.datacollect.repository.ArticleRepository; +import com.example.datacollect.strategy.CrawlStrategy; +import com.example.datacollect.strategy.StrategyFactory; +import com.example.datacollect.view.ConsoleView; +import java.util.List; +import java.util.Optional; + +public class CrawlCommand implements Command { + private final ConsoleView view; + private final StrategyFactory strategyFactory; + + // 构造函数注入View和Factory + public CrawlCommand(ConsoleView view, StrategyFactory strategyFactory) { + this.view = view; + this.strategyFactory = strategyFactory; + } + + @Override + public String getName() { + return "crawl"; + } + + @Override + public void execute(String[] args, ArticleRepository repository) { + if (args.length < 2) { + view.printError("Usage: crawl "); + return; + } + + String url = args[1]; + view.printInfo("Crawling: " + url); + + // 1. 通过工厂获取对应的爬取策略 + Optional optionalStrategy = strategyFactory.getStrategy(url); + if (optionalStrategy.isEmpty()) { + view.printError("Unsupported website: " + url); + return; + } + + // 2. 用策略爬取文章 + CrawlStrategy strategy = optionalStrategy.get(); + List
crawledArticles = strategy.crawl(url); + + // 3. 通过Repository批量保存文章 + repository.addAll(crawledArticles); + + view.printSuccess("Crawled " + crawledArticles.size() + " articles successfully!"); + } +} \ No newline at end of file diff --git a/W10/java-cli/java-cli/src/main/java/com/example/datacollect/command/ExitCommand.java b/W10/java-cli/java-cli/src/main/java/com/example/datacollect/command/ExitCommand.java new file mode 100644 index 0000000..4e77bed --- /dev/null +++ b/W10/java-cli/java-cli/src/main/java/com/example/datacollect/command/ExitCommand.java @@ -0,0 +1,23 @@ +package com.example.datacollect.command; + +import com.example.datacollect.repository.ArticleRepository; +import com.example.datacollect.view.ConsoleView; + +public class ExitCommand implements Command { + private final ConsoleView view; + + public ExitCommand(ConsoleView view) { + this.view = view; + } + + @Override + public String getName() { + return "exit"; + } + + @Override + public void execute(String[] args, ArticleRepository repository) { + view.printSuccess("Exiting..."); + System.exit(0); + } +} \ No newline at end of file diff --git a/W10/java-cli/java-cli/src/main/java/com/example/datacollect/command/HelpCommand.java b/W10/java-cli/java-cli/src/main/java/com/example/datacollect/command/HelpCommand.java new file mode 100644 index 0000000..5fd93a5 --- /dev/null +++ b/W10/java-cli/java-cli/src/main/java/com/example/datacollect/command/HelpCommand.java @@ -0,0 +1,27 @@ +package com.example.datacollect.command; + +import com.example.datacollect.repository.ArticleRepository; +import com.example.datacollect.view.ConsoleView; + +public class HelpCommand implements Command { + private final ConsoleView view; + + public HelpCommand(ConsoleView view) { + this.view = view; + } + + @Override + public String getName() { + return "help"; + } + + @Override + public void execute(String[] args, ArticleRepository repository) { + view.printInfo("Available commands:"); + view.printInfo(" crawl - Crawl articles from the given URL"); + view.printInfo(" list - List all crawled articles"); + view.printInfo(" analyze - Analyze URL without saving"); + view.printInfo(" help - Show this help message"); + view.printInfo(" exit - Exit the program"); + } +} \ No newline at end of file diff --git a/W10/java-cli/java-cli/src/main/java/com/example/datacollect/command/HistoryCommand.java b/W10/java-cli/java-cli/src/main/java/com/example/datacollect/command/HistoryCommand.java new file mode 100644 index 0000000..6f4784d --- /dev/null +++ b/W10/java-cli/java-cli/src/main/java/com/example/datacollect/command/HistoryCommand.java @@ -0,0 +1,32 @@ +package com.example.datacollect.command; + +import com.example.datacollect.Main; +import com.example.datacollect.repository.ArticleRepository; +import com.example.datacollect.view.ConsoleView; +import java.util.List; + +public class HistoryCommand implements Command { + private final ConsoleView view; + + public HistoryCommand(ConsoleView view) { + this.view = view; + } + + @Override + public String getName() { + return "history"; + } + + @Override + public void execute(String[] args, ArticleRepository repository) { + List history = Main.getCommandHistory(); + if (history.isEmpty()) { + view.printInfo("No command history yet."); + return; + } + view.printInfo("Command history:"); + for (int i = 0; i < history.size(); i++) { + view.printInfo((i + 1) + ". " + history.get(i)); + } + } +} \ No newline at end of file diff --git a/W10/java-cli/java-cli/src/main/java/com/example/datacollect/command/ListCommand.java b/W10/java-cli/java-cli/src/main/java/com/example/datacollect/command/ListCommand.java new file mode 100644 index 0000000..e107ea9 --- /dev/null +++ b/W10/java-cli/java-cli/src/main/java/com/example/datacollect/command/ListCommand.java @@ -0,0 +1,23 @@ +package com.example.datacollect.command; + +import com.example.datacollect.repository.ArticleRepository; +import com.example.datacollect.view.ConsoleView; + +public class ListCommand implements Command { + private final ConsoleView view; + + public ListCommand(ConsoleView view) { + this.view = view; + } + + @Override + public String getName() { + return "list"; + } + + @Override + public void execute(String[] args, ArticleRepository repository) { + // 从Repository获取不可修改的文章列表 + view.display(repository.getAll()); + } +} \ No newline at end of file diff --git a/W10/java-cli/java-cli/src/main/java/com/example/datacollect/controller/CrawlerController.java b/W10/java-cli/java-cli/src/main/java/com/example/datacollect/controller/CrawlerController.java new file mode 100644 index 0000000..12e2b1a --- /dev/null +++ b/W10/java-cli/java-cli/src/main/java/com/example/datacollect/controller/CrawlerController.java @@ -0,0 +1,50 @@ +package com.example.datacollect.controller; + +import com.example.datacollect.command.*; +import com.example.datacollect.repository.ArticleRepository; +import com.example.datacollect.strategy.StrategyFactory; +import com.example.datacollect.view.ConsoleView; + +import java.util.HashMap; +import java.util.Map; + +public class CrawlerController { + private final ConsoleView view; + private final ArticleRepository repository; + private final Map commands = new HashMap<>(); + + // 构造方法:注入所有依赖并注册全部命令 + public CrawlerController(ConsoleView view, ArticleRepository repository, StrategyFactory factory) { + this.view = view; + this.repository = repository; + + // 注册所有命令(按字母顺序,方便维护) + commands.put("analyze", new AnalyzeCommand(view, factory)); + commands.put("crawl", new CrawlCommand(view, factory)); + commands.put("exit", new ExitCommand(view)); + commands.put("help", new HelpCommand(view)); + commands.put("history", new HistoryCommand(view)); + commands.put("list", new ListCommand(view)); + } + + // 处理用户输入的命令 + public void handle(String input) { + if (input == null || input.isBlank()) { + return; + } + + // 按空格分割命令和参数 + String[] parts = input.trim().split("\\s+"); + String commandName = parts[0]; + Command command = commands.get(commandName); + + if (command == null) { + view.printError("Unknown command: " + commandName); + view.printInfo("Type 'help' to see available commands."); + return; + } + + // 执行命令,传入参数和仓库对象 + command.execute(parts, repository); + } +} \ No newline at end of file diff --git a/W10/java-cli/java-cli/src/main/java/com/example/datacollect/model/Article.java b/W10/java-cli/java-cli/src/main/java/com/example/datacollect/model/Article.java new file mode 100644 index 0000000..805f0fa --- /dev/null +++ b/W10/java-cli/java-cli/src/main/java/com/example/datacollect/model/Article.java @@ -0,0 +1,47 @@ +package com.example.datacollect.model; + +public class Article { + private String title; + private String url; + private String content; + private String author; + private String publishDate; + + 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 + '\'' + + '}'; + } +} diff --git a/W10/java-cli/java-cli/src/main/java/com/example/datacollect/repository/ArticleRepository.java b/W10/java-cli/java-cli/src/main/java/com/example/datacollect/repository/ArticleRepository.java new file mode 100644 index 0000000..23815eb --- /dev/null +++ b/W10/java-cli/java-cli/src/main/java/com/example/datacollect/repository/ArticleRepository.java @@ -0,0 +1,41 @@ +package com.example.datacollect.repository; + +import com.example.datacollect.model.Article; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public class ArticleRepository { + // 内部私有集合,外部不能直接操作 + private final List
articles = new ArrayList<>(); + + // 新增单个文章 + public void add(Article article) { + if (article != null) { + articles.add(article); + } + } + + // 课后作业要求:带null防御的addAll + public void addAll(List
newArticles) { + // 防御:传入null或空集合,直接返回 + if (newArticles == null || newArticles.isEmpty()) { + return; + } + // 过滤掉集合里的null元素,再批量添加 + newArticles.stream() + .filter(Objects::nonNull) + .forEach(articles::add); + } + + // 验收标准:返回不可修改的集合,防止外部篡改 + public List
getAll() { + return Collections.unmodifiableList(articles); + } + + // 可选:清空数据,方便测试 + public void clear() { + articles.clear(); + } +} \ No newline at end of file diff --git a/W10/java-cli/java-cli/src/main/java/com/example/datacollect/strategy/CrawlStrategy.java b/W10/java-cli/java-cli/src/main/java/com/example/datacollect/strategy/CrawlStrategy.java new file mode 100644 index 0000000..d889e67 --- /dev/null +++ b/W10/java-cli/java-cli/src/main/java/com/example/datacollect/strategy/CrawlStrategy.java @@ -0,0 +1,11 @@ +package com.example.datacollect.strategy; + +import com.example.datacollect.model.Article; +import java.util.List; + +public interface CrawlStrategy { + // 判断当前策略是否支持该URL + boolean supports(String url); + // 爬取URL并返回文章列表(可以模拟) + List
crawl(String url); +} \ No newline at end of file diff --git a/W10/java-cli/java-cli/src/main/java/com/example/datacollect/strategy/CsdnCrawlStrategy.java b/W10/java-cli/java-cli/src/main/java/com/example/datacollect/strategy/CsdnCrawlStrategy.java new file mode 100644 index 0000000..de47685 --- /dev/null +++ b/W10/java-cli/java-cli/src/main/java/com/example/datacollect/strategy/CsdnCrawlStrategy.java @@ -0,0 +1,21 @@ +package com.example.datacollect.strategy; + +import com.example.datacollect.model.Article; +import java.util.List; + +public class CsdnCrawlStrategy implements CrawlStrategy { + @Override + public boolean supports(String url) { + // 只要URL包含csdn.net,就用这个策略 + return url != null && url.contains("csdn.net"); + } + + @Override + public List
crawl(String url) { + // 模拟爬取,直接返回测试数据 + return List.of( + new Article("CSDN测试文章1", url, "这是CSDN模拟内容1"), + new Article("CSDN测试文章2", url, "这是CSDN模拟内容2") + ); + } +} \ No newline at end of file diff --git a/W10/java-cli/java-cli/src/main/java/com/example/datacollect/strategy/JuejinCrawlStrategy.java b/W10/java-cli/java-cli/src/main/java/com/example/datacollect/strategy/JuejinCrawlStrategy.java new file mode 100644 index 0000000..15a9c50 --- /dev/null +++ b/W10/java-cli/java-cli/src/main/java/com/example/datacollect/strategy/JuejinCrawlStrategy.java @@ -0,0 +1,21 @@ +package com.example.datacollect.strategy; + +import com.example.datacollect.model.Article; +import java.util.List; + +public class JuejinCrawlStrategy implements CrawlStrategy { + @Override + public boolean supports(String url) { + // 只要URL包含juejin.cn,就用这个策略 + return url != null && url.contains("juejin.cn"); + } + + @Override + public List
crawl(String url) { + // 模拟爬取,直接返回测试数据 + return List.of( + new Article("掘金测试文章1", url, "这是掘金模拟内容1"), + new Article("掘金测试文章2", url, "这是掘金模拟内容2") + ); + } +} \ No newline at end of file diff --git a/W10/java-cli/java-cli/src/main/java/com/example/datacollect/strategy/StrategyFactory.java b/W10/java-cli/java-cli/src/main/java/com/example/datacollect/strategy/StrategyFactory.java new file mode 100644 index 0000000..f1236ec --- /dev/null +++ b/W10/java-cli/java-cli/src/main/java/com/example/datacollect/strategy/StrategyFactory.java @@ -0,0 +1,19 @@ +package com.example.datacollect.strategy; + +import java.util.List; +import java.util.Optional; + +public class StrategyFactory { + // 在这里注册所有爬取策略,新增网站时只需要加在这里 + private final List strategies = List.of( + new CsdnCrawlStrategy(), + new JuejinCrawlStrategy() + ); + + // 根据URL获取对应的爬取策略 + public Optional getStrategy(String url) { + return strategies.stream() + .filter(strategy -> strategy.supports(url)) + .findFirst(); + } +} \ No newline at end of file diff --git a/W10/java-cli/java-cli/src/main/java/com/example/datacollect/view/ConsoleView.java b/W10/java-cli/java-cli/src/main/java/com/example/datacollect/view/ConsoleView.java new file mode 100644 index 0000000..dee1127 --- /dev/null +++ b/W10/java-cli/java-cli/src/main/java/com/example/datacollect/view/ConsoleView.java @@ -0,0 +1,44 @@ +package com.example.datacollect.view; + +import com.example.datacollect.model.Article; +import java.util.List; +import java.util.Scanner; + +public class ConsoleView { + private static final String ANSI_RESET = "\u001B[0m"; + private static final String ANSI_GREEN = "\u001B[32m"; + private static final String ANSI_RED = "\u001B[31m"; + private static final String ANSI_BLUE = "\u001B[34m"; + + // 只创建一次Scanner,避免重复创建导致的输入混乱 + private final Scanner scanner = new Scanner(System.in); + + // 读取用户输入,只读取控制台>后面的内容 + public String readLine() { + System.out.print("> "); + return scanner.nextLine().trim(); + } + + public void printSuccess(String msg) { + System.out.println(ANSI_GREEN + msg + ANSI_RESET); + } + + public void printError(String msg) { + System.out.println(ANSI_RED + msg + ANSI_RESET); + } + + public void printInfo(String msg) { + System.out.println(ANSI_BLUE + msg + ANSI_RESET); + } + + public void display(List
articles) { + if (articles.isEmpty()) { + printInfo("暂无文章,请先执行 crawl 命令爬取数据。"); + return; + } + for (int i = 0; i < articles.size(); i++) { + Article article = articles.get(i); + System.out.println((i + 1) + ". " + article.getTitle() + " | " + article.getUrl()); + } + } +} \ No newline at end of file diff --git a/W10/java-cli/java-cli/target/W9工程架构 - 教案v3.md b/W10/java-cli/java-cli/target/W9工程架构 - 教案v3.md new file mode 100644 index 0000000..09de868 --- /dev/null +++ b/W10/java-cli/java-cli/target/W9工程架构 - 教案v3.md @@ -0,0 +1,758 @@ +--- + +# 教案:《高级程序设计》第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铺垫(策略模式+仓库层) \ No newline at end of file diff --git a/W10/java-cli/java-cli/target/maven-archiver/pom.properties b/W10/java-cli/java-cli/target/maven-archiver/pom.properties new file mode 100644 index 0000000..08a8f9f --- /dev/null +++ b/W10/java-cli/java-cli/target/maven-archiver/pom.properties @@ -0,0 +1,5 @@ +#Generated by Maven +#Thu Apr 30 11:50:54 CST 2026 +artifactId=datacollect-cli +groupId=com.example +version=0.1.0 diff --git a/W10/java-cli/java-cli/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/W10/java-cli/java-cli/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst new file mode 100644 index 0000000..e69de29 diff --git a/W10/java-cli/java-cli/target/w9-ppt.md b/W10/java-cli/java-cli/target/w9-ppt.md new file mode 100644 index 0000000..5ddd5ad --- /dev/null +++ b/W10/java-cli/java-cli/target/w9-ppt.md @@ -0,0 +1,530 @@ +## 高级程序设计 · 第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 才是唯一输出出口 | + +--- + +## 谢谢! + +### 课件已上传,模板在课程群 + +**保持工程洁癖,下周见!** \ No newline at end of file diff --git a/W10/架构审计报告.html b/W10/架构审计报告.html new file mode 100644 index 0000000..eb3a056 --- /dev/null +++ b/W10/架构审计报告.html @@ -0,0 +1,19 @@ +

1. 策略模式(Strategy Pattern)解耦情况 ✅

+

接口抽象清晰:CrawlStrategy 定义了统一的爬取接口,包含 supports(String url) 和 crawl(String url) 两个核心方法,所有网站爬取逻辑都实现了该接口。

+

具体实现解耦:

+


+

CsdnCrawlStrategy、JuejinCrawlStrategy 完全独立,各自封装了网站匹配和模拟爬取逻辑。

+

新增网站爬取策略时,只需新增一个实现类并在 StrategyFactory 中注册,无需修改任何旧代码,严格遵循开闭原则。

+


+

高层依赖抽象:CrawlCommand、AnalyzeCommand 仅依赖 CrawlStrategy 接口,不依赖任何具体实现类,实现了依赖倒置。

+ +

2. 工厂模式(Factory Pattern)职责单一 ✅

+

StrategyFactory 负责所有策略的注册与分发,集中管理策略匹配逻辑。

+

通过 getStrategy(String url) 方法,根据 URL 自动匹配对应策略,命令类无需关心策略的创建与匹配细节。

+ +

3. 仓库模式(Repository Pattern)封装与数据安全 ✅

+

数据封装严格:ArticleRepository 内部持有私有 List<Article>,外部无法直接访问和修改,所有数据操作必须通过其提供的方法进行。

+

数据安全防护:

+


+

addAll 方法实现了双层 null 防御:判空 / 空集合直接返回 + 过滤集合内 null 元素,避免空指针异常。

+

getAll() 返回 Collections.unmodifiableList() 不可修改视图,防止外部篡改内部数据。

\ No newline at end of file diff --git a/W10/策略模式类结构检查.docx b/W10/策略模式类结构检查.docx deleted file mode 100644 index 256ee9b..0000000 Binary files a/W10/策略模式类结构检查.docx and /dev/null differ