OptaPlanner Constraint Streams如何动态定义约束为硬或软约束
场景说明
在使用OptaPlanner开发员工排班系统时,需要将原有DRL编写的排班规则重构为Constraint Streams实现,要求支持用户自定义单条规则的约束类型(硬约束/软约束)。
原有DRL实现可在规则then代码段访问Roster规划实体,动态判断当前规则需要触发硬扣分还是软扣分,但Constraint Streams的penalize方法默认要求传入固定的权重常量,原有写法无法直接复用,现有待改造代码如下:
Constraint holiday(ConstraintFactory constraintFactory) { return constraintFactory.forEach(Shift.class) .join( Absence.class, Joiners.equal(Shift::getEmployee, Absence::getEmployee), Joiners.greaterThanOrEqual(Shift::getDate, Absence::getStartDate), Joiners.lessThanOrEqual(Shift::getDate, Absence::getEndDate) ).penalize( "holiday", // 此处需要动态判断使用ONE_HARD还是ONE_SOFT HardMediumSoftLongScore.ONE_HARD, (shift,absence) -> { Roster roster = shift.getRoster(); return roster.getRules().get("holiday").getPenaltyValue(); } ); }
可行实现方案
Constraint Streams的单条约束权重层级必须在约束构建阶段固定,不支持在事实匹配阶段动态切换硬/软权重层级,可根据业务场景选择以下两种实现方式:
方案一:约束构建前提前加载配置(推荐,性能最优)
Roster作为@PlanningSolution标注的规划解决方案对象,其携带的规则配置可以在构建Solver实例前提前读取,直接在约束定义阶段传入对应类型的固定权重,不需要在事实匹配阶段做动态判断。
实现时可给ConstraintProvider增加带配置参数的构造函数,初始化时传入当前求解场景的规则配置:public class RosterConstraintProvider implements ConstraintProvider { // 持有当前场景的规则配置 private final Map<String, RuleProperty> ruleConfigMap; // 构建ConstraintProvider时提前传入配置 public RosterConstraintProvider(Map<String, RuleProperty> ruleConfigMap) { this.ruleConfigMap = ruleConfigMap; } @Override public Constraint[] defineConstraints(ConstraintFactory constraintFactory) { return new Constraint[] { holiday(constraintFactory) }; } private Constraint holiday(ConstraintFactory constraintFactory) { RuleProperty holidayRule = ruleConfigMap.get("holiday"); // 构建约束时直接确定权重层级 HardMediumSoftLongScore baseWeight = holidayRule.isHard() ? HardMediumSoftLongScore.ONE_HARD : HardMediumSoftLongScore.ONE_SOFT; return constraintFactory.forEach(Shift.class) .join( Absence.class, Joiners.equal(Shift::getEmployee, Absence::getEmployee), Joiners.greaterThanOrEqual(Shift::getDate, Absence::getStartDate), Joiners.lessThanOrEqual(Shift::getDate, Absence::getEndDate) ).penalizeLong( "holiday", baseWeight, (shift, absence) -> holidayRule.getPenaltyValue() ); } }该方案没有额外的运行时判断开销,分数计算性能最好,是官方推荐的实现方式。如果不同排班场景的规则配置存在差异,每个场景求解前传入对应配置重新构建Solver实例即可。
方案二:拆分双约束分支(适配无法提前读取配置的场景)
如果受架构限制无法在约束构建阶段拿到规则配置,必须从匹配到的Shift关联的Roster对象上读取配置,可将单条业务规则拆分为硬约束、软约束两个独立的约束分支,匹配时先通过filter过滤出对应配置类型的事实,再分别执行对应层级的扣分:private Constraint[] holiday(ConstraintFactory constraintFactory) { // 抽离公共匹配逻辑,避免重复编写关联逻辑 BiConstraintStream<Shift, Absence> baseMatchStream = constraintFactory.forEach(Shift.class) .join( Absence.class, Joiners.equal(Shift::getEmployee, Absence::getEmployee), Joiners.greaterThanOrEqual(Shift::getDate, Absence::getStartDate), Joiners.lessThanOrEqual(Shift::getDate, Absence::getEndDate) ); // 硬约束分支:仅当规则配置为硬约束时扣分 Constraint hardHoliday = baseMatchStream .filter((shift, absence) -> shift.getRoster().getRules().get("holiday").isHard()) .penalizeLong("holiday", HardMediumSoftLongScore.ONE_HARD, (shift, absence) -> shift.getRoster().getRules().get("holiday").getPenaltyValue()); // 软约束分支:仅当规则配置为软约束时扣分 Constraint softHoliday = baseMatchStream .filter((shift, absence) -> !shift.getRoster().getRules().get("holiday").isHard()) .penalizeLong("holiday", HardMediumSoftLongScore.ONE_SOFT, (shift, absence) -> shift.getRoster().getRules().get("holiday").getPenaltyValue()); return new Constraint[]{hardHoliday, softHoliday}; }注意:该方案会产生两条独立的约束匹配链路,运行时性能弱于方案一,仅作为兼容场景使用。绝对不要尝试在
penalize的评分lambda中直接返回包含不同层级的Score对象,单条约束的权重层级必须和传入的基础权重保持一致,否则会出现评分计算错误。
内容的提问来源于stack exchange,提问作者Mischa Stone

