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

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

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

RESTful与API规划&管理

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

DDD范畴驱动规划与事务模块化(概念与理解)

DDD范畴驱动规划与事务模块化(落地与完成)

DDD范畴驱动规划与事务模块化(薛定谔模型)(本文)

DDD范畴驱动规划与事务模块化(优化与重构)

RPC模块化规划与分布式事务

事件模块化规划与生命周期日志

在之前的文章 服务端模块化架构规划|项目结构与模块化构建思路 中,咱们以的部分功能为例,建立了一个支撑模块化的后端服务项目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

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

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

前情回忆

在上一篇 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范畴驱动规划与事务模块化(优化与重构)