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.

31 KiB

教案:《高级程序设计》第10周——设计模式:灵活性与可扩展性

项目 内容
课程名称 高级程序设计
周次 第10周
主题 设计模式——灵活性与可扩展性
学时 2学时(90分钟)
授课对象 已完成第9周CLI+MVC架构学习,具备Command模式基础
教学环境 JDK 17+、IntelliJ IDEA、Maven
前情提要 W9搭建了CLI骨架:MVC分层 + Command路由,但留下了两大隐患——解析逻辑耦合在Command中、List<Article>共享引用裸奔

教学调整说明:为什么W10要在“骨架”上装“盔甲”?

W9成果:一个可扩展的命令行骨架 → W9痛点:解析器与数据存储仍在“裸奔”

维度 W9状态 W10目标
架构 MVC分层清晰 MVC + 策略模式 + 仓库层
命令扩展 新增命令不改Controller 新增解析器不改任何旧代码
数据安全 List<Article>全员可写 Repository封装,只暴露安全接口
解析逻辑 硬编码在CrawlCommand内 策略模式,按URL自动匹配
代码量 ~8个类 ~12个类,但每个更小更纯粹

决策理由

  1. W9学生已经感受到Command模式的好处——多态带来的扩展性
  2. 策略模式是多态思想的又一次实战,是接口抽象的深化
  3. 仓库层是“封装”这一OOP核心原则的落地,补上W9留下的课
  4. 解析器工厂让学生看到**“自动匹配”**的威力——增加网站支持只需新增一个类

更深层的教育价值

W9教会学生“怎么把代码分开”,W10要教会学生“怎么把代码分开后还能优雅地合上”——接口即合同,工厂即自动匹配,仓库即数据守卫。这三句话,就是本周的全部精华。


一、教学目标

目标维度 具体描述
知识掌握 理解策略模式的定义与多态本质;掌握工厂模式的两类变体(工厂方法/简单工厂)及适用场景;理解仓库模式对数据访问的封装原理。
工程实践 能在爬虫项目中用策略模式封装不同网站的解析逻辑;能实现解析器工厂,根据URL自动匹配解析策略;能用Repository模式替代裸List,提供安全的数据访问接口。
思维转型 从“写死逻辑”转向“策略可插拔”;从“直接操作集合”转向“通过仓库存取”;理解“对扩展开放,对修改关闭”的开闭原则。
工具应用 利用AI审查策略模式实现是否真正解耦;让AI扮演“网站结构分析师”辅助编写具体解析策略;用AI生成Repository的安全接口建议。

二、教学重点与难点

项目 内容 突破方法
重点 策略模式的多态本质、解析器工厂的自动匹配机制、Repository对数据访问的封装 以“新增网站需要改什么”为切入点,展示策略模式的开闭原则达成;通过“攻击”当前List裸奔的问题,引出Repository的必然性
难点 理解“接口即合同”的抽象思维、工厂模式中反射/Map注册的实现、仓库层与Strategy模式的协同 用“插座与电器”类比接口标准;现场演示从硬编码→工厂→反射的演进路径;用时序图展示“用户→Command→Strategy→Repository”的完整调用链

三、教学过程设计(90分钟)

