栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java > SpringBoot

领域驱动设计,构建简单的新闻系统,20分钟够吗?

SpringBoot 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

领域驱动设计,构建简单的新闻系统,20分钟够吗?

让我们使用领域驱动的方式,构建一个简单的系统。

1. 需求

新闻系统的需求如下:

  1. 创建新闻类别;
  2. 修改新闻类别,只能更改名称;
  3. 禁用新闻类别,禁用后的类别不能添加新闻;
  4. 启用新闻类别;
  5. 根据类别id获取类别信息;
  6. 指定新闻类别id,创建新闻;
  7. 更改新闻信息,只能更改标题和内容;
  8. 禁用新闻;
  9. 启用新闻;
  10. 分页查找给定类别的新闻,禁用的新闻不可见。
2. 工期估算

大家觉得,针对上面需求,大概需要多长时间可以完成,可以先写下来。

3. 起航 3.1. 项目准备

构建项目,使用 http://start.spring.io 或使用模板工程,构建我们的项目(Sprin Boot 项目),在这就不多叙述。

3.1.1. 添加依赖

首先,添加 gh-ddd-lite 相关依赖和插件。



    4.0.0
    com.geekhalo
    gh-ddd-lite-demo
    1.0.0-SNAPSHOT

    
 com.geekhalo
 gh-base-parent
 1.0.0-SNAPSHOT
    

    
 demo
 gh-${service.name}-service
 v1
 ${service.name} Api
 /${service.name}-api
    

    
 
     com.geekhalo
     gh-ddd-lite
     1.0.0-SNAPSHOT
 
 
     com.geekhalo
     gh-ddd-lite-spring
     1.0.0-SNAPSHOT
 
 
     com.geekhalo
     gh-ddd-lite-codegen
     1.0.1-SNAPSHOT
     provided
 
 
     com.querydsl
     querydsl-apt
     provided
 

 
     org.springframework.boot
     spring-boot-starter-web
 
 
     org.springframework.boot
     spring-boot-starter-data-jpa
 
 
     org.springframework.boot
     spring-boot-starter-test
     test
 
 
     org.hibernate.javax.persistence
     hibernate-jpa-2.1-api
 
 
     mysql
     mysql-connector-java
 
 
     org.flywaydb
     flyway-core
 
 
     javax.servlet
     javax.servlet-api
     provided
 
 
     io.springfox
     springfox-swagger2
 
 
     io.springfox
     springfox-swagger-ui
 
    

    
 
     
  org.springframework.boot
  spring-boot-maven-plugin
  
      
   
repackage
   
      
  
  
      true
      ZIP
  
     
     
  com.mysema.maven
  apt-maven-plugin
  1.1.3
  
      
   
process
   
   
target/generated-sources/java
com.querydsl.apt.jpa.JPAAnnotationProcessor

   
      
  
     
 
    



3.1.2. 添加配置信息

在 application.properties 文件中添加数据库相关配置。

spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/db_test?useUnicode=true&characterEncoding=utf8&useSSL=false
spring.datasource.username=root
spring.datasource.password=
spring.application.name=ddd-lite-demo
server.port=8090
management.endpoint.beans.enabled=true
management.endpoint.conditions.enabled=true
management.endpoints.enabled-by-default=false
management.endpoints.web.exposure.include=beans,conditions,env
3.1.3. 添加入口类

新建 UserApplication 作为应用入口类。

@SpringBootApplication
@EnableSwagger2
public class UserApplication {
    public static void main(String... args){
 SpringApplication.run(UserApplication.class, args);
    }
}

使用 SpringBootApplication 和 EnableSwagger2 启用 Spring Boot 和 Swagger 特性。

3.2. NewsCategory 建模

首先,我们对新闻类型进行建模。

3.2.1. 建模 NewsCategory 状态

新闻类别状态,用于描述启用、禁用两个状态。在这使用 enum 实现。

@GenCodebasedEnumConverter
public enum  NewsCategoryStatus implements CodebasedEnum {
    ENABLE(1),
    DISABLE(0);

