服务端模块化架构设计|DDD 领域驱动设计与业务模块化(薛定谔模型)

6,167 阅读11分钟

本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!

本专栏 将通过以下几块内容来搭建一个 模块化:可以根据项目的功能需求和体量进行任意模块的组合或扩展 的后端服务

项目结构与模块化构建思路

RESTful与API设计&管理

网关路由模块化支持与条件配置

DDD领域驱动设计与业务模块化(概念与理解)

DDD领域驱动设计与业务模块化(落地与实现)

DDD领域驱动设计与业务模块化(薛定谔模型)(本文)

DDD领域驱动设计与业务模块化(优化与重构)

RPC模块化设计与分布式事务

v2.0:项目结构优化升级

v2.0:项目构建+代码生成「插件篇」

v2.0:扩展模块实现技术解耦

v2.0:结合DDD与MVC的中庸之道(启发与思路)

v2.0:结合DDD与MVC的中庸之道(标准与实现)

v2.0:结合DDD与MVC的中庸之道(优化与插件)

未完待续......

在之前的文章 服务端模块化架构设计|项目结构与模块化构建思路 中,我们以掘金的部分功能为例,搭建了一个支持模块化的后端服务项目juejin,其中包含三个模块:juejin-user(用户)juejin-pin(沸点)juejin-message(消息)

通过添加启动模块来任意组合和扩展功能模块

  • 示例1:通过启动模块juejin-appliaction-systemjuejin-user(用户)juejin-message(消息)合并成一个服务减少服务器资源的消耗,通过启动模块juejin-appliaction-pin来单独提供juejin-pin(沸点)模块服务以支持大流量功能模块的精准扩容

  • 示例2:通过启动模块juejin-appliaction-singlejuejin-user(用户)juejin-message(消息)juejin-pin(沸点)直接打包成一个单体应用来运行,适合项目前期体量较小的情况

PS:示例基于IDEA + Spring Cloud

模块化项目结构.jpg

为了能更好的理解本专栏中的模块化,建议读者先阅读 服务端模块化架构设计|项目结构与模块化构建思路

前情回顾

在上一篇 DDD领域驱动设计与业务模块化(落地与实现) 中,我们参考DDD中的概念实现了沸点的部分功能,但是我发现有很多地方可以优化,所以这篇文章会对其中比较核心的一块内容重新设计(没有看过落地与实现的话建议先看落地与实现哦)

领域模型数据加载问题

不知道大家还记不记得,在上一篇中,我们设计的领域模型Pin(沸点)是能够直接获得Club(沸点圈子)的模型的

也就是说当我们生成一个Pin实例时,不管我们是否需要用到Club的数据,必定会先查询出Club的数据生成对应的领域模型

如果我们在整个业务过程中根本不需要Club的数据,那么就相当于我们执行了一次多余的查询,比如之前的PinFacadeAdapter

/**
 * 沸点领域模型和视图的转换适配器
 */
@Component
public class PinFacadeAdapterImpl implements PinFacadeAdapter {

    /**
     * 圈子存储
     */
    @Autowired
    private ClubRepository clubRepository;

    @Override
    public Pin from(PinCreateCommand create, User user) {
        return new PinImpl.Builder()
                .id(generateId())
                .content(create.getContent())
                .club(getClub(create.getClubId()))
                .user(user)
                .build();
    }
    
    /**
     * 获得圈子领域模型
     */
    public Club getClub(String clubId) {
        if (clubId == null) {
            return null;
        }
        return clubRepository.get(clubId);
    }

    public String generateId() {
        //雪花算法等方式生成ID
        return UUID.randomUUID().toString();
    }
}

在构建Pin模型的时候需要通过ClubRepository获得Club模型,但是我们在保存沸点的时候其实一般只需要clubId作为关联关系就行了,那么这一步就变得很没有必要了

