文章目录
- 一、需求背景
 - 二、详细设计
 - UML设计
 - 包设计
 
- 三、程序设计
 - 1、VideoAdStatCaliberEnum
 - 2、LiveDashboardBusiness
 - 3、StatHandleDispatcher
 - 4、StatCaliberEnum
 - 5、StatContext
 - 5、AbstractStatHandler
 - 6、LoggerService
 - 7、AbstractVideoAdStatHandler
 - 1、VideoAdStatContext
 - 2、VideoAdStatByDailyHandler
 - 3、VideoAdStatByJobNumberHandler
 
- 四、总结
 
我们经常会遇到形形色色的产品需求,在快速的迭代中,我们设计的代码会变得越来越臃肿。之所以会这样,来源于我们没有更好的抽象设计,仅仅是基于
Controller、Service、DAO三层分层设计,我们把更多的业务逻辑代码通过一个个方法堆积在Service层。我相信大家心有体会,这个Service伴随着业务迭代会越来越多的代码。试想一下,我们应该怎么可以更好的抽象设计,来达到避免这种现象产生呢。
一、需求背景
有个需求,从交互上就是两种统计口径。
 第一种口径就是:曝光日期+直播间id
 第二种口径就是:曝光日期+直播间id+职位编号。
 本质上这两种统计口径其实业务处理逻辑有相似之处,但亦有差异之处,我们怎么设计才能更好的复用代码呢?同时后续再有其他场景的统计也可以进而复用这部分抽象设计呢,鉴于此,我通过思考这种问题,有了本篇文章。
二、详细设计
UML设计

包设计
整个模块包划分,后续新增场景只需要在handler子包增加子包,如果某个场景增加统计口径只需要在对应场景子包中新增子类Handler即可。我们可以看出一个子包,只需要创建一个上下文类来继承基类,同时新增Hander来负责具体的业务处理。每个子类做到职责单一,符合开闭原则。
 
三、程序设计
1、VideoAdStatCaliberEnum
定义一个枚举,通过这个枚举对外暴露内部支持的业务场景。
/**
 * 视频广告数据看板统计口径
 *
 * @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;
    }
}
 
2、LiveDashboardBusiness
对外暴露的业务类,相当于一个门面。
/**
 * @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;
    }
}
 
3、StatHandleDispatcher
统计处理的路由分发器,该类封装了一系列处理类集合。
/**
 * 统计处理分发器
 *
 * @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));
    }
}
 
4、StatCaliberEnum
具体子包场景模块的统计口径枚举,该类为作为具体场景模块的一个成员属性。
/**
 * 数据看板统计口径
 *
 * @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;
    }
}
 
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
统计处理类,所有子类需要继承该抽象类。
/**
 * 抽象统计处理器
 *
 * @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
具体场景模块的抽象类
/**
 * 抽象 视频广告统计处理器
 *
 * @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、整个类通过上下线接,各司其职,最终达到开闭原则。如果修改某个统计处理只需要找到处理类即可;如果新增场景,只需要新增一个处理类来扩展即可。这就是抽象设计的美妙之处。



![[附源码]Python计算机毕业设计Django的桌游信息管理系统](https://img-blog.csdnimg.cn/df73f78571b64ffb93e5bae450d041ba.png)
















