news 2026/9/7 10:39:54

动物运动会系统:设计模式大作业实战指南与代码实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
动物运动会系统:设计模式大作业实战指南与代码实现

简介:《动物运动会》是一套基于Java语言的综合性设计模式教学项目,面向需要完成设计模式大作业、课程设计或希望深入理解软件架构的开发者。整个系统并非零散案例,而是将三十一种设计模式整合在同一应用框架内,其中包括抽象工厂、适配器等经典实现,展示各模式在运动会场景下的协作方式。压缩包共三百三十个文件,以一百六十二个Java源文件为主体,便于阅读与二次开发,另附一百六十三个编译后的类文件、文档设计说明及工程配置文件,整体仅一点二八兆字节,轻量易用。目前共有一千八百五十九人学习下载,内容包含完整源码、UML类图和系统效果说明,可帮助理清模式选择、接口设计与扩展思路。对于正在撰写课程报告、准备答辩或自学设计模式的读者,这是一份具有完整性和参考价值的实践资料。 最近好几个学弟学妹私信问我设计模式大作业怎么选题,我每次都推荐一个非常经典又不容易翻车的方向:动物运动会系统。原因很简单,这个场景足够生活化,类与类之间的协作关系清晰,设计模式往上一套,既不会显得生搬硬套,又有很强的扩展空间——老师看了觉得你动了脑子,你自己写起来也不会像在背八股。今天我就把这套系统的完整设计思路、模式落地方案、关键代码写法以及答辩避坑经验一次讲清楚。

很多同学拿到“设计模式大作业”这个题目时容易犯一个毛病:为了用模式而用模式,把代码搞得特别绕。比如一个简单的起床操系统,硬是套上装饰器加观察者加桥接,最后类数量比功能还多。动物运动会的好处在于,它的业务天然就带着“创建”、“策略选择”、“事件通知”、“状态切换”这些设计模式高频场景,不需要你强行找角度,模式就像螺丝一样自己咬合上去。

1. 系统设计:先把场景建模想明白

1.1 需求拆解与分析

动物运动会系统,核心需求大致是这么几条:有若干种动物参赛,比如猫、狗、鸟、鱼;有若干种比赛项目,比如跑步、游泳、跳高、飞行;每场比赛要有开始、进行、结束的过程;比赛结束后要产生成绩,最好还能排名;系统要能灵活地增加新动物或新比赛项目,而不是改动大量已有代码。

就这几条需求,其实已经把模式选型逼出来了。动物种类多、将来可能要扩展新动物,这就是典型的“创建对象不直接new”的场景,工厂模式上场;比赛项目各不相同,但流程骨架相同——准备、检录、开跑、计时、公布成绩,这就是模板方法模式的最佳示范;不同动物在同一个项目中能力不同,成绩计算方式不同,又可以把成绩策略提取出来,用策略模式动态切换;比赛成绩出来以后,播报系统、计分板、记录系统都要响应,这就是观察者模式的主场。

一句话总结:需求里的“扩展性”和“联动性”是所有模式选型的出发点,而不是反过来先选模式再去编需求。

1.2 动物与竞赛的核心抽象

在设计类结构之前,我建议先画一个极简的领域模型。动物这个体系,抽象出一个Animal父类是肯定的,它至少要有namespecies属性,以及一个共通的run()或者perform()能力。但这里有个很容易想歪的点:动物在比赛时,它比赛的方式是跟项目相关的,跑步和游泳对同一只动物来说差别很大。如果你直接把run()swim()全部塞进Animal里,那每次新增比赛项目都要改所有动物类,开闭原则直接破功。

所以更合理的做法是,把“比赛能力”从“动物本身”中解耦出来。动物只负责自身基本属性,比赛能力单独抽象为接口,比如RunBehaviorSwimBehavior,每个动物的具体行为类去实现这些接口。这样新增比赛项目时,只要新增一个行为接口和对应的实现类,完全不需要动到动物的代码。

竞赛侧,需要一个Race抽象类代表比赛流程,比赛的各个阶段作为模板方法固定下来,阶段的细节延迟到子类实现。同时还需要一个ScoreStrategy来处理不同项目的计分逻辑——跳高按高度、跑步按耗时、飞行按距离,这种差异用策略模式处理非常自然。成绩出来后,Race内部持有一组监听器,通过观察者模式通知所有关注成绩的对象。

2. 设计模式落地方案

2.1 简单工厂与工厂方法:动物创建

