User类应封装校验(isValidEmail、isAdult)、展示逻辑(getDisplayName)及构造时强校验;UserRepository抽象数据操作,内存实现便于测试,接口统一便于切换;UserService专注跨库协调与事务操作;严格区分实体与DTO,禁止敏感字段暴露。

用 User 类封装核心字段和行为,别只写 getter/setter
一个空有 name、email、age 字段的 User 类,不算真正面向对象。关键在于把「用户该做什么」收进类里:isValidEmail() 校验邮箱格式、isAdult() 判断是否成年、getDisplayName() 统一返回带昵称或姓名的展示名。这些方法让业务逻辑不散落在各处,也方便后续加日志、监控或替换实现。
常见错误是把校验塞进 controller 或 service 层——结果改个邮箱规则要翻三四个文件。把校验逻辑放进 User 本身,构造时就抛 IllegalArgumentException,强制数据从源头干净:
public User(String email, int age) {
if (!isValidEmail(email)) throw new IllegalArgumentException("Invalid email");
if (age < 0 || age > 150) throw new IllegalArgumentException("Invalid age");
this.email = email.toLowerCase().trim();
this.age = age;
}
用 UserRepository 抽象数据操作,先内存实现再换数据库
别一上来就写 JDBC 或 MyBatis。先定义接口 UserRepository,只暴露 save()、findById()、findByEmail()、findAll() 这几个方法。然后写个 InMemoryUserRepository 用 ConcurrentHashMap 存,开发调试飞快,单元测试也不依赖外部环境。
这样设计的好处是:service 层完全不关心数据在哪,只认接口;等要上 MySQL,你只需新增 JdbcUserRepository 实现同一接口,改一行 Spring 配置就能切换,不用动业务代码。
立即学习“Java免费学习笔记(深入)”;
-
findByEmail()必须返回Optional,避免 null 判空污染调用方 - 内存实现里用
AtomicLong做自增 id,比手动 ++ 更安全 - 所有方法加
throws UserNotFoundException(自定义异常),比泛型RuntimeException更易定位问题
用 UserService 聚合跨实体逻辑,别让它变成“万能工具箱”
UserService 不是放所有用户相关方法的大筐。它只做两件事:协调多个 repository(比如注册时同时写 User 和 UserProfile),以及封装需要事务/缓存/幂等性控制的操作(如 changeEmail(String oldEmail, String newEmail))。
容易踩的坑:
- 把密码加密逻辑写在 service 里——应该抽成
PasswordEncoder接口,让User构造时调用 - 在 service 里拼 SQL 或手写 JSON 返回——交给 controller 层做序列化
- 给每个字段都加 update 方法(
updateName()、updatePhone())——统一用update(UserUpdateRequest),避免状态不一致
区分 User 实体类和 DTO,别用 new User(...) 穿透到前端
直接把 User 对象扔给 REST 接口,等于把数据库字段、内部 ID、密码哈希值全暴露出去。必须拆开:User 是领域实体,只用于业务逻辑;UserResponse 是只读 DTO,字段精简(去掉 passwordHash、createdAt 等敏感/冗余字段),且用 @JsonIgnore 或 MapStruct 显式转换。
更隐蔽的问题是:DTO 里加了 getAgeInDays() 这种计算属性,结果被 Jackson 序列化成字段。解决办法只有两个:@JsonIgnore 标记,或者确保 DTO 只有 final 字段 + 构造器初始化,不提供 getter(除非真需要)。
复杂点往往不在结构,而在边界——比如邮箱修改后要不要发验证邮件?这个动作该由谁触发、失败如何回滚、重试几次?这些不是类设计能覆盖的,得靠事件机制或明确的责任划分。










