我是如何将同事的代码改成DDD风格的
创始人
2024-05-06 22:26:36

eeccba9d660fda83b1c976c7363059e3.jpeg

DDD是领域驱动设计的简写。前段时间听群友说行业里少有DDD的代码案例,进而对DDD没有一个感性的认识。我想这是行业里普遍存在的现象吧。所以,就有了写此文的想法。

文章标题说的是“同事的代码”,其实只是为了让此文更具传播,没别的意思。

本文开篇介绍了行业里比较普遍的代码风格,接着,我采用DDD风格对其进行修改。

我无意说服读者要按照我认为的DDD的风格来写代码,只是想告诉大家,这个世界上,还存在另一种代码风格。

本文虽是以Java语言为案例演示,也希望对其它语言的读者朋友有帮助。

如果各位觉得这样的风格好,可以尝试一下。非常欢迎大家反馈,平时太少人和我交流这些了。

如果你觉得此文对你有帮助,麻烦转发。干货好文不易。谢谢。

行业里普遍的代码风格,简称A风格

代码结构如下:

├── domain domain模块被同事认为是用于存放专门和DB打交道的类的地方- 包路径太长省略/account/repository/AbcLoginInfoRepository.java- 包路径太长省略/domain/account/AbcLoginInfo.java
├── repository-impl-  包路径太长省略/AbcLoginInfoRepositoryImlp.java
├── server- 包路径太长省略/server/login/LoginService.java- 包路径太长省略/server/login/LoginController.java- 包路径太长省略/server/login/AuthCodeVo.java- 包路径太长省略/server/login/UserInfoVo.java- 包路径太长省略/config/AbcWebMvcConfigurer.java

Server模块

A风格下,整个业务系统的业务逻辑都在此模块中。

LoginController.java 实现http服务:

@Controller  
@RequestMapping  
public class LoginController {  
@Autowired  
LoginService loginService;  
// 省略一些不重要的代码@GetMapping(value = "/login")  
@ResponseBody  
public UserInfoVo login(String code) throws IOException {  UserInfoVo userInfoVo = loginService.login(code, httpServletResponse);  httpServletResponse.sendRedirect("/");  return userInfoVo;  
}  @GetMapping(value = "/logout")  
@ResponseBody  
public boolean logout() {  return loginService.logout(httpServletRequest,httpServletResponse);  
}  
}

UserInfoVo.java是返回给前端的用户信息的结构体:

public class UserInfoVo {  private String id;  private String userType;  // 省略一些其它字段// 省略一些getter setter方法
}

AuthCodeVo.java是用于存储一些认证过程中的数据的结构体

public class AuthCodeVo {  private String token;  private Integer expiresIn;  // 省略一些getter setter方法
}

A风格的特点是:除了VO,行业里,还有各种O,如PO、DTO、DO。

刚入行的小伙伴很难分清各种O,所以,只有跟着前辈的老代码依葫芦画瓢。进而导致大家对于Java代码的印象:不就是各种O之间的转换嘛。

这里并不是说DDD风格下的代码没有O。在DDD风格下,O本身是有业务逻辑方法的,并不只是一堆字段、getter和setter方法。

AbcWebMvcConfigurer.java这个类用于实现对所有的请求的拦截,以实现统一认证:

@Configuration  
public class AbcWebMvcConfigurer implements WebMvcConfigurer {  
@Autowired  
LoginService loginService;  @Override  
public void addInterceptors(InterceptorRegistry registry) {  registry.addInterceptor(new UserAuthInterceptorRegistry())  // 省略代码
}  class UserAuthInterceptorRegistry implements HandlerInterceptor {  @Override  public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)  throws Exception {  if (loginService.isLoginSuccess(request)) {  return true;  }  response.sendRedirect("登录页面的url");  return false;  }  
}  
}

AbcLoginInfo.java,AbcLoginInfoRepository.java,AbcLoginInfoRepositoryImlp.java 三个文件实现了登录信息的存储。

其中AbcLoginInfo.java只是用户的信息及getter和setter方法,典型的贫血型模型。

AbcLoginInfoRepository是AbcLoginInfo对象的持久化接口,而AbcLoginInfoRepositoryImlp是该接口的实现。

登录逻辑LoginService

这个就是登录服务直接实现逻辑所在。源代码将近200行代码