虽然说体量小的时候无伤大雅,但是当体量慢慢增大的时候,这就会变成一块不容小觑的资源消耗,难道这就是用DDD的代价么?别着急,用我独创的薛定谔模型大法就能解决这个问题

薛定谔模型

现在我们就用薛定谔模型解决上面提到的问题

我们原来的Club模型是这样的

/**
 * 圈子
 */
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PROTECTED)
public class ClubImpl implements Club {

    /**
     * 圈子ID
     */
    protected String id;

    /**
     * 圈子名称
     */
    protected String name;

    /**
     * 圈子图标
     */
    protected String logo;

    /**
     * 圈子描述
     */
    protected String description;

    public static class Builder {

        protected String id;

        protected String name;

        protected String logo;

        protected String description;

        public Builder id(String id) {
            this.id = id;
            return this;
        }

        public Builder name(String name) {
            this.name = name;
            return this;
        }

        public Builder logo(String logo) {
            this.logo = logo;
            return this;
        }

        public Builder description(String description) {
            this.description = description;
            return this;
        }

        public Club build() {
            if (!StringUtils.hasText(id)) {
                throw new IllegalArgumentException("Id required");
            }
            if (!StringUtils.hasText(name)) {
                throw new IllegalArgumentException("Name required");
            }
            if (!StringUtils.hasText(logo)) {
                throw new IllegalArgumentException("Logo required");
            }
            if (!StringUtils.hasText(description)) {
                throw new IllegalArgumentException("Description required");
            }
            return new Club(id, name, tag, description);
        }
    }
}

薛定谔的Club模型是这样的

/**
 * 薛定谔的圈子模型
 */
@Getter
public class SchrodingerClub extends ClubImpl implements Club {

    /**
     * 圈子存储
     */
    protected ClubRepository clubRepository;

    protected SchrodingerClub(String id, ClubRepository clubRepository) {
        this.id = id;
        this.clubRepository = clubRepository;
    }

    /**
     * 获得圈子名称
     */
    @Override
    public String getName() {
        //如果名称为 null 则先从存储读取
        if (this.name == null) {
            load();
        }
        return this.name;
    }

    /**
     * 获得圈子图标
     */
    @Override
    public String getLogo() {
        //如果图标为 null 则先从存储读取
        if (this.logo == null) {
            load();
        }
        return this.logo;
    }

    /**
     * 获得圈子描述
     */
    @Override
    public String getDescription() {
        //如果描述为 null 则先从存储读取
        if (this.description == null) {
            load();
        }
        return this.description;
    }

    /**
     * 根据 id 加载其他的数据
     */
    public void load() {
        Club club = clubRepository.get(id);
        if (club == null) {
            throw new JuejinException("Club not found: " + id);
        }
        this.name = club.getName();
        this.tag = club.getTag();
        this.description = club.getDescription();
    }

    public static class Builder {

        protected String id;

        protected ClubRepository clubRepository;

        public Builder id(String id) {
            this.id = id;
            return this;
        }

        public Builder clubRepository(ClubRepository clubRepository) {
            this.clubRepository = clubRepository;
            return this;
        }

        public SchrodingerClub build() {
            if (!StringUtils.hasText(id)) {
                throw new IllegalArgumentException("Id required");
            }
            if (clubRepository == null) {
                throw new IllegalArgumentException("ClubRepository required");
            }
            return new SchrodingerClub(id, clubRepository);
        }
    }
}

我们只生成一个Club的壳模型,里面只有id没有其他的数据,只有当通过Get方法获取其他数据的时候,才会根据id去查询,相当于一个懒加载,这样既能够适配DDD的模型,又不会进行多余的查询

为什么要叫薛定谔模型呢,因为我们在通过id查询的时候如果返回null那么就会报错,而我们在没有调用方法之前是不知道会不会返回null,所以这个时候可以当作是=null!=null的迭加态,然后当我们调用方法的时候相当于进行了观测,于是就导致了坍缩,要不就是=null,要不就是!=null