环节 时间 教学内容 师生活动 AI协同点
1. W9回顾与痛点暴露 8' 回顾W9成果(CLI骨架),暴露两大隐患:①CrawlCommand里解析逻辑硬编码;②List<Article>全员可读可写 教师演示:展示W9代码,用“事故场景”引发思考
2. 策略模式:解析器的“插头标准化” 18' 策略模式定义、接口设计、多态调用、与Command模式的对比 类比:插座与电器;教师演示:从if-else到策略模式的演进 让AI生成“策略模式vs switch-case”对比
3. 解析器工厂:自动匹配的魔法 14' 工厂模式的两种形态(简单工厂→Map注册工厂),解析器工厂实现 教师演示:先用if-else判断host,再升级为Map注册工厂 让AI解释工厂模式与策略模式如何协同
4. Repository模式:武装数据访问 12' Repository定义、接口设计、替换List<Article>后的影响 教师演示:在原代码中把List替换为Repository,展示改动点 学生用AI审计Repository接口的“最小完备性”
5. 整体架构串联 8' 用一张时序图串联:用户→CLI→Controller→Command→Strategy→Repository→Model 师生互动:让学生在白板上画出调用链
6. 代码落地 20' 实现CrawlStrategy接口 + 两个策略 + 解析器工厂 + ArticleRepository 教师演示:分步写出代码,刻意埋入“策略匹配失败”的异常处理 完成后用AI检查策略模式实现
7. 架构反思与W11预告 5' 当前架构还有什么隐患?(异常处理不统一、日志缺失)→ 预告W11健壮性工程 师生互动:如果解析器工厂找不到匹配策略,会发生什么?
8. 实践任务 5' 实现策略模式和仓库层,完成本周代码升级 学生现场编码,教师巡视

四、核心教学内容脚本

4.1 W9回顾与痛点暴露(8分钟)

教师口播

"上节课我们搭了一个很漂亮的骨架——CLI+MVC+Command模式。我们先来表扬一下自己:新增一个命令,只要新建一个类,Controller零改动。但请大家想一个问题——"

投影展示W9的CrawlCommand存根

public class CrawlCommand implements Command {
    // ...
    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]);
    }
}

提问引导

  1. "这个存根下周要填坑了。假设我们现在要真正实现爬取,代码写在哪?"
  2. "如果我要支持两个网站——比如一个技术博客和一个新闻网站——它们的HTML结构完全不一样,这个execute方法会变成什么样?"

展示“噩梦版”CrawlCommand

public void execute(String[] args, List<Article> articles) {
    String url = args[1];
    // 五十行if-else地狱...
    if (url.contains("blog.example.com")) {
        // 解析技术博客的HTML
        Document doc = Jsoup.connect(url).get();
        Elements titles = doc.select(".post-title");
        for (Element e : titles) {
            articles.add(new Article(e.text(), url, ""));
        }
    } else if (url.contains("news.example.com")) {
        // 解析新闻网站的HTML
        Document doc = Jsoup.connect(url).get();
        Elements items = doc.select(".article-headline");
        for (Element e : items) {
            articles.add(new Article(e.text(), url, ""));
        }
    } else {
        view.printError("Unsupported website!");
    }
}

痛点提炼

"看到了吗?每支持一个新网站,就要在这里加一个else if。这就是W1我们痛批的'牵一发而动全身',只不过这次灾难地点从main搬到了CrawlCommand。"

"更重要的是,我们上节课辛辛苦苦实现了Command模式,难道解析逻辑又要回到if-else地狱吗?这就是W10要解决的第一个问题:怎么让解析逻辑也可插拔?"

第二个隐患——共享状态的回顾

"还有一件事,我们上节课结束前提到的:List<Article> articles在所有Command之间共享。任何一个Command都可以往里面塞东西、删东西、甚至清空。这是W10要解决的第二个问题:怎么给数据装上'防盗门'?"


4.2 策略模式:解析器的“插头标准化”(18分钟)

4.2.1 从类比切入

教师口播

"先讲个生活场景。你家里墙上有一个三孔插座,你可以插电视、插电脑、插手机充电器——任何符合这个标准的电器都能用。插座不在乎你是什么电器,它只认接口标准。"

类比映射

生活场景 代码对应
三孔插座 CrawlStrategy 接口
电视/电脑充电器 具体解析策略(BlogStrategy/NewsStrategy)
电流 输入:URL + Document;输出:List<Article>
你(使用者) CrawlCommand
插座面板 解析器工厂

"策略模式的核心思想就是:定义一个算法接口,让具体的算法实现可以互相替换,而使用算法的客户端不受影响。"