@Service  
public class LoginService {  
// 省略一些不重要的代码
public UserInfoVo login(String code, HttpServletResponse response) {  AuthCodeVo authCodeVo = authCode(code);  // 省略部分代码UserInfoVo userInfoVo = getUserInfo(authCodeVo.getAccessToken(), authCodeVo.getExpiresIn());  // 省略部分代码LoginInfo loginInfo =loginInfoRepository.findByUid(userInfoVo.getUid());  if (loginInfo == null) {  loginInfo = new LoginInfo();  }  setLoginInfo(loginInfo, authCodeVo, userInfoVo);  loginInfoRepository.save(loginInfo);  addLoginCookie(loginInfo, response);  return userInfoVo;  
}  private void addLoginCookie(LoginInfo loginInfo, HttpServletResponse response) {  Cookie tokenCookie = new Cookie(TOKEN_COOKIE, loginInfo.getAccessToken());  response.addCookie(tokenCookie);  
}  public boolean isLoginSuccess(HttpServletRequest request) {  Cookie[] cookies = request.getCookies();  if (cookies == null) {  return false;  }  String token = null;  String uid = null;  // 此处省略代码,即从cookies中取出token和uid将设置到变量中。  LoginInfo loginInfo = loginInfoRepository.findByUid(uid);  // 此处对acessToken和过期时间进行校验if (token.equals(loginInfo.getAccessToken())  && new Date().compareTo(loginInfo.getExpiresDate()) < 0) {  return true;  }  return false;  
}  public boolean logout(HttpServletRequest request, HttpServletResponse response) {  Cookie[] cookies = request.getCookies();  // 对cookie进行过期处理return true;  
}  private LoginInfo setLoginInfo(LoginInfo loginInfo,  AuthCodeVo authCodeVo,  UserInfoVo userInfoVo) {  long nowTime = System.currentTimeMillis();  // 根据过期时长计算过期时间,并设置到LoginInfo中Date expiresDate = new Date(nowTime + authCodeVo.getExpiresIn() * 1000);  // 此处省略一些拿authCodeVo和userInfoVo中的信息set到loginInfo的代码return loginInfo;  
}  public AuthCodeVo authCode(String code) {  Map params = new HashMap<>();  // 省略params参数的组装的代码// 请求access token的地址,并拿到AuthCodeVo结构体的内容Map resultMap = restTemplate.postForObject(ACCESS_TOKEN_URL, null, Map.class, params);  AuthCodeVo authCodeVo = new AuthCodeVo();  // 将resultMap中的值set到authCodeVo中return authCodeVo;  
}  public UserInfoVo getUserInfo(String accessToken, Integer expiresIn) {  Map params = new HashMap<>();  // 省略params参数的组装的代码// 请求用户的信息的地址,并拿到用户信息。注意这里直接使用restTemplate这个技术实现。Map resultMap = restTemplate.getForObject(PROFILE_URL, Map.class, params);  UserInfoVo userInfoVo = new UserInfoVo();  // 将resultMap中的值set到userInfoVo中return userInfoVo;  
}  }

A风格小结

  1. 1. 登录的主逻辑放在LoginService中;

  2. 2. LoginService即处理http请求技术逻辑(cookie的操作),也处理业务逻辑(登录信息的持久化、登录判断、token过期时间设置);

  3. 3. LoginService存放在Server模块;

  4. 4. 所有的实体、各种O中,只有字段,getter和setter方法。这导致lombok这样的代码生成库大量被使用,因为A风格觉得为每个字段写getter和setter方法是必须,但是又是浪费时间的事情。

我们暂不讨论A风格的问题,接着看DDD风格的代码。

DDD风格的代码,简称D风格

代码仓库结构:

├── domain- domain是用于存放整个业务系统的核心逻辑
├── abc-o2-auth- 存放所有abc-o2的相关逻辑,下文详细介绍
├── server- 包路径太长省略/server/login/LoginController.java- 包路径太长省略/config/AbcWebMvcConfigurer.java
├── repository-impl-  包路径太长省略/AccountRepositoryImpl.java
├── spring-ioc-impl- spring的IoC的实现,D风格下,IoC的实现也应该是可以被低成本地替换的
├── tech-lib- 公共技术逻辑的接口
├── tech-lib-impl- 公共技术逻辑的接口的实现

abc-o2-auth模块

我们把o2-auth的所有的逻辑放在这个新的模块中。考虑到将来可能需要实现新的认证逻辑。以下是该模块的Java代码的结构:

./abc-o2-auth/src/main/java/com/xxx/domain/o2
├── AccessToken.java
├── AccessTokenFetcher.java
├── AccessTokenFetcherImpl.java
├── Account.java
├── AccountProfile.java
├── AccountProfileFetcher.java
├── AccountProfileFetcherImpl.java
├── AuthConfig.java
└── repository└── AccountRepository.java

登录逻辑Account

Account.java是整个o2-auth的认证方式的核心逻辑。源代码将近330行。

