什么是「日历」服务,相信大家都用过,或者看到过。就像非计算机时代,大家 也会买个挂历,然后把什么时候要做什么事用笔圈起来,然后每过一个月,一天,就撒一页,这样到了做标记处理事情的日子,我们就可以知道今天有个什么事情要 做,比如妈妈的生日,同学聚会的日子等。当然现在互联网应用时代我们会用更好的软件应用管理好我们的日历提醒事件,比如大家最常用的Google日 历,QQ日历:
如上图所示,就是Google的日历产品,我添加了一个每月7号还贷的事件,这样每个月的7号前,比如说6号上午9点,我就会收到一封 Google的邮件,或者手机短信提示我明天要还房贷了,这样我就会立即处理这个事情。现在大家应该对这样类似的产品有个感性的认识了,在生活中也能给我 们很多帮助,这样可以在亲人生日,还贷还款,朋友聚会,商务会议等很多场景中帮我们记忆事件活动提醒,不用自己每天记一堆的事情,而且还容易忘记。好了, 说完这个产品的背景,现在我们想想应该怎样从技术上设计,实现这个产品呢?
设计实现一个「日历」服务产品,我觉得有两个难点,一是「重复事件规则计算」,二是「到点事件准时提醒」。这篇文章主要讲解第一点,「重复规则的设计和实现」,下一篇博客讨论下怎样保证准时到点提醒。
首先,我们必须定义什么是「事件」或者称「活动」。所谓事件/活动,在「日历产品」中就是我们创建的提醒事件本身,如「每月7号还房贷」就是一个事件/活动。
/** * 日历事件/活动. * * * @author tony.li.fly@gmail.com */ public class Event { /** * 事件标题 */ private String title; /** * 事件描述 */ private String description; /** * 事件发生地点 */ private String location; /** * 事件发生的开始时间 */ private Date startDate; /** * 事件发生的结束时间 */ private Date endDate; /** * 日历类型: 1,公历 2,农历 */ private int calendarType; /** * 重复事件规则表达式 */ private String rule;
所以一个事件的基本的几个属性有:标题,描述,地点,事件开始时间,事件结束时间,还有后面要重点讨论的重复规则表达式和日历类型。
看下Google产品中设置重复事件的页面:
上图是设置一个重复事件的设置页面,可以看到设置项还是挺多的,有「重复类型」:按日,周,月,年重复;「重复频率」:每几天,几周....等发生 一次;「重复日期」:按月重复时是「一月中的某天呢」,还是「一周中的某天呢」;「结束日期」:重复事件到什么时候结束呢。所以从Google产品功能和 我们的描述可以知道想要准确描述一个「重复事件」,要具备如下几个元素:
现在问题来了,我们怎样定义「重复规则」的数据结构呢?基于重复规则的复杂性和弹性可变性(之所以说弹性可变性,因为我们不能保证自己的产品不会有一些个性化的规则,比如支持农历日历,怎样表示清明节这样的日期),用字符串表达式定义,持久化存储更为理想,就像正则表达式一样,我们可以用一个字符串表达任何丰富的信息在里面。其实对于重复事件的描述,设计,我们可以遵守一定的业界标准。在RFC2445中有详细的定义,这里我精简总结下:
freq *( ; either UNTIL or COUNT may appear in a ‘recur‘, ; but UNTIL and COUNT MUST NOT occur in the same ‘recur‘ ( ";" "UNTIL" "=" enddate ) / ( ";" "COUNT" "=" 1*DIGIT ) / ; the rest of these keywords are optional, ; but MUST NOT occur more than once ( ";" "INTERVAL" "=" 1*DIGIT ) / ( ";" "BYDAY" "=" bywdaylist ) / ( ";" "BYMONTHDAY" "=" bymodaylist ) / ( ";" "BYYEARDAY" "=" byyrdaylist ) / ( ";" "BYWEEKNO" "=" bywknolist ) / ( ";" "BYMONTH" "=" bymolist ) )
上面是「重复规则表达式」的公式定义,详细解释如下:
需要注意几点:
通过上面的公式定义,基本上可以表示出任何一个重复事件的定义,下面来做一些练习:
目标 : 按天重复, 且每3天重复一次
表达式: DAILY;INTERVAL=3
目标 : 按天重复, 重复到今年结束
表达式: DAILY;UNTIL=20140101T000000Z
目标 : 按天重复, 重复20次
表达式: DAILY;COUNT=20
目标 : 每周二,周四,周日重复,每隔2周发生一次
表达式: WEEKLY;INTERVAL=2;BYDAY=TU,TH,SU
目标 : 每年的七月,八月两月最后一天
表达式: YEARLY;BYMONTH=7,8;BYMONTHDAY=-1
目标 : 每年六月的第三个星期日
表达式: YEARLY;BYMONTH=6;BYDAY=3SU
目标 : 农历每年的最后一天
表达式: YEARLY;BYMONTH=12;BYMONTHDAY=-1
目标 : 每年11月的第四个星期四
表达式: YEARLY;BYMONTH=11;BYDAY=4TH
目标 : 每年5月的第二个星期日
表达式: YEARLY;BYMONTH=5;BYDAY=2SU
什么叫重复规则的解析和计算?解析,就是把上面的重复规则表达式解析成我们程序内部结构化的对象;计算,就是我们能知道某个重复事件在将来的哪些时间点上会发生。
上图所示,我们先把重复表达式字符串解析成程序内部的结构化数据实例,然后计算在整个将来的时间轴上该重复事件在什么时间点发生。
我们先定义一个接口 Rule ,它就是根据重复事件解析后的规则引擎实例接口,它应该具有如下方法:
上图中,定义一个按月重复活动(每月15号发生)。执行nextOccurDate(‘2013-11-28‘)方法返回的结果就是以2013-11-28这个时间为起始值,计算下一次事件发生的时间点,即返回2013-12-15. 其实nextOccurDate和includes两 个方法表达的意思一样的,只是从不同的两个方面去定义。我们知道了任一时间点之后的事件发生时间,那么我们也就知道了指定的一个时间是否满足事件发生的要 求。现在我们要考虑的重点问题是:怎样简单高效的实现这两个方法,保证计算的准确性和性能。在实现之前,我们有必要来认清这个计算中的难点在哪里:
我自己能想到的实现方法有两种:「实时计算法」和「枚举法」。下面来分别讨论一下:
实时计算,可以理解成「无状态」的实时求值。每次根据传入的参数计算并返回。
每次我们计算时,都没有任何上下文信息,只要知道开始时间和「重复规则配置」,实时根据公式计算出下一次的发生时间。我们分析下这个计算过程的可 行性:根据事件最先发生的开始时间和当前传入的时间值,我们知道两者的时间差,然后根据「重复周期值:interval」可以知道下次发生的时间所在的周 期区间。缩小在指定时间周期区间后,再根据具体的某天,某月,某年的信息,即可以算出最终的下次发生的时间点。所以从这里分析来看,好像理论上是可行的。但是有几点障碍使我觉得这种计算方法不能很完美:
枚举法,可以理解成「有状态」的比较计算。每次调用都是根据传入的值和「预存计算好的值」比较。
我们总是先把该重复事件所有要发生的时间线上的点都计算出来,并保存起来。以后每次调用计算方法时,只要根据传入的参数值马上知道它的上次和下次发 生时间点。相比上面的「实时计算法」,它的优点显而易见:简单,快速,并且可以解决上面方法中无法处理的两点。但是缺点你也想到了:那要多少空间存储这些 预计算的值? 但是任何产品,都有它的实际使用场景,我想任何人使用「日历产品」的时候我们关注的时间区间都是以今天为中心两边延伸的时间区间,而且一般这个区间不会超 过1年,或者2年吧。所以我们可以先计算出以今天为中心的前后各十年(这个看你估量设置)的时间区间上所有发生时间点。
当我们创建完一个活动事件后(Event),我们就可以通过该事件(Event)的「重复规则表达式字符串」,利用 RuleFactory 来创建Rule对象。有了Rule对象,我们就可以进行相应的计算求值了。我们知道 Rule 只是一个接口,我们返回接口这也符合设计的准则,对外屏蔽内部的具体实现,使调用者根本不用知道里面的计算实现方法。Rule的层级关系如下图:
这张图看起来类有点多,但是一点都不复杂,它的层次设计也是完全按照业务模型来设计的。简要说明一下这几个类:
具体的代码请参见git项目地址:https://github.com/hongfuli/simplecal ,参考代码注意几点:里面有两个分支,master和redis这两个分支对象于「实时计算」和「枚举」两种实现方式;代码没用maven管理,如果缺少什么jar包请上网下载;「枚举」分支用的redis实现,请了解下redis的使用。
好了,关于规则的设计和讨论我就写到这里,最后还是真的希望大家留言把更好的设计告诉我,一起参与讨论下。后面文章还会写关于扫描提醒方面的东西。
原文:http://www.cnblogs.com/ericsun/p/3529005.html