原创|一个统计查询模块基于设计模式的抽象设计

基层程序员世界中,我们经常会遇到形形色色的产品需求,在快速的迭代中,我们设计的代码会变得越来越臃肿。之所以如此,来源于我们没有抽出时间来做更好的抽象设计,仅仅是基于SpringMVC的ControllerServiceRepository三层分层设计。我们把诸如更多的业务逻辑代码通过一个个方法成员方法不断编织在Service层,呈现给后续的其他研发人员,就是各种private修饰的方法,一个Service类代码行数随着日积月累不断堆积,我相信大家心有体会。试想一下,我们未尝不想更好的设计,以提高程序的可维护性,但是对于我们应该怎么可以做到更好的抽象设计,来达到避免这种现象产生呢,本文就是一篇指导案例,带给你心灵的共鸣。

一、需求背景

有个产品需求,需要做一个统计查询,从交互设计上就是两个选项卡,也代表两种不同统计口径。

  • 第一种口径就是:广告曝光日期+直播间ID,作为查询条件。
  • 第二种口径就是:广告曝光日期+直播间ID+职位编号,作为查询条件。

本质上这两种查询统计口径,其实业务处理逻辑有相似之处,但亦有差异之处,我们怎么设计才能更好的复用共性代码呢?同时也能处理各自场景的差异化代码呢?其实这里我们汇能想到,学过设计模式的程序员都能想到,使用“模板方法模式”,就是可以针对处理这种需求呃。同时后续再有其他场景的统计也可以进而复用这部分抽象设计呢,鉴于此,我通过思考这种问题,有了本篇文章的产生。

二、详细设计

UML设计

UML设计
如上图,在这其中,有四个关键的类,StatCaliberEnumStatHandleDispatcherStatContextAbstractStatHandle,这四个类是整个模块的关键所在。

包设计

整个统计模块包划分,位于business层的dashboard包下,后续新增场景只需要在handler子包增加子包(类似videoad,这是一个视频广告的统计查询)。如果某个场景增加统计口径,只需要在对应场景子包中新增子类Handler来继承基类(AbstractHandler)即可。

我们可以看出一个子包,只需要创建一个上下文类(譬如VideoAdStatContext)来继承基类StatContext,同时新增Hander来负责具体的业务处理。

每个子类做到职责单一,符合开闭原则,这就是整个模块的设计巧妙之处。
在这里插入图片描述

三、程序设计

1、StatCaliberEnum

所有统计场景模块的统计口径枚举。

它起到对具体场景的细分传统模式下,我们可能都会使用Integer或者String来表示,方法中来通过if else 程序条件来控制路由。但枚举的好处在于,我们把这种分支通过枚举成员来统一维护,通过查看枚举成员我们就可以知道当前场景的分类。后续增加场景,也只需要在枚举中增加成员即可。

/**
 * 数据看板统计口径
 *
 * @author : 石冬冬-Sieg Heil
 * @since 2022/11/30 9:39 AM
 */
@Getter
public enum StatCaliberEnum {
    /**
     * (视频广告)按天的明细数据
     */
    VIDEO_AD_BY_DAILY(1, "按天的明细数据"),
    /**
     * (视频广告)按岗位的明细数据
     */
    VIDEO_AD_BY_JOB_NUMBER(2, "按岗位的明细数据"),
    ;

    private final int value;

    private final String name;

    StatCaliberEnum(Integer value, String name) {
        this.value = value;
        this.name = name;
    }
}

2、VideoAdStatCaliberEnum

定义一个枚举,通过这个枚举对外暴露内部支持的业务场景,这是具体某个场景的细分。

它起到对具体场景的细分传统模式下,我们可能都会使用Integer或者String来表示,方法中来通过if else 程序条件来控制路由。但枚举的好处在于,我们把这种分支通过枚举成员来统一维护,通过查看枚举成员我们就可以知道当前场景的分类。后续增加场景,也只需要在枚举中增加成员即可。

/**
 * 视频广告数据看板统计口径
 *
 * @author : 石冬冬-Sieg Heil
 * @since 2022/11/30 9:39 AM
 */