虽然它是一个实体,但它不是整个业务系统的核心逻辑,所以,没有被放到整个业务系统的domain模块中。

Account类中所有的技术逻辑都被抽象成接口。

公共的技术接口,比如Json数据的操作接口json-util,我们统一放在tech-lib模块中。公共的技术接口的实现,目前统一放在tech-lib-impl中。

当然,也可以以更小粒度的模块来实现解耦,比如创建一个json-util-jackson-impl模块来实现json-util接口。

P.S. 为什么不直接使用Jackson呢?你想想,当发生像Fastjson那样的安全事故时,你该如何快速的更换json的实现?如果按照D风格,是不是就很容易更换了。

当Account中的逻辑被真正运行时,需要用到这些技术接口的具体实现时,就从InstanceFactory实例工厂类的getInstance静态方法获取。InstanceFactory是什么,我们下面再说。

当前,你只需要知道,通过InstanceFactory的getInstance静态方法可以拿到接口的实现实例就可以了。

采用D风格,在写业务逻辑时,就不需要关心技术逻辑的实现了。这样就能很好的解决“无法写单元测试”的问题。

@Entity  
@Table(name = "abc_o2_accounts")
public class Account {
// 省略所有的字段,getter和setter代码public static Optional login(String code) {  // AccessTokenFetcher是accessToken的拉取接口// 因为accessToken需要请求第三方系统AccessTokenFetcher accessTokenFetcher = InstanceFactory.getInstance(AccessTokenFetcher.class);  Optional accessTokenOptional = accessTokenFetcher.auth(code);  if (accessTokenOptional.isEmpty()) {  throw new LoginBizException("401");  }  // AccountProfileFetcher是accountProfile的拉取接口AccountProfileFetcher accountProfileFetcher = InstanceFactory.getInstance(AccountProfileFetcher.class);  Optional accountProfileOptional = accountProfileFetcher.fetch(accessTokenOptional.get().getAccessToken(), accessTokenOptional.get().getExpiresIn());  if (accountProfileOptional.isEmpty()) {  throw new LoginBizException("401");  }  // 登录成功后,将登录信息持久化AccountRepository accountRepository = InstanceFactory.getInstance(AccountRepository.class);  Optional accountOptional = accountRepository.findByUid(accountProfileOptional.get().getUid());  if (accountOptional.isEmpty()) {  Account account = buildBy(accessTokenOptional.get(),accountProfileOptional.get());  account.save();  return Optional.of(account);  } else {  Account account = accountOptional.get();  account.update(accessTokenOptional.get(), accountProfileOptional.get());  return Optional.of(account);  }  
}  
// 登录的url是从配置中获取的。至于是从数据库,还是Etcd配置中心获取,登录核心逻辑并不关心,
// 而由AuthConfig的实现决定。这样,将来我们想换配置中心,成本就很低了。
public static String loginUrl(){  AuthConfig authConfig = InstanceFactory.getInstance(AuthConfig.class);  return authConfig.getLoginUrlWithRedirect();  
}  
// 再次review此代码时,发现这个方法叫isLoggedIn更能体现方法内的逻辑。
public static boolean isLoginSuccess(String token, String uid) {  AccountRepository accountRepository = InstanceFactory.getInstance(AccountRepository.class);  Optional accountOptional = accountRepository.findByUid(uid);  if (accountOptional.isEmpty()) {  return false;  }  return StringUtils.equals(token, accountOptional.get().getAccessToken()) && new Date().before(accountOptional.get().getExpiresDate());  
}  public AccountProfile getProfile() {  AccountProfile result = new AccountProfile();  // 将Account中的信息设置到AccountProfile中,因为前端只需要Account中的部分信息return result;  
}  private void update(AccessToken accessToken, AccountProfile accountProfile) {  Date expiresDate = calExpiredDate(accessToken.getExpiresIn());  // 省略部分更新Account对象的代码。// save方法即保存此对象save();  
}  // 计算出最新的过期时间
private static Date calExpiredDate(int expiresIn) {  long nowTime = System.currentTimeMillis();  return new Date(nowTime + expiresIn * 1000L);  
}  
// D风格的代码的一大特点:行为跟着数据走。
// 因为数据结构在Account类中,所以数据的持久化方法save也应该放在Account类中。
// 虽然底层实现都是accountRepository.save(xxx)
// A风格下,持久化方法放在LoginService中,而数据结构放在另一个类中。
private void save() {  AccountRepository accountRepository = InstanceFactory.getInstance(AccountRepository.class);  accountRepository.save(this);  
}  private static Account buildBy(AccessToken accessToken, AccountProfile accountProfile) {  Account result = new Account();  // calExpiredDate方法的实现放在AccessToken类中更合理,我们只需要调用accessToken。getExpiresDate()。// 因为根据过期时长计算过期日期的逻辑应该属于AccessToken,而不属于AccountDate expiresDate = calExpiredDate(accessToken.getExpiresIn());  // 省略根据accessToken和accountProfile构建一个Account实例return result;  
}
}

