开发平台后端基类的抽象与封装设计

上一篇介绍了整体架构规划与设计,今天来说一下基类的抽象与封装设计。

技术组件上,使用的是SSM+MyBatisPlus的组合。基于经典三层架构,Controller层负责接收UI请求和响应结果,Service层负责业务逻辑的处理,DAO层负责数据库的读写。

后端内核,设计思想是面向对象。进行抽象,创建基类,通用属性与通用操作由基类统一处理,子类通过继承来扩展属性。个性化操作,子类可以通过新增方法,或覆写基类方法来实现。

实体

首先是实体基类的设计,如下:

package com.huayuan.platform.common.base;

import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;

import java.time.LocalDateTime;


/**
 * 实体 基类
 *
 * @author wqliu
 * @date 2023-03-06
 */
@Data
public class BaseEntity {
    /**
     * 标识
     */
    @TableId(value = "id", type = IdType.ASSIGN_ID)
    private String id;


    /**
     * 创建人标识
     */

    @TableField(value = "create_id", fill = FieldFill.INSERT)
    private String createId;

    /**
     * 创建时间
     */
    @TableField(value = "create_time", fill = FieldFill.INSERT)
    private LocalDateTime createTime;

    /**
     * 更新人标识
     */

    @TableField(value = "update_id", fill = FieldFill.INSERT_UPDATE)
    private String updateId;

    /**
     * 更新时间
     */
    @TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;

    /**
     * 版本
     */

    @TableField(value = "version", fill = FieldFill.INSERT)
    @Version
    private Integer version;

    /**
     * 逻辑删除
     */
    @TableField(value = "delete_flag", fill = FieldFill.INSERT)
    @TableLogic
    private String deleteFlag;
}

首先,定义了无意义标识id,用来做技术标识,采用雪片算法生成。类型设定为字符串而非数值,是因为将来可能会出现数据合并,例如,当前系统要导入某个旧系统的数据,如果采用数值类型,则可能存在主键冲突问题,这时候,使用字符串类型,很容易解决这问题,将旧系统的id加个字母前缀O(old)就可以了。

其次,附加了几个通用属性,创建人、创建时间、修改人、修改时间。这几个字段可以用来做自身系统的数据审计。基于修改时间可以做系统间数据的增量同步。

再次,版本用于做乐观锁,避免并发修改引发的数据错误。逻辑删除标志位用来实现逻辑删除,避免业务数据因为误操作物理删除导致的数据丢失,也利于系统异常排查阶段定位原因。

上面几个字段的抽取与设计,设计上是基于经验,技术上是使用MybatisPlus提供的功能完成。id的自动生成,创建人、创建时间、修改人、修改时间、版本号和逻辑删除位自动填充,业务开发过程中不需要额外处理。

所有业务上的实体类,都继承自此基类 ,一方面保证了整体上的一致,后续进行通用功能设计易于实现;另一方面,后续有扩展需求了,则可以直接扩展基类,同样很轻松。

数据库读写

由于MybatisPlus对于该层已经做了很好的封装和处理,因此没有再扩展,直接使用包com.baomidou.mybatisplus.core.mapper下的BaseMapper作为基类,重点放在下面的业务逻辑

服务

业务逻辑层,也就是通常所说的Service层,会定义一个接口类,一个实现类。MybatisPlus提供了功能强大的基类,我这里进行了扩展,使用模板方法的理念,将通用操作集中到基类中处理,子类仅需要处理具体的业务需求就行了。

首先是服务接口基类,继承自mybatisplus封装的基类IService,定义了初始化、增删改查的方法,以及批量操作。

package com.huayuan.platform.common.base;

import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.extension.service.IService;

import java.util.List;

/**
 * 服务接口,定义通用操作
 *
 * @author wqliu
 * @date 2023-03-06
 */
public interface BaseService<T> extends IService<T> {

    /**
     * 初始化
     *
     * @return 实体对象
     */
    T init();


    /**
     * 新增
     *
     * @param entity 实体对象
     * @return ture:成功;false:失败
     */
    boolean add(T entity);


    /**
     * 修改
     *
     * @param entity 实体对象
     * @return ture:成功;false:失败
     */
    boolean modify(T entity);


    /**
     * 删除-通过id
     *
     * @param idListString id列表字符串,多个id用逗号间隔
     * @return
     */
    boolean remove(String idListString);


