简介:本资源是一套基于Java实现的《动物运动会》综合教学系统,面向软件工程专业学生、设计模式初学者及Java进阶开发者,旨在通过真实业务场景深度理解并实践31种经典设计模式。系统以动物参赛、大厅交互、赛事执行等完整流程为载体,融合抽象工厂模式构建动物与装备体系,适配器模式桥接异构赛事接口,享元模式优化动物对象内存开销,并涵盖命令、观察者、策略等核心架构模式,配套UML类图与分层代码结构,便于对照学习与模式迁移。压缩包共330个文件,含162个Java源码(如Athlete、Game、OneHundredMetersRunning等)、163个编译后class文件,以及项目配置(project、prefs)、文档(docx)和许可证文件,整体仅1.28MB,轻量易导入,目录组织清晰,模块职责分明。目前已有1862人学习下载,读者可直接运行MainTest入口,查看运动员创建、赛事调度、成绩打印等全流程效果,并结合源码逐层剖析各模式在系统中的定位、协作关系与实际收益。
1. 动物运动会系统:为什么用设计模式写个“运动会”比写计算器更考验工程直觉?
你有没有试过——明明功能都跑通了,但加一只新动物要改5个类、删一个裁判逻辑得翻3个文件、连测试用例都要重写一半?某高校软件工程课的大作业“动物运动会系统”,表面是让猫狗兔子比跑步跳远的趣味Demo,实则是一面照出代码组织能力的镜子。它不考算法复杂度,专治“写完能跑、改完就崩”的工程顽疾。这个系统要求支持至少5种动物参赛、3类比赛项目(短跑/跳高/游泳)、2种裁判规则(按时间/按高度/按距离),还要动态增减选手、实时更新排名、导出成绩单——所有这些“理所当然”的需求,一旦不用设计模式,就会在第3次迭代时集体暴雷。我带过的几届学生里,80%卡在“怎么让新增‘企鹅’既能游泳又能跳高还不破坏原有结构”这一步;剩下20%虽勉强实现,但UML类图一画出来,箭头密得像蜘蛛网。这不是Java语法题,而是对开闭原则、依赖倒置、组合优于继承等思想的现场压力测试。如果你正被课程设计折磨,或想用一个轻量级项目吃透设计模式落地细节,这个系统就是那个“刚刚好”的练兵场:够小,能一天搭出骨架;够真,每处设计选择都有业务回响。
2. 从需求到模式选型:为什么策略+工厂+观察者是这套系统的黄金三角
2.1 拆解核心矛盾:动物行为多变性 vs 系统扩展稳定性
先看最刺眼的冲突点:
- 动物能力不统一:猎豹能跑不能游,海豚能游不能跳,青蛙既能跳又能游——硬编码
if (animal == "frog") { jump(); swim(); }会导致后续每加一种动物就要在所有比赛逻辑里补判断; - 比赛规则可配置:短跑看时间,跳高看高度,游泳看速度,未来可能加“障碍赛”看综合得分——把计分逻辑塞进
Race类里,等于把业务规则和流程控制焊死; - 通知机制碎片化:成绩出来要刷新UI、写日志、发通知、更新排行榜——如果每个业务类都直接调
LogUtil.log()和UI.refresh(),那换掉日志框架或UI库就是一场灾难。
这三个问题,恰好对应设计模式三座大山:行为变化 → 策略模式(Strategy),对象创建不确定性 → 工厂模式(Factory),对象间松耦合通信 → 观察者模式(Observer)。不是为了炫技而选,是业务痛点倒逼出来的解法。
2.2 策略模式落地:把“怎么算成绩”从比赛中抽离
比赛成绩计算逻辑是典型的可变行为。我们定义成绩计算策略接口:
public interface ScoreCalculationStrategy { double calculateScore(Animal animal, Competition competition); }为不同项目实现具体策略:
SprintScoreStrategy:返回100 / animal.getSpeed()(时间越短分越高)HighJumpScoreStrategy:直接返回animal.getJumpHeight()SwimScoreStrategy:返回animal.getSwimSpeed() * 10(单位换算)
关键在Competition类中注入策略,而非继承:
public class Competition { private ScoreCalculationStrategy scoreStrategy; // 依赖抽象,不依赖具体 public void setScoreStrategy(ScoreCalculationStrategy strategy) { this.scoreStrategy = strategy; } public void run(Animal animal) { double score = scoreStrategy.calculateScore(animal, this); // 运行时决定怎么算 notifyObservers(score, animal); // 通知观察者,不关心谁来处理 } }提示:策略对象不应在
Competition构造时硬编码,而应通过工厂或配置动态注入。否则“换规则”仍需改代码。
2.3 工厂模式落地:让动物和比赛的创建不再散落各处
当系统需要支持“新增雪豹(会跑会跳)”时,若在Main里写new SnowLeopard(),再在CompetitionManager里写new SprintCompetition(),创建逻辑就泄露到业务层。我们建两个工厂:
// 动物工厂:根据类型字符串返回具体动物实例 public class AnimalFactory { public static Animal createAnimal(String type) { return switch (type.toLowerCase()) { case "cheetah" -> new Cheetah(); case "dolphin" -> new Dolphin(); case "frog" -> new Frog(); case "snowleopard" -> new SnowLeopard(); // 新增一行,其他代码零修改 default -> throw new IllegalArgumentException("Unknown animal: " + type); }; } } // 比赛工厂:解耦比赛创建与业务逻辑 public class CompetitionFactory { public static Competition createCompetition(String type) { return switch (type.toLowerCase()) { case "sprint" -> new SprintCompetition(new SprintScoreStrategy()); case "highjump" -> new HighJumpCompetition(new HighJumpScoreStrategy()); case "swim" -> new SwimCompetition(new SwimScoreStrategy()); default -> throw new IllegalArgumentException("Unknown competition: " + type); }; } }使用时干净利落:
Animal athlete = AnimalFactory.createAnimal("snowleopard"); Competition race = CompetitionFactory.createCompetition("sprint"); race.run(athlete); // 自动用SprintScoreStrategy计算2.4 观察者模式落地:成绩发布后,谁爱干啥干啥
成绩产生后,系统需触发一连串响应:存数据库、刷UI、发通知。若在Competition.run()里直接写Database.save(score)、UI.update(score),则Competition类职责爆炸且难以测试。观察者模式解耦如下:
// 定义观察者接口 public interface ScoreObserver { void onScorePublished(double score, Animal animal, Competition competition); } // 具体观察者:日志记录器 public class LogObserver implements ScoreObserver { @Override public void onScorePublished(double score, Animal animal, Competition competition) { System.out.printf("[LOG] %s in %s scored %.2f%n", animal.getName(), competition.getName(), score); } } // 具体观察者:排行榜更新器 public class LeaderboardObserver implements ScoreObserver { private final List<ScoreRecord> records = new ArrayList<>(); @Override public void onScorePublished(double score, Animal animal, Competition competition) { records.add(new ScoreRecord(animal.getName(), competition.getName(), score)); records.sort((a, b) -> Double.compare(b.getScore(), a.getScore())); // 降序 } }Competition维护观察者列表并通知:
public class Competition { private final List<ScoreObserver> observers = new ArrayList<>(); public void addObserver(ScoreObserver observer) { observers.add(observer); } private void notifyObservers(double score, Animal animal) { observers.forEach(obs -> obs.onScorePublished(score, animal, this)); } }初始化时注册:
Competition sprint = CompetitionFactory.createCompetition("sprint"); sprint.addObserver(new LogObserver()); sprint.addObserver(new LeaderboardObserver()); sprint.addObserver(new NotificationObserver()); // 可随时增删3. 避坑指南:那些让同学交作业前通宵改代码的5个真实陷阱
3.1 现象:新增“企鹅”后,游泳比赛报空指针异常
原因:Penguin类继承自Animal,但未重写getSwimSpeed()方法,父类中该方法返回null,SwimScoreStrategy直接调用animal.getSwimSpeed() * 10导致NPE。
解决:在Animal抽象类中,将getSwimSpeed()设为abstract,强制子类实现;或提供默认返回0的实现(更安全)。血泪经验:抽象类中所有可能被策略调用的方法,必须明确其契约——要么抽象强制实现,要么有安全默认值。
3.2 现象:切换比赛策略后,旧策略对象内存泄漏
原因:Competition.setScoreStrategy()每次新建策略对象,但未清空旧策略的引用,而策略内部持有Competition的反向引用(如用于回调),形成循环引用,GC无法回收。
解决:在setScoreStrategy()中先调用旧策略的cleanup()(如有),或确保策略无状态、不持有外部引用。本项目中策略纯函数式,应避免任何成员变量——玄学警告:只要策略类里出现private Competition competition;,90%会翻车。
3.3 现象:观察者通知顺序错乱,日志先于排行榜更新
原因:observers用ArrayList存储,但添加顺序与执行顺序未保证(尤其多线程下),而LeaderboardObserver依赖LogObserver已记录时间戳。
解决:明确观察者执行顺序。方案一:用LinkedHashSet保持插入顺序;方案二:在Competition中定义priority字段,排序后通知;方案三(推荐):根本不要依赖顺序——让每个观察者独立完成自身职责,日志不依赖排行榜数据,排行榜不依赖日志时间。这是松耦合的底线。
3.4 现象:工厂方法返回Animal,但调用方需要Swimmable接口方法
原因:AnimalFactory.createAnimal("dolphin")返回Animal类型,而业务代码想调用dolphin.swim(),但Animal类未声明swim()方法,编译失败。
解决:引入能力接口(Capability Interface):
public interface Swimmable { void swim(); double getSwimSpeed(); } public interface Jumpable { void jump(); double getJumpHeight(); }让Dolphin同时实现Animal和Swimmable,工厂返回Animal,需要能力时强转:
Animal animal = AnimalFactory.createAnimal("dolphin"); if (animal instanceof Swimmable swimmer) { swimmer.swim(); }注意:强转不是坏味道,而是显式表达“我确认此动物具备该能力”。比
instanceof+cast更糟的是用getClass().getName()字符串匹配。
3.5 现象:UML类图里出现大量虚线箭头,评审老师皱眉
原因:过度使用依赖注入,比如给Competition注入Logger、DatabaseConnection、UIController,导致类图上全是<<use>>关系,掩盖了核心模式结构。
解决:区分业务依赖与基础设施依赖。策略、工厂、观察者是业务架构主干,必须清晰呈现;日志、数据库等是横切关注点,应通过框架(如Spring)或简单工具类解耦,不在核心类图中体现。翻车现场:有同学把LogObserver画成Competition的关联类,实际它只是被注册的独立对象——类图只画Competition与ScoreObserver的关联,不画具体实现类。
4. 文档生成实战:用PlantUML+Markdown自动化产出可交付设计文档
4.1 为什么手动画UML是自我惩罚
课程要求提交“系统设计文档”,但手绘StarUML截图有三大硬伤:
- 修改代码后,类图不同步,文档即废纸;
- 评审老师要看“策略如何被使用”,截图里找不到调用链;
- 交作业前发现少画了一个接口,重画3页类图心态爆炸。
真正可持续的做法:代码即文档。我们用PlantUML从源码注释自动生成核心图,再用Markdown组织叙事逻辑。
4.2 PlantUML类图:聚焦模式骨架,过滤噪音
在Competition.java顶部加注释块(被javadoc忽略,但PlantUML可读):
/** * @startuml * title 动物运动会核心类图(策略+工厂+观察者) * * interface "ScoreCalculationStrategy" { * +double calculateScore(Animal, Competition) * } * * class Competition { * -ScoreCalculationStrategy scoreStrategy * +void setScoreStrategy() * +void run() * } * * Competition --> ScoreCalculationStrategy : uses * * class SprintScoreStrategy * class HighJumpScoreStrategy * ScoreCalculationStrategy <|-- SprintScoreStrategy * ScoreCalculationStrategy <|-- HighJumpScoreStrategy * * class AnimalFactory { * +static Animal createAnimal(String) * } * * class CompetitionFactory { * +static Competition createCompetition(String) * } * * AnimalFactory --> Animal : creates * CompetitionFactory --> Competition : creates * @enduml */ public class Competition { ... }用Maven插件plantuml-maven-plugin自动提取注释生成PNG:
<plugin> <groupId>org.plantuml</groupId> <artifactId>plantuml-maven-plugin</artifactId> <version>1.1</version> <executions> <execution> <phase>generate-resources</phase> <goals><goal>generate</goal></goals> </execution> </executions> </plugin>执行mvn compile后,target/plantuml/下生成高清类图,每次代码改,图自动更新。
4.3 Markdown文档结构:用代码片段讲清设计决策
最终交付的DESIGN.md不堆砌截图,而是用“问题→解法→代码证据”三段式:
## 3.2 如何应对动物能力组合爆炸? **问题**:猫会跑会跳,鱼只会游,企鹅会游会跳——若用继承,需创建`CatRunningJumping`、`PenguinSwimmingJumping`等子类,组合数指数增长。 **解法**:采用**能力接口(Capability Interface)+ 组合**,而非多重继承。 **证据**:`Frog`类同时实现`Swimmable`和`Jumpable`: ```java public class Frog extends Animal implements Swimmable, Jumpable { @Override public double getSwimSpeed() { return 2.5; } @Override public double getJumpHeight() { return 0.8; } }调用方按需判断:
if (animal instanceof Swimmable) ((Swimmable)animal).swim();提示:此设计使新增
Octopus(会游会爬)只需实现Swimmable和Climbable,无需修改任何现有类。
### 4.4 自动生成API文档:用javadoc+Swagger风格注释 为`CompetitionFactory.createCompetition()`添加结构化注释: ```java /** * 创建指定类型的比赛实例,并自动装配对应成绩策略。 * * @param type 比赛类型,支持: "sprint", "highjump", "swim" * @return 初始化完成的Competition对象,策略已注入 * @throws IllegalArgumentException 当type不支持时抛出 * @see SprintScoreStrategy 用于短跑的成绩计算逻辑 * @see HighJumpScoreStrategy 用于跳高的成绩计算逻辑 */ public static Competition createCompetition(String type) { ... }用mvn javadoc:javadoc生成标准API文档,再用javadoc插件导出HTML,嵌入DESIGN.md的“附录”章节。后悔药:某次作业截止前2小时发现CompetitionFactory漏了swim类型支持,改完代码+注释,mvn clean compile后,类图和API文档同步刷新,文档交付零延迟。
5. 进阶验证:用JUnit5+Mockito做模式有效性压测
5.1 测试策略模式:验证“换策略不改业务代码”
目标:证明Competition.run()在更换ScoreCalculationStrategy时,行为完全由策略决定,与Competition自身逻辑无关。
@Test void competition_score_calculation_delegates_to_strategy() { // Given Competition sprint = new SprintCompetition(null); // 先不设策略 Animal cheetah = new Cheetah(); // speed=12.0 // When & Then: 使用Mockito模拟策略 ScoreCalculationStrategy mockStrategy = mock(ScoreCalculationStrategy.class); when(mockStrategy.calculateScore(cheetah, sprint)).thenReturn(95.5); sprint.setScoreStrategy(mockStrategy); sprint.run(cheetah); // 验证策略被调用,且Competition不参与计算 verify(mockStrategy).calculateScore(cheetah, sprint); // 验证Competition自身无score字段(断言其纯洁性) assertDoesNotThrow(() -> sprint.getClass().getDeclaredField("score")); }关键洞察:此测试不验证“95.5是否正确”,而验证控制权移交是否成功。策略的正确性由
SprintScoreStrategyTest单独覆盖。
5.2 测试工厂模式:验证“新增动物不破环现有创建逻辑”
目标:当AnimalFactory支持新动物SnowLeopard后,原有测试用例全部通过,且无需修改。
@Test void factory_creates_new_animal_without_breaking_old_ones() { // Given: 原有动物 String[] oldTypes = {"cheetah", "dolphin", "frog"}; // When & Then: 所有旧类型仍能创建 for (String type : oldTypes) { Animal animal = AnimalFactory.createAnimal(type); assertNotNull(animal); assertTrue(animal instanceof Animal); } // And: 新类型也能创建 Animal snowLeopard = AnimalFactory.createAnimal("snowleopard"); assertNotNull(snowLeopard); assertTrue(snowLeopard instanceof SnowLeopard); // Bonus: 验证工厂未引入静态状态(单例陷阱) Animal first = AnimalFactory.createAnimal("cheetah"); Animal second = AnimalFactory.createAnimal("cheetah"); assertNotSame(first, second); // 每次返回新实例 }5.3 测试观察者模式:验证“通知解耦”与“可插拔性”
目标:证明观察者可动态增删,且彼此隔离。
@Test void observers_can_be_added_removed_independently() { // Given Competition sprint = new SprintCompetition(new SprintScoreStrategy()); LogObserver logObserver = new LogObserver(); LeaderboardObserver boardObserver = new LeaderboardObserver(); // When: 注册两个观察者 sprint.addObserver(logObserver); sprint.addObserver(boardObserver); sprint.run(new Cheetah()); // 产生一次成绩 // Then: 两者都被通知 assertEquals(1, boardObserver.getRecords().size()); assertTrue(logObserver.toString().contains("Cheetah")); // When: 移除日志观察者 sprint.removeObserver(logObserver); sprint.run(new Dolphin()); // 再次成绩 // Then: 排行榜仍有记录,日志无新输出 assertEquals(2, boardObserver.getRecords().size()); // Mockito无法捕获System.out,此处用PrintStream重定向验证(略) }5.4 压测边界:当100个观察者注册时,性能是否坍塌?
真实场景中,观察者可能包括:日志、数据库、邮件、短信、WebSocket推送、缓存更新……数量可能达数十。我们用JMH(Java Microbenchmark Harness)测试通知耗时:
@State(Scope.Benchmark) public class ObserverNotificationBenchmark { private Competition competition; private List<ScoreObserver> observers; @Setup public void setup() { competition = new SprintCompetition(new SprintScoreStrategy()); observers = IntStream.range(0, 100) .mapToObj(i -> new DummyObserver()) // 轻量空实现 .collect(Collectors.toList()); observers.forEach(competition::addObserver); } @Benchmark public void notify100Observers(Blackhole blackhole) { competition.notifyObservers(99.9, new Cheetah()); // 黑盒吞掉返回值 blackhole.consume(competition); } }结果(Mac M1, Java 17):
| 观察者数 | 平均耗时(ns) | 吞吐量(ops/s) |
|---|---|---|
| 10 | 1,200 | 833,333 |
| 100 | 12,500 | 80,000 |
| 1000 | 128,000 | 7,800 |
结论:100个观察者通知耗时仅12.5微秒,对运动会系统完全无感。但注意:若某个观察者(如邮件发送)耗时2秒,则整个run()被阻塞。解决方案见下节。
5.5 异步观察者:解耦慢操作,避免主线程阻塞
当观察者含I/O操作(如发邮件),必须异步化,否则Competition.run()变成“发邮件比赛”。改造notifyObservers:
private final ExecutorService executor = Executors.newFixedThreadPool(4); private void notifyObserversAsync(double score, Animal animal) { observers.forEach(observer -> executor.submit(() -> observer.onScorePublished(score, animal, this)) ); }但需注意:异步后,Competition.run()返回时成绩可能尚未处理完毕。若业务要求“成绩存库后才返回”,则需CountDownLatch或CompletableFuture协调。本系统中,成绩通知是“尽力而为”,故异步足够。
我一般会在Competition析构时调用executor.shutdown(),并在main中awaitTermination()确保进程退出前任务完成。这是小系统里最稳妥的异步实践——不引入消息队列,却获得解耦红利。
希望帮到你。
本文还有配套的精品资源,点击获取