    private final int code;

    NewsCategoryStatus(int code) {
 this.code = code;
    }

    @Override
    public int getCode() {
 return code;
    }
}

3.2.2. 建模 NewsCategory

NewsCategory 用于描述新闻类别,其中包括状态、名称等。

3.2.2.1. 新建 NewsCategory
@EnableGenForAggregate

@Data
@Entity
@Table(name = "tb_news_category")
public class NewsCategory extends JpaAggregate {

    private String name;

    @Setter(AccessLevel.PRIVATE)
    @Convert(converter = CodebasedNewsCategoryStatusConverter.class)
    private NewsCategoryStatus status;
}
3.2.2.2. 自动生成 base 代码

在命令行或ida中执行maven命令,以对项目进行编译,从而触发代码的自动生成。

mvn clean compile
3.2.2.3. 建模 NewsCategory 创建逻辑

我们使用 NewsCategory 的静态工厂,完成其创建逻辑。

首先,需要创建 NewsCategoryCreator,作为工程参数。

public class NewsCategoryCreator extends baseNewsCategoryCreator{
}

其中 baseNewsCategoryCreator 为框架自动生成的,具体如下:

@Data
public abstract class baseNewsCategoryCreator {
  @Setter(AccessLevel.PUBLIC)
  @Getter(AccessLevel.PUBLIC)
  @ApiModelProperty(
      value = "",
      name = "name"
  )
  private String name;

  public void accept(NewsCategory target) {
    target.setName(getName());
  }
}

接下来,需要创建静态工程,并完成 NewsCategory 的初始化。

public static NewsCategory create(NewsCategoryCreator creator){
    NewsCategory category = new NewsCategory();
    creator.accept(category);
    category.init();
    return category;
}

private void init() {
    setStatus(NewsCategoryStatus.ENABLE);
}
3.2.2.4. 建模 NewsCategory 更新逻辑

更新逻辑,只对 name 进行更新操作。

首先,创建 NewsCategoryUpdater 作为,更新方法的参数。

public class NewsCategoryUpdater extends baseNewsCategoryUpdater{
}

同样,baseNewsCategoryUpdater 也是框架自动生成,具体如下:

@Data
public abstract class baseNewsCategoryUpdater {
  @Setter(AccessLevel.PRIVATE)
  @Getter(AccessLevel.PUBLIC)
  @ApiModelProperty(
      value = "",
      name = "name"
  )
  private DataOptional name;

  public T name(String name) {
    this.name = DataOptional.of(name);
    return (T) this;
  }

  public T acceptName(Consumer consumer) {
    if(this.name != null){ 
    	consumer.accept(this.name.getValue());
    }
    return (T) this;
  }

  public void accept(NewsCategory target) {
    this.acceptName(target::setName);
  }
}

添加 update 方法:

public void update(NewsCategoryUpdater updater){
    updater.accept(this);
} 
3.2.2.5. 建模 NewsCategory 启用逻辑

启用,主要是对 status 的操作.

代码如下:

public void enable(){
    setStatus(NewsCategoryStatus.ENABLE);
}
3.2.2.6. 建模 NewsCategory 禁用逻辑

禁用,主要是对 status 的操作。

代码如下:

public void disable(){
    setStatus(NewsCategoryStatus.DISABLE);
}

至此,NewsCategory 的 Command 就建模完成,让我们总体看下 NewsCategory:

@EnableGenForAggregate

@Data
@Entity
@Table(name = "tb_news_category")
public class NewsCategory extends JpaAggregate {

    private String name;

    @Setter(AccessLevel.PRIVATE)
    @Convert(converter = CodebasedNewsCategoryStatusConverter.class)
    private NewsCategoryStatus status;

    private NewsCategory(){

    }

    
    public static NewsCategory create(NewsCategoryCreator creator){
 NewsCategory category = new NewsCategory();
 creator.accept(category);
 category.init();
 return category;
    }

    
    public void update(NewsCategoryUpdater updater){
 updater.accept(this);
    }

    
    public void enable(){
 setStatus(NewsCategoryStatus.ENABLE);
    }

    
    public void disable(){
 setStatus(NewsCategoryStatus.DISABLE);
    }

    
    private void init() {
 setStatus(NewsCategoryStatus.ENABLE);
    }
}