4.2.2 策略模式定义

// src/main/java/com/crawler/strategy/CrawlStrategy.java
package com.crawler.strategy;

import com.crawler.model.Article;
import org.jsoup.nodes.Document;
import java.util.List;

public interface CrawlStrategy {
    /**
     * 从已获取的Document中解析文章列表
     * @param url  原始请求URL(用于填充Article)
     * @param doc  Jsoup解析后的Document
     * @return 解析出的文章列表
     */
    List<Article> parse(String url, Document doc);

    /**
     * 判断此策略是否为给定URL服务
     * @param url 待判断的URL
     * @return true表示此策略可以处理该URL
     */
    boolean supports(String url);
}

教师口播

"注意,策略接口里有两个方法。parse是干活的那个,supports是'我能不能干这个活'——这是什么?这是合同! 任何网站想被我们爬虫支持,就必须签署这份合同:告诉我你是不是我的客户(supports),以及怎么解析你(parse)。"

4.2.3 具体策略实现示例

// BlogStrategy.java - 技术博客解析策略
public class BlogStrategy implements CrawlStrategy {
    @Override
    public boolean supports(String url) {
        return url.contains("blog.example.com");
    }

    @Override
    public List<Article> parse(String url, Document doc) {
        List<Article> articles = new ArrayList<>();
        Elements titles = doc.select(".post-title");
        for (Element e : titles) {
            articles.add(new Article(e.text(), url, ""));
        }
        return articles;
    }
}

// NewsStrategy.java - 新闻网站解析策略
public class NewsStrategy implements CrawlStrategy {
    @Override
    public boolean supports(String url) {
        return url.contains("news.example.com");
    }

    @Override
    public List<Article> parse(String url, Document doc) {
        List<Article> articles = new ArrayList<>();
        Elements items = doc.select(".article-headline");
        for (Element e : items) {
            articles.add(new Article(e.text(), url, ""));
        }
        return articles;
    }
}

对比:策略模式 vs 硬编码if-else

维度 if-else屎山 策略模式
新增网站 改CrawlCommand,加else if 新写一个类,实现CrawlStrategy
修改解析逻辑 在CrawlCommand里翻找对应的else if 只改对应策略类
测试 必须启动整个爬虫 单独对Strategy做单元测试
是否符合开闭原则 对修改开放 对扩展开放,对修改关闭

与Command模式的对比(加深理解)

"上节课Command模式,我们为每个命令定义一个类;这节课策略模式,我们为每个网站的解析算法定义一个类。本质上都是同一个OOP思想:用多态替代条件分支。 只不过Command的接口是execute(),Strategy的接口是parse()。"

"这张图你们可以记下来:接口是消除if-else的利器,多态是接口的灵魂。"


4.3 解析器工厂:自动匹配的魔法(14分钟)

4.3.1 问题引出

教师口播

"现在我们有A网站的策略、B网站的策略。问题来了:谁来选策略?谁来遍历所有策略,找到一个supports返回true的?"

"如果把这个逻辑写在CrawlCommand里,那策略模式就白用了——CrawlCommand还是得'知道'有哪些策略。我们要的是一个黑盒子:把URL丢进去,自动弹出一个合适的解析器。"

4.3.2 解析器工厂的实现

// src/main/java/com/crawler/strategy/StrategyFactory.java
package com.crawler.strategy;

import java.util.ArrayList;
import java.util.List;

public class StrategyFactory {
    private final List<CrawlStrategy> strategies = new ArrayList<>();

    // 注册策略——新的网站只需在这里加一行
    public StrategyFactory() {
        strategies.add(new BlogStrategy());
        strategies.add(new NewsStrategy());
        // 未来增加新网站:strategies.add(new XxxStrategy());
    }