    /**
     * 批量新增
     *
     * @param list 实体列表
     * @return
     */
    boolean batchAdd(List<T> list);

    /**
     * 批量修改
     *
     * @param list 实体列表
     * @return
     */
    boolean batchModify(List<T> list);


    /**
     * 删除-通过条件表达式
     * 注意,删除大量数据会导致性能问题
     *
     * @param wrapper
     * @return
     */
    @Override
    boolean remove(Wrapper<T> wrapper);


    /**
     * 查询
     *
     * @param id 标识
     * @return
     */
    T query(String id);


}

然后是服务实现基类,继承自mybatisplus封装的基类BaseService,实现了我扩展定义的服务接口。

package com.huayuan.platform.common.base;

import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.huayuan.platform.common.exception.CommonException;
import com.huayuan.platform.common.exception.CustomException;
import com.huayuan.platform.common.utils.CacheUtil;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

/**
 * 服务类基类,处理通用操作
 *
 * @author wqliu
 */
public class BaseServiceImpl<M extends BaseMapper<T>, T extends BaseEntity>
        extends ServiceImpl<M, T> implements BaseService<T> {

    @Autowired
    protected CacheUtil cacheUtil;

    @Override
    public T init() {

        return null;
    }


    @Transactional(rollbackFor = Exception.class)
    @Override
    public boolean add(T entity) {

        beforeAdd(entity);
        beforeAddOrModifyOp(entity);
        boolean result = super.save(entity);
        afterAddOrModifyOp(entity);
        afterAdd(entity);
        return result;
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public boolean modify(T entity) {

        beforeModify(entity);
        beforeAddOrModifyOp(entity);
        if (entity instanceof BaseEntity) {
            BaseEntity baseEntity = (BaseEntity) entity;
            baseEntity.setUpdateId(null);
            baseEntity.setUpdateTime(null);
        }

        boolean result = super.updateById(entity);
        afterAddOrModifyOp(entity);
        afterModify(entity);
        return result;
    }

    /**
     * 删除-通过id
     *
     * @param idListString id列表字符串,多个id用逗号间隔
     * @return
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public boolean remove(String idListString) {
        String[] idArray = StringUtils.split(idListString.toString(), ",");
        for (String item : idArray) {
            T entity = getEntity(item);
            beforeRemove(entity);
            super.removeById(item);
            afterRemove(entity);
        }
        return true;
    }

    @Override
    public T query(String id) {
        beforeQuery(id);
        T result = getById(id);
        // 对象非空判断
        if (result == null) {
            throw new CustomException(CommonException.NOT_EXIST);
        }
        afterQuery(result);
        return result;
    }


    @Transactional(rollbackFor = Exception.class)
    @Override
    public boolean batchAdd(List<T> list) {
        for (T entity : list) {
            add(entity);
        }
        return true;
    }


    @Override
    public boolean batchModify(List<T> list) {
        for (T entity : list) {
            modify(entity);
        }
        return true;
    }


    @Transactional(rollbackFor = Exception.class)
    @Override
    public boolean remove(Wrapper<T> wrapper) {
        List<T> boList = super.list(wrapper);
        for (T entity : boList) {
            beforeRemove(entity);
        }
        boolean result = super.remove(wrapper);
        for (T entity : boList) {
            afterRemove(entity);
        }
        return result;
    }


    /**
     * 新增前
     *
     * @param entity 实体
     */
    protected void beforeAdd(T entity) {
        // 子类根据需要覆写
    }

    /**
     * 新增后
     *
     * @param entity 实体
     */
    protected void afterAdd(T entity) {
        // 子类根据需要覆写
    }


    /**
     * 修改前
     *
     * @param entity 实体
     */
    protected void beforeModify(T entity) {
        // 子类根据需要覆写
    }

    /**
     * 修改后
     *
     * @param entity 实体
     */
    protected void afterModify(T entity) {
        // 子类根据需要覆写
    }


    /**
     * 新增和修改前公用操作
     */
    protected void beforeAddOrModifyOp(T entity) {
        // 子类根据需要覆写
    }

    /**
     * 新增和修改后公用操作
     */
    protected void afterAddOrModifyOp(T entity) {
        // 子类根据需要覆写
    }


    /**
     * 删除前
     *
     * @param entity 实体
     */
    protected void beforeRemove(T entity) {
        // 子类根据需要覆写

    }

    /**
     * 删除后
     *
     * @param entity 实体
     */
    protected void afterRemove(T entity) {
        // 子类根据需要覆写
    }


    /**
     * 查询前
     *
     * @param id id
     */
    protected void beforeQuery(String id) {
        // 子类根据需要覆写
    }

    /**
     * 查询后
     *
     * @param entity 实体
     */
    protected void afterQuery(T entity) {
        // 子类根据需要覆写
    }

    /**
     * 获取实体
     *
     * @param id id
     * @return {@link T}
     */
    protected T getEntity(String id) {
        // 标识非空判断
        if (id == null) {
            throw new CustomException(CommonException.ID_EMPTY);
        }

        T entity = query(id);
        return entity;
    }


}

对业务实体的增删改查,通常有前置操作和后置操作,并且需要事务处理。
以新增操作为例,通过基类来定义算法的框架,在add方法中增加事务注解,如下:


    @Transactional(rollbackFor = Exception.class)
    @Override
    public boolean add(T entity) {

        beforeAdd(entity);
        beforeAddOrModifyOp(entity);
        boolean result = super.save(entity);
        afterAddOrModifyOp(entity);
        afterAdd(entity);
        return result;
    }



    protected void beforeAdd(T entity) {
        //子类根据需要覆写
    }

    protected void afterAdd(T entity) {
        //子类根据需要覆写
    }

子类根据需要覆写

@Override
    public void beforeAdd(Organization entity) {
        //验证同节点下是否存在名称相同的组织机构
        QueryWrapper<Organization> queryWrapper = new QueryWrapper<>();
        queryWrapper.lambda().eq(Organization::getName, entity.getName())
                .eq(Organization::getParentId, entity.getParentId());
        int count = count(queryWrapper);
        if (count > 0) {
            throw new CustomException(ExceptionEnum.ORGANIZATION_NAME_EXIST);
        }

    }


   @Override
    public void afterAdd(Organization entity) {
        //将ID与名称放到redis缓存
        cacheUtil.set(Constant.ORGANIZATION_CACHE_PREFIX + entity.getId(), entity.getName());

    }

注意:该实例与标准的模板方法还有一些差异,基类并没有将beforeAdd和afterAdd两个方法定义为抽象方法。原因在于这两个处理环节对于具体的业务实体并不是必须的,如定义为抽象方法,则会要求子类必须实现,反而不符合实际需求并带来繁琐性。

对于设计模式,很多都在标准模式下有变种和变通,我们应该领会其神,活学活用。

通过上述封装后,业务实体通用的增删改查都已经实现了。在进行具体业务系统的功能开发时,仅需要继承该父类,实现特定的业务逻辑即可,代码量大幅减少,代码简洁直观,易于维护,以组织机构为例。

服务接口类,继承基类后,仅需定义三个个性化服务接口即可。

package com.huayuan.platform.system.service;


import com.huayuan.platform.common.base.BaseService;
import com.huayuan.platform.system.entity.Organization;

import java.util.List;


/**
 * 组织机构 服务类
 *
 * @author wqliu
 * @date 2023-03-06
 */
public interface OrganizationService extends BaseService<Organization> {


    /**
     * 启用
     *
     * @param id 标识
     */
    void enable(String id);

    /**
     * 停用
     *
     * @param id 标识
     */
    void disable(String id);


    /**
     * 获取当前组织机构所有上级(包括自身)
     *
     * @param organizationId 组织机构标识
     * @return 组织机构列表
     */
    List<String> getParentId(String organizationId);

}

服务实现类,继承基类后,除了实现个性化的服务接口外,仅根据实际需要,覆写基类预留的方法,如通过beforeAdd,验证同一组织机构下是否已存在同名的记录,通过beforeRemove,验证是否存在下级组织机构,以及该组织机构下是否存在用户,通过afterAdd等方法,更新缓存数据。

package com.huayuan.platform.system.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.huayuan.platform.common.base.BaseServiceImpl;
import com.huayuan.platform.common.constant.CacheConstant;
import com.huayuan.platform.common.constant.TreeDefaultConstant;
import com.huayuan.platform.common.enums.StatusEnum;
import com.huayuan.platform.common.exception.CommonException;
import com.huayuan.platform.common.exception.CustomException;
import com.huayuan.platform.common.utils.CacheUtil;
import com.huayuan.platform.system.entity.Organization;
import com.huayuan.platform.system.entity.User;
import com.huayuan.platform.system.exception.OrganizationExceptionEnum;
import com.huayuan.platform.system.mapper.OrganizationMapper;
import com.huayuan.platform.system.service.DictionaryTypeService;
import com.huayuan.platform.system.service.OrganizationService;
import com.huayuan.platform.system.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.validation.Validator;

import java.util.ArrayList;
import java.util.List;

/**
 * 组织机构 服务实现类
 *
 * @author wqliu
 * @since 2023-03-06
 */
@Service
@Slf4j
public class OrganizationServiceImpl extends BaseServiceImpl<OrganizationMapper, Organization>
        implements OrganizationService {

    @Autowired
    private UserService userService;
    @Autowired
    private DictionaryTypeService dictionaryTypeService;


    @Autowired
    private CacheUtil cacheUtil;


    @Autowired
    private Validator validator;


    @Override
    public Organization init() {
        Organization entity = new Organization();
        entity.setStatus(StatusEnum.NORMAL.name());
        return entity;
    }


    @Override
    public void beforeAdd(Organization entity) {
        // 验证同节点下是否存在同名节点
        QueryWrapper<Organization> queryWrapper = new QueryWrapper<>();
        queryWrapper.lambda().eq(Organization::getName, entity.getName())
                .eq(Organization::getParentId, entity.getParentId());
        long count = count(queryWrapper);
        if (count > 0) {
            throw new CustomException(CommonException.NAME_EXIST_IN_SAME_NODE);
        }

    }


    @Override
    public void beforeModify(Organization entity) {

        // 父节点不能为自己
        if (entity.getId().equals(entity.getParentId())) {
            throw new CustomException(CommonException.UP_CANNOT_SELF);
        }
        // 所选父节点不能为子节点
        if (this.hasChild(entity.getId(), entity.getParentId())) {
            throw new CustomException(CommonException.UP_CANNOT_BE_CHILD);
        }
        // 验证同节点下是否存在名称相同的组织机构
        QueryWrapper<Organization> queryWrapper = new QueryWrapper<>();
        queryWrapper.lambda().eq(Organization::getName, entity.getName())
                .eq(Organization::getParentId, entity.getParentId())
                .ne(Organization::getId, entity.getId());
        long count = count(queryWrapper);
        if (count > 0) {
            throw new CustomException(CommonException.NAME_EXIST_IN_SAME_NODE);
        }
    }

    @Override
    public void beforeRemove(Organization entity) {
        // 验证是否有下级
        if (super.lambdaQuery().eq(Organization::getParentId, entity.getId()).count() > 0) {
            throw new CustomException(CommonException.HAS_CHILDREN);
        }

        // 验证是否存在人员
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.lambda().eq(User::getOrganizationId, entity.getId());
        long count = userService.count(queryWrapper);
        if (count > 0) {
            throw new CustomException(OrganizationExceptionEnum.HAS_USER);
        }
    }


    @Override
    public void afterAdd(Organization entity) {
        cacheUtil.set(CacheConstant.ORGANIZATION_CACHE_PREFIX + entity.getId(), entity.getName());

    }

    @Override
    public void afterModify(Organization entity) {
        cacheUtil.set(CacheConstant.ORGANIZATION_CACHE_PREFIX + entity.getId(), entity.getName());

    }

    @Override
    public void afterRemove(Organization entity) {
        cacheUtil.remove(CacheConstant.ORGANIZATION_CACHE_PREFIX + entity.getId());

    }


    /**
     * 判断选中的树节点与变化树节点的父子关系
     *
     * @param selectId
     * @param changeId
     * @return
     */
    private Boolean hasChild(String selectId, String changeId) {
        // 默认不为子节点,有子节点则停止递归查询
        Boolean ret = false;
        QueryWrapper<Organization> queryWrapper = new QueryWrapper<>();
        queryWrapper.lambda().eq(Organization::getParentId, selectId);
        List<Organization> childList = this.list(queryWrapper);
        if (childList != null) {
            for (Organization o : childList) {
                if (changeId.equals(o.getId())) {
                    return true;
                }
                ret = hasChild(o.getId(), changeId);
                if (ret) {
                    return ret;
                }
            }
        }
        return ret;

    }

    @Override
    public void enable(String id) {
        Organization entity = query(id);
        entity.setStatus(StatusEnum.NORMAL.name());
        modify(entity);
    }

    @Override
    public void disable(String id) {
        Organization entity = query(id);
        entity.setStatus(StatusEnum.DEAD.name());
        modify(entity);
    }


    @Override
    public List<String> getParentId(String organizationId) {
        List<String> list = new ArrayList<>(10);

        Organization entity = null;
        do {
            // 获取当前实体
            entity = getEntity(organizationId);
            // 添加到集合
            list.add(organizationId);
            // 将当前实体父标识设置为实体标识
            organizationId = entity.getParentId();
        }
        while (!TreeDefaultConstant.DEFAULT_TREE_ROOT_PARENT_ID.equals(organizationId));
        return list;
    }
}

控制器层

控制器层进行的封装比较少,主要是考虑到不同的业务实体会存在差异化需求,例如,某个业务实体不允许修改或删除,那么在controller层就不应当暴露修改或删除的rest api。

控制器层仍有共性部分,将分页与排序对象的绑定,以及工具类的定义,放到基类统一处理,子类直接使用即可,具体如下:

package com.huayuan.platform.common.base;

import com.huayuan.platform.common.query.QueryGenerator;
import com.huayuan.platform.common.utils.CacheUtil;
import com.huayuan.platform.common.utils.DictionaryUtil;
import ma.glasnost.orika.MapperFacade;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder;

/**
 * 控制器基类
 *
 * @author wqliu
 * @date 2023-03-06
 */
public class BaseController {

    @Autowired
    protected MapperFacade mapperFacade;

    @Autowired
    protected QueryGenerator queryGenerator;

    @Autowired
    protected DictionaryUtil dictionaryUtil;

    @Autowired
    protected CacheUtil cacheUtil;


    /**
     * 分页
     *
     * @param binder
     */
    @InitBinder("pageInfo")
    public void initPageInfo(WebDataBinder binder) {
        binder.setFieldDefaultPrefix("page_");
    }

    /**
     * 排序
     *
     * @param binder
     */
    @InitBinder("sortInfo")
    public void initSort(WebDataBinder binder) {
        binder.setFieldDefaultPrefix("sort_");
    }


}

视图对象

视图对象的基类保持与实体基类一致,具体如下:

package com.huayuan.platform.common.base;

import lombok.Data;

import java.time.LocalDateTime;

/**
 * 视图对象 基类
 *
 * @author wqliu
 * @date 2023-03-06
 */
@Data
public class BaseVO {


    /**
     * 标识
     */
    private String id;
    
    /**
     * 创建人标识
     */
    private String createId;

    /**
     * 创建时间
     */
    private LocalDateTime createTime;

    /**
     * 更新人标识
     */

    private String updateId;

    /**
     * 更新时间
     */
    private LocalDateTime updateTime;

    /**
     * 版本
     */
    private Integer version;

    /**
     * 删除标志
     */
    private String deleteFlag;
}

这里也顺便提一下,为什么要有视图对象(vo)?直接使用实体对象(entity)行不行?

在前后端分离模式下,通过json交换数据,视图对象非常重要。引入视图对象,相对于直接使用实体对象,有以下优点:
1.可限制前端能输入的数据,如某个属性不允许作为查询条件输入;
2.可减少返回给前端的数据,如实体有30个属性,返回给前端只有10个属性,可屏蔽不希望提供给前端展示的敏感数据,如用户密码,商品采购成本等
3.可增加属性数量,在controller层,将业务实体的字典编码,转换为字典名称,输出给前端,需要额外属性承载。
4.可进行数据聚合,将来源于几个entity的属性聚合成一份供前端使用的数据
5.前后端解耦,前端更换UI控件库,如将ElementUI更换为Eelement Plus、IView或AntD of Vue,有vo层做缓冲将大幅降低需要改造和适配的工作量。这里指的不是业务实体对应的vo,而是与前端UI控件库对应的vo,如不同的UI库,树的数据结构不同。

平台设计与设计专栏地址:https://blog.csdn.net/seawaving/category_12230971.html
开源项目地址:https://gitee.com/popsoft/huayuan-development-platform

欢迎收藏、点赞、评论。

你可能感兴趣的:(开发平台,开发平台,开发平台设计,基类,父类,模板方法)