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.

11 KiB

id title slug status view_count created_at updated_at
24 w10-设计模式 w10-design-patterns draft 0 2026-05-07T12:00:00+08:00 2026-05-07T14:00:00.000000000+08:00

高级程序设计 · 第10周

设计模式:灵活性与可扩展性

策略模式 + 工厂 + Repository 实战


📌 本周导航

  • W9回顾:骨架的成就与隐患
  • 策略模式:解析器的“插头标准”
  • 解析器工厂:自动匹配的魔法
  • Repository:武装数据访问
  • 整体架构串联:调用链全程
  • 代码落地 + 实践任务
  • 架构反思 + W11 预告

1️⃣ W9回顾:骨架的成就与隐患

我们建了一座漂亮的房子

  • MVC 分层清晰
  • Command 模式:新增命令,Controller 零改动
  • 所有输出走 ConsoleView
  • 工程包结构标准

但问题也随之而来

// CrawlCommand 里解析逻辑怎么办?
if (url.contains("blog.example.com")) {
    // 博客解析...
} else if (url.contains("news.example.com")) {
    // 新闻解析...
} else {
    view.printError("Unsupported website!");
}

😫 每支持一个新网站,就要加一个 else if


还有另一个“裸奔”的数据

List<Article> articles = new ArrayList<>();
// 所有 Command 都可以:
articles.clear();
articles.add(null);
articles.remove(0);

🚨 数据没有任何保护,靠口头约定是靠不住的


本周任务

  1. 解析逻辑可插拔 → 策略模式 + 工厂
  2. 数据访问加守卫 → Repository 模式

W9 搭骨架,W10 装盔甲


2️⃣ 策略模式:解析器的“插头标准”

墙上的插座,为什么什么电器都能插?

  • 三孔插座 是标准接口
  • 电视、电脑、手机充电器都实现这个接口
  • 插座不关心你是什么电器

爬虫的世界也一样

  • CrawlStrategy = 插座接口
  • BlogStrategyNewsStrategy = 具体电器
  • CrawlCommand = 使用电器的人
  • StrategyFactory = 插座面板

接口即合同

public interface CrawlStrategy {
    List<Article> parse(String url, Document doc);
    boolean supports(String url);
}
  • supports():我能不能处理这个 URL?
  • parse():怎么解析?
  • 任何网站想被爬,签这份合同!

策略 vs 硬编码

维度 if-else 屎山 策略模式
新增网站 改 Command 新建策略类
修改解析 翻找 else if 只改对应类
测试 启动整个爬虫 单独测策略
开闭原则 修改开放 扩展开放,修改关闭

具体策略示例

public class BlogStrategy implements CrawlStrategy {
    public boolean supports(String url) {
        return url.contains("blog.example.com");
    }
    public List<Article> parse(String url, Document doc) {
        List<Article> articles = new ArrayList<>();
        for (Element e : doc.select(".post-title")) {
            articles.add(new Article(e.text(), url, ""));
        }
        return articles;
    }
}

一个新网站,一个独立类,各扫门前雪


3️⃣ 解析器工厂:自动匹配的魔法

谁来选择策略?

  • 如果 CrawlCommand 遍历所有策略 → 策略模式白用了
  • 我们需要一个黑盒子:丢入 URL,返回合适的解析器

工厂登场

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

    public StrategyFactory() {
        strategies.add(new BlogStrategy());
        strategies.add(new NewsStrategy());
    }

    public CrawlStrategy getStrategy(String url) {
        for (CrawlStrategy s : strategies) {
            if (s.supports(url)) return s;
        }
        return null;
    }
}

🔧 新增网站只需:新建策略类 + 工厂里注册一行


开闭原则的胜利

  • CrawlCommand 完全不改
  • 新增 XxxStrategy 和一行注册
  • 所有策略的调用方式完全一致

这就是 “对扩展开放,对修改关闭”


重构后的 CrawlCommand

public void execute(String[] args, ArticleRepository repository) {
    String url = args[1];
    CrawlStrategy strategy = strategyFactory.getStrategy(url);
    if (strategy == null) {
        view.printError("No strategy for: " + url);
        return;
    }
    Document doc = Jsoup.connect(url).get();
    List<Article> parsed = strategy.parse(url, doc);
    for (Article a : parsed) {
        repository.add(a);
    }
    view.printSuccess("Crawled " + parsed.size() + " articles.");
}

🧠 CrawlCommand 现在只做 “调度”,不做解析


4️⃣ Repository:武装数据访问

共享 List 的问题

articles.clear();   // 清空
articles.add(null); // 塞 null
articles.remove(0); // 随意删除

靠约定维护的秩序,终将被打破