    /**
     * 根据URL自动匹配解析策略
     * @param url 目标URL
     * @return 匹配的策略,如果没有匹配返回null
     */
    public CrawlStrategy getStrategy(String url) {
        for (CrawlStrategy s : strategies) {
            if (s.supports(url)) {
                return s;
            }
        }
        return null; // 未找到匹配策略
    }
}

教师口播

"这个工厂类足够简单:一个List存所有策略,一个方法遍历找到匹配的。但简单不等于不强大。**

关键点:新增网站支持,只需要——"

  1. 写一个XxxStrategy实现CrawlStrategy
  2. 在工厂构造器里加一行strategies.add(new XxxStrategy())

"CrawlCommand一行不改。这就是开闭原则的胜利。"

4.3.3 从简单工厂到更高级的注册机制(拓展思维)

教师口播

"有同学可能会问:还要在工厂构造器里加一行,能不能做到完全零改动?当然可以——用反射或者SPI。"

演示概念(不要求实现)

// 进阶思路:扫描指定包下的所有CrawlStrategy实现类
// 用反射自动注册,真正做到“新增类即生效”
// 这是Spring框架的核心思想之一

"这个技术我们暂时不要求掌握,但我希望你们知道:你现在写的每一个new XxxStrategy(),在未来都可能进化为框架级别的自动装配。你现在建立的思维习惯,决定了你未来能走多高。"

4.3.4 重构后的CrawlCommand

public class CrawlCommand implements Command {
    private ConsoleView view;
    private StrategyFactory strategyFactory;
    private ArticleRepository repository;  // 注意:这里是Repository了!

    public CrawlCommand(ConsoleView v, StrategyFactory f, ArticleRepository r) {
        this.view = v;
        this.strategyFactory = f;
        this.repository = r;
    }

    public String getName() { return "crawl"; }

    public void execute(String[] args, List<Article> articles) {
        if (args.length < 2) {
            view.printError("Usage: crawl <url>");
            return;
        }
        String url = args[1];

        // 1. 工厂自动选策略
        CrawlStrategy strategy = strategyFactory.getStrategy(url);
        if (strategy == null) {
            view.printError("No strategy found for: " + url);
            return;
        }

        // 2. 抓取页面
        view.printInfo("Crawling: " + url);
        try {
            Document doc = Jsoup.connect(url).get();
            List<Article> parsed = strategy.parse(url, doc);
            
            // 3. 通过仓库存入(而不是直接操作List)
            for (Article a : parsed) {
                repository.add(a);
            }
            view.printSuccess("Crawled " + parsed.size() + " articles.");
        } catch (IOException e) {
            view.printError("Failed to crawl: " + e.getMessage());
        }
    }
}

教师口播

"注意这个CrawlCommand现在的职责:拿到URL → 交给工厂选策略 → 执行解析 → 交给仓库存储。它自己在干什么?在调度! 这就是上节课我们讲的Controller的'调度思维',现在向Command内部延伸了。"


4.4 Repository模式:武装数据访问(12分钟)

4.4.1 问题重提

教师口播

"回到上节课结束时的那个问题:List<Article>在所有Command之间共享。任何一个Command都可以做这些事——"

articles.clear();      // 清空所有文章
articles.add(null);    // 塞入null
articles.remove(0);    // 随意删除

"如果一个新同事接手开发,他不知道'不要动这个List'的潜规则,写了一个articles.clear(),你的list命令就突然什么都不显示了。靠代码约定维护的秩序,早晚会被打破。我们需要实体的'规则'——代码层面的约束。"

4.4.2 ArticleRepository的定义

// src/main/java/com/crawler/repository/ArticleRepository.java
package com.crawler.repository;