3.2.2.7. 建模 NewsCategory 查找逻辑

查找逻辑主要由 NewsCategoryRepository 完成。

新建 NewsCategoryRepository,如下:

@GenApplication
public interface NewsCategoryRepository extends baseNewsCategoryRepository{
    @Override
    Optional getById(Long aLong);
}

同样, baseNewsCategoryRepository 也是自动生成的。

interface baseNewsCategoryRepository extends SpringDataRepositoryAdapter, Repository, QuerydslPredicateExecutor {
}

领域对象 NewsCategory 不应该暴露到其他层,因此,我们使用 DTO 模式处理数据的返回,新建 NewsCategoryDto,具体如下:

public class NewsCategoryDto extends baseNewsCategoryDto{
    public NewsCategoryDto(NewsCategory source) {
 super(source);
    }
}

baseNewsCategoryDto 为框架自动生成,如下:

@Data
public abstract class baseNewsCategoryDto extends JpaAggregateVo implements Serializable {
  @Setter(AccessLevel.PACKAGE)
  @Getter(AccessLevel.PUBLIC)
  @ApiModelProperty(
      value = "",
      name = "name"
  )
  private String name;

  @Setter(AccessLevel.PACKAGE)
  @Getter(AccessLevel.PUBLIC)
  @ApiModelProperty(
      value = "",
      name = "status"
  )
  private NewsCategoryStatus status;

  protected baseNewsCategoryDto(NewsCategory source) {
    super(source);
    this.setName(source.getName());
    this.setStatus(source.getStatus());
  }
}

3.2.3. 构建 NewsCategoryApplication

至此,领域的建模工作已经完成,让我们对 Application 进行构建。

@GenController("com.geekhalo.ddd.lite.demo.controller.baseNewsCategoryController")
public interface NewsCategoryApplication extends baseNewsCategoryApplication{
    @Override
    NewsCategory create(NewsCategoryCreator creator);

    @Override
    void update(Long id, NewsCategoryUpdater updater);

    @Override
    void enable(Long id);

    @Override
    void disable(Long id);

    @Override
    Optional getById(Long aLong);
}

自动生成的 baseNewsCategoryApplication 如下:

public interface baseNewsCategoryApplication {
  Optional getById(Long aLong);

  NewsCategory create(NewsCategoryCreator creator);

  void update(@Description("主键") Long id, NewsCategoryUpdater updater);

  void enable(@Description("主键") Long id);

  void disable(@Description("主键") Long id);
}

得益于我们的 EnableGenForAggregate 和 GenApplication 注解,baseNewsCategoryApplication 包含我们想要的 Command 和 Query 方法。

接口已经准备好了,接下来,处理实现类,具体如下:

@Service
public class NewsCategoryApplicationImpl extends baseNewsCategoryApplicationSupport
    implements NewsCategoryApplication {

    @Override
    protected NewsCategoryDto convertNewsCategory(NewsCategory src) {
 return new NewsCategoryDto(src);
    }
}

自动生成的 baseNewsCategoryApplicationSupport 如下:

abstract class baseNewsCategoryApplicationSupport extends AbstractApplication implements baseNewsCategoryApplication {
  @Autowired
  private DomainEventBus domainEventBus;

  @Autowired
  private NewsCategoryRepository newsCategoryRepository;

  protected baseNewsCategoryApplicationSupport(Logger logger) {
    super(logger);
  }

  protected baseNewsCategoryApplicationSupport() {
  }

  protected NewsCategoryRepository getNewsCategoryRepository() {
    return this.newsCategoryRepository;
  }

  protected DomainEventBus getDomainEventBus() {
    return this.domainEventBus;
  }

  protected  List convertNewsCategoryList(List src,
      Function converter) {
    if (CollectionUtils.isEmpty(src)) return Collections.emptyList();
    return src.stream().map(converter).collect(Collectors.toList());
  }

