文章目录
- 一、需求背景
- 二、详细设计
- 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、整个类通过上下线接,各司其职,最终达到开闭原则。如果修改某个统计处理只需要找到处理类即可;如果新增场景,只需要新增一个处理类来扩展即可。这就是抽象设计的美妙之处。