@Getter
@ThriftStruct
public enum VideoAdStatCaliberEnum {
    /**
     * 未知
     */
    UNKNOWN(-1, "未知"),
    /**
     * 按天的明细数据
     */
    BY_DAILY(1, "按天的明细数据"),
    /**
     * 按岗位的明细数据
     */
    BY_JOB_NUMBER(2, "按岗位的明细数据"),
    ;

    @ThriftField(1)
    private final int value;

    private final String name;

    VideoAdStatCaliberEnum(Integer value, String name) {
        this.value = value;
        this.name = name;
    }

    public static VideoAdStatCaliberEnum valueOf(Integer value) {
        for (VideoAdStatCaliberEnum each : VideoAdStatCaliberEnum.values()) {
            if (each.getValue() == value) {
                return each;
            }
        }
        return VideoAdStatCaliberEnum.UNKNOWN;
    }
}

3、LiveDashboardBusiness

对外暴露的业务类,相当于一个门面。

从该类注入的成员我们可以看到,它仅仅依赖StatHandleDispatcher这个类,并没有直接依赖底层的Handler,做到降低耦合。内部方法中,通过实例化Context对象,并调用StatHandleDispatcher的方法来进行处理分发。

/**
 * @author : 石冬冬-Sieg Heil
 * @since 2022/11/29 4:02 PM
 */
@Service
public class LiveDashboardBusiness {

    @Autowired
    private StatHandleConverter statHandleConverter;

    @Autowired
    private StatHandleDispatcher statHandlerDispatcher;

    /**
     * 获取视频广告数据看板
     * <p>
     * 可以参考:{@link com.zhaopin.c.live.operation.business.LiveRoomBusiness#getDashboard(DashboardRequestBO)}
     *
     * @param request 请求参数
     * @return 返回值
     */
    public VideoAdDashboardBO getVideoAdDashboard(VideoAdDashboardRequestBO request) {
        VideoAdStatCaliberEnum statCaliber = VideoAdStatCaliberEnum.valueOf(request.getCaliber());
        if (Objects.equals(VideoAdStatCaliberEnum.UNKNOWN, statCaliber)) {
            throw new ForbiddenException("非法参数,statCaliber={}", statCaliber.getName());
        }
        StatCaliberEnum caliber = null;
        if (Objects.equals(VideoAdStatCaliberEnum.BY_DAILY, statCaliber)) {
            caliber = StatCaliberEnum.VIDEO_AD_BY_DAILY;
        }
        if (Objects.equals(VideoAdStatCaliberEnum.BY_JOB_NUMBER, statCaliber)) {
            caliber = StatCaliberEnum.VIDEO_AD_BY_JOB_NUMBER;
        }
        VideoAdStatContext context = statHandleConverter.convertToVideoAdStatContext(caliber, request);
        statHandlerDispatcher.execute(context);
        VideoAdDashboardBO response = context.getResponse();
        return response;
    }
}

4、StatHandleDispatcher

统计处理的路由分发器,该类封装了一系列处理类集合。

从分发的处理逻辑来看,输入参数Context封装了场景的枚举;同时,Handler对外暴露了获取处理场景的枚举。这就是可以查找到对应一个Handler来处理。整个内部封装,无需关注所有的Handler子类,增加子类,也无需修改这个类。

/**
 * 统计处理分发器
 *
 * @author : 石冬冬-Sieg Heil
 * @since 2022/11/30 10:00 AM
 */
@Component
public class StatHandleDispatcher {

    @Autowired
    private List<AbstractStatHandler> handlers;

    /**
     * 分发处理
     *
     * @param context 统计上下文对象
     */
    public void execute(StatContext context) {
        handlers.stream()
            .filter(each -> Objects.equals(each.getStatCaliber(), context.getStatCaliber()))
            .forEach(each -> each.execute(context));
    }
}

5、StatContext

统计上下文对象,封装了内部处理所依赖的请求参数,以及输出的统计结果。
该类是一个泛型基类,需要具体场景来继承该类。


/**
 * 统计上下文对象
 *
 * @author : 石冬冬-Sieg Heil
 * @since 2022/11/30 9:46 AM
 */