  protected  Page convvertNewsCategoryPage(Page src,
      Function converter) {
    return src.map(converter);
  }

  protected abstract NewsCategoryDto convertNewsCategory(NewsCategory src);

  protected List convertNewsCategoryList(List src) {
    return convertNewsCategoryList(src, this::convertNewsCategory);
  }

  protected Page convvertNewsCategoryPage(Page src) {
    return convvertNewsCategoryPage(src, this::convertNewsCategory);
  }

  @Transactional(
      readonly = true
  )
  public  Optional getById(Long aLong, Function converter) {
    Optional result = this.getNewsCategoryRepository().getById(aLong);
    return result.map(converter);
  }

  @Transactional(
      readonly = true
  )
  public Optional getById(Long aLong) {
    Optional result = this.getNewsCategoryRepository().getById(aLong);
    return result.map(this::convertNewsCategory);
  }

  @Transactional
  public NewsCategory create(NewsCategoryCreator creator) {
    	NewsCategory result = creatorFor(this.getNewsCategoryRepository())
  .publishBy(getDomainEventBus())
  .instance(() -> NewsCategory.create(creator))
  .call(); 
    logger().info("success to create {} using parm {}",result.getId(), creator);
    return result;
  }

  @Transactional
  public void update(@Description("主键") Long id, NewsCategoryUpdater updater) {
    	NewsCategory result = updaterFor(this.getNewsCategoryRepository())
  .publishBy(getDomainEventBus())
  .id(id)
  .update(agg -> agg.update(updater))
  .call(); 
    logger().info("success to update for {} using parm {}", id, updater);
  }

  @Transactional
  public void enable(@Description("主键") Long id) {
    	NewsCategory result = updaterFor(this.getNewsCategoryRepository())
  .publishBy(getDomainEventBus())
  .id(id)
  .update(agg -> agg.enable())
  .call(); 
    logger().info("success to enable for {} using parm ", id);
  }

  @Transactional
  public void disable(@Description("主键") Long id) {
    	NewsCategory result = updaterFor(this.getNewsCategoryRepository())
  .publishBy(getDomainEventBus())
  .id(id)
  .update(agg -> agg.disable())
  .call(); 
    logger().info("success to disable for {} using parm ", id);
  }
}

该类中包含我们想要的所有实现。

3.2.4. 构建 NewsCategoryController

NewsInfoApplication 构建完成后,新建 NewsCategoryController 将其暴露出去。

新建 NewsCategoryController, 如下:

@RequestMapping("news_category")
@RestController
public class NewsCategoryController extends baseNewsCategoryController{
}

是的,核心逻辑都在自动生成的 baseNewsCategoryController 中:

abstract class baseNewsCategoryController {
  @Autowired
  private NewsCategoryApplication application;

  protected NewsCategoryApplication getApplication() {
    return this.application;
  }

  @ResponseBody
  @ApiOperation(
      value = "",
      nickname = "create"
  )
  @RequestMapping(
      value = "/_create",
      method = RequestMethod.POST
  )
  public ResultVo create(@RequestBody NewsCategoryCreator creator) {
    return ResultVo.success(this.getApplication().create(creator));
  }

  @ResponseBody
  @ApiOperation(
      value = "",
      nickname = "update"
  )
  @RequestMapping(
      value = "{id}/_update",
      method = RequestMethod.POST
  )
  public ResultVo update(@PathVariable("id") Long id,
      @RequestBody NewsCategoryUpdater updater) {
    this.getApplication().update(id, updater);
    return ResultVo.success(null);
  }

  @ResponseBody
  @ApiOperation(
      value = "",
      nickname = "enable"
  )
  @RequestMapping(
      value = "{id}/_enable",
      method = RequestMethod.POST
  )
  public ResultVo enable(@PathVariable("id") Long id) {
    this.getApplication().enable(id);
    return ResultVo.success(null);
  }