好了,上面都是我乱编的,只是觉得好像挺有意思的,就这样命名了,嘿嘿

于是我们的PinFacadeAdapter就变成了这样

/**
 * 沸点领域模型和视图的转换适配器
 */
@Component
public class PinFacadeAdapterImpl implements PinFacadeAdapter {

    /**
     * 圈子存储
     */
    @Autowired
    private ClubRepository clubRepository;

    @Override
    public Pin from(PinCreateCommand create, User user) {
        return new PinImpl.Builder()
                .id(generateId())
                .content(create.getContent())
                .club(getClub(create.getClubId()))
                .user(user)
                .build();
    }

    /**
     * 获得圈子领域模型
     */
    public Club getClub(String clubId) {
        if (clubId == null) {
            return null;
        }
        //返回薛定谔的圈子模型
        return new SchrodingerClub.Builder()
                .id(clubId)
                .clubRepository(clubRepository)
                .build();
    }

    public String generateId() {
        //雪花算法等方式生成ID
        return UUID.randomUUID().toString();
    }
}

SchrodingerClub的实例在生成的时候,并不会去查询数据,而是引用了ClubRepository,当需要Club的其他数据时,才会通过id查询

集合结构的薛定谔模型

接下来是另一个问题,我们的Pin模型中很多条Comment模型,也就是评论列表,我们之前是用Map<String, Comment>来持有的,在某些情况下就很难用,比如我们在上一篇中遗留的这个问题

/**
 * 基于 MyBatis-Plus 的沸点存储实现
 */
@Repository
public class MBPPinRepository extends MBPDomainRepository<Pin, PinPO> implements PinRepository {

    //省略其他代码

    @Override
    public Pin po2do(PinPO po) {
        //评论和点赞的信息要到另外的表查询
        //这个是上一篇文章遗留的问题
        return null;
    }
}

如果现在我们一共有10w条评论,总不能全部查询出来吧,或是想要从中获得最新的5条评论也不太好搞,所以还是需要用到我们的薛定谔模型

改造 Pin 模型

在这之前我们需要先对Pin进行一些修改

我们先定义一个Comments方便扩展成薛定谔模型

/**
 * 评论
 */
@Getter
public class CommentsImpl implements Comments {

    private final Map<String, Comment> comments = new HashMap<>();

    /**
     * 添加评论
     */
    @Override
    public void add(Comment comment) {
        if (comment == null) {
            throw new IllegalArgumentException("Comment required");
        }
        comments.put(comment.getId(), comment);
    }

    /**
     * 删除评论
     */
    @Override
    public void delete(Comment comment) {
        if (comment == null) {
            throw new IllegalArgumentException("Comment required");
        }
        comments.remove(comment.getId());
    }

    /**
     * 获得评论
     */
    @Override
    public Comment get(String commentId) {
        if (!StringUtils.hasText(commentId)) {
            throw new IllegalArgumentException("Comment id required");
        }
        return comments.get(commentId);
    }

    /**
     * 评论数量
     */
    @Override
    public long count() {
        return comments.size();
    }
    
    /**
     * 最新的n条评论
     */
    @Override
    public List<Comment> getNewestList(int count) {
        return comments.values()
                .stream()
                .sorted((o1, o2) -> o2.getCreateTime().intValue() - o1.getCreateTime().intValue())
                .limit(count)
                .collect(Collectors.toList());
    }
}

我们用单独的Comments来表示评论列表,CommentsImpl是默认的实现,数据是全部在内存中的,接下来我们要把Comments实现成薛定谔模型

薛定谔的 Comments 模型

/**
 * 薛定谔的评论集合
 */
@Getter
@AllArgsConstructor(access = AccessLevel.PROTECTED)
public class SchrodingerComments extends CommentsImpl implements Comments {

    /**
     * 沸点ID
     */
    protected String pinId;

    /**
     * 评论ID
     */
    protected String commentId;