@ToString
@Getter
@Setter
public class StatContext<Request, Response> {
    /**
     * 统计口径
     */
    private StatCaliberEnum statCaliber;
    /**
     * 请求参数
     */
    private Request request;
    /**
     * 统计结果
     */
    private Response response;
}

5、AbstractStatHandler

统计处理类,所有子类需要继承该抽象类。

相关子类需要实现抽象方法doHandle(Context context)

/**
 * 抽象统计处理器
 *
 * @author : 石冬冬-Sieg Heil
 * @since 2022/11/30 9:46 AM
 */
public abstract class AbstractStatHandler<Context extends StatContext> implements LoggerService {
    /**
     * 统计口径
     */
    protected StatCaliberEnum statCaliber;

    /**
     * 构造函数
     *
     * @param statCaliber 统计口径
     */
    public AbstractStatHandler(StatCaliberEnum statCaliber) {
        this.statCaliber = statCaliber;
    }

    /**
     * 公共方法
     *
     * @param context 上下文对象
     */
    public void execute(Context context) {
        if (logDebug()) {
            getLog().info("[StatHandle]req={}", JsonUtils.toJson(context.getRequest()));
        }
        doHandle(context);
        if (logDebug()) {
            getLog().info("[StatHandle]res={}", JsonUtils.toJson(context.getResponse()));
        }
    }

    /**
     * 处理
     *
     * @param context 上下文对象
     */
    protected abstract void doHandle(Context context);

    public StatCaliberEnum getStatCaliber() {
        return statCaliber;
    }
}

6、LoggerService

日志DEBUG服务接口,相关子类可以实现该接口,来通过配置进而控制日志debug输出。

import org.slf4j.Logger;
/**
 * 日志DEBUG服务接口
 *
 * @author : 石冬冬-Sieg Heil
 * @since 2022/11/29 11:40 AM
 */
public interface LoggerService {
    /**
     * 日志门面
     *
     * @return 日志门面
     */
    Logger getLog();

    /**
     * 是否启用日志输出
     *
     * @return 是否启用日志输出
     */
    default boolean logDebug() {
        return true;
    }
}

7、AbstractVideoAdStatHandler

具体场景模块的抽象类,这个是视频广告统计查询的进一步抽象封装,从业务逻辑看来,它实现了抽象方法doHandle(Context context),同时又定义一个抽象方法abstract VideoAdEffectBO doQueryEffect(VideoAdDashboardRequestBO request);,意味着子类来实现。

/**
 * 抽象 视频广告统计处理器
 *
 * @author : 石冬冬-Sieg Heil
 * @since 2022/11/30 10:20 AM
 */
public abstract class AbstractVideoAdStatHandler extends AbstractStatHandler<VideoAdStatContext> {

    @Autowired
    protected LiveDashboardConverter liveDashboardConverter;

    @Autowired
    protected ThirdLiveRoomBusiness liveRoomBusiness;

    @Autowired
    protected ThirdVideoAdEffectBusiness thirdVideoAdEffectBusiness;

    /**
     * 构造函数
     *
     * @param statCaliber 统计口径
     */
    public AbstractVideoAdStatHandler(StatCaliberEnum statCaliber) {
        super(statCaliber);
    }

    @Override
    protected void doHandle(VideoAdStatContext context) {
        VideoAdDashboardRequestBO request = context.getRequest();
        Long roomId = request.getRoomId();
        RoomBasicInfoBO basicInfo = liveRoomBusiness.getRoomByRoomId(roomId);
        if (Objects.isNull(basicInfo)) {
            throw new NotExistException("直播间不存在[" + roomId + "]", JsonUtils.toJson(request));
        }
        if (!Objects.equals(ProductTypeEnum.VIDEO_AD.getValue(), basicInfo.getProductType())) {
            throw new ServerException("非视频广告直播间[" + roomId + "]", JsonUtils.toJson(request));
        }
        VideoAdEffectBO adEffectBO = doQueryEffect(request);
        VideoAdDashboardBO dashboard = liveDashboardConverter.convertToVideoAdDashboardBO(adEffectBO, request, basicInfo);
        context.setResponse(dashboard);
    }

    /**
     * 统计查询处理
     *
     * @param request 请求参数
     * @return 统计结果
     */
    abstract VideoAdEffectBO doQueryEffect(VideoAdDashboardRequestBO request);
}

1、VideoAdStatContext

视频广告统计上下文对象

/**
 * 视频广告统计上下文对象
 *
 * @author : 石冬冬-Sieg Heil
 * @since 2022/11/30 9:49 AM
 */
@ToString(callSuper = true)
@Getter
@Setter
public class VideoAdStatContext extends StatContext<VideoAdDashboardRequestBO, VideoAdDashboardBO> {
    /**
     * 请求参数
     */
    private VideoAdDashboardRequestBO request;
    /**
     * 统计结果
     */
    private VideoAdDashboardBO response;
}

2、VideoAdStatByDailyHandler

具体场景模块的子类

/**
 * 视频广告统计处理器(按天)
 *
 * @author : 石冬冬-Sieg Heil
 * @since 2022/11/30 9:55 AM
 */
@Service
@Slf4j
public class VideoAdStatByDailyHandler extends AbstractVideoAdStatHandler {

    /**
     * 构造函数
     */
    public VideoAdStatByDailyHandler() {
        super(StatCaliberEnum.VIDEO_AD_BY_DAILY);
    }

    @Override
    VideoAdEffectBO doQueryEffect(VideoAdDashboardRequestBO request) {
        VideoAdEffectRequestBO adEffectRequestBO = liveDashboardConverter.convertToVideoAdEffectRequestBO(request);
        VideoAdEffectBO adEffectBO = thirdVideoAdEffectBusiness.getByDaily(adEffectRequestBO);
        return adEffectBO;
    }

    @Override
    public Logger getLog() {
        return log;
    }
}

3、VideoAdStatByJobNumberHandler

具体场景模块的子类

/**
 * 视频广告统计处理器(按职位)
 *
 * @author : 石冬冬-Sieg Heil
 * @since 2022/11/30 9:55 AM
 */
@Service
@Slf4j
public class VideoAdStatByJobNumberHandler extends AbstractVideoAdStatHandler {

    /**
     * 构造函数
     */
    public VideoAdStatByJobNumberHandler() {
        super(StatCaliberEnum.VIDEO_AD_BY_JOB_NUMBER);
    }

    @Override
    VideoAdEffectBO doQueryEffect(VideoAdDashboardRequestBO request) {
        VideoAdEffectRequestBO adEffectRequestBO = liveDashboardConverter.convertToVideoAdEffectRequestBO(request);
        VideoAdEffectBO adEffectBO = thirdVideoAdEffectBusiness.getByDailyWithJobNumber(adEffectRequestBO);
        return adEffectBO;
    }

    @Override
    public Logger getLog() {
        return log;
    }

}

四、总结

  • 1、StatHandleDispatcher 这个类相当于统计处理的路由分发类,它通过公共方法execute(StatContext context) 来对外调用;从源码我们看到,通过@Autowired 自动装配了AbstractStatHandler的所有子类实现;它起到的另外一个作用,就是外部不需要知道某种场景具体该调用某个处理类,起到一个桥梁代理的左右。
  • 2、StatContext这个类也是个关键类,具体内部处理逻辑都依赖这个对象,它是一个上下文,所有封装请求参数和处理结果。具体相关场景需要继承该类,并指定具体的入参类和出参类。
  • 3、StatCaliberEnum 这个类,是个统计口径枚举,外部通过查看这个类,就可以知道当前内部统计处理支持哪些场景,它不仅作为StatContext这个类的成员变量,同时也作为AbstractStatHandler这个类的构造函数成员,意味着所有处理类都需要重写抽象类的构造函数,进而指定某个处理类是支持统计场景。
  • 4、整个类通过上下线接,各司其职,最终达到开闭原则。如果修改某个统计处理只需要找到处理类即可;如果新增场景,只需要新增一个处理类来扩展即可。这就是抽象设计的美妙之处。

在这里插入图片描述

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值