动物运动会系统:用策略+工厂+观察者模式实践开闭原则

发布时间:2026/10/14 21:40:39
动物运动会系统:用策略+工厂+观察者模式实践开闭原则 简介本资源是一套基于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 ListScoreRecord 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 ListScoreObserver 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()方法父类中该方法返回nullSwimScoreStrategy直接调用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 Interfacepublic 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(); }注意强转不是坏味道而是显式表达“我确认此动物具备该能力”。比instanceofcast更糟的是用getClass().getName()字符串匹配。3.5 现象UML类图里出现大量虚线箭头评审老师皱眉原因过度使用依赖注入比如给Competition注入Logger、DatabaseConnection、UIController导致类图上全是use关系掩盖了核心模式结构。解决区分业务依赖与基础设施依赖。策略、工厂、观察者是业务架构主干必须清晰呈现日志、数据库等是横切关注点应通过框架如Spring或简单工具类解耦不在核心类图中体现。翻车现场有同学把LogObserver画成Competition的关联类实际它只是被注册的独立对象——类图只画Competition与ScoreObserver的关联不画具体实现类。4. 文档生成实战用PlantUMLMarkdown自动化产出可交付设计文档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自动提取注释生成PNGplugin groupIdorg.plantuml/groupId artifactIdplantuml-maven-plugin/artifactId version1.1/version executions execution phasegenerate-resources/phase goalsgoalgenerate/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文档用javadocSwagger风格注释 为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. 进阶验证用JUnit5Mockito做模式有效性压测5.1 测试策略模式验证“换策略不改业务代码”目标证明Competition.run()在更换ScoreCalculationStrategy时行为完全由策略决定与Competition自身逻辑无关。Test void competition_score_calculation_delegates_to_strategy() { // Given Competition sprint new SprintCompetition(null); // 先不设策略 Animal cheetah new Cheetah(); // speed12.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推送、缓存更新……数量可能达数十。我们用JMHJava Microbenchmark Harness测试通知耗时State(Scope.Benchmark) public class ObserverNotificationBenchmark { private Competition competition; private ListScoreObserver 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/s101,200833,33310012,50080,0001000128,0007,800结论100个观察者通知耗时仅12.5微秒对运动会系统完全无感。但注意若某个观察者如邮件发送耗时2秒则整个run()被阻塞。解决方案见下节。5.5 异步观察者解耦慢操作避免主线程阻塞当观察者含I/O操作如发邮件必须异步化否则Competition.run()变成“发邮件比赛”。改造notifyObserversprivate 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()确保进程退出前任务完成。这是小系统里最稳妥的异步实践——不引入消息队列却获得解耦红利。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询