    /**
     * 评论存储
     */
    protected CommentRepository commentRepository;

    /**
     * 获得某条评论
     */
    @Override
    public Comment get(String commentId) {
        //查询本身缓存
        Comment exist = super.get(commentId);
        if (exist == null) {
            //如果没有则从存储中查询
            Comment comment = commentRepository.get(commentId);
            if (comment == null) {
                throw new JuejinException("Comment not found: " + commentId);
            }
            //放入缓存
            comments.put(commentId, comment);
            return comment;
        }
        return exist;
    }

    /**
     * 获得沸点或评论的评论数
     */
    @Override
    public long count() {
        //如果存在 commentId 是评论的评论数,否则是沸点的评论数
        Conditions conditions = new Conditions();
        conditions.equal("pinId", getPinId());
        String commentId = getCommentId();
        if (commentId != null) {
            conditions.equal("commentId", commentId);
        }
        CommentRepository commentRepository = context.get(CommentRepository.class);
        return commentRepository.count(conditions);
    }
    
    /**
     * 获得沸点最新的n条评论数
     * 回复好像是全展示的
     * 所以这里没有处理回复的情况
     * 严谨一点的话需要加上
     */
    @Override
    public List<Comment> getNewestList(int count) {
        CommentRepository commentRepository = context.get(CommentRepository.class);
        Conditions conditions = new Conditions()
                .equal("pinId", pinId)
                .isNull("commentId")
                .orderBy("createTime", true)
                .limit(count);
        return commentRepository.list(conditions);
    }

    public static class Builder {

        protected String pinId;

        protected String commentId;

        protected CommentRepository commentRepository;

        public Builder pinId(String pinId) {
            this.pinId = pinId;
            return this;
        }

        public Builder commentId(String commentId) {
            this.commentId = commentId;
            return this;
        }

        public Builder commentRepository(CommentRepository commentRepository) {
            this.commentRepository = commentRepository;
            return this;
        }

        public SchrodingerComments build() {
            if (!StringUtils.hasText(pinId)) {
                throw new IllegalArgumentException("Pin id required");
            }
            if (commentRepository == null) {
                throw new IllegalArgumentException("CommentRepository required");
            }
            return new SchrodingerComments(pinId, commentId, commentRepository);
        }
    }
}

这里获得评论数和最新评论的方法,我们可以通过Conditions来进行条件查询,这样就不需要在Repository中额外定义一些和业务相关的方法了,就算发现表设计的有问题,修改起来也十分方便,不会影响到核心业务逻辑

当然如果是Conditions不好实现的复杂逻辑,那还是在Repository中加个单独的方法可能更好实现,会耦合部分业务也没有办法了

改造后的 Pin 模型

/**
 * 沸点
 */
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PROTECTED)
public class PinImpl implements Pin {

    /**
     * 沸点ID
     */
    protected String id;

    /**
     * 沸点内容
     */
    protected String content;

    /**
     * 沸点圈子
     */
    protected Club club;

    /**
     * 沸点用户
     */
    protected User user;

    /**
     * 评论
     */
    protected Comments comments;

    /**
     * 点赞
     */
    protected Likes likes;

    /**
     * 发布时间
     */
    protected Long createTime;

    public static class Builder {

        protected String id;

        protected String content;

        protected Club club;

        protected User user;

        protected Comments comments;

        protected Likes likes;

        protected Long createTime;

        public Builder id(String id) {
            this.id = id;
            return this;
        }

        public Builder content(String content) {
            this.content = content;
            return this;
        }

        public Builder club(Club club) {
            this.club = club;
            return this;
        }

        public Builder user(User user) {
            this.user = user;
            return this;
        }

        public Builder comments(Comments comments) {
            this.comments = comments;
            return this;
        }

        public Builder likes(Likes likes) {
            this.likes = likes;
            return this;
        }

        public Builder createTime(Long createTime) {
            this.createTime = createTime;
            return this;
        }