import com.crawler.model.Article;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class ArticleRepository {
    private final List<Article> articles = new ArrayList<>();

    /**
     * 添加一篇文章。注意:不接受null,这是代码层面的规则,不是口头约定。
     */
    public void add(Article article) {
        if (article == null) {
            throw new IllegalArgumentException("Article cannot be null");
        }
        articles.add(article);
    }

    /**
     * 获取所有文章的只读视图
     * 调用者无法通过此返回值修改内部数据
     */
    public List<Article> getAll() {
        return Collections.unmodifiableList(articles);
    }

    /**
     * 获取文章数量
     */
    public int size() {
        return articles.size();
    }

    /**
     * 清空(仅管理员可调——下一篇:权限控制)
     */
    public void clear() {
        articles.clear();
    }
}

教师口播

"三个关键设计点——"

  • add()拒绝null:规则写在代码里,不是写在邮件里
  • getAll()返回不可修改的视图Collections.unmodifiableList()——调用者如果尝试add/remove,会直接抛异常,不是'悄悄的bug'
  • ClearCommand要清空数据?调repository.clear(),而不是直接操作List

"这就是面向对象的第一课——封装。把数据藏起来,只暴露安全的方法。从'直接操作集合'到'通过仓库存取',是程序员成熟度的分水岭。"

4.4.3 仓库引入后的架构变化

Command接口的execute方法调整

// 调整前(W9)
public interface Command {
    String getName();
    void execute(String[] args, List<Article> articles);
}

// 调整后(W10)
public interface Command {
    String getName();
    void execute(String[] args, ArticleRepository repository);
}

教师口播

"这个改动很小——把List<Article>换成ArticleRepository。但语义完全不同:之前是'给你数据随便玩',现在是'给你一个安全的存取通道'。"

所有Command同步调整

// 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, ArticleRepository repository) {
        view.display(repository.getAll());  // 通过仓库获取数据
    }
}

// ClearCommand.java(新增示例)
public class ClearCommand implements Command {
    private ConsoleView view;
    public ClearCommand(ConsoleView v) { this.view = v; }
    public String getName() { return "clear"; }
    public void execute(String[] args, ArticleRepository repository) {
        repository.clear();
        view.printSuccess("All articles cleared.");
    }
}

Controller和main的调整

// App.java - 调整后
public class App {
    public static void main(String[] args) {
        ConsoleView view = new ConsoleView();
        ArticleRepository repository = new ArticleRepository();      // 替代 List<Article>
        StrategyFactory factory = new StrategyFactory();              // 新增

        CrawlerController controller = new CrawlerController(view, repository, factory);
        
        view.printSuccess("Welcome to CLI Crawler v2.0!");
        view.printInfo("Type 'help' for commands.");

        while (true) {
            controller.handle(view.readLine());
        }
    }
}

4.5 整体架构串联(8分钟)

教师口播

"现在我们把所有部件串起来,看看一个crawl https://blog.example.com命令走过的完整路径。"

时序图(口述配白板绘制)

用户输入 "crawl https://blog.example.com"
    │
    ▼
ConsoleView.readLine()
    │
    ▼
CrawlerController.handle("crawl https://blog.example.com")
    │  Map查找 "crawl" → CrawlCommand
    ▼
CrawlCommand.execute(args, repository)
    │
    ├─► StrategyFactory.getStrategy(url)
    │       │ 遍历List<CrawlStrategy>
    │       │ BlogStrategy.supports(url) → true!
    │       ▼
    │   返回 BlogStrategy
    │
    ├─► Jsoup.connect(url).get() → Document
    │
    ├─► BlogStrategy.parse(url, doc) → List<Article>
    │
    └─► for each article: repository.add(article)
            │
            ▼
         ArticleRepository.articles.add(article)
    
最终:ConsoleView.printSuccess("Crawled N articles.")

教师口播

"七步调用,每一步职责清晰:View负责输入输出,Controller负责路由,Command负责调度,Factory负责匹配,Strategy负责解析,Repository负责存储。没有哪个类干了两个人的活,也没有哪个类不知道自己的活是什么。"

"这就是工程化——不是把代码写得快,是把代码写得对。"


4.6 代码落地(20分钟)