Server模块

LoginController.java 只负责调用Account实体的的login方法和操作Cookie这类、HTTP服务相关的技术逻辑。

@Controller  
@RequestMapping  
public class LoginController {  @GetMapping(value = "/login")  
@ResponseBody  
public AccountProfile login(String code, HttpServletResponse response) throws IOException {  Optional accountOptional = Account.login(code);  if (accountOptional.isPresent()) {responseLoginCookie(accountOptional.get().getAccessToken(), accountOptional.get().getUid(), response);  response.sendRedirect("/");  return accountOptional.get().getProfile();  } // 此处省略部分代码
}  @GetMapping(value = "/logout")  
@ResponseBody  
public boolean logout(HttpServletRequest request, HttpServletResponse response) {  Cookie[] cookies = request.getCookies();  // 遍历Cookie,并设置cookie过期return true;  
}  
private void responseLoginCookie(String accessToken, String uid, HttpServletResponse response) {  // 登录成功,设置cookie
}
}

以下是D风格AbcWebMvcConfigurer的代码:

@Configuration  
public class AbcWebMvcConfigurer implements WebMvcConfigurer {  @Override  
public void addInterceptors(InterceptorRegistry registry) {  registry.addInterceptor(new UserAuthInterceptorRegistry())  // 部分代码省略
}  class UserAuthInterceptorRegistry implements HandlerInterceptor {  @Override  public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)  throws Exception {  Cookie[] cookies = request.getCookies();  //省略代码String token = null;  String uid = null;  // 从Cookie中取值,并设置到token和uid变量中 if (Account.isLoginSuccess(token, uid)) {  return true;  }  response.sendRedirect(Account.loginUrl());  return false;  }  
}  
}

D风格的AbcWebMvcConfigurer.java 与A风格的区别是:

  1. 1. 关于Cookie的操作,A风格放在LoginService类中,而D风格放在AbcWebMvcConfigurer。因为D风格认为Cookie的操作属于HTTP服务行为,不属于核心业务。另,UserAuthInterceptorRegistry,可以考虑移到abc-o2-auth模块中;

  2. 2. 关于是否已经登录的判断逻辑,A风格放在LoginService类中,而D风格放在Account类中。因为是否已经登录的判断逻辑,D风格认为属于abc-o2-auth模块的核心逻辑,而不属于server模块。

  3. 3.

    InstanceFactory实例工厂的魔法

    在D风格中会大量使用InstanceFactory静态类,它使我们能做到与IoC的实现的解耦。

InstanceFactory代码来自https://github.com/dayatang/dddlib 。

DDDLib是我的恩师所创建。我在十年前跟他学习到的DDD。大家可以start并从该仓库学习到DDD的一些代码样例。

D风格小结

  1. 1. 登录的主逻辑放在abc-o2-auth中模块的Account实体中。D风格中的实体类包含各种业务方法,是充血型模型。每一类的设计都有业务含义的,不仅仅只是一个数据结构;

  2. 2. 在写代码时,时刻在思考:这是技术逻辑,还是业务逻辑?这是核心业务逻辑,还是非核心逻辑。

篇外话

5年前,我还是一名Java程序员的时候,我一直按照DDD风格要求自己。

但是,软件行业的绝大数公司才不管你写的代码好,还是坏。更不会管这代码在几年后还能否被维护,维护成本是多少。

这是DDD风格不流行的原因之一。另一个原因就是:根本没有几个人知道这样写代码。所以,本文算是一篇科普文。

(全文完)

如果对你有帮助,那么请你也帮助我转发,这是对我的支持。谢谢。

往期好文推荐:

  • 这十年,我所经历的领域驱动设计(DDD)

  • 耦合的本质

  • 你们的 save 方法是写在实体上,还是写 DAO 上?

相关内容

热门资讯

应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...
脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...
世界上最漂亮的人 世界上最漂亮... 此前在某网上,选出了全球265万颜值姣好的女性。从这些数量庞大的女性群体中,人们投票选出了心目中最美...
demo什么意思 demo版本... 618快到了,各位的小金库大概也在准备开闸放水了吧。没有小金库的,也该向老婆撒娇卖萌服个软了,一切只...