        public PinImpl build() {
            if (!StringUtils.hasText(id)) {
                throw new IllegalArgumentException("Id required");
            }
            if (!StringUtils.hasText(content)) {
                throw new IllegalArgumentException("Content required");
            }
            if (user == null) {
                throw new IllegalArgumentException("User required");
            }
            if (comments == null) {
                throw new IllegalArgumentException("Comments required");
            }
            if (likes == null) {
                throw new IllegalArgumentException("Likes required");
            }
            if (createTime == null) {
                createTime = System.currentTimeMillis();
            }
            return new PinImpl(
                    id,
                    content,
                    club,
                    user,
                    comments,
                    likes,
                    createTime);
        }
    }
}

我们把UserLikes也用同样的方式实现薛定谔模型之后,之前的po2do方法就变成了下面这样

/**
 * 基于 MyBatis-Plus 的沸点存储实现
 */
@Repository
public class MBPPinRepository extends MBPDomainRepository<Pin, PinPO> implements PinRepository {

    //省略其他代码

    @Override
    public Pin po2do(PinPO po) {
        return new PinImpl.Builder()
            .id(po.getId())
            .content(po.getContent())
            .club(new SchrodingerClub.Builder()
                    .id(po.getClubId())
                    .clubRepository(clubRepository)
                    .build())
            .user(new SchrodingerUser.Builder()
                    .id(po.getUserId())
                    .userRepository(userRepository)
                    .build())
            .comments(new SchrodingerComments.Builder()
                    .pinId(po.getId())
                    .commentRepository(commentRepository)
                    .build())
            .likes(new SchrodingerLikes.Builder()
                    .pinId(po.getId())
                    .likeRepository(likeRepository)
                    .build())
            .createTime(po.getCreateTime().getTime())
            .build();
    }
}

这样,不管沸点模型中有多少其他表的数据,数据有多大,都不会有问题

薛定谔模型与原始领域模型

通过薛定谔模型我们能够减少一些资源的浪费,但是这样的实现已经和最初的领域模型有很大的区别了

在最初的设想中,评论沸点的流程应该是这样的

Pin pin = getPin(pinId);
pin.getComments().add(comment);
update(pin);

Pin作为一个聚合根,作为添加评论的载体,在添加评论后,全量更新整个沸点数据

但是我当时想到如果在评论数量十分庞大的情况下,读写这条沸点都会是一个非常大的开销,所以才有了薛定谔模型

有了薛定谔模型之后,评论沸点的流程是这样的

insert(comment);

我们不需要依赖沸点模型,直接进行局部更新,不过这样就弱化了领域模型的行为属性

领域模型网络

虽然说薛定谔模型弱化了领域模型的行为属性,但也得益于我们的薛定谔模型,让我们不需要一次性加载全部内容,所以我们可以通过任何一个领域模型来获得所有需要的信息

private void test(User user) {
    //通过用户获得关注的圈子
    user.getClubs().stream().forEach(club -> {
        //通过圈子获得里面的沸点
        club.getPins().stream().forEach(pin -> {
            //通过沸点获得下面的评论
            pin.getComments().stream().forEach(comment -> {
                //通过评论获得对应的用户
                test(comment.getUser());
            });
        });
    });
}

通过一个点就可以获得一整个面的信息,而不需要去考虑怎么从数据库中查询,方便我们灵活的实现各种业务需求,而且在处理业务的时候屏蔽了底层的持久层框架,做到业务和技术的隔离,减少耦合

总结

薛定谔模型让我们不需要担心一个领域模型数据的全量查询和全量更新以及持有大量的数据在内存中,同时更方便我们通过各个Get方法来获得对应的模型而不需要考虑如果通过持久层框架来查询,减少业务与技术的耦合度

源码

上一篇:DDD领域驱动设计与业务模块化(落地与实现)

下一篇:DDD领域驱动设计与业务模块化(优化与重构)