先解决最基础的创建问题。如果运动会系统里到处都是new Cat()new Dog(),代码确实也能跑,但问题非常明显:main方法或者调用方会高度耦合具体类,一旦要统一给所有动物加上编号、注册到比赛系统、初始化健康状态,就得把所有new的地方全部翻出来改。工厂模式就是把这些创建逻辑收拢到一处。

我用的方案是简单工厂AnimalFactory,它接收一个字符串或枚举类型,内部用switch或者Map返回对应的动物实例。对一个大作业来说简单工厂程度刚刚好,代码清晰,模式识别的名称也好解释。你还可以再进一步做工厂方法模式,也就是让每个动物子类对应一个子工厂,专门负责创建该类动物,这样做的好处是进一步把创建流程的每一步,比如日志记录、装备发放、检录时间锁定,都放进子工厂自由定制。

补充一个实操细节:工厂类内部创建动物时,建议把动物初始能力值也一起注入,比如Cat默认奔跑速度30km/h、Fish不能跑步只能游泳。这个初始化数据可以用一个配置类存起来,工厂从配置里取值,这样后续改数值不需要重新编译,也更好跟老师解释“我对扩展开放”。

2.2 模板方法模式:稳定比赛流程骨架

这一块是整个系统最值得展开讲的部分。比赛流程看似简单,其实有严格时序:检录 → 就位 → 开始 → 计时 → 结束 → 成绩统计 → 颁奖。这个流程长度固定、顺序固定,但每个环节的具体实现随项目不同而变化。这正是模板方法模式最经典的适用场景。

我设计了一个抽象类Race,里面用final修饰的runRace()方法定义流程骨架,流程中每个步骤调用对应的抽象方法或钩子方法。子类RunRaceSwimRaceHighJumpRace只重写自己关注的那几个步骤。

这里有一个容易踩坑的点:模板方法不要写得太碎,也不要写得太粗。太碎的话子类要重写的方法过多,代码冗余;太粗的话钩子太少,子类之间的流程差异无处安放。我实践下来,最佳粒度是把流程拆成6步左右,其中prepare()start()是公共的,直接由父类实现;doRace()calculateScore()是每个项目核心差异点,必须由子类实现;announceResult()在父类里调用观察者广播即可。

模板方法模式的关键词理解是“流程复用”,答辩时最好能画一个普通的时序图,标出哪些是父类的方法、哪些是子类重写的方法,老师一般看到这个图就知道你真的懂了。

2.3 策略模式:灵活切换计分与能力策略

策略模式在整个系统里其实出现两次,第一次是动物的运动能力,第二次是比赛的计分机制。这里我重点说说计分机制。

不同比赛项目的排名方式完全不同。跑步和游泳看谁用时最短;跳高和跳远看谁高度/远度最大;飞行类项目可能还要结合距离和姿态分。如果把这些计分规则硬编码进Race的子类里,每个子类的score方法里写一坨if-else,虽然也能跑,但一旦新增计分规则,就得不停修改已经写好的类。

策略模式的解法是:定义ScoreStrategy接口,接口里有double calculateScore(RaceResult result)方法。每种计分规则做成独立策略类,比如TimeScoreStrategyHeightScoreStrategyLengthScoreStrategyRace持有的不是一个固定算法,而是一个ScoreStrategy引用,通过构造函数或setter注入。

这样设计最强的地方在于:支持运行时切换策略。比如有的赛事突然改规则,取消高度上限限制,你只需要在代码里换一个新的HeightUnlimitedScoreStrategy,完全不影响其他逻辑。答辩时把这个“运行时切换”的演示做出来,非常加分。

动物能力这一侧的策略选择也有讲究。我在前面提到了Animal组合行为接口,其实这就是策略模式结合组合优于继承思想的典型写法。Animal内部持有MoveStrategy接口引用,不同动物通过构造函数注入不同的策略实现。比如Bird注入FlyingMoveStrategyFish注入SwimmingMoveStrategy。这比单纯用继承去表达“鸟会飞、鱼会游”要灵活得多,因为有些动物是多种技能组合的,比如鸭子既能跑又能游泳还能飞到一定高度,多继承在Java里不支持,但组合多个策略却毫无压力。

2.4 观察者模式:成绩播报与计分板更新

比赛结束后,成绩信息要触达多个对象:现场播报员要念出成绩,大屏幕计分板要刷新数据,官方记录系统要存档历史数据,可能还有自媒体平台的实时热点更新器。如果让Race在比赛结束点挨个调用这些对象的更新方法,代码会耦合到爆炸,新增一个信息接收方就要修改Race类。

