简介:本资源是面向软件工程专业本科生与设计模式初学者的课程大作业实践项目——服装管理系统,聚焦设计模式在真实业务系统中的综合应用。项目完整实现了单例、工厂、观察者、策略、装饰器、代理及MVC等经典模式,并融合三层架构思想,覆盖需求分析、数据库设计、源码实现到演示汇报全流程,有效解决学习者对模式落地难、架构抽象高的痛点。压缩包共39个文件,含17个Java核心源码(实现各设计模式逻辑)、14张PNG界面与模式示意图(如单例、策略、装饰等场景可视化)、4个XML配置文件(支撑MVC与数据访问)、1份PPTX项目演示文稿及1份DOC格式软件设计说明书,整体2.4MB,结构清晰、即下即学。已有5459人学习下载,读者可直接复用代码结构、参考UML图解、对照文档理解模式选型依据,并通过完整可运行项目掌握从体系结构设计到编码落地的闭环能力。
1. 为什么一个“服装管理系统”能考倒全班?——从期末大作业看设计模式与体系结构的真实分水岭
某高校《软件设计模式与体系结构》课程期末大作业要求实现一个“服装管理系统”,表面看只是增删改查:录入T恤、牛仔裤、连衣裙,按品牌/尺码/季节筛选,生成库存报表。但交卷后发现:30%的同学代码无法扩展新类型(比如加入“配饰”模块);42%的类之间强耦合,改个折扣计算逻辑要动7个文件;还有人把数据库连接、界面渲染、价格校验全塞进一个ClothingManager.java里——运行能跑,重构即崩溃。这根本不是“写不写得出来”的问题,而是“有没有在用设计模式思考架构”的分水岭。本作业真正考察的,是能否把抽象工厂隔离对象创建、用观察者解耦库存变更与通知逻辑、靠策略模式让不同促销规则(满减/会员折/限时秒杀)自由切换——不是背定义,是在真实业务流里识别出“哪里该插一个模式”。适合正在啃GOF23却总卡在“知道但不会用”的同学,也适合想用一个可运行系统反向吃透MVC分层、包依赖、开闭原则落地细节的实践者。
2. 用四层架构搭骨架:从UI到Domain,每一层只做一件事
服装管理系统的混乱,往往始于“哪层该放什么逻辑”的模糊。我带过三届学生做这个作业,最稳的方案是严格遵循四层架构(Presentation → Application → Domain → Infrastructure),而非简单套用Spring Boot默认包结构。关键不是分包名,而是职责切割的不可逆性:Presentation层绝不能直接new Service,Domain层绝不能import任何数据库或HTTP相关类。下面用最小可行代码演示核心分层逻辑。
2.1 Presentation层:只负责“转译”,不碰业务规则
// src/main/java/presentation/ClothingController.java @RestController @RequestMapping("/api/clothing") public class ClothingController { private final ClothingApplicationService clothingService; public ClothingController(ClothingApplicationService clothingService) { this.clothingService = clothingService; } @PostMapping public ResponseEntity<ClothingResponse> createClothing( @RequestBody ClothingRequest request) { // 仅做DTO→Domain对象的轻量转换,无校验逻辑 Clothing clothing = new Clothing( request.getName(), request.getBrand(), Size.valueOf(request.getSize()), Season.valueOf(request.getSeason()) ); // 业务逻辑全交给Application层 Clothing saved = clothingService.create(clothing); return ResponseEntity.ok(new ClothingResponse(saved)); } }逻辑说明:Controller只做三件事——接收HTTP请求、将JSON转为DTO、调用Application Service。所有字段合法性校验(如尺码是否在枚举范围内)必须在Application层完成,这里不做
if (request.getSize() == null)判断。
参数说明:ClothingRequest是纯数据载体,不含方法;ClothingResponse同理。避免在DTO里加getFormattedPrice()这类业务方法,那是Domain层的职责。
2.2 Application层:编排业务流程,不碰具体实现
// src/main/java/application/ClothingApplicationService.java @Service public class ClothingApplicationService { private final ClothingRepository clothingRepository; private final InventoryService inventoryService; // 领域服务,非基础设施 public ClothingApplicationService(ClothingRepository clothingRepository, InventoryService inventoryService) { this.clothingRepository = clothingRepository; this.inventoryService = inventoryService; } @Transactional public Clothing create(Clothing clothing) { // 1. 领域层校验(例:禁止录入无品牌的服装) if (clothing.getBrand().isBlank()) { throw new IllegalArgumentException("品牌不能为空"); } // 2. 调用领域服务处理库存联动逻辑 inventoryService.onClothingAdded(clothing); // 3. 持久化(调用Infrastructure层接口) return clothingRepository.save(clothing); } }逻辑说明:Application层是“导演”,协调Domain和Infrastructure。它不实现
onClothingAdded(),只声明需要这个行为;也不写SQL,只调用clothingRepository.save()接口。事务注解在此层生效,确保业务操作原子性。
参数说明:InventoryService是Domain层定义的接口,其具体实现(如DatabaseInventoryService)在Infrastructure层注入,实现依赖倒置。
2.3 Domain层:放核心业务规则,拒绝框架污染
// src/main/java/domain/Clothing.java public class Clothing { private final String name; private final String brand; private final Size size; private final Season season; private BigDecimal price; // 价格可变,需封装set逻辑 public Clothing(String name, String brand, Size size, Season season) { this.name = Objects.requireNonNull(name); this.brand = validateBrand(brand); // 域内校验 this.size = size; this.season = season; } private String validateBrand(String brand) { if (brand == null || brand.trim().length() < 2) { throw new DomainException("品牌长度至少2字符"); } return brand.trim(); } public void setPrice(BigDecimal price) { if (price == null || price.compareTo(BigDecimal.ZERO) < 0) { throw new DomainException("价格不能为负数"); } this.price = price; } }逻辑说明:Domain对象是贫血还是充血?本作业强烈建议充血模型——校验逻辑内聚在类内部。
validateBrand()和setPrice()中的约束,是业务本质规则,与数据库字段长度、前端输入限制无关。
参数说明:Size和Season是枚举类,定义在Domain包下,避免用String硬编码导致散落各处的魔法值。
2.4 Infrastructure层:实现技术细节,对上层透明
// src/main/java/infrastructure/persistence/jpa/JpaClothingRepository.java @Repository public class JpaClothingRepository implements ClothingRepository { private final SpringDataClothingJpaRepository jpaRepository; public JpaClothingRepository(SpringDataClothingJpaRepository jpaRepository) { this.jpaRepository = jpaRepository; } @Override public Clothing save(Clothing clothing) { // 将Domain对象转为JPA Entity(含ID生成、时间戳等技术细节) ClothingEntity entity = ClothingEntity.fromDomain(clothing); ClothingEntity saved = jpaRepository.save(entity); return saved.toDomain(); // 转回Domain对象,上层无感知 } }逻辑说明:Infrastructure层是“翻译官”,负责Domain对象与技术组件(JDBC/Redis/HTTP Client)之间的双向转换。
ClothingEntity.fromDomain()是静态工厂方法,封装了ID生成策略、审计字段填充等细节。
参数说明:SpringDataClothingJpaRepository是Spring Data JPA自动生成的接口,仅在此层使用,Domain层完全不知晓JPA存在。
3. 在关键节点植入设计模式:不是炫技,是解决真实耦合点
学生常问:“老师说要用设计模式,但我没看到哪里‘必须’用?”——其实模式不是装饰,是当某个痛点反复出现时的标准化止痛药。在服装管理系统中,有三个高频耦合场景,对应三种模式的刚性需求。
3.1 创建不同服装子类:抽象工厂模式隔离变化
需求迭代后要支持“儿童服装”和“运动服装”,二者有额外属性(儿童服需年龄范围,运动服需透气指数)。若用if-else创建对象:
// ❌ 反模式:每次加新类型都要改这里 if ("children".equals(type)) { return new ChildrenClothing(...); } else if ("sport".equals(type)) { return new SportClothing(...); }正确解法:抽象工厂 + 工厂注册表
// src/main/java/domain/factory/ClothingFactory.java public interface ClothingFactory { Clothing create(String name, String brand, Size size, Season season); } // src/main/java/infrastructure/factory/ChildrenClothingFactory.java @Component public class ChildrenClothingFactory implements ClothingFactory { @Override public Clothing create(String name, String brand, Size size, Season season) { return new ChildrenClothing(name, brand, size, season, AgeRange.KIDS); } } // Application层使用(通过Spring自动注入所有实现) @Service public class ClothingApplicationService { private final Map<String, ClothingFactory> factoryMap; public ClothingApplicationService(List<ClothingFactory> factories) { this.factoryMap = factories.stream() .collect(Collectors.toMap( f -> f.getClass().getSimpleName().replace("Factory", "").toLowerCase(), Function.identity() )); } public Clothing createByType(String type, String name, String brand, Size size, Season season) { ClothingFactory factory = factoryMap.get(type); if (factory == null) { throw new IllegalArgumentException("不支持的服装类型: " + type); } return factory.create(name, brand, size, season); } }为什么必须用:新增
SportClothingFactory只需写新类+加@Component,无需修改任何已有代码,完美符合开闭原则。工厂注册表用List<ClothingFactory>注入,由Spring自动收集所有实现,避免硬编码if-else。
3.2 库存变动触发多通知:观察者模式解耦事件链
当库存减少时,需同时:更新缓存、发送邮件通知采购员、记录审计日志。若在InventoryService.decrease()里直接调用三个方法:
// ❌ 硬编码耦合:每加一个通知方都要改decrease方法 public void decrease(String id, int quantity) { // ...业务逻辑 cacheService.evict(id); // 通知1 emailService.sendAlert(id); // 通知2 auditLogService.record(id); // 通知3 }正确解法:发布-订阅事件总线
// src/main/java/domain/event/InventoryChangedEvent.java public record InventoryChangedEvent(String clothingId, int delta, LocalDateTime timestamp) {} // src/main/java/application/InventoryService.java @Service public class InventoryService { private final ApplicationEventPublisher eventPublisher; // Spring事件总线 public void decrease(String id, int quantity) { // ...库存扣减逻辑 eventPublisher.publishEvent(new InventoryChangedEvent(id, -quantity, LocalDateTime.now())); } } // src/main/java/infrastructure/notification/CacheEvictionListener.java @Component public class CacheEvictionListener { @EventListener public void handleInventoryChange(InventoryChangedEvent event) { cacheService.evict(event.clothingId()); } } // src/main/java/infrastructure/notification/EmailAlertListener.java @Component public class EmailAlertListener { @EventListener public void handleInventoryChange(InventoryChangedEvent event) { if (event.delta() < -10) { // 仅当缺货严重时发邮件 emailService.sendAlert(event.clothingId()); } } }为什么必须用:新增通知方(如“短信告警”)只需写一个
@EventListener方法,完全不侵入InventoryService。事件携带完整上下文(delta值),监听器可自行决定是否响应,实现松耦合。
3.3 多种促销策略共存:策略模式让算法自由切换
系统需支持三种折扣:普通会员95折、VIP会员88折、限时秒杀5折。若用if-else:
// ❌ 算法散落:折扣逻辑分散在多个Service中 if (user.isVip()) { price = price.multiply(new BigDecimal("0.88")); } else if (isFlashSale()) { price = price.multiply(new BigDecimal("0.5")); }正确解法:策略接口 + Spring Bean注册
// src/main/java/domain/strategy/DiscountStrategy.java public interface DiscountStrategy { BigDecimal calculateDiscountedPrice(BigDecimal originalPrice, User user, Clothing clothing); String getStrategyCode(); // 用于路由 } // src/main/java/infrastructure/strategy/VipDiscountStrategy.java @Component public class VipDiscountStrategy implements DiscountStrategy { @Override public BigDecimal calculateDiscountedPrice(BigDecimal originalPrice, User user, Clothing clothing) { return originalPrice.multiply(new BigDecimal("0.88")); } @Override public String getStrategyCode() { return "VIP"; } } // Application层动态选择策略 @Service public class PricingService { private final Map<String, DiscountStrategy> strategyMap; public PricingService(List<DiscountStrategy> strategies) { this.strategyMap = strategies.stream() .collect(Collectors.toMap(DiscountStrategy::getStrategyCode, Function.identity())); } public BigDecimal getPriceWithDiscount(String strategyCode, BigDecimal originalPrice, User user, Clothing clothing) { DiscountStrategy strategy = strategyMap.get(strategyCode); if (strategy == null) { return originalPrice; // 无策略则不打折 } return strategy.calculateDiscountedPrice(originalPrice, user, clothing); } }为什么必须用:促销规则是高频变更点。新增“学生认证折扣”只需实现
DiscountStrategy接口并加@Component,无需改PricingService。策略码(VIP/FLASH)可来自配置中心或数据库,实现运行时动态切换。
4. 避坑指南:那些让答辩现场沉默的5个致命错误
学生在实现服装管理系统时,常因几个看似微小的决策导致架构崩塌。以下是我在三次课程助教中记录的真实翻车现场,按“现象→原因→解决”整理,每一条都对应一个重构噩梦。
4.1 现象:添加“服装图片上传”功能后,整个系统编译失败
原因:在Domain层的Clothing类中直接引入org.springframework.web.multipart.MultipartFile,导致Domain层依赖Spring Web框架。当其他模块(如纯命令行工具)想复用Clothing类时,因缺少Spring Web jar包而编译报错。
解决:图片上传是Infrastructure层职责。Domain层只定义clothing.setImageUrl(String url),上传逻辑在Controller中完成,再将URL传入Application Service。Domain绝不持有任何框架特定类型。
4.2 现象:单元测试覆盖率高达95%,但一改数据库字段就大面积报错
原因:在Presentation层(Controller)中直接返回JPA Entity对象(如ClothingEntity),导致DTO与Entity混用。当数据库表加了created_at字段,Entity增加createdAt属性,所有Controller返回值、前端JS代码、Swagger文档全部失效。
解决:强制规定Presentation层只能返回DTO(如ClothingResponse),且DTO字段名与前端约定一致(如createdAt→created_at)。Entity与DTO转换用MapStruct或手动构造,禁止return entity;。
4.3 现象:用策略模式实现折扣,但“VIP折扣”和“秒杀折扣”计算结果一样
原因:所有DiscountStrategy实现类被Spring单例管理,且在calculateDiscountedPrice()中修改了共享状态(如静态变量lastAppliedStrategy)。多线程并发调用时,A用户VIP折扣被B用户的秒杀折扣覆盖。
解决:策略类必须是无状态的(Stateless)。所有计算基于入参,禁用静态变量、成员变量。若需缓存(如折扣率配置),用@Cacheable注解或注入ConfigService,而非在策略类内部维护。
4.4 现象:抽象工厂创建儿童服装时,抛出NullPointerException,但堆栈指向ClothingFactory接口
原因:ChildrenClothingFactory类上漏写了@Component,导致Spring未将其注入factoryMap。factoryMap.get("children")返回null,后续调用create()时NPE。因接口无实现,IDE无法在编译期提示。
解决:在工厂注册逻辑中增加启动时校验:
@PostConstruct public void validateFactories() { if (!factoryMap.containsKey("children")) { throw new IllegalStateException("Missing ChildrenClothingFactory bean"); } }4.5 现象:观察者模式下,邮件通知发了3次,缓存却没刷新
原因:@EventListener方法抛出未捕获异常(如邮件服务器超时),Spring事件总线默认停止后续监听器执行。CacheEvictionListener因EmailAlertListener异常而跳过。
解决:为每个监听器添加异常兜底:
@EventListener public void handleInventoryChange(InventoryChangedEvent event) { try { cacheService.evict(event.clothingId()); } catch (Exception e) { log.error("缓存清除失败,不影响主流程", e); } }或使用@Async让监听器异步执行,互不阻塞。
5. 验证架构健康度:用3个命令揪出隐藏的架构腐化
写完代码不等于架构合格。很多学生以为分了包、用了模式就万事大吉,直到答辩时被问“如果现在要把MySQL换成MongoDB,要改多少文件?”才意识到耦合已深入骨髓。以下是我验证服装管理系统架构质量的三个实操命令,每条都能暴露真实问题。
5.1 查看Domain层的“纯洁度”:它是否偷偷依赖了技术框架?
# 在项目根目录执行(Linux/macOS) find src/main/java/domain -name "*.java" | xargs grep -l "spring\|jpa\|http\|javax.servlet"预期结果:应返回空。若输出src/main/java/domain/Clothing.java,说明Domain层违规引入了@Entity或HttpServletResponse。
原理:Domain层是业务核心,必须100%技术无关。任何框架类名(@RestController,JdbcTemplate,ResponseEntity)的出现,都是架构污染的铁证。
补救:立即删除违规import,将技术逻辑下沉到Infrastructure层。例如,Clothing类中的@Id注解必须移除,ID生成逻辑由Infrastructure层的ClothingEntity承担。
5.2 统计Application层的“胶水厚度”:它是否过度编排?
# 统计ClothingApplicationService.java中调用的外部类数量 grep -oE '([A-Z][a-zA-Z0-9]*)\.' src/main/java/application/ClothingApplicationService.java | sort | uniq -c | sort -nr | head -10预期结果:前3名应是clothingRepository.、inventoryService.、pricingService.等明确的领域服务。若出现userRepository.、notificationClient.、redisTemplate.等非领域服务,说明Application层在干Infrastructure的活。
原理:Application层是“用例编排者”,只应协调Domain服务。调用Redis、HTTP Client等属于Infrastructure层职责,强行上提会导致Application层膨胀、难以测试。
补救:将redisTemplate.opsForValue().set(...)封装进InventoryService的某个方法(如updateStockCache()),Application层只调用inventoryService.updateStockCache()。
5.3 检测包依赖的“流向正确性”:是否存在反向依赖?
# 使用jdeps分析模块依赖(需JDK9+) jdeps --multi-release 11 --class-path target/classes \ --recursive --require java.base \ src/main/java/presentation/预期结果:输出中presentation包应只依赖application,不直接依赖domain或infrastructure。若出现-> domain.Clothing,说明Controller直接new了Domain对象。
原理:依赖必须单向流动:Presentation → Application → Domain ← Infrastructure。反向依赖(如Domain import Infrastructure)会锁死技术选型,让替换数据库变成不可能任务。
补救:检查所有new Clothing(...)调用点。Presentation层必须通过Application Service获取Domain对象,禁止new。
这三个命令不是玄学,是我在某实验室带模拟项目X时沉淀的“架构CT扫描仪”。它们不关心你用了多少设计模式,只冷酷地指出:你的代码是否真的在践行分层与解耦。有次一个学生跑完第一条命令发现Domain层有5个文件import了
org.springframework.data.jpa.repository,当场重写了三天——但答辩时他成了唯一能清晰回答“如果客户要求接入微信小程序,哪些层要动、哪些不用动”的人。希望帮到你。
本文还有配套的精品资源,点击获取