从图中能够看出,每门课所需的时间都是固定一小时。具体到这个模式的应用,由于其原理、结构和实现起来都至关简单,本文不经过示例详细讲解了。可参考示例包中的Course timetabling中的设计和代码。html
从上图能够看到,每一个会议所需的时间长度是不相等的,可是其长度必然是一个Time Grain的倍数,从图中上方的时间刻度能够比划出一个TimeGrain应该是15分钟。例如Sales meeting占用了4个Time Grain,即时长1小时。Time Grain模式的使用会相对Time Slot更灵活,适用范围会更广。经过设置可知,其实适用于Time Slot模型的情形,是彻底能够经过TimeGrain模式实现的,只是实现起来会更复杂一些。那么Time Grain模式的设计要点在哪里呢?要了解其设计原理,就得先掌握Time Grain的结构及其对时间的提供方法。微信
public int calculateOverlap(MeetingAssignment other) { if (startingTimeGrain == null || other.getStartingTimeGrain() == null) { return 0; }
int start = startingTimeGrain.getGrainIndex(); int end = start + meeting.getDurationInGrains(); int otherStart = other.startingTimeGrain.getGrainIndex(); int otherEnd = otherStart + other.meeting.getDurationInGrains(); if (end < otherStart) { return 0; } else if (otherEnd < start) { return 0; } return Math.min(end, otherEnd) - Math.max(start, otherStart); }
Chained Through Time模式与其两种时间规划模式不一样,本质上它并不对时间进行规划,只对实体之间的关系进行规划优化。所以,在引擎每个原子操做中须要经过对VariableListener接口的实现,来对时间进行推算,并在完成推算后,由引擎经过评分机制进行约束评分。一个Move有可能对应多个原子操做,一个Move的操做种类,能够参见开发 手册中关于Move Selector一章,在之后对引擎行为进行深刻分析的文章中,我将会写一篇关于Move Seletor的文件,来揭示引擎的运行原理。在须要进行时间推算时,能够经过实现接口的afterVariableChanged方法,对当前所处理的规划实体的时间进行更新。由于Chained Through Timea模式下,全部已初始化的规划实体都处在一条链上;所以,当一个规划实体的时间被更新后,跟随着它的后一个规划实体的时间也须要被更新,如此类推,直到链上最后一个实体,或出现一个时间正好不须要更新的规划实体,即该规划实体前面的全部实体的时间出现更新后,其时间不用变化,那么链上从它日后的规划实体的时候也无需更新。网络
如下是VariableListener接口的afterVariableChanged及其处理方法。ide
// 实现VariableListener的类 public class StartTimeUpdatingVariableListener implements VariableListener<Task> { // 实现afterVariableChanged方法 @Override public void afterVariableChanged(ScoreDirector scoreDirector, Task task) { updateStartTime(scoreDirector, task); } @Override public void beforeEntityAdded(ScoreDirector scoreDirector, Task task) { // Do nothing } @Override public void afterEntityAdded(ScoreDirector scoreDirector, Task task) { updateStartTime(scoreDirector, task); } . . . }
//当一个任务的时候被更新时,顺着链将它后面全部任务的时候都更新 protected void updateStartTime(ScoreDirector scoreDirector, Task sourceTask) { Step previous = sourceTask.getPreviousStep(); Task shadowTask = sourceTask; Integer previousEndTime = (previous == null ? null : previous.getEndTime()); Integer startTime = calculateStartTime(shadowTask, previousEndTime); while (shadowTask != null && !Objects.equals(shadowTask.getStartTime(), startTime)) { scoreDirector.beforeVariableChanged(shadowTask, "startTime"); shadowTask.setStartTime(startTime); scoreDirector.afterVariableChanged(shadowTask, "startTime"); previousEndTime = shadowTask.getEndTime(); shadowTask = shadowTask.getNextTask(); startTime = calculateStartTime(shadowTask, previousEndTime); } }
上一步咱们介绍了如何经过链在引擎的运行过程当中进行时间推算,那么如何设定才能让引擎能够执行VariableListener中的方法呢,这就须要在规划实体的设计过程当中,反映出Chained Through Time的特性了。咱们以上面的类图为例,理解下面其设计要求,在此示例中,把Task做为规划实体(Planning Entity), 那么在Task类中须要定义一个Planning Variable(genuine planning variable), 它的类型是Step,它表示当前Task的上一个步骤(多是另外一个Task,也多是一Machine). 此外,在 @PlanningVariable注解中,添加graphType = PlanningVariableGraphType.CHAINED说明。以下代码:工具
// Planning variables: changes during planning, between score calculations. @PlanningVariable(valueRangeProviderRefs = {"machineRange", "taskRange"}, graphType = PlanningVariableGraphType.CHAINED) private Step previousStep;
以上代码说明,规划实体(Task)的genuine planning variable名为previousStep, 它的Value Range有两个来源,分别是机台列表(machineRange)和任务列表(taskRange),而且添加了属性grapType=planningVariableGraphType.CHAINED, 代表将应用Chained Through Time模式运行。优化
有了genuine planning variable, 还须要Shadow variable, 所谓的Shadow variable,在Chained Through Time模式下有两种做用,分别是:ui
1. 用于创建两个对象(Entity或Anchor)之间的双向依赖关系;即示例中的Machine与Task, 相邻的两个Task。google
2. 用于指定当genuine planning variable的值在规划运算过程产生变化时,须要更改哪一个变量;即上面提到的开始时间。spa
,对于第一个做用,其代码体现以下,在规划实体(Task)中,以@AnchorShadowVariable注解,并在该注解的sourceVariableName中指定该Shadow Variable在链上的前一个对象指向的是哪一个变量。翻译
// Shadow variables // Task nextTask inherited from superclass @AnchorShadowVariable(sourceVariableName = "previousStep") private Machine machine;
上述代码说明成员machine是一个Anchor Shadow Variable, 在链上,它链接的前一个实体是实体类的一个成员 - previousStep.
Chained Through Time中的链须要造成双向关系(bi-directional),下图是路线规划示例中。一个客户与上一个停靠点之间的双向关系。
在规划实体(Task)中咱们已经定义了前一个Step,并以@AnchorShadowVariable注解标识。而双向关系中的另外一方,则须要在相邻节点中的前一个节点定义。经过链的内存模型,咱们能够知道,在生产计划示例中,一个实体的前一个节点的类型多是另外一个Task, 也要能是一个Machine, 所以,前一个节点指向后一个节点的规划变量,只能在Task与Machine的共同父类中定义,也就是须要在Step中实现。所以,在Step类中须要定义另外一个Shadow Variable, 由于相对于Task中的Anchor Shadow variable, 它是反现的,所以,它须要经过@InverseRelationShadowVariable注解,说明它在链上起到反向链接做用,即它是指向后一个节点的。代码以下:
@PlanningEntity public abstract class Step{ // Shadow variables @InverseRelationShadowVariable(sourceVariableName = "previousStep") protected Task nextTask; . . . }
能够从代码中看到,Step类也是一个规划实体.其中的一个成员nextTask, 它的类型是Task,它表示在链中指向后面的Entity. 你们能够想一下,为何它能够是一个Task, 而无需是一个Step。
经过上述设计,已经实现了Chained Through Time的基本模式,可能你们还会问,上面咱们实现了VariableListener, 引擎是如何触发它的呢。这就须要用到另一种Shadow Variable了,这种Shadow Varible是用于实如今运算过程当中执行额外处理的,所以称为Custom Shadow Variable.
// 自定义Shadow Variable, 它表示当 genuine被引擎改变时,须要处理哪一个变量。 @CustomShadowVariable(variableListenerClass = StartTimeUpdatingVariableListener.class, sources = {@PlanningVariableReference(variableName = "previousStep")}) private Integer startTime; // 由于时间在规划过程当中以相对值进行运算,所以以整数表示。
上面的代码经过@CustomShadowVariable注解,说明了Task的成员startTime是一个自定义的Shadow Variable. 同时在注解中添加了variableListenerClass属性,其值指定为刚才咱们定义的,实现了VariableListener接口的类 - StartTimeUpdatingVariableListener,同时,能冠军sources属性指定,当前Custom Shadow Variable是跟随着genuine variable - previousStep的变化而变化的。
至此,关于Chained Through Time中的关键要点已所有设计实现,具体的使用能够参照示例包中有用到此模式的代码。
总结
关于时间的规划,在实际的系统开发时,并不仅本文描述的那么简单,关于最为复杂的Chained Through Time模式,你们能够经过本文了解其概念、结构和要点,再结合示例包中的代码进来理解,才能掌握其要领。且现实项目中也有许许多多的个性规则和要求,须要经过你们的技巧来实现;但万变不离其宗,全部处理特殊状况的技巧,都须要甚至Optaplanner这些既有特性。所以,你们能够先经过示例包中的代码将这些特性掌握,再进行更复杂状况下的设计开如。将来若时间容许,我将分享我在项目中遇到的一些特殊,甚至是苛刻的规则要求,及其处理办法。
如需了解更多关于Optaplanner的应用,请发电邮致:kentbill@gmail.com
或到讨论组发表你的意见:https://groups.google.com/forum/#!forum/optaplanner-cn
如有须要可添加本人微信(13631823503)或QQ(12977379)实时沟通,但因本人平常工做繁忙,经过微信,QQ等工具可能没法深刻沟通,较复杂的问题,建议以邮件或讨论组方式提出。(讨论组属于google邮件列表,国内网络可能较难访问,需自行解决)