一 抽象类和设计模式在实际业务中的使用
1.1 场景
平时常见的后台管理系统,系统运行时会有各种数据,例如:登录站点的IP量、交易产生的流水、给注册新号的用户发送了多少次验证码等等,很多指标都需要后台程序监测;后台管理员可以设置自己想要查看的指标,后台会根据设置的指标类型进行统计;管理员可以给指标的统计结果设置一个阈值,一旦该指标统计数值超过阈值,会认为数据有异常,然后给予管理者直观的提醒;
场景分析:
其实主要就两点:
1 随着项目越来越大,业务越来越多,可以监测的指标真的很多。这里假设目前要监测100个指标;想想平时游戏的趣味统计数据,就是服务器后台在悄悄统计;
2 需要判断指标是否超出设置阈值,超出则需要告知管理员;
2 实际写代码中遇到的痛点?
某科技公司近期就要做一个后台监测程序。在一次需求分析会上,团队领导们经过一番讨论,最终决定,将这个任务交给新来公司一个月的张三去做,锻炼锻炼张三的代码能力,这也是给新人展示自己的机会嘛。
张三心里暗道不妙,这个需求有点难搞,自己水平还是不高,怎能按时将需求搞掉,yo!
于是张三会后对这个业务进行了一波分析推理:
2.1 首先,怎么统计?为何需要抽象类?
应该有如下三步:
1 构建DTO,方便去各个微服务模块统计数据;考虑到是统计,常见的变量包括:开始结束时间、指标的唯一ID;
2 真正的去调用其他微服务,统计数据;
3 对统计结果进行分析和持久化,或者放入缓存中;
public class StatisticsServiceImpl implements StatisticService {
@Resource
private XXXXService xxxxService;
@Override
public void doStatistics(Object o){
//构造查询用的对象
xxxxService.createObjectToStatistics(....);
//去做查询工作;
doStatistics(....);
//将查询的结果进行处理,比如分析、存储到表中等等;
xxxxService.doAnalysisAndPersistence();
}
}
100个指标,如果直接用上面的方法,会发现每个指标类还是有共性可以抽取;首先,构造查询用的DTO时,其实内部成员变量往往都是相近的参数,比如时间段,所以第一步算是共性部分;然后,对统计结果做分析判断,对有异常的数据要通知管理员,然后将记录添加到数据库中,这个过程都是一样的,所以也可以提取共性;
而且你会有一种感觉,明明自己只是想统计下数据,重要的是如何去获取到想要监测的数据结果,你只想做好第二步,但是每一个统计类都要重复以上三个步骤;
所以,可以用抽象模板类解决;
2.2 怎么运行这些统计?或者说,怎么根据管理员的设置,把其想要的指标都统计起来?–》 设计模式的运用
后台管理员可以设置多个指标,这些指标的唯一标识传进来,我们就得用if else 或者switch去判断,然后调用对应的统计实例去统计;实际操作的时候,这里的判断会很繁琐 ----》运用策略模式,根据这个唯一标识直接获取所需的统计类;
运行统计需要用到统计类实例,然后调用其统计方法;但是我们其实不想关心怎么一个一个创建具体的统计实体类,这很麻烦,也容易犯错,耦合性还高,我们只是想调用其统计方法;----》工厂模式创建,屏蔽创建细节;
工厂模式、策略模式的概念和用法可以看下面这篇文章的说明:
设计模式在外卖营销业务中的实践
设计模式之工厂模式(factory pattern) - alpha_panda - 博客园 (cnblogs.com)
那么在这个业务里,怎么设计呢?
1 根据某个唯一值可以确定对应的策略,由工厂模式+策略模式生成该策略;所有的统计类可以统一维护在一个缓存中,或者利用spring也是可以的;
2 定义统计接口,所有统计指标的类实现该统计接口;
3 后台定时任务遍历所有统计类,统计指标的值;
这样设计,可以让每个策略接口专注于指标的统计;同时,如果新加了指标,只需加一个类即可,扩展性好。
严谨的说法,叫:符合开闭原则、单一职责原则、里氏代换原则、依赖倒转原则、接口隔离原则、迪米特法则;
其中,里氏代换原则参考: (28条消息) 里氏代换原则 举例 分析_xie__jin__cheng的博客-CSDN博客_里氏替换原则
七大原则中六个原则参考: (28条消息) Java中常用的设计模式_Superme-CSDN博客_java设计模式
七大原则参考: (28条消息) 设计模式7大原则_mindcarver-CSDN博客
3 示例:
3.1 首先定义一个策略接口,这个接口有两个方法,1 定义统计流程 2 真正做统计工作的方法
public interface StatisticsStrategy {
/**
* 统计数据的全部流程
* @param id 唯一id
*/
void countData(Long id);
/**
* 真正去统计数据的方法
* @param dto dto
* @return 统计结果
*/
Object doCountData(Object dto);
}
3.2 用抽象类去实现接口,但是不完全实现接口的所有方法。前面说过,要抽取共性部分,而接口中的doCountData是每个策略都不同的,但是统计流程中有很多部分相同,所以抽象类只实现countData方法;
public abstract class BaseStatisticsStrategy implements StatisticsStrategy{
@Resource
private AnalyzeAndPersistenceService analyzeAndPersistenceService;
@Override
public void countData(Long id) {
Object dto = constructDTO(id);
Object countResult = doCountData(dto);
analyzeAndPersistenceService.analyzeAndPersistence(countResult);
}
private Object constructDTO(Object dto) {
//构建DTO
//......
return null;
}
private void analyzeAndPersistenceData(Long id) {
//分析和持久化数据
}
}
3.3 对于每个策略实现类,只用关心自己的指标如何统计即可:
@Component
public class SimpleStatisticsStrategy extends BaseStatisticsStrategy {
@Resource
private CountService countService;
@Override
public Object doCountData(Object dto) {
return countService.count(dto);
}
}
4 总结
张三按照上面的设计,提交代码之后,瞬间神清气爽,这样写剩余的指标,只用专注于统计指标就好了,不用每次都要复制粘贴,工作量减少,心情大好;
领导看到张三的设计,对张三进行了表扬;张三很谦虚的说,哪里哪里,基本操作;
其他同事:可恶,让张三装到了。
二 使用时考虑的问题:
1 如果抽象类内部需要用到bean,如何进行依赖注入?
如上面的代码所示,直接用@Autowired或@Resource 注入即可;
这里有个疑问,抽象类都没有添加@Component注解,spring扫描不到,怎么注入依赖?
答:其子类有@Component,为子类注入时,其父类需要的依赖也会被注入;
2 如果 用到了事务,什么情况会使事务失效?try catch与声明式事务的冲突?
先说spring声明式事务会失效的几种情况,具体可参考:
@Transactional 注解失效场景-布布扣-bubuko.com
这里就说几个业务中一定要注意的失效场景:
1)方法没用public修饰;
注意下即可;
2)同一个类中方法调用;
这个一定要注意。什么事方法内调用,如下:
public abstract class BaseStatisticsStrategy implements StatisticsStrategy{
@Override
public void countData(Long id) {
Object dto = constructDTO(id);
Object countResult = doCountData(dto);
analyzeAndPersistence(countResult);
}
private Object constructDTO(Object dto) {
//构建DTO
//......
return null;
}
@Transactional(rollbackFor = Exception.class)
public void analyzeAndPersistenceData(Long id) {
//分析和持久化数据
}
}
分析和持久化,可能涉及很多写操作,可能会在这里加事务。
但是这个方法会被本类中countData调用,这就属于同一个类中方法调用,此时事务是不生效的;
要想生效,可以将事务加在countData方法上,而countData方法是被外部调用的,所以会生效;
3)注意方法内部的try…catch;
spring的事务是在调用业务方法之前开始的,业务方法执行完毕之后才执行commit or rollback,
事务是否回滚,取决于是否抛出runtime异常。如果抛出runtime exception 并在你的业务方法中没有catch到的话,事务会回滚。
在业务方法中一般不需要catch异常,如果非要catch一定要抛出throw new RuntimeException(),否则会导致事务失效,数据commit造成数据不一致,所以有些时候try catch反倒会画蛇添足。
2.3 抽象类可以实现接口吗?怎么配合才可以有好的效果?
肯定是可以的,上面的代码里都用到了。
抽象类实现接口时,可以只实现部分接口的方法,甚至不实现接口方法,没有任何的问题;
那为什么需要部分实现?
就和上面示例中所展示的一样,每个策略不需要完全实现所有接口方法,而模板类也不需要统计所有的指标。