DDD 领域驱动设计与业务模块化(优化与重构)
本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!
本专栏将通过以下几块内容来搭建一个模块化:可以根据项目的功能需求和体量进行任意模块的组合或扩展的后端服务
项目结构与模块化构建思路
RESTful与API设计&管理
网关路由模块化支持与条件配置
DDD领域驱动设计与业务模块化(概念与理解)
DDD领域驱动设计与业务模块化(落地与实现)
DDD领域驱动设计与业务模块化(薛定谔模型)
DDD领域驱动设计与业务模块化(优化与重构)(本文)
RPC模块化设计与分布式事务
事件模块化设计与生命周期日志
在之前的文章服务端模块化架构设计|项目结构与模块化构建思路中,我们以掘金的部分功能为例,搭建了一个支持模块化的后端服务项目juejin,其中包含三个模块:juejin-user(用户),juejin-pin(沸点),juejin-message(消息)
通过添加启动模块来任意组合和扩展功能模块
示例1:通过启动模块juejin-appliaction-system将juejin-user(用户)和juejin-message(消息)合并成一个服务减少服务器资源的消耗,通过启动模块juejin-appliaction-pin来单独提供juejin-pin(沸点)模块服务以支持大流量功能模块的精准扩容
示例2:通过启动模块juejin-appliaction-single将juejin-user(用户),juejin-message(消息),juejin-pin(沸点)直接打包成一个单体应用来运行,适合项目前期体量较小的情况
PS:示例基于IDEA + Spring Cloud
模块化项目结构.jpg为了能更好的理解本专栏中的模块化,建议读者先阅读服务端模块化架构设计|项目结构与模块化构建思路
前情回顾在上一篇DDD领域驱动设计与业务模块化(薛定谔模型)中,我们单独讲述了薛定谔模型的设计,但是还有很多地方可以优化,所以下面就对于这部分内容来讲述一下思路(没有看过落地与实现和薛定谔模型的话建议先看落地与实现和薛定谔模型哦)
校验器 DomainValidator还记得我们的Builder是怎么校验的么
/**
*沸点
*/
publicclassPinImplimplementsPin{
//省略属性
publicstaticclassBuilder{
//省略属性和属性方法
publicPinImplbuild(){
if(!StringUtils.hasText(id)){
thrownewIllegalArgumentException("Idrequired");
}
if(!StringUtils.hasText(content)){
thrownewIllegalArgumentException("Contentrequired");
}
if(user==null){
thrownewIllegalArgumentException("Userrequired");
}
if(comments==null){
thrownewIllegalArgumentException("Commentsrequired");
}
if(likes==null){
thrownewIllegalArgumentException("Likesrequired");
}
if(createTime==null){
createTime=System.currentTimeMillis();
}
returnnewPinImpl(
id,
content,
club,
user,
comments,
likes,
createTime);
}
}
}
每个领域模型的Builder都需要我们手动校验每一个属性,这样写太麻烦了,平时在写Controller的时候我们可以用@Valid或者@Validated配合一些注解来做参数校验,所以我们也可以用同样的注解来实现这个功能
首先我们定义一个DomainValidator
/**
*领域校验器
*/
publicinterfaceDomainValidator{
/**
*校验
*/
voidvalidate(Objecttarget);
}
然后在Builder中用DomainValidator来校验就行了
/**
*沸点
*/
publicclassPinImplimplementsPin{
//省略属性
publicstaticclassBuilder{
@NotEmpty
protectedString
@NotEmpty
protectedStringcontent;
protectedClubclub;
@NotNull
protectedUseruser;
@NotNull
protectedCommentscomments;
@NotNull
protectedLikeslikes;
@NotNull
protectedLongcreateTime;
/**
*需要传入一个校验器
*/
protectedDomainValidatorvalidator
//省略方法
publicPinImplbuild(){
if(createTime==null){
createTime=System.currentTimeMillis();
}
validator.validate(this);//校验属性
returnnewPinImpl(
id,
content,
club,
user,
comments,
likes,
createTime);
}
}
}
我们在Builder中配置DomainValidator并且在属性上标记注解,然后只需要在build方法中调用validate就可以对属性进行校验了
至于DomainValidator的实现类我们可以根据我们的需求灵活实现,比如想要直接复用Spring的校验逻辑就可以实现一个对应的实现类
/**
*基于Spring的校验器
*/
@AllArgsConstructor
publicclassApplicationDomainValidatorimplementsDomainValidator{
/**
*org.springframework.validation.Validator
*/
privateValidatorvalidator;
@Override
publicvoidvalidate(Objecttarget){
BindingResultbindingResult=createBindingResult(target);
validator.validate(target,bindingResult);
onBindingResult(target,bindingResult);
}
/**
*创建一个绑定结果容器
*/
protectedBindingResultcreateBindingResult(Objecttarget){
returnnewDirectFieldBindingResult(target,target.getClass().getSimpleName());
}
/**
*处理绑定结果
*/
protectedvoidonBindingResult(Objecttarget,BindingResultbindingResult){
if(bindingResult.hasFieldErrors()){
FieldErrorerror=Objects.requireNonNull(bindingResult.getFieldError());
Strings=target.getClass().getName()+"#"+error.getField();
thrownewIllegalArgumentException(s+","+error.getDefaultMessage());
}
}
}
这样就相当于给我们的Builder加上了一个@Valid/@Validated注解来做校验了
上下文 DomainContext在我们之前的薛定谔模型中,需要传入指定的Repository
/**
*薛定谔的圈子模型
*/
@Getter
publicclassSchrodingerClubextendsClubImplimplementsClub{
/**
*圈子存储
*/
protectedClubRepositoryclubRepository;
protectedSchrodingerClub(Stringid,ClubRepositoryclubRepository){
this.id=
this.clubRepository=clubRepository;
}
/**
*获得圈子名称
*/
@Override
publicStringgetName(){
//如果名称为null则先从存储读取
if(this.name==null){
load();
}
returnthis.name;
}
/**
*获得圈子图标
*/
@Override
publicStringgetLogo(){
//如果图标为null则先从存储读取
if(this.logo==null){
load();
}
returnthis.logo;
}
/**
*获得圈子描述
*/
@Override
publicStringgetDescription(){
//如果描述为null则先从存储读取
if(this.description==null){
load();
}
returnthis.description;
}
/**
*根据id加载其他的数据
*/
publicvoidload(){
Clubclub=getClubRepository().get(id);
if(club==null){
thrownewJuejinException("Clubnotfound:"+
}
this.name=club.getName();
this.tag=club.getTag();
this.description=club.getDescription();
}
publicstaticclassBuilder{
protectedString
protectedClubRepositoryclubRepository;
protectedDomainValidatorvalidator;
//省略属性方法
publicSchrodingerClubbuild(){
validator.validate(this);
returnnewSchrodingerClub(id,clubRepository);
}
}
}
我们的SchrodingerClub需要传入ClubRepository
但是这样会有两个问题
当需要再扩展一个PinRepository来获得圈子下的沸点数量的时候,需要之前所有用到的地方都添加代码来多设置一个PinRepository,如果这个模型用的地方很多,那么改起来也会比较麻烦newSchrodingerClub.Builder()
.id(id)
.clubRepository(clubRepository)
.pinRepository(pinRepository)//每个地方都要加
.validator(validator)
.build();
容易出现循环依赖,如SchrodingerClub现在需要PinRepository来获得圈子下的沸点数量,而PinRepository中又需要ClubRepository来生成沸点对应的圈子SchrodingerClub,这样就出现了循环依赖针对上面两个问题,我们可以用ApplicationContext#getBean来解决
定义一个DomainContext作为抽象
/**
*领域上下文
*/
publicinterfaceDomainContext{
/**
*通过类获得实例
*/
TTget(Classtype);
}
然后基于ApplicationContext实现一个ApplicationDomainContext
/**
*基于{@linkApplicationContext}实现领域上下文
*/
@AllArgsConstructor
publicclassApplicationDomainContextimplementsDomainContext{
privateApplicationContextcontext;
@Override
publicTTget(Classtype){
returncontext.getBean(type);
}
}
我们的薛定谔模型就可以改成这样
/**
*薛定谔的圈子模型
*/
@Getter
publicclassSchrodingerClubextendsClubImplimplementsClub{
protectedDomainContextcontext;
protectedSchrodingerClub(Stringid,DomainContextcontext){
this.id=
this.context=context;
}
/**
*获得圈子名称
*/
@Override
publicStringgetName(){
//如果名称为null则先从存储读取
if(this.name==null){
load();
}
returnthis.name;
}
/**
*获得圈子图标
*/
@Override
publicStringgetLogo(){
//如果图标为null则先从存储读取
if(this.logo==null){
load();
}
returnthis.logo;
}
/**
*获得圈子描述
*/
@Override
publicStringgetDescription(){
//如果描述为null则先从存储读取
if(this.description==null){
load();
}
returnthis.description;
}
/**
*根据id加载其他的数据
*/
publicvoidload(){
ClubRepositoryclubRepository=context.get(ClubRepository.class);
Clubclub=clubRepository.get(id);
if(club==null){
thrownewJuejinException("Clubnotfound:"+
}
this.name=club.getName();
this.tag=club.getTag();
this.description=club.getDescription();
}
publicstaticclassBuilder{
protectedString
protectedDomainContextcontext;
protectedDomainValidatorvalidator;
//省略属性方法
publicSchrodingerClubbuild(){
validator.validate(this);
returnnewSchrodingerClub(id,context);
}
}
}
最后我们使用的时候就只要传入id,DomainValidator,DomainContext就行了
newSchrodingerClub.Builder()
.id(id)
.context(context)
.validator(validator)
.build();
就算SchrodingerClub需要100个Repository都不需要添加参数,也不用担心会循环依赖了
薛定谔模型动态代理我们的薛定谔模型需要重写除了getId外的所有方法,如果模型的字段很多,重写方法的时候也就会很费力,所以我就想能不能用动态代理在调用方法之前统一进行查询
改造之后的SchrodingerClub
/**
*薛定谔的圈子模型
*/
@Getter
publicclassSchrodingerClubimplementsInvocationHandler{
/**
*圈子id
*/
protectedString
/**
*圈子懒加载
*/
protectedClubclub;
protectedDomainContextcontext;
protectedSchrodingerClub(Stringid,DomainContextcontext){
this.id=
this.context=context;
}
@Override
publicObjectinvoke(Objectproxy,Methodmethod,Object[]args)throwsThrowable{
//如果是获取id的话直接返回
if("getId".equals(method.getName())){
return
}
returnmethod.invoke(getClub(),args);
}
protectedClubgetClub(){
//如果为null则先查询一次数据
if(this.club==null){
ClubRepositoryclubRepository=context.get(ClubRepository.class);
Clubclub=clubRepository.get(id);
if(club==null){
thrownewJuejinException("Clubnotfound:"+
}
this.club=club;
}
returnthis.club;
}
publicstaticclassBuilder{
@NotNull
protectedString
@NotNull
protectedDomainContextcontext;
protectedDomainValidatorvalidator;
//省略属性方法
publicClubbuild(){
validator.validate(this);
//动态代理Club接口
returnProxy.newProxyInstance(getClass().getClassLoader(),newClass[]{Club.class},newSchrodingerClub(id,context));
}
}
}
这样我们就可以解放双手,不用一个一个重写方法啦
不过每生成一个对象都要动态代理一次的话,性能肯定是没有手动重写方法的方式好,所以我还写了个程序来对比创建对象的时间差距
publicstaticvoidtest(){
intcount=10000000;
Listlist=newArrayList(count*2);
longstart=System.currentTimeMillis();
for(inti=0;icount;i++){
list.add(nativeNew());
}
longtime=System.currentTimeMillis();
System.out.println(time-start);
for(inti=0;icount;i++){
list.add(proxyNew());
}
longend=System.currentTimeMillis();
System.out.println(end-time);
}
publicstaticIAnativeNew(){
returnnew
}
publicstaticIAproxyNew(){
return(IA)Proxy.newProxyInstance(
IA.class.getClassLoader(),
newClass[]{IA.class},
newInvocationHandler(){
@Override
publicObjectinvoke(Objectproxy,Methodmethod,Object[]args)throwsThrowable{
returnnull;
}
}
publicinterfaceIA{
}
publicstaticclassAimplementsIA{
}
记录一下实验数据
次数原生(运行3次平均ms)代理(运行3次平均ms)1w2.3320.6610w8.00190.33100w35.001404.66100w次需要1.4s我感觉其实就算每次生成都用动态代理体感也没差,如果真的很在意性能的话就自己手动重写方法就行了
数据模型缓存我们可以使用一些缓存防止多次查询相同的数据,这样性能上应该也能说得过去
定义缓存接口Cache
/**
*缓存
*/
publicinterfaceCacheT{
/**
*设置缓存
*/
voidset(Stringid,Tcache);
/**
*获得缓存
*/
Tget(Stringid);
/**
*清除缓存
*/
voidremove(Stringid);
}
然后定义缓存提供者CacheProvider和缓存适配器CacheAdapter,我们可以通过缓存提供者去遍历所有的缓存适配器来适配适合的缓存
/**
*缓存提供者
*/
publicinterfaceCacheProvider{
/**
*根据key获得对应的缓存
*/
TCacheget(Objectkey);
}
/**
*缓存适配器
*/
publicinterfaceCacheAdapter{
/**
*是否支持key
*/
booleansupport(Objectkey);
/**
*获得缓存
*/
TCacheadapt(Objectkey);
}
提供一个CacheProvider的默认实现CacheProviderImpl
/**
*缓存提供者通过缓存适配器来返回各种缓存
*/
@Component
@AllArgsConstructor
publicclassCacheProviderImplimplementsCacheProvider{
/**
*所有的缓存适配器
*/
privateListCacheAdaptercacheAdapters;
/**
*按顺序进行匹配
*/
@Override
publicTCacheget(Objectkey){
for(CacheAdapteradapter:cacheAdapters){
if(adapter.support(key)){
returnadapter.adapt(key);
}
}
returnnewNoCache();
}
/**
*当没有匹配到时返回无缓存
*/
staticclassNoCacheTimplementsCacheT{
@Override
publicvoidset(Stringid,Tcache){
}
@Override
publicTget(Stringid){
returnnull;
}
@Override
publicvoidremove(Stringid){
}
}
}
通过缓存适配器CacheAdapter我们可以非常灵活的控制缓存实现
如我们可以定义一个全局的内存缓存支持所有的情况
/**
*内存缓存适配器
*/
@Order
@Component
publicclassInMemoryCacheAdapterimplementsCacheAdapter{
/**
*全部支持
*/
@Override
publicbooleansupport(Objectkey){
returntrue;
}
/**
*返回内存缓存实现
*/
@Override
publicTCacheadapt(Objectkey){
returnnewInMemoryCache();
}
}
现在我们希望把评论缓存到Redis,我们就可以针对CommentRepository实现一个单独的CommentCacheAdapter
/**
*评论缓存适配器
*/
@Order(0)
@Component
publicclassCommentCacheAdapterimplementsCacheAdapter{
@Autowired
privateRedisTemplateString,Stringtemplate;
/**
*支持评论
*/
@Override
publicbooleansupport(Objectkey){
returnkeyinstanceofCommentRepository;
}
/**
*返回Redis缓存实现
*/
@Override
publicTCacheadapt(Objectkey){
returnnewRedisCache(template);
}
}
这样我们就可以通过自定义缓存适配器CacheAdapter来统一指定或是单独指定缓存实现
然后我们改造一下我们的AbstractDomainRepository就可以支持所有的Repository了
/**
*领域存储抽象类
*
*@paramT领域模型
*@paramP数据模型
*/
publicabstractclassAbstractDomainRepositoryTextendsDomainObject,PextendsIdProviderimplementsDomainRepositoryT{
/**
*缓存提供者
*/
@Autowired
protectedCacheProvidercacheProvider;
/**
*缓存
*/
protectedvolatileCachecache;
protectedCachegetCache(){
if(cache==null){
synchronized(this){
if(cache==null){
//自身作为key
cache=cacheProvider.get(this);
}
}
}
returncache;
}
@Override
publicvoidupdate(Tobject){
doUpdate(do2po(object));
//更新的时候清除缓存
getCache().remove(object.getId());
}
protectedabstractvoiddoUpdate(Ppo);
@Override
publicvoidupdate(Collection?extendsTobjects){
doUpdate(objects.stream().map(this::do2po).collect(Collectors.toList()));
//更新的时候清除缓存
objects.stream().map(DomainObject::getId).forEach(getCache()::remove);
}
protectedabstractvoiddoUpdate(Collection?extendsPpos);
@Override
publicvoiddelete(Tobject){
doDelete(do2po(object));
//删除的时候清除缓存
getCache().remove(object.getId());
}
protectedabstractvoiddoDelete(Ppo);
@Override
publicvoiddelete(Stringid){
doDelete(id);
//删除的时候清除缓存
getCache().remove(id);
}
protectedabstractvoiddoDelete(Stringid);
@Override
publicvoiddelete(CollectionStringids){
doDelete(ids);
//删除的时候清除缓存
ids.forEach(getCache()::remove);
}
protectedabstractvoiddoDelete(CollectionStringids);
@Override
publicTget(Stringid){
//读取的时候先从缓存读
//如果没有缓存则查询后放入缓存
Pcache=getCache().get(id);
if(cache==null){
Ppo=doGet(id);
getCache().set(id,
if(po==null){
returnnull;
}
returnpo2do(po);
}
returnpo2do(cache);
}
protectedabstractPdoGet(Stringid);
@Override
publicCollectionselect(CollectionStringids){
//读取的时候先从缓存读
Collectionselect=newArrayList();
CollectionStringunCachedIds=newArrayList();
for(Stringid:ids){
Pcache=getCache().get(id);
if(cache==null){
//没有缓存的先保存id
unCachedIds.add(id);
}else{
//有缓存的直接用
select.add(cache);
}
}
//一次性查询没有缓存的ids
Collectionpos=doSelect(unCachedIds);
//把这些放到缓存中
pos.forEach(it-getCache().set(it.getId(),it));
select.addAll(pos);
returnselect
.stream()
.map(this::po2do)
.collect(Collectors.toList());
}
protectedabstractCollectiondoSelect(CollectionStringids);
@Override
publicvoiddelete(Conditionsconditions){
doDelete(conditions);
stream(conditions).map(IdProvider::getId).forEach(getCache()::remove);
}
protectedabstractvoiddoDelete(Conditionsconditions);
//省略其他方法
通过AbstractDomainRepository的扩展,将缓存模版化,对于通用的增删改查直接生效,不会影响到具体的上层业务代码,不过如果是一些特殊的场景还是需要单独实现对应的缓存
这里还需要注意缓存存储的应该是数据模型,因为领域模型是不太好做序列化和反序列化的
Conditions Lambda支持我们之前实现的Conditions需要硬编码属性字段
Conditionsconditions=newConditions();
conditions.equal("pinId",getPinId());
这肯定是很容易出问题的,所以我们看看能不能用Lambda来优化
Conditionsconditions=newConditions();
conditions.equal(Pin::getId,getPinId());
如果我们能够实现上面这样的代码就能解决这个问题
那么怎么把Pin::getId对应到pinId呢
答案就是SerializedLambda
我们先定义一个接收Pin::getId的接口
@FunctionalInterface
publicinterfaceLambdaFunctionT,RextendsSerializable{
Rget(Tt);
}
接着给Condition添加对应的方法
publicT,RConditionsequal(LambdaFunctionT,Rlf,Objectvalue){
Methodmethod=lf.getClass().getDeclaredMethod("writeReplace");
method.setAccessible(true);
SerializedLambdasl=(SerializedLambda)method.invoke(lf);
Stringkey=handleKey(sl);
equal(key,value);
returnthis;
}
我们可以通过反射,获得LambdaFunction中的SerializedLambda对象,从SerializedLambda中我们就可以获得"Pin"和"getId"这两个字符串,然后只要根据我们的需求处理字符串就行了
自定义扩展与条件配置当项目需要扩展功能的时候,比如现在发布沸点需要显示地理位置,一般情况下我们需要修改我们的各种模型添加这个字段,但是如果这是某一个项目定制化的功能,只有这个项目需要而其他项目不需要话,有没有其他的方式能够满足只扩展这一个项目呢
我这里给大家提供一种思路,我们可以单独添加配置类,用@Bean和@ConditionalOnMissingBean来注入Controller,Service,Repository等组件,同时指定扫描的包路径只包含单独添加的配置类,这样我们就可以非常方便的进行扩展了
首先我们先添加配置类
/**
*沸点领域相关配置
*/
@Configuration
publicclassDomainPinConfiguration{
/**
*沸点Controller
*/
@Bean
@ConditionalOnMissingBean
publicPinControllerpinController(){
returnnewPinController();
}
/**
*沸点Service
*/
@Bean
@ConditionalOnMissingBean
publicPinServicepinService(){
returnnewPinService();
}
/**
*沸点模型与视图的转换适配器
*/
@Bean
@ConditionalOnMissingBean
publicPinFacadeAdapterpinFacadeAdapter(){
returnnewPinFacadeAdapterImpl();
}
/**
*沸点搜索器
*/
@Bean
@ConditionalOnMissingBean
publicPinSearcherpinSearcher(){
returnnewPinSearcherImpl();
}
}
其他模块也是一样添加一个配置类,可以都放一起,也可以分开,我这边是分开的
然后指定扫描的包
@ComponentScan(basePackages="com.bytedance.juejin.*.config")
这里要注意一下,@ComponentScan是只有一个会生效,其他都会被覆盖,我这里只需要扫描这一个路径即可,如果大家有其他需要扫描的包路径的话,要在一个@ComponentScan中配置所有的包路径
假设我们的juejin-application-single就是需要单独扩展地理位置的项目,我们可以直接在juejin-application-single中进行扩展
首先给我们的模型都加上一个字段,这里只列举其中几个模型,其他的模型也都一样,继承之前的模型然后加一个字段即可
/**
*沸点v2
*/
publicinterfacePin2extendsPin{
/**
*获得地理位置
*/
StringgetLocation();
}
/**
*沸点视图v2
*/
@Data
@ToString(callSuper=true)
@EqualsAndHashCode(callSuper=true)
publicclassPinVO2extendsPinVO{
@Schema(description="地理位置")
privateStringlocation;
}
/**
*沸点数据模型v2
*/
@TableName("t_pin")
@Data
@ToString(callSuper=true)
@EqualsAndHashCode(callSuper=true)
classPinPO2extendsPinPO{
/**
*地理位置
*/
privateStringlocation;
}
接着扩展我们的PinController
/**
*沸点Controllerv2
*/
publicclassPinController2extendsPinController{
@Operation(summary="发布沸点v2")
@PostMapping("/v2")
publicvoidcreate(@RequestBodyPinCreateCommand2create,@LoginUseruser){
super.create(create,user);
}
}
添加一个新的接口,参数PinCreateCommand2也是继承PinCreateCommand之后添加了一个location字段
然后是扩展我们的PinFacadeAdapter
/**
*沸点领域模型和视图的转换适配器v2
*/
publicclassPinFacadeAdapterImpl2extendsPinFacadeAdapterImpl{
/**
*在build之前设置location
*/
@Override
protectedvoidbeforeBuild(PinImpl.Builderbuilder,PinCreateCommandcreate){
((PinImpl2.Builder)builder).location(((PinCreateCommand2)create).getLocation());
}
/**
*给vo设置location
*/
@Override
publicPinVOdo2vo(Pinpin){
PinVOvo=super.do2vo(pin);
((PinVO2)vo).setLocation(((Pin2)pin).getLocation());
return
}
}
这里就是重写了一些方法,然后设置了一下location的值
还有我们的MBPPinRepository也需要扩展
/**
*基于MyBatis-Plus的沸点存储实现v2
*/
publicclassMBPPinRepository2extendsMBPPinRepositoryPinPO2{
@Autowired
privatePinMapper2pinMapper2;
/**
*给po设置location
*/
@Override
publicPinPO2do2po(Pinpin){
PinPO2po=super.do2po(pin);
po.setLocation(((Pin2)pin).getLocation());
return
}
/**
*创建PinPO2
*/
@Override
protectedPinPO2newPO(){
returnnewPinPO2();
}
/**
*在build之前设置location
*/
@Override
protectedvoidbeforeBuild(PinImpl.Builderbuilder,PinPO2po){
((PinImpl2.Builder)builder).location(po.getLocation());
}
/**
*指定数据模型为PinPO2
*/
@Override
publicClassPinPO2getFetchClass(){
returnPinPO2.class;
}
/**
*返回BaseMapper为PinMapper2
*/
@Override
publicBaseMapperPinPO2getBaseMapper(){
returnpinMapper2;
}
}
主要也是重写方法然后设置location,最后只要把这些扩展的组件注入到Spring就行了
虽然看起来要添加好几个类,还要重写好几个类,但是这些都是一次性的,每个功能实现一遍就可以了,如果在当前扩展的基础上再新加字段就没有那么麻烦了
同时这样扩展完全不会影响到之前的业务逻辑,整个过程都是扩展添加,没有修改,完全不用担心会不会影响其他的项目
实例化器 Instantiator在这个扩展的过程中我又发现了一个问题,那就是我们在手动实例化所有的模型的时候都是直接new
/**
*沸点领域模型和视图的转换适配器
*/
@Component
publicclassPinFacadeAdapterImplimplementsPinFacadeAdapter{
//省略其他代码
@Override
publicPinfrom(PinCreateCommandcreate,Useruser){
returnnewPinImpl.Builder()
.id(generateId())
.content(create.getContent())
.club(getClub(create.getClubId()))
.user(user)
.build();
}
}
这里我们直接用new Pin.Builder()会导致我们扩展了Pin2和PinImpl2.Builder之后不好扩展
所以我们可以抽象出来一个沸点实例化器PinInstantiator
/**
*沸点实例化器
*/
publicinterfacePinInstantiator{
/**
*实例化普通的Builder
*/
PinImpl.BuildernewBuilder();
/**
*实例化薛定谔的Builder
*/
SchrodingerPin.BuildernewSchrodingerBuilder();
/**
*实例化视图
*/
PinVOnewView();
}
提供一个默认实现
/**
*沸点实例化器实现
*/
@Component
publicclassPinInstantiatorImplimplementsPinInstantiator{
@Override
publicPinImpl.BuildernewBuilder(){
returnnewPinImpl.Builder();
}
@Override
publicSchrodingerPin.BuildernewSchrodingerBuilder(){
returnnewSchrodingerPin.Builder();
}
@Override
publicPinVOnewView(){
returnnewPinVO();
}
}
现在当我们要扩展的时候,只要继承PinInstantiatorImpl重写里面的方法就行了
/**
*沸点实例化器v2
*/
publicclassPinInstantiatorImpl2extendsPinInstantiatorImpl{
@Override
publicPinImpl.BuildernewBuilder(){
returnnewPinImpl2.Builder();
}
@Override
publicSchrodingerPin.BuildernewSchrodingerBuilder(){
returnnewSchrodingerPin2.Builder();
}
@Override
publicPinVOnewView(){
returnnewPinVO2();
}
}
这样我们就能不修改原来的代码,直接通过扩展来支持新的模型实例化
持久层模块化因为在我的设想中持久层应该是可以任意替换的,所以我们应该可以通过配置文件进行指定持久层的启用
juejin:
repository:
mybatis-plus:
enabled:true
#jpa:
#enabled:true
这里我没有实现jpa的方式,但是大概就是这样的想法,可以有多套持久层实现,然后根据要求支持任意切换
添加一个@ConditionalOnMyBatisPlus
/**
*启用MyBatis-Plus时才注入
*/
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@ConditionalOnProperty(name="juejin.repository.mybatis-plus.enabled",havingValue="true")
public@interfaceConditionalOnMyBatisPlus{
}
单独创建一个注解方便条件配置,不用每次都写里面的配置文件属性
然后我们的DomainPinConfiguration就变成了这样
/**
*沸点领域相关配置
*/
@Configuration
publicclassDomainPinConfiguration{
/**
*沸点Controller
*/
@Bean
@ConditionalOnMissingBean
publicPinControllerpinController(){
returnnewPinController();
}
/**
*沸点Service
*/
@Bean
@ConditionalOnMissingBean
publicPinServicepinService(){
returnnewPinService();
}
/**
*沸点模型与视图的转换适配器
*/
@Bean
@ConditionalOnMissingBean
publicPinFacadeAdapterpinFacadeAdapter(){
returnnewPinFacadeAdapterImpl();
}
/**
*沸点实例化器
*/
@Bean
@ConditionalOnMissingBean
publicPinInstantiatorpinInstantiator(){
returnnewPinInstantiatorImpl();
}
/**
*沸点搜索器
*/
@Bean
@ConditionalOnMissingBean
publicPinSearcherpinSearcher(){
returnnewPinSearcherImpl();
}
/**
*沸点MyBatis-Plus配置
*/
@Configuration
@ConditionalOnMyBatisPlus
publicstaticclassMyBatisPlusConfiguration{
/**
*id生成器
*/
@Bean
@ConditionalOnMissingBean
publicPinIdGeneratorpinIdGenerator(){
returnnewMBPPinIdGenerator();
}
/**
*基于MyBatis-Plus的沸点存储
*/
@Bean
@ConditionalOnMissingBean
publicPinRepositorypinRepository(){
returnnewMBPPinRepository();
}
}
}
然后我们的扩展配置PinConfiguration2是这样的
/**
*沸点扩展配置v2
*/
@Configuration
publicclassPinConfiguration2{
@Bean
publicPinControllerpinController(){
returnnewPinController2();
}
@Bean
publicPinFacadeAdapterpinFacadeAdapter(){
returnnewPinFacadeAdapterImpl2();
}
@Bean
publicPinInstantiatorpinInstantiator(){
returnnewPinInstantiatorImpl2();
}
@Configuration
@ConditionalOnMyBatisPlus
publicstaticclassMyBatisPlusConfiguration2{
@Bean
publicPinRepositorypinRepository(){
returnnewMBPPinRepository2();
}
}
}
这里我的PinConfiguration2是在扫描的路径中的,所以能够直接扫到,同时由于我们原来的配置使用了@ConditionalOnMissingBean,所以只会注入我们扩展的组件而不会再注入我们之前的组件了,这样就完成了我们的功能扩展
总结在重构优化的过程中我发现有很多模版化的方法于是就把他们单独提取出来作为一个基础模块,而且在实现各个组件的时候我发现很多都是把领域模型换一下,其他都一个样,所以我还在考虑可以写一个代码生成的插件,这样就更方便了
另外我们其实可以把实现的所有的业务模块都发布成一个一个的jar,当我们开发一个新项目的时候,可以把已经实现过的功能模块直接依赖进来,需要扩展的就扩展一下,如果是没有实现过的功能就实现一遍,然后继续发布成一个jar,作为之后的项目依赖
这样我们就不需要再花费大量的时间去实现几乎相同的功能需求,可以把更多的时间放到其他的技术上再应用到我们的项目中,形成一个良性的循环
阅读原文
网站开发网络凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设、网站改版、域名注册、主机空间、手机网站建设、网站备案等方面的需求...
请立即点击咨询我们或拨打咨询热线:13245491521 13245491521 ,我们会详细为你一一解答你心中的疑难。 项目经理在线