3. 项目目录结构与模块划分:分层架构与模块化设计
聊到项目结构,我见过太多新手一上来就把所有代码塞进一个包。刚开始跑得挺欢,等业务逻辑一复杂,那场面……嗯,惨不忍睹。今天咱们就聊聊怎么把架子搭稳。
3.1 分层架构:Controller / Service / DAO
分层架构说白了,就是各司其职。每一层只关心自己的事,不越界。我习惯把这三层比作餐厅:
- Controller —— 服务员,负责接单、传菜,不负责做菜。
- Service —— 厨师,负责业务逻辑,怎么搭配食材、火候多大。
- DAO —— 采购员,负责去仓库拿原材料,不问菜怎么做。
来看一个典型的包结构:
com.yourapp
├── controller
│ └── UserController.java
├── service
│ ├── UserService.java
│ └── impl
│ └── UserServiceImpl.java
├── dao
│ ├── UserDao.java
│ └── impl
│ └── UserDaoImpl.java
├── entity
│ └── User.java
└── config
└── AppConfig.java
你想想看,如果哪天数据库从 MySQL 换成 PostgreSQL,我只需要改 DAO 层。Controller 和 Service 完全不用动。这就是分层的好处。
核心原则:上层依赖下层,下层不依赖上层。Controller 调用 Service,Service 调用 DAO。反过来不行。
3.2 模块化设计原则
模块化不是把代码拆得越碎越好。我踩过这个坑——曾经把一个工具类拆成十几个小模块,结果找文件比写代码还累。
模块划分有几个硬指标:
| 原则 | 说明 | 反例 |
|---|---|---|
| 高内聚 | 模块内部功能紧密相关 | 一个模块既管订单又管物流 |
| 低耦合 | 模块间依赖尽量少 | 订单模块直接调用支付模块的内部类 |
| 单一职责 | 一个模块只做一件事 | 用户模块里混入短信发送逻辑 |
| 接口隔离 | 暴露最小接口给外部 | 一个接口塞了20个方法,调用方只用3个 |
举个例子。假设我们做一个电商系统,模块可以这样分:
com.yourapp
├── module-order # 订单模块
│ ├── controller
│ ├── service
│ └── dao
├── module-payment # 支付模块
│ ├── controller
│ ├── service
│ └── dao
├── module-user # 用户模块
│ ├── controller
│ ├── service
│ └── dao
└── module-common # 公共模块(工具类、常量等)
└── util
我的习惯:每个模块内部也保持三层结构。模块之间通过接口通信,不要直接 new 对方的实现类。这样哪天要拆成微服务,改改配置就行。
3.3 包命名规范与职责划分
包命名这事,看着小,但影响很大。我接手过一个项目,包名全是拼音缩写,光猜意思就花了两天。
业界通用的规范是这样的:
- 域名倒置:com.公司名.项目名.模块名
- 全小写:不要驼峰,不要下划线
- 按功能分层:controller、service、dao、entity、config
- 按模块分包:module-order、module-payment
来看一个完整的例子:
com.yourapp
├── controller
│ ├── OrderController.java
│ └── PaymentController.java
├── service
│ ├── OrderService.java
│ └── impl
│ └── OrderServiceImpl.java
├── dao
│ ├── OrderDao.java
│ └── impl
│ └── OrderDaoImpl.java
├── entity
│ ├── Order.java
│ └── OrderItem.java
├── dto
│ ├── OrderCreateRequest.java
│ └── OrderVO.java
├── config
│ ├── DataSourceConfig.java
│ └── MyBatisConfig.java
├── enums
│ └── OrderStatus.java
├── exception
│ └── OrderNotFoundException.java
└── util
└── OrderNumberGenerator.java
每个包的职责很清晰:
| 包名 | 职责 | 典型内容 |
|---|---|---|
| controller | 接收请求、返回响应 | @RestController、参数校验 |
| service | 业务逻辑编排 | 事务控制、规则判断 |
| dao | 数据访问 | SQL、ORM 映射 |
| entity | 数据模型 | 数据库表映射对象 |
| dto | 数据传输对象 | 请求/响应封装 |
| config | 配置类 | 数据源、拦截器、过滤器 |
| enums | 枚举常量 | 状态、类型、错误码 |
| exception | 自定义异常 | 业务异常、系统异常 |
| util | 工具类 | 日期处理、ID生成 |
避坑指南:我曾经把工具类放在 entity 包里,结果 entity 包越来越大,最后连数据库字段和工具方法混在一起。后来重构时花了整整一周。记住:工具类永远放在 util 包,不要往 entity 里塞。
3.4 分层架构的核心逻辑图
下面这张图展示了三层之间的调用关系和数据流向。我建议你把它存下来,写代码前看一眼。
3.5 实际项目中的常见问题
说了这么多理论,聊聊我实际遇到的坑。
问题一:Controller 里写业务逻辑
我见过有人直接在 Controller 里写 SQL 拼接。结果呢?单元测试没法写,代码没法复用,改个逻辑要改三个地方。记住:Controller 只做两件事——接参数、调 Service、返回结果。
问题二:Service 层太胖
Service 层不是垃圾桶。我曾经把一个模块的 Service 写了 2000 行,里面既有订单逻辑又有支付逻辑。后来拆成三个 Service,每个不到 300 行,清爽多了。
问题三:DAO 层返回 Map
有些人图省事,DAO 层直接返回 Map<String, Object>。短期看是快了,长期看就是灾难。调用方不知道 Map 里有什么 key,改个字段名全盘崩。老老实实用实体类或者 DTO。
总结一下:分层架构不是银弹,但它能帮你把代码组织得井井有条。模块化设计也不是越细越好,找到合适的粒度才是关键。包命名规范看着是小事,但团队协作时能省下大量沟通成本。