观察者模式在这里作用非常突出。Race内部维护一个List<RaceListener>,提供registerListener()unregisterListener()方法,比赛结束时通过notifyListeners(RaceResult result)遍历列表调用每个监听器的onRaceFinished(result)方法。播报员、计分板、记录系统各自实现RaceListener接口,然后在系统启动时注册到对应的Race对象上。

这个模式的扩容体验非常爽。运动会系统上线后,老师如果说要加一个“历史最佳成绩比对屏”,你只需要写一个BestRecordCompareBoard implements RaceListener,两分钟注册进去,Race类一行都不改动。观察者模式本质上是把“事件发生源”和“事件响应方”解耦,让两个方向的扩展都对各自独立。

2.5 单例模式与状态模式的辅助作用

设计模式大作业如果想拿高分,展示出“模式组合”的意识非常重要。也就是说,不要一个个模式孤立使用,而是让模式之间产生协作。这里说两个辅助但有存在感的模式。

第一个是单例模式。运动会中的比赛场地和总控台是整个系统里逻辑上唯一的存在,场地资源不能重复创建,总控台保存所有注册信息。我用饿汉式单例实现了VenueManagerEventCenter,饿汉式的写最简单,线程安全问题天然规避,答辩时还能顺手讲出“为什么用饿汉而不是懒汉”这种经典问题。

第二个是状态模式。这个我用来处理动物的参赛状态生命周期,比如READY(待赛)、RACING(比赛中)、FINISHED(完赛)、DISQUALIFIED(取消资格)。如果把状态转换逻辑用if-else写在动物类里,随着状态增多代码会越来越乱。用状态模式的话,Animal内部持有AnimalState引用,状态转换调用transitionTo()方法直接切换对象。状态对象本身可以是单例,也可以用枚举实现。这个设计对大部分同学来说已经是一个亮点了,因为状态模式在所有大作业里的使用频率比工厂观察者低不少,老师会眼前一亮。

3. 关键代码实现与详解

3.1 动物抽象类与行为策略

先贴出Animal的基础代码,这段对应上面说的“组合优于继承”思路:

public abstract class Animal { protected String name; protected String species; protected AnimalState state; protected MoveStrategy moveStrategy; public Animal(String name, String species) { this.name = name; this.species = species; this.state = ReadyState.getReadyState(); } public void setMoveStrategy(MoveStrategy moveStrategy) { this.moveStrategy = moveStrategy; } public String move() { return moveStrategy.move(this); } public void updateState(AnimalState newState) { this.state = newState; } public abstract String introduce(); }

这里MoveStrategy是一个接口,move()方法接收动物对象并返回一句描述。比如FlyingMoveStrategy返回“XXX扇动翅膀,以每小时XX公里的速度飞行”,RunningMoveStrategy返回“XXX四腿发力,狂奔追赶”。这样就把“行为”从“动物”中抽离了。

然后是具体动物类:

public class Dog extends Animal { public Dog(String name) { super(name, "犬科"); this.setMoveStrategy(new RunningMoveStrategy()); } @Override public String introduce() { return "我是一只叫" + name + "的狗,擅长奔跑和游泳,很有运动精神。"; } }

工厂类的实现细节,我推荐用枚举配合Map做,比纯switch可读性高:

public class AnimalFactory { private static final Map<String, Supplier<Animal>> ANIMAL_CREATORS = new HashMap<>(); static { ANIMAL_CREATORS.put("cat", Cat::new); ANIMAL_CREATORS.put("dog", Dog::new); ANIMAL_CREATORS.put("bird", Bird::new); ANIMAL_CREATORS.put("fish", Fish::new); } public static Animal createAnimal(String type, String name) { Supplier<Animal> creator = ANIMAL_CREATORS.get(type.toLowerCase()); if (creator == null) { throw new IllegalArgumentException("未知的动物类型: " + type); } Animal animal = creator.get(); // 利用反射设置名字,或者让Supplier变成二元函数 return animal; } }

这里有个细节,上面的createAnimal没展示名字注入,因为Supplier是无参的。如果动物类构造需要传name,我建议把Map改成BiFunction<String, String, Animal>或者直接用lambda形态完成名字绑定,否则就要在工厂里再处理。这个小瑕疵在答辩时说清楚你的取舍思路,反而是加分项。

3.2 比赛调度器与模板方法流程

比赛部分,Race抽象类的模板方法这样写:

public abstract class Race { protected String raceName; protected List<Animal> participants = new ArrayList<>(); protected ScoreStrategy scoreStrategy; protected List<RaceListener> listeners = new ArrayList<>(); public final void runRace() { prepare(); checkIn(); start(); doRace(); finish(); calculateAndAnnounce(); } protected void prepare() { System.out.println(raceName + "场地准备工作完成,裁判就位。"); } protected abstract void checkIn(); protected void start() { System.out.println("所有参赛选手就位,发令枪响。"); } protected abstract void doRace(); protected void finish() { System.out.println("比赛结束,选手减速并确认成绩。"); } protected void calculateAndAnnounce() { RaceResult result = scoreStrategy.calculateResult(participants); notifyListeners(result); } public void registerListener(RaceListener listener) { listeners.add(listener); } private void notifyListeners(RaceResult result) { for (RaceListener listener : listeners) { listener.onRaceFinished(result); } } }

子类实现时只需要关注checkIn()doRace()。比如SwimRacedoRace()里,模拟鱼类和狗类在水中前进,用moveStrategy的个性化输出制造比赛过程描述;HighJumpRacedoRace()里,按不同动物身体参数计算跳跃高度,再交给高度计分策略排序。

有一个细节值得展开:runRace()方法必须加上final关键字。这是模板方法模式的规则之一,防止子类重写整个流程,从而保证流程骨架不被破坏。这个点老师问到的概率极高,要能说明白为什么。

3.3 观察者与成绩播报联动

当一个运动员冲过终点,需要让所有观察者及时响应。核心代码如下:

public interface RaceListener { void onRaceFinished(RaceResult result); } public class Announcer implements RaceListener { private String name; public Announcer(String name) { this.name = name; } @Override public void onRaceFinished(RaceResult result) { System.out.println("【解说员" + name + "】" + result.getWinner() + "夺得了" + result.getRaceName() + "的冠军,成绩是" + result.getChampionScore()); } } public class ScoreBoard implements RaceListener { @Override public void onRaceFinished(RaceResult result) { // 把结果写入内存中的排行榜,并在大屏渲染 System.out.println("【计分板】正在刷新排行榜……"); } }

RaceResult里包含比赛名称、全部选手成绩、冠军姓名、夺冠成绩、最后更新时间。这个数据对象格式统一之后,所有观察者拿到的都是完整数据,之后各自决定怎么处理,互不干扰。在main方法里,把AnnouncerScoreBoard注册到RunRace对象上,再执行runRace(),控制台就会依次输出播报和计分板刷新日志,整个系统的运行效果非常直观,适合录成演示视频。

4. 文档写作要点与答辩避坑

4.1 文档结构与UML图要求

大作业压缩包名带着“文档”两个字,说明文档的分量不轻。我给自己的文档定的结构是:需求分析、技术选型、总体设计、核心类图、模式分析和应用场景、代码说明、运行效果截图、总结与反思。最后一个“总结与反思”一定要写真实的体会,比如“使用模板方法后新增项目的时间从30分钟缩短到5分钟”,用具体数据说明模式的价值。

UML类图是大作业的重点,也是很多同学的痛点。不需要画完整的巨型UML,重点画出三类关系:继承关系(动物抽象类和具体动物)、组合关系(动物和MoveStrategy)、依赖关系(Race依赖ScoreStrategy、集合容器和RaceListener)。老师看了类图就能确认你的类设计是提前规划过的,而不是写着写着为了凑模式硬加出来的。

画图工具方面,免费的draw.io足够,语法上用PlantUML也行。但如果对PlantUML不熟悉,我建议直接用draw.io手动画,控制细节更精准,导出的PNG图片放在文档里也更清晰。类图放在文档第二到三页,第一页放项目简介和运行环境。

4.2 老师高频提问整理

答辩阶段老师通常聚焦三个方向:为什么要用这个模式、和另一个模式有什么区别、换一种模式会不会更好。这里把高频问题梳理一下:

  1. 模板方法模式和策略模式有什么区别?我的回答思路:模板方法关注流程骨架复用,一般通过继承实现,父类控制流程,子类重写步骤;策略模式关注算法行为的动态切换,通过组合委托实现,对象可以随时更换算法。简单说,模板方法是“流程复用”,策略模式是“算法互换”。

  2. 简单工厂和工厂方法有什么区别?回答思路:前者创建逻辑集中在一个工厂类,通过参数区分产品类型;后者把创建动作延迟到子类工厂,每个工厂对应一种产品,新增产品时开一个子工厂即可。大作业规模用简单工厂足够,但能讲出什么时候该升级为工厂方法就行。