给数据装上防盗门

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

    public void add(Article article) {
        if (article == null) throw new IllegalArgumentException(...);
        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 返回不可变视图 任何修改立即抛异常
必须通过 repository 访问 封装内部结构,只暴露安全方法

所有 Command 签名改变

// W9
public void execute(String[] args, List<Article> articles);

// W10
public void execute(String[] args, ArticleRepository repository);

语义变化:从“给你数据随便玩” → “给你安全的存取通道”


5️⃣ 整体架构串联

一个 crawl 命令的完整旅程

用户输入 "crawl https://blog.example.com"
    ↓
ConsoleView 解析
    ↓
Controller 路由 → CrawlCommand
    ↓
StrategyFactory.getStrategy(url) → BlogStrategy
    ↓
Jsoup 抓取 → Document
    ↓
BlogStrategy.parse(url, doc) → List<Article>
    ↓
Repository.add() 存储
    ↓
ConsoleView 输出成功信息

架构全景图

mvc-strategy-repo

flowchart TD
    User(["👤 用户输入<br/>crawl https://blog.example.com"]) --> View

    subgraph View["🎨 View 层 (ConsoleView)"]
        ReadLine["readLine()"]
        Display["display() / printSuccess()"]
    end

    ReadLine --> Controller

    subgraph Controller["🧭 Controller 层"]
        Router["CrawlerController<br/>Map 路由"]
    end

    Router --> Command

    subgraph Command["⚡ Command 层"]
        CrawlCmd["CrawlCommand<br/>(调度者)"]
    end

    CrawlCmd --> Factory

    subgraph Strategy["🧩 Strategy 层"]
        Factory["StrategyFactory<br/>(自动匹配)"]
        StrategyI["<<interface>> CrawlStrategy"]
        BlogS["BlogStrategy"]
        NewsS["NewsStrategy"]
        Factory --> StrategyI --> BlogS
        StrategyI --> NewsS
    end

    BlogS --> Repository

    subgraph Repository["🔐 Repository 层"]
        Repo["ArticleRepository<br/>(add / getAll)"]
        RepoList["List<Article> (私有)"]
        Repo --> RepoList
    end

    RepoList --> Model

    subgraph Model["📦 Model 层"]
        Article["Article"]
    end

    CrawlCmd --> Display
    Repository --> Display

🗺️ 每一层都有清晰的职责,每一处扩展都只需要新增而不是修改


6️⃣ 代码落地(分步升级)

从 W9 升级到 W10 的改动清单

  1. 新建 strategy/ 包 → CrawlStrategy 接口
  2. 实现 BlogStrategyNewsStrategy
  3. 实现 StrategyFactory
  4. 新建 repository/ 包 → ArticleRepository
  5. 修改 Command 接口签名
  6. 重写 CrawlCommand
  7. 调整其他所有 Command
  8. 调整 ControllerApp.java

关键代码演示

  • Collections.unmodifiableList() 的用法
  • StrategyFactory.getStrategy() 的遍历逻辑
  • CrawlCommand 从“写死解析”到“调度组装”
// 一个改动示例
for (Article a : parsed) {
    repository.add(a);   // 旧: articles.add(a);
}

找茬点

  • StrategyFactory 没匹配到策略时返回 null
  • CrawlCommand 检查 null 并报错
  • 有没有更优雅的方式避免 null 判断?

🔍 课后用 AI 探索 “空对象模式” 的前奏


7️⃣ 架构反思 + 下周预告

当前架构的脆弱点

  • 异常处理单一笼统
  • 没有重试机制
  • 网络超时无控制
  • 日志仅输出到终端

W11 目标:健壮性工程

  • 自定义异常体系:把“出错了”变成具体的业务异常
  • 工程化日志:记录谁、什么时间、做了什么
  • 防御式编程 + 重试机制:网络抖动不再致命

W9 搭骨架 → W10 装盔甲 → W11 让它经得起毒打


8️⃣ 实践任务(现场)

必做

  1. 基于 W9 项目升级到 W10
  2. 至少实现 2 个 CrawlStrategy(可模拟)
  3. 实现 StrategyFactoryArticleRepository
  4. 测试完整 crawllist 流程

验收标准

  • 新增策略只加类+注册,零改动旧代码
  • getAll() 返回不可修改视图
  • CrawlCommand 不含网站特定解析
  • 所有 Command 用 Repository
  • 无地方直接操作 List<Article>

9️⃣ 课后作业

必做

  1. 完善 ArticleRepository:增加 addAll,防御 null
  2. ★ AnalyzeCommand:复用策略解析但不存储,输出统计信息
  3. AI 架构审计:发送类签名给 AI,检查策略解耦与封装

选做

  • 正则策略匹配、默认策略、策略优先级
  • 思考题:两个策略都 supports 同一 URL 时怎么办?

🤖 AI 协同升级

架构审计师(必做)

  • 画出类依赖图
  • 发给 AI:“检查开闭原则达成度,Repository 封装完备性,是否存在循环依赖”

进阶探究

  • 不用工厂,直接用 Map<String, CrawlStrategy> 存起来 vs StrategyFactory 的区别?

📚 总结

  • 策略模式:算法可插拔,新增网站零痛苦
  • 工厂:自动匹配,URL → 策略的魔法
  • Repository:数据守卫,规则从口头约定变成代码强制
  • 架构:从“分开”到“优雅合上”,对扩展开放,对修改关闭

W11 预告

自定义异常体系 + 日志 + 重试机制

🚀 让我们造的爬虫,经得住现实的考验


谢谢!

保持工程洁癖,下周见!


居中标题

居中副标题

居中内容