  @ResponseBody
  @ApiOperation(
      value = "",
      nickname = "disable"
  )
  @RequestMapping(
      value = "{id}/_disable",
      method = RequestMethod.POST
  )
  public ResultVo disable(@PathVariable("id") Long id) {
    this.getApplication().disable(id);
    return ResultVo.success(null);
  }

  @ResponseBody
  @ApiOperation(
      value = "",
      nickname = "getById"
  )
  @RequestMapping(
      value = "/{id}",
      method = RequestMethod.GET
  )
  public ResultVo getById(@PathVariable Long id) {
    return ResultVo.success(this.getApplication().getById(id).orElse(null));
  }
}
3.2.5. 数据库准备

至此,我们的代码就完全准备好了,现在需要准备建表语句。

使用 Flyway 作为数据库的版本管理,在 resources/db/migration 新建 V1.002__create_news_category.sql 文件,具体如下:

create table tb_news_category
(
	id bigint auto_increment primary key,

	name varchar(32) null,
	status tinyint null,

	create_time bigint not null,
	update_time bigint not null,
	version tinyint not null
);
3.2.6. 测试

至此,我们就完成了 NewsCategory 的开发。
执行 maven 命令,启动项目:

mvn clean spring-boot:run

浏览器中输入 http://127.0.0.1:8090/swagger-ui.html , 通过 swagger 查看我们的成果。

可以看到如下

当然,可以使用 swagger 进行简单测试。

3.3. NewsInfo 建模

在 NewsCategory 的建模过程中,我们的主要精力放在了 NewsCategory 对象上,其他部分基本都是框架帮我们生成的。既然框架为我们做了那么多工作,为什么还需要我们新建 NewsCategoryApplication 和 NewsCategoryController呢?

答案,需要为复杂逻辑预留扩展点。

3.3.1. NewsInfo 建模

整个过程,和 NewsCategory 基本一致,在此不在重复,只选择差异点进行说明。
NewsInfo 最终代码如下:

@EnableGenForAggregate

@Index("categoryId")

@Data
@Entity
@Table(name = "tb_news_info")
public class NewsInfo extends JpaAggregate {
    @Column(name = "category_id", updatable = false)
    private Long categoryId;

    @Setter(AccessLevel.PRIVATE)
    @Convert(converter = CodebasedNewsInfoStatusConverter.class)
    private NewsInfoStatus status;

    private String title;
    private String content;

    private NewsInfo(){

    }

    
    @GenApplicationIgnore
    public static NewsInfo create(Optional category, NewsInfoCreator creator){
 // 对 NewsCategory 的存在性和状态进行验证
 if (!category.isPresent() || category.get().getStatus() != NewsCategoryStatus.ENABLE){
     throw new IllegalArgumentException();
 }
 NewsInfo newsInfo = new NewsInfo();
 creator.accept(newsInfo);
 newsInfo.init();
 return newsInfo;
    }

    public void update(NewsInfoUpdater updater){
 updater.accept(this);
    }

    public void enable(){
 setStatus(NewsInfoStatus.ENABLE);
    }

    public void disable(){
 setStatus(NewsInfoStatus.DISABLE);
    }

    private void init() {
 setStatus(NewsInfoStatus.ENABLE);
    }
}
3.3.1.1. NewsInfo 创建逻辑建模

NewsInfo 的创建逻辑中,需要对 NewsCategory 的存在性和状态进行检查,只有存在并且状态为 ENABLE 才能添加 NewsInfo。

具体实现如下:

@GenApplicationIgnore
public static NewsInfo create(Optional category, NewsInfoCreator creator){
    // 对 NewsCategory 的存在性和状态进行验证
    if (!category.isPresent() || category.get().getStatus() != NewsCategoryStatus.ENABLE){
 throw new IllegalArgumentException();
    }
    NewsInfo newsInfo = new NewsInfo();
    creator.accept(newsInfo);
    newsInfo.init();
    return newsInfo;
}

该方法比较复杂,需要我们手工处理。

在 NewsInfoApplication 中手工添加创建方法:

@GenController("com.geekhalo.ddd.lite.demo.controller.baseNewsInfoController")
public interface NewsInfoApplication extends baseNewsInfoApplication{
    // 手工维护方法
    NewsInfo create(Long categoryId, NewsInfoCreator creator);
}

在 NewsInfoApplicationImpl 添加实现:

@Autowired
private NewsCategoryRepository newsCategoryRepository;

@Override
public NewsInfo create(Long categoryId, NewsInfoCreator creator) {
    return creatorFor(getNewsInfoRepository())
     .publishBy(getDomainEventBus())
     .instance(()-> NewsInfo.create(this.newsCategoryRepository.getById(categoryId), creator))
     .call();
}

其他部分不需要调整。

3.3.2. NewsInfo 查找逻辑建模

查找逻辑设计两个部分:

  1. 根据 categoryId 进行分页查找;
  2. 禁用的 NewsInfo 在查找中不可见。
3.3.2.1. Index 注解

在 NewsInfo 类上多了一个 @Index(“categoryId”) 注解,该注解会在 baseNewsInfoRepository 中添加以 categoryId 为维度的查询。

interface baseNewsInfoRepository extends SpringDataRepositoryAdapter, Repository, QuerydslPredicateExecutor {
  Long countByCategoryId(Long categoryId);

  default Long countByCategoryId(Long categoryId, Predicate predicate) {
    BooleanBuilder booleanBuilder = new BooleanBuilder();
    booleanBuilder.and(QNewsInfo.newsInfo.categoryId.eq(categoryId));;
    booleanBuilder.and(predicate);
    return this.count(booleanBuilder.getValue());
  }

  List getByCategoryId(Long categoryId);

  List getByCategoryId(Long categoryId, Sort sort);

  default List getByCategoryId(Long categoryId, Predicate predicate) {
    BooleanBuilder booleanBuilder = new BooleanBuilder();
    booleanBuilder.and(QNewsInfo.newsInfo.categoryId.eq(categoryId));;
    booleanBuilder.and(predicate);
    return Lists.newArrayList(findAll(booleanBuilder.getValue()));
  }

  default List getByCategoryId(Long categoryId, Predicate predicate, Sort sort) {
    BooleanBuilder booleanBuilder = new BooleanBuilder();
    booleanBuilder.and(QNewsInfo.newsInfo.categoryId.eq(categoryId));;
    booleanBuilder.and(predicate);
    return Lists.newArrayList(findAll(booleanBuilder.getValue(), sort));
  }

  Page findByCategoryId(Long categoryId, Pageable pageable);

  default Page findByCategoryId(Long categoryId, Predicate predicate, Pageable pageable) {
    BooleanBuilder booleanBuilder = new BooleanBuilder();
    booleanBuilder.and(QNewsInfo.newsInfo.categoryId.eq(categoryId));;
    booleanBuilder.and(predicate);
    return findAll(booleanBuilder.getValue(), pageable);
  }
}

这样,并解决了第一个问题。

3.3.2.2. 默认方法

查看 NewsInfoRepository 类,如下:

@GenApplication
public interface NewsInfoRepository extends baseNewsInfoRepository{

    default Page findValidByCategoryId(Long categoryId, Pageable pageable){
 // 查找有效状态
 Predicate valid = QNewsInfo.newsInfo.status.eq(NewsInfoStatus.ENABLE);
 return findByCategoryId(categoryId, valid, pageable);
    }
}

通过默认方法将业务概念转为为数据过滤。

3.3.3. NewsInfo 数据库准备

至此,整个结构与 NewsCategory 再无区别。
最后,我们添加数据库文件 V1.003__create_news_info.sql :

create table tb_news_info
(
	id bigint auto_increment primary key,

  category_id bigint not null,
	status tinyint null,
	title varchar(64) not null,
	content text null,

	create_time bigint not null,
	update_time bigint not null,
	version tinyint not null
);
3.3.4. NewsInfo 测试

启动项目,进行简单测试。

4. 总结

你用了多长时间完成整个系统呢?

项目地址见:https://gitee.com/litao851025/geekhalo-ddd

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/235338.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号