  3. 观察者模式中,被观察者持有观察者列表会不会内存泄漏?这个问题如果被问到,属于加问题。可以答:如果观察者长时间不用且没有移除,确实会导致对象无法被GC回收。所以我在系统中提供了unregisterListener()方法,在广播系统关闭时手动解绑。这个回答能展示你对内存管理的意识,很加分。

另外建议在答辩前准备一个“模式重构对比”的小实验:同样的功能,先写一版不用设计模式的代码,再写一版用了模式的,对比增量开发时改动的行数差异。比如增加一种新比赛项目,普通代码要改多少行,用模板方法模式要改多少行,用数据说话,老师绝对认可。

5. 常见问题与排查技巧

我在开发和指导学生开发这套系统的过程中,总结了一些特别常见的问题,列成速查表,供大家直接对照排查。

问题现象原因分析解决方式
编译报错“类Race的runRace无法从外部调用”可能把模板方法写在非抽象父类中,且子类里误重写了流程方法检查runRace方法定义在抽象类中并加final,子类只重写步骤方法
运行结果裁判播报全部在其它比赛之前输出观察者列表没有在正确的比赛对象上注册,或runRace的notifyListeners没放在最后一步检查registerListener是否注册到了同一个Race实例,确认计算成绩后再广播
新增动物类型后,工厂处报空指针Map的key没配对,或代码中用了未注册的类型字符串在createAnimal中增加判空逻辑,抛出明确的IllegalArgumentException,附带合法类型列表
状态转换错乱,如直接调用处于RACING状态的动物参赛状态模式下状态对象没有校验转换合法性在状态的transitionTo方法中加入合法性判断,非法转换直接拒绝并输出提示
程序能跑但看不到任何“运动会”的感觉输出内容太干瘪,只有成绩没有过程描述结合各动物MoveStrategy的描述文本,模拟比赛解说过程,让输出富有故事性
UML图与代码不一致画图是纯手工,代码写完了没同步更新先用代码完成主体,再倒推画类图,保证图是代码的真实映射
压缩包解压后发现缺少文档打包时文档放在out目录没被一起打包提交前一定要重新解压测试一遍,确认zip里有代码、README、运行截图、UML图

还有一个非常实用的调试技巧:在开发阶段给Race的每个步骤加一行调试日志,不然你看到异常时很难定位是哪一步出的问题。等全部调通后,再统一把调试日志关掉或者降级为注释,保持正式输出的清爽。

6. 从大作业到能力提升的一点延伸

做完这套系统之后,我的感受是这个大作业的最大价值不在于“为了交差”,而在于把一个一个孤立的设计模式,变成可以互相协作、解决真实问题的工具链。你会在写代码的过程中慢慢产生模式直觉,比如当你发现一个类的职责越来越多时,会下意识地想到是否该拆分策略了;当你发现新增功能要改旧代码时,会立刻警惕是否违背了开闭原则。这种本能的建立,比背会23种模式的定义重要得多。

最后再分享一个小技巧:把所有模式的应用场景和代码位置做成一个索引表,放在文档最后。这个索引表包括模式名称、使用位置、解决的问题、核心类名,四列即可。答辩时教授问到哪里,你都可以迅速定位,展示出你对项目的掌控力。这个细节很小,但在很多答辩现场都是区分“背代码”和“懂设计”的关键信号。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 10:39:34

奔驰开源ARDEP车载开发板:基于AURIX TC397的嵌入式学习实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 10:39:25

分布式训练从单卡到万卡:并行策略、通信瓶颈与系统挑战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 10:37:14

非阻塞控制新思路:用树莓派Pico PIO精密驱动步进电机

刚拿到树莓派 Pico 玩步进电机的时候&#xff0c;我最直观的感受是&#xff1a;控制个脉冲怎么这么折腾&#xff1f;要么用延时函数死等&#xff0c;要么写中断里小心翼翼维护状态&#xff0c;稍微加个加减速逻辑CPU占用就飙上去。后来把PIO&#xff08;可编程输入输出&#xf…

作者头像 李华
网站建设 2026/9/7 10:35:46

全面解析检索技术:全景图与深度分析

目录 一、必要性分析 二、现代业务系统应用举例 三、简单的知识全景图分析 (一)存储介质的选择 (二)数据结构与算法层 (三)检索专业知识 工程架构 算法策略 QP策略 召回策略算法 粗排算法 常见的粗排算法 精排算法 加权评分策略算法 过滤策略算法 重排策略…

作者头像 李华