教师准备:课前准备一份“W9升级到W10”的改动清单,现场演示关键改动。

改动清单

  1. 新建strategy/包,创建CrawlStrategy接口
  2. 新建strategy/BlogStrategy.java
  3. 新建strategy/NewsStrategy.java
  4. 新建strategy/StrategyFactory.java
  5. 新建repository/包,创建ArticleRepository.java
  6. 修改Command接口的execute签名
  7. 修改CrawlCommand,引入StrategyFactoryArticleRepository
  8. 修改其余所有Command实现类
  9. 修改CrawlerController构造器
  10. 修改App.java

教师演示关键步骤(重点演示):

  • ArticleRepositoryCollections.unmodifiableList()
  • StrategyFactory的遍历匹配逻辑
  • CrawlCommand重写后的调度结构

刻意埋入的“找茬点”

"我在StrategyFactory.getStrategy()里,如果没有匹配的策略就返回null。然后在CrawlCommand里检查null。这其实叫'null object pattern的前奏'——如果我不想让Command检查null,我应该怎么改工厂?大家带着这个问题用AI探究。"


4.7 架构反思与W11预告(5分钟)

教师口播

"现在我们的架构比W9强壮多了:解析逻辑可插拔,数据访问有守卫。但还有一些漏洞——"

逐一点破

  1. 异常处理CrawlCommand用了一个笼统的catch (IOException e),如果解析过程中抛出其他异常怎么办?
  2. 网络超时:如果目标网站3秒没响应,当前代码会一直等吗?
  3. 日志缺失:所有的成功/失败信息只输出到终端,如果程序半夜跑,第二天想看昨晚抓了多少——看不了。
  4. 重试机制:如果一次失败就直接报错,要不要给个重试的机会?

W11预告

"下周,我们会做三件事:自定义异常体系工程化日志框架防御式编程与重试机制。W9搭骨架,W10装盔甲,W11要让这个系统经得起现实的毒打。"


4.8 实践任务(5分钟)

任务要求

  1. 从W9代码出发,完成W10升级
  2. 实现至少两个CrawlStrategy(可以是模拟的,不要求真实爬取)
  3. 实现StrategyFactoryArticleRepository
  4. 确保所有Command通过Repository访问数据
  5. 运行并测试完整流程

验收标准

  • 新增策略类只需新建文件+工厂注册一行,其余代码零改动
  • ArticleRepositorygetAll()返回不可修改视图
  • CrawlCommand不包含任何网站特定的解析逻辑
  • StrategyFactory能根据URL自动匹配正确的策略
  • 所有Command的execute方法签名已更新为ArticleRepository
  • 无任何地方直接操作List<Article>

五、课后作业

5.1 必做任务

  1. 完善ArticleRepository:增加addAll(List<Article>)批量添加方法,注意防御null

  2. ★ AnalyzeCommand(集大成作业)

    • 实现analyze <url>命令
    • 内部调用StrategyFactory匹配策略
    • 调用策略解析文章后,不存到Repository,而是分析统计信息:
      • 文章总数
      • 标题平均长度
      • 按某种规则排名的Top 5
    • 结果只输出,不存储
    • 提示:这就是策略的复用——同一个解析策略,既能为crawl服务(存入仓库),也能为analyze服务(仅分析)
  3. AI架构审计:将完整代码的类图(或类名与方法签名列表)发给AI,指令:

    "作为Java架构审计师,请检查:①策略模式的实现是否正确解耦(CrawlCommand是否仍然包含网站特定逻辑);②Repository是否真正封装了数据访问(是否存在绕过Repository直接操作List的地方);③工厂的匹配逻辑是否存在性能隐患。请给出具体的改进建议。"

5.2 选做任务

  1. 正则策略匹配:将Supports()的判断从url.contains()改为正则表达式,让一张策略可以匹配一类URL
  2. 默认策略(DefaultStrategy):当没有策略匹配时,提供一个通用的“标题提取”逻辑
  3. 策略优先级:给每个策略加一个priority字段,工厂按优先级匹配(而不是按注册顺序)
  4. 思考并回答(200字)

    "策略模式中,策略的supports()方法有可能让两个策略都返回true,这时该选哪个?StrategyFactory的遍历顺序会如何影响结果?你有什么解决方案?"

5.3 思考题

  1. Repository与List的区别是什么? 如果Repository只是包了一层List,为什么还要用?
  2. 策略工厂的演进:如果网站数量增加到100个,逐个注册的写法还合适吗?你想到什么解决方案?
  3. Collections.unmodifiableList()返回的是什么? 它真的“不可修改”吗?如果原List被修改,这个不可修改视图会怎样?

六、AI协同升级

架构审计师任务(必做)

学生执行步骤

  1. 画出当前项目的类依赖图(手绘或工具生成)
  2. 将类名和依赖关系发给AI
  3. 输入指令:

    "作为Java架构审计师,请检查这个爬虫项目的架构。重点关注:①策略模式是否真正实现了开闭原则(增加新网站是否真的只需新增类);②Repository封装是否完整(是否有绕过Repository的路径);③是否存在循环依赖。请逐一指出问题并给出改进建议。"

预期AI输出

  • 指出是否还存在“改一处影响多处”的耦合
  • 判断Repository的API设计是否完备
  • 评价整体架构的开闭原则达成度

进阶AI探究(选做)

"假设我有一个CrawlStrategy接口和10个实现类。不用工厂模式,直接用一个Map<String, CrawlStrategy>存起来,key是策略名称。这和StrategyFactory设计有什么本质区别?各自的优缺点是什么?"


七、教学反思与调整记录

日期 事项 调整内容
2026-05-01 首次编写 基于W9骨架,引入策略模式+工厂+Repository
2026-05-07 结构优化 调整策略模式与工厂的讲解顺序,先策略后工厂更自然

附录1:W9到W10改动对照表

改动项 W9代码 W10代码
数据存储 List<Article> articles ArticleRepository repository
Command接口 execute(String[], List<Article>) execute(String[], ArticleRepository)
解析逻辑位置 CrawlCommand内部 CrawlStrategy实现类
URL匹配 无(硬编码) StrategyFactory.getStrategy(url)
数据添加 articles.add(article) repository.add(article)
数据读取 直接遍历articles repository.getAll()

附录2:常见问题速查

问题 解答
策略模式和Command模式有什么区别? Command封装“动作”(做什么事),Strategy封装“算法”(怎么做)。在爬虫中:crawl是命令(动作),如何解析是策略(算法)。
工厂一定要叫Factory吗? 不必须。但叫Factory意味着“创建对象”的职责,符合模式命名的惯例。
Collections.unmodifiableList()有什么用? 返回一个只读视图,调用add/remove等方法会抛UnsupportedOperationException
Repository和DAO有什么区别? 在我们的上下文中可以视为同义词。严谨地说,Repository是领域驱动设计的概念,更偏向“集合语义”;DAO更偏数据库操作。
策略的supports()返回true但解析失败怎么办? 那是策略实现的bug,该策略应修复。Factory不负责验证策略的正确性。

附录3:教学逻辑说明

顺序 内容 设计理由
1 W9回顾+痛点暴露 承上启下,从已知问题引出新知识
2 策略模式 解决解析逻辑耦合问题,深化多态理解
3 解析器工厂 解决策略选择问题,引入工厂模式
4 Repository模式 解决数据安全问题,实践封装原则
5 架构串联 将所有部件统一,形成完整心智模型
6 代码落地 实践验证,从“听懂”到“会做”
7 架构反思+预告 暴露新问题,为W11健壮性工程铺垫

版本说明

  • v1(本版):基于W9教案模式首次编写,包含策略模式、工厂模式、Repository模式的完整引入