记录学习王争的设计模式之美 课程 笔记和练习代码,以便回顾复习,共同进步
对工程师来说,不能只把自己放到执行者的角色,不能只是代码实现者,还要有独立负责一个系统的能力,能端到端(end to end)开发一个完整的系统。其中包括:前期的需求沟通分析、中期的代码设计实现、后期的系统上线维护。
业务开发如何用到设计原则、思想、模式?积分兑换系统的开发案例,展示业务系统从需求分析到上线维护的整个开发套路,一是能举一反三用到其他系统的开发,二是展示看似没技术含量的业务开发,实际蕴含的设计原则、思想、模式。
积分是一种常见的营销手段,很多产品通过它促进消费、增加用户粘性,如支付宝积分、信用卡积分、商场的消费积分等。如果你是一家电商平台的工程师,平台暂时没有积分系统。leader希望你来负责开发这样一个系统,如何做?
作为技术人,如何做产品设计?要学会“借鉴”。爱因斯坦说过,“创造的一大秘诀是要懂得如何隐藏你的来源”。要站在巨人肩膀上。可以先找几个类似的产品,如淘宝的积分系统设计,借鉴到我们产品中。可以亲自用用淘宝,看积分如何使用,也可直接百度“淘宝积分规则”。基本上就能摸清积分系统如何设计。此外,也要结合自己公司的产品,做适当的微创新。
总的来说,积分系统有两个大的功能点:
当然还有一些细节,如积分的有效期问题。为了防止遗漏,除了借鉴之外,还有通过产品的线框图、用户用例(user case)或者叫用户故事(user story)来细化业务流程,挖掘比较细节、不易想到的功能点。
用户用例类似单元测试用例,侧重情景化,也就是模拟用户如何使用我们的产品,描述用户在一个特定的应用场景里的一个完整的业务操作流程。包含更多细节,也更易理解。如,有关积分有效期的用户用例,进行如下设计:
通过上述方法,将功能需求大致搞清楚。
积分系统的需求,大致如下
积分的赚取渠道包括:下订单、每日签到、评论等
积分兑换规则比较通用,如签到送10积分;按照订单总金额的10%兑换成积分,也就是100块钱的订单积累10积分。此外,积分兑换规则还能细化,如不同的店铺、不同的商品,可设置不同的积分兑换比例。
对积分的有效期,可根据不同渠道,设置不同的有效期。积分到期后作废;消费积分的时候,优先使用快到期的积分。
消费渠道包括:抵扣订单金额、兑换优化券、积分换购、参与活动扣积分等。
根据不同消费渠道,设置不同的积分兑换规则。如积分换算为消费抵扣金额的比例为10%,也就是10积分抵扣1块钱;100积分可兑换15块钱的优惠券等。
查询用户的总积分,及赚取积分和消费积分的历史记录。
面向对象设计聚焦在代码层面(主要针对类),系统设计聚焦在架构层面(主要针对模块)。面向对象设计的四个步骤,可借鉴做系统设计。
面向对象设计的本质就是把合适的代码放到合适的类中。合理的划分代码可实现代码的高内聚、低耦合,类和类之间交互简单清晰,代码整体结构一目了然。类比面向对象设计,系统设计就是将合适的功能放到合适的模块中。合理的划分模块也可做到模块层面的高内聚、低耦合。架构清晰整洁。
对于之前罗列的功能点,有三种模块划分方法:
举例解释,如用户通过下订单赚取积分。订单系统通过异步发送消息或者同步调用接口的方式,告知营销系统订单交易成功。营销系统根据拿到的订单信息,查询订单对应的积分兑换规则(兑换比例、有效期等),计算得到订单可兑换的积分数量,然后调用积分系统的接口给用户增加积分。
还是刚才的下订单赚取积分的例子,这种情况,用户下订单成功后,订单系统根据商品对应的积分兑换比例,计算能兑换的积分数量,然后调用积分系统给用户增加积分。
还是同样例子,用户下单成功后,订单系统直接告知积分系统订单交易成功,积分系统根据订单信息查询积分兑换规则,给用户增加积分。
那怎么判断哪种模块划分合理呢?
可以看它是否符合高内聚、低耦合特性判断。如果一个功能的修改或添加,经常跨团队、跨系统才能完成,说明模块划分不够合理,职责不够清晰,耦合过于严重。
此外,为避免业务知识的耦合,让下层系统更通用,一般的话,不希望下层系统(也就是被调用的系统)包含太多上层系统(也就是调用系统)的业务信息,但是,可接受上层系统包含下层系统的业务信息。如,订单系统、优惠券系统、换购商城作为调用积分系统的上层系统,可包含一些积分相关的业务信息。反过来,积分系统最好不要包含太多跟订单、优惠券、换购等相关的信息。
因此,综合考虑,更倾向于第一种和第二种模块划分方式。不管哪种,积分系统负责的工作一样,只包含积分的增减、查询,以及积分明细的记录和查询。
类比面向对象设计,系统设计在系统职责划分好后,就是设计系统之间的交互,也就是确定有哪些系统跟积分系统之间有交互以及如何交互。
常见的交互方式有两种:
比如,用户下订单成功后,订单系统推送一条消息到消息中间件,营销系统订阅订单成功消息,触发执行相应的积分兑换逻辑。这样订单系统跟营销系统完全解耦。
此外,上下层系统之间的调用倾向于通过接口同步,同层之间的调用倾向于异步消息调用。如营销系统和积分系统是上下层关系,推荐同步接口调用。
再看模块本身如何设计。实际上,业务系统本身的设计无外乎三方面工作:接口设计、数据库设计和业务模型设计。
三方面:接口设计、数据库设计、业务模型设计(也就是业务逻辑)
数据库和接口的设计非常重要,一旦设计好并投入使用后,不能轻易改动。改动数据库表结构,涉及数据的迁移和适配;改动接口,需要推动接口的使用者做相应的代码修改。相反,业务逻辑代码侧重内部实现,不涉及被外部依赖的接口,也不包含持久化的数据,对改动容忍性更大。
数据库设计较简单,只需一张记录积分流水明细的表即可。表中记录积分的赚取和消费流水,用户积分的各种统计数据如总积分、总可用积分等都可通过该表计算得到。积分明细表(credit_transaction)的字段如下表:
id | 明细id |
---|---|
channel_id | 赚取或消费渠道ID |
event_id | 相关事件ID,如订单ID、评论ID、优惠券换购交易ID |
credit | 积分(赚取为正值、消费为负值) |
create_time | 积分赚取或消费时间 |
expired_time | 积分过期时间 |
接口设计要符合单一职责原则,但粒度太小也会带来一些问题。如一个功能的实现要调用多个小接口,一方面如果接口调用走网络(特别是公网),多次远程接口调用会影响性能;另一方面,本该在一个接口完成的原子操作,拆分多个小接口完成,可能会涉及分布式事务的数据一致性问题(一个接口成功,另一个接口执行失败)。为了兼容易用性和性能,借鉴facade外观模式,在职责单一的细粒度接口上,再封装一层粗粒度的接口给外部调用。需要设计的接口如下:
接口 | 参数 | 返回值 |
---|---|---|
赚取积分 | userId,channelId,eventId,credit,createTime,expiredTime | 积分明细ID |
消费积分 | userId,channelId,eventId,credit,createTime,expiredTime | 积分明细ID |
查询积分 | userId | 总可用积分 |
查询总积分明细 | userId+分页参数 | id,userId,channelId,eventId,credit,createTime,expiredTime |
查询赚取积分明细 | userId+分页参数 | id,userId,channelId,eventId,credit,createTime,expiredTime |
查询消费积分明细 | userId+分页参数 | id,userId,channelId,eventId,credit,createTime,expiredTime |
service层就是业务模型。
开发角度说,可把积分系统作为一个独立的项目,独立开发,也可跟其他业务代码(如营销系统)放到同一个项目中开发。从运维角度,可跟其他业务一块部署,也可作为微服务独立部署。具体参考公司的当前技术架构。
实际更倾向于将它跟营销系统放到一个项目中开发部署。只要做好代码的模块化和解耦,让积分相关的业务代码跟其他业务代码之间边界清晰,没有过多耦合。后期需要拆分为独立的项目开发部署,也好重构。
为什么要分层开发?一层代码搞定所有的数据读取、业务逻辑、接口暴露不好吗?
原因:
针对三层,分别有BO、VO、Entity三个数据对象。包含重复的字段,甚至字段完全一样。那是否定义一个公共的数据对象更好呢?
更倾向于分别定义,原因:
那如何解决代码重复的问题呢?
如果有代码洁癖,可将公共字段定义在父类,用继承解决。此外,也能用组合解决。将公共的字段抽取到公共类,VO、BO、Entity通过组合复用这个类的代码。
不同分层之间的数据对象该如何转化呢?
最简单就是手动复制。java也提供了多种数据对象转化工具,如BeanUtils、Dozer等,简化对象转化工作。
希望设计开发一个小的框架,能获取接口调用的各种统计信息,如响应时间的最大值max、最小值min、平均值avg、百分位值percentile、接口调用次数count、频率tps等。并支持将统计结果以各种显示格式(如json格式、网页格式、自定义显示格式等)输出到各种终端(console命令行、http网页、email、日志文件、自定义输出终端等),以方便查看。
假设这是真实项目中的一个开发需求,如果让你负责开发这样一个通用的框架,应用到各种业务系统中,支持实时计算、查看数据的统计信息,如何设计和实现呢?
性能计数器作为和业务无关的功能,可开发为一个独立的框架或者类库,集成到很多业务系统中。作为可被复用的框架,除了功能性需求外,非功能性需求也很重要。
相对于一长串的文字描述,人脑更容易理解短的、罗列规整的、分门别类的列表信息,我们需要拆解为若干条:
此外,借助产品设计使用的线框图,将最终数据的显示样式画出来,更好:
其实,从线框图中,还挖掘出下面几个隐藏的需求:
对通用的框架开发,还要考虑很多非功能性的需求,包括:
对于扩展性,举例说明。feign是个http客户端框架,可不修改框架源码情况下,用如下方式来扩展我们自己的编解码方式、日志、拦截器等。
Feign feign = Feign.builder()
.logger(new CustomizedLogger())
.encoder(new FormEncoder(new JacksonEncoder))
.decoder(new JacksonDecoder)
.errorDecoder(new ResponseErrorDecoder())
.requestInterceptor(new RequestHeadersInterceptor()).build();
public class RequestHeadersInterceptor implements RequestInterceptor{
@Override
public void apply(RequestTemplate template){
template.header("appId","...");
template.header("version","...");
template.header("timestamp","...");
template.header("token","...");
template.header("idempotent-token","...");
template.header("sequence-id","...");
}
public class CustomizedLogger extends feign.Logger{
//...
}
public class ResponseErrorDecoder implements ErrorDecoder{
@Override
public Exception decode(String methodKey,Response response){
//...
}
}
}
针对需求做框架设计。
先借鉴TDD(测试驱动开发)和prototype(最小原型)的思想,聚焦一个简单的应用场景,基于此设计实现一个简单的原型。尽管这个最小原型系统在功能和非功能特性上都不完善,但比较具体,能有效地帮我们缕清更复杂的设计思路,是迭代设计的基础。
对性能计数器这个框架的开发来说,可以先聚焦一个非常具体、简单的应用场景,如统计用户注册、登录这两个接口的响应时间的最大值和平均值、接口调用次数,并将统计结果以json格式输出到命令行。
现在的需求简单、具体、明确,设计实现难度降低很多。
先给出应用场景的代码:
// 应用场景:统计下面两个接口(注册和登录)的响应时间和访问次数
public class UserController{
public void register(UserVo user){
//...
}
public UserVo login(String phone,String password){
//...
}
}
要输出接口的响应时间的最大值、平均值和接口调用次数,先要采集每次接口请求的响应时间,并存储起来,再按照某个时间间隔做聚合统计,最后输出结果。原型系统的代码实现中,可先把所有代码塞到一个类,怎么简单怎么来。
最小原型的代码实现如下。recordResponseTime()和recordTimestamp()两个方法分别用来记录接口请求的响应时间和访问时间。startRepeatedReport()方法以指定的频率统计数据并输出结果。
public class Metrics{
//map的key是接口名称,value对应接口请求的响应时间或时间戳
private Map<String,List<Double>> responseTimes = new HashMap<>();
private Map<String,List<Double>> timestamp = new HashMap<>();
private ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
public void recordResponseTime(String apiName,double responseTime){
responseTimes.putIfAbsent(apiName,new ArrayList<>());
responseTimes.get(apiName).add(responseTime);
}
public void recordTimestamp(String apiName,double timestamp){
timestamps.putIfAbsent(apiName,new ArrayList<>());
timestamps.get(apiName).add(timestamp);
}
public void startRepeatedReport(long period,TimeUnit unit){
executor.scheduleAtFixedRate(new Runnable(){
@Override
public void run(){
Gson gson = new Gson();
Map<String,List<Double>> stats = new HashMap<>();
for(Map.Entry<String,List<Double>> entry:responseTimes.entrySet()){
String apiName = entry.getKey();
List<Double> apiRespTimes = entry.getValue();
stats.putIfAbsent(apiName,new HashMap<>());
stats.get(apiName).put("max",max(apiRespTimes));
stats.get(apiName).put("avg",avg(apiRespTimes));
}
for(Map.Entry<String,List<Double>> entry:timestamps.entrySet()){
String apiName = entry.getKey();
List<Double> apiTimestamps = entry.getValue();
stats.putIfAbsent(apiName,new HashMap<>());
stats.get(apiName).put("count",(double)apiTimestamps.size());
}
System.out.println(gson.toJson(stats));
}
},0,period,unit);
}
private double max(List<Double> dataset){//省略代码实现}
private double avg(List<Double> dataset){//省略代码实现}
}
用不到50行的代码就实现了最小原型,接下来看如何用它统计注册、登录接口的响应时间和访问次数。具体代码如下:
// 应用场景:统计下面两个接口(注册和登录)的响应时间和访问次数
public class UserController{
private Metrics metrics = new Metrics();
public UserController(){
metrics.startRepeatedReport(60,TimeUnit.SECONDS);
}
public void register(UserVo user){
long startTimestamp = System.currentTimeMills();
metrics.recordTimestamp("register",startTimestamp);
//...
long respTime = System.currentTimeMills()-startTimestamp;
metrics.recordResponseTime("register",respTime);
}
public UserVo login(String phone,String password){
long startTimestamp = System.currentTimeMills();
metrics.recordTimestamp("login",startTimestamp);
//...
long respTime = System.currentTimeMills()-startTimestamp;
metrics.recordResponseTime("login",respTime);
}
}
现在基于这个最小原型的代码做最终的框架设计。下面是针对性能计数器框架画的粗略的系统设计图。
如图,分为四个模块:数据采集、存储、聚合统计、显示
软件设计开发是个迭代的过程,分析、设计和实现这三个阶段的界限划分并不明显。
作者写文章的时候,试图去实现上面罗列的所有功能需求,希望写个完美的框架,结果太过烧脑,其实罗马不是一天建成的,对于现在的互联网项目来说,小步快跑、逐步迭代是更好的开发模式。分多个版本逐步完善,第一版先实现基本功能,更高级、更复杂的功能,以及非功能性需求不做过高的要求,后续2.0、3.0版本继续优化。
在V1.0版本,暂时只实现下面这些功能:
学会结合具体的需求,做合理的预判、假设、取舍,规划版本的迭代设计开发,是资深工程师必须的能力。
按照之前讲的面向对象设计的几个步骤,重新划分、设计类。
根据需求,先大致识别出下面几个接口或类。
大致识别出几个核心的类之后,习惯先在IDE中创建好这几个类,然后试着开始定义它们的属性和方法。在设计类、类与类之间交互的时候,不断的用之前学过的设计原则和思想来审视设计是否合理,如是否满足单一职责原则、开闭原则、依赖注入、KISS原则、DRY原则、迪米特法则,是否符合基于接口而非实现编程思想,代码是否高内聚、低耦合,是否可抽象出可复用代码等。
MetricsCollector类的定义很简单,对比之前最小原型的代码,通过引入RequestInfo类来封装原始数据信息,用一个采集方法代替之前的两个方法。
public class MetricsCollector{
private MetricsStorage metricsStorage;//基于接口而非实现编程
//依赖注入
public MetricsCollector(MetricsStorage metricsStorage){
this.metricsStorage = metricsStorage;
}
//用一个方法代替最小原型中的两个方法
public void recordRequest(RequestInfo requestInfo){
if(requestInfo==null || StringUtils.isBlank(requestInfo.getApiName)){
return;
}
metricsStorage.saveRequestInfo(requestInfo);
}
}
public class RequestInfo{
private String apiName;
private double responseTime;
private long timestamp;
//...省略constructor/getter/setter方法
}
MetricsStorage类和RedisMetricsStorage类的属性和方法比较明确,具体代码如下。注意,一次性取太长时间区间的数据,可能会拉取太多的数据到内存,可能撑爆内存。对java来说,可能触发OOM,即便不出现OOM,也会因为内存吃紧,导致频繁的FullGC,导致系统接口请求处理变慢,甚至超时。之后会解决这个问题。
public interface MetricsStorage{
void saveRequestInfo(RequestInfo requestInfo);
List<RequestInfo> getRequestInfo(String apiName,long startTimeInMills,long endTimeInMillis);
Map<String,List<RequestInfo>> getRequestInfos(long startTimeInMills,long endTimeInMillis );
}
public class RedisMetricsStorage implements MetricsStorage{
//...省略属性和构造方法等
@Override
public void saveRequestInfo(RequestInfo requestInfo){
//...
}
@Override
public List<RequestInfo> getRequestInfo(String apiName,long startTimeInMills,long ){
//...
}
@Override
public Map<String,List<RequestInfo>> getRequestInfos(long startTimeInMills,long ){
//...
}
}
统计和显示这两个功能有很多设计思路,如果把统计显示所要完成的功能逻辑细分,包括4点:
其实,一句话总结的话,面向对象设计和实现要做的事情,就是把合适的代码放到合适的类中。把上述的4个功能逻辑划分到几个类中,划分方法有多种,如把前两个逻辑放到一个类中,第三个逻辑放到另一个类,第4个逻辑作为上帝类(God Class)组合前两个类来触发前3个逻辑的执行。也可以把第二个逻辑单独放到一个类,1,3,4放到另一个类中。
到底选择哪种排列组合方式,判定的标准是,让代码尽量满足低耦合、高内聚、单一职责、对扩展开放对修改关闭等设计原则和思想,让设计满足代码易复用、易读、易扩展、易维护。
暂时将1,3,4逻辑放到COnsoleReporter或EmailReporter类中,第2个逻辑放到Aggeregator类中。其中,Aggregator类负责的逻辑较简单,设计为只包含静态方法的工具类。
public class Aggregator{
public static RequestStat aggregate(List<RequestInfo> requestInfos,long durationInMills){
double maxRespTime = Double.MAX_VALUE;
double minRespTime = Double.MIN_VALUE;
double avgRespTime = -1;
double p999RespTime = -1;
double p99RespTime = -1;
double sumRespTime = 0;
long count = 0;
for(RequestInfo requestInfo:requestInfos){
count++;
double respTime = requestInfo.getResponseTime();
if(maxRespTime<respTime){
maxRespTime = respTime;
}
if(minRespTime>respTime){
minRespTime = respTime;
}
sumRespTime +=respTime;
}
if(count!=0){
avgRespTime = sumRespTime/count;
}
long tps = (long)(count/durationInMills*1000);
Collections.sort(requestInfos,new Comparator<RequestInfo>(){
@Override
public int compare(RequestInfo o1,RequestInfo o2){
double diff = o1.getResponseTime()-o2.getResponseTime();
if(diff < 0.0){
return -1;
}else if(diff >0.0){
return 1;
}else{
return 0;
}
}
});
int idx999 = (int)(count * 0.999);
int idx99 = (int)(count *0.99);
if(count !=0){
p999RespTime = requestInfos.get(idx999).getResponseTime();
p99RespTime = requestInfos.get(idx99).getResponseTime();
}
RequestStat requestStat = new RequestStat();
requestStat.setMaxResponseTime(maxRespTime);
requestStat.setMinResponseTime(minRespTime);
requestStat.setAvgResponseTime(avgRespTime);
requestStat.setP999ResponseTime(p999RespTime);
requestStat.setP99ResponseTime(p99RespTime);
requestStat.setCount(count);
requestStat.setTps(tps);
return requestStat;
}
}
public class RequestStat{
private double maxResponseTime;
private double minResponseTime;
private double avgResponseTime;
private double p999ResponseTime;
private double p99ResponseTime;
private long count;
private long tps;
//...省略getter/setter方法...
}
ConsoleReporter类相当于一个上帝类,定时根据给定的时间区间,从数据库中取出数据,借助Aggregator类完成统计公祖,并将统计结果输出到命令行。
public class ConsoleReporter{
private MetricsStorage metricsStorage;
private ScheduledExecutorService executor;
public ConsoleReporter(MetricsStorage metricsStorage){
this.metricsStorage = metricsStorage;
this.executor = Executors.newSingleThreadScheduledExecutor();
}
//第4个代码逻辑:定时触发1,2,3代码逻辑的执行
public void startRepeatedReport(long periodInSeconds,long durationInSeconds){
executor.scheduleAtFixedRate(new Runnable(){
@Override
public void run(){
//第1个代码逻辑:根据给定的时间区间,从数据库拉取数据
long durationInMills = durationInSeconds*1000;
long endTimeInMills = System.currentTimeMillis();
long startTimeInMills = endTimeInMills-durationInMills;
Map<String,List<RequestInfo>> requestInfos =
metricsStorage.getRequestInfos(startTimeInMills,endTimeInMills);
Map<String, RequestStat> stats = new HashMap<>();
for(Map.Entry<String,List<RequestInfo>> entry:requestInfos.entrySet()){
String apiName = entry.getKey();
List<RequestInfo> requestInfosPerApi = entry.getValue();
//第2个代码逻辑:根据原始数据,计算得到统计数据
RequestStat requestStat = Aggregator.aggregate(requestInfosPerApi,durationInMills);
stats.put(apiName,requestStat);
}
//第3个代码逻辑:将统计数据显示到终端(命令行或邮件)
System.out.println("Time Span: ["+startTimeInMills+", "+endTimeInMills);
Gson gson = new Gson();
System.out.println(gson.toJson(stats));
}
}, 0, periodInSeconds, TimeUnit.SECONDS);
}
}
public class EmailReporter{
private static final Long DAY_HOURS_IN_SECONDS = 86400L;
private MetricsStorage metricsStorage;
private EmailSender emailSender;
private List<String> toAddresses = new ArrayList<>();
public EmailReporter(MetricsStorage metricsStorage){
this(metricsStorage, new EmailSender(//省略参数));
}
public EmailReporter(MetricsStorage metricsStorage,EmailSender emailSender){
this.metricsStorage = metricsStorage;
this.emailSender = emailSender;
}
public void addToAddress(String address){
toAddresses.add(address);
}
public void startDailyReport(){
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.DATE,1);
calendar.set(Calendar.HOUR_OF_DAY,0);
calendar.set(Calendar.MINUTE,0);
calendar.set(Calendar.SECOND,0);
calendar.set(Calendar.MILLISECOND,0);
Date firstTime = calendar.getTime();
Timer timer = new Timer();
timer.schedule(new TimerTask(){
@Override
public void run(){
long durationInMillis = DAY_HOURS_IN_SECONDS*1000;
long endTimeInMillis = System.currentTimeMillis();
long startTimeInMillis = endTimeInMillis-durationInMillis;
Map<String,List<RequestInfo>> requestInfos = metricsStorage.getRequestInfos(startTimeInMillis,endTimeInMillis);
Map<String,RequestStat> stats = new HashMap<>();
for(Map.Entry<String,List<RequestInfo>> entry:requestInfos.entrySet()){
String apiName = entry.getKey();
List<RequestInfo> requestInfosPerApi = entry.getValue();
RequestStat requestStat = Aggregator.aggregate(requestInfosPerApi,durationInMillis);
stats.put(apiName,requestStat);
}
//TODO 格式化为html格式,并发送邮件
}
}, firstTime,DAY_HOURS_IN_SECONDS*1000);
}
}
因为这个框架稍微特殊,有两个执行入口,一个是MetricsCollector类,提供了一组API采集原始数据;另一个是ConsoleReporter类和EmailReporter类,用来触发统计显示。具体的使用方式如下
public class Demo{
public static void main(String[] args){
MetricsStorage storage = new RedisMetricsStorage();
ConsoleReporter consoleReporter = new ConsoleReporter(storage);
consoleReporter.startRepeatedReport(60,60);
EmailReporter emailReporter = new EmailReporter(storage);
emailReporter.addToAddress("[email protected]");
emailReporter.startDailyReport();
MetricsCollector collector = new MetricsCollector(storage);
collector.recordRequest(new RequestInfo("register",123,10234));
collector.recordRequest(new RequestInfo("register",223,11234));
collector.recordRequest(new RequestInfo("register",323,12234));
collector.recordRequest(new RequestInfo("login",23,12434));
collector.recordRequest(new RequestInfo("login",1123,14434));
try{
Thread.sleep(10000);
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
前面讲过SOLID KISS DRY YAGNI LOD等设计原则,基于接口而非实现编程、多用组合少用继承、高内聚低耦合等设计思想,上面的代码是否符合这些设计原则和思想。
MetricsCollector负责采集和存储数据,职责相对较为单一。基于接口而非实现编程,通过依赖注入的方式来传递MetricsStorage对象,可在不需要修改代码的情况下,灵活的替换不同的存储方式,符合开闭原则。
MetricsStorage RedisMetricsStorage的设计较为简单。需要实现新的存储方式的时候,只要实现MetricsStorage即可。
Aggregator类是一个工具类,只有一个静态方法,50行左右的代码,负责各种统计数据的计算,需要扩展新的统计功能时,需要修改aggregate()方法代码,可能存在职责不够单一、不易扩展的缺点,后续版本继续优化
ConsoleReporter EmailReporter存在代码重复的问题,显示部分的代码较为复杂(如Email的展示部分),最好抽出来拆分为单独的类。此外,涉及到线程操作,并且调用Aggregator的静态方法,代码可测试性不好。