用于处理复杂条件评估的设计模式

Qua*_*cem 5 java refactoring design-patterns conditional-statements

我的目的是维护一个系统,该系统考虑三个变量的值来确定将采取哪个操作。

我想重构它以使用设计模式,但找不到适合它需要的设计模式。

为了解释这种情况,我将以健身房系统为例。

每个健身房用户都有一个TYPE_OF_CONTRACT,可能是:

  • PLATINUM_会员资格
  • 金卡会员
  • 银级会员

健身房有一些GYM_CLASSES

  • 举重
  • 身体平衡
  • 纺纱
  • 尊巴舞
  • 个人培训

每个健身房用户都有一个PHYSICAL_CONDITION

  • 无限制
  • OVER_65
  • LIMITED_MOBILITY
  • 医疗条件
  • 下面_18

对于这三个特征的每种组合,都应该执行一组任意操作。例如:

如果是 PLATINUM_MEMBERSHIP + PERSONAL_TRAINING + OVER_65:

  1. 需要医疗批准
  2. 签署表格

如果是 GOLD_MEMBERSHIP + 个人培训 + OVER_65:

  1. 需要医疗批准
  2. 签署表格
  3. 额外月费

如果是 SILVER_MEMBERSHIP + PERSONAL_TRAINING + OVER_65:

  1. 拒绝订阅

如果(任何会员资格)+ 步骤 + 医疗条件:

  1. 需要医疗批准
  2. 签署表格

如果 PLATINUM_MEMBERSHIP + WEIGHT_LIFTING + LIMITED_MOBILITY:

  1. 需要医疗批准
  2. 签署表格
  3. 专职工作人员协助

等等。

特征的组合可以具有一组动作,这些动作不是排他性的并且并非所有组合都得到保证。

遗留代码使用嵌套开关作为实现。例子:

switch (contractType):

    case PLATINUM_MEMBERSHIP:

        switch (gymClass):            

            case (PERSONAL_TRAINING):

                switch (physicalCondition):            

                    case (OVER_65):

                        requiresMedicalApproval();
                        requiresSignedForm();

...
Run Code Online (Sandbox Code Playgroud)

我的问题是:

  • 有 3 个条件组合起来定义一组规则;
  • 这些规则不一定是唯一的;
  • 并非每个组合都定义一个集合;

我使用提取方法技术进行了一些重构并稍微清理了代码,但无法摆脱这 3 个开关。

我希望使用设计模式来改进设计,但到目前为止我还没有成功。

我考虑过多态性和策略,但无法找到使用它们的方法。

我也在谷歌上进行了研究,但没有找到任何我可以使用的东西。

你有什么建议?

谢谢。


编辑:

我在研究 @Paul 的决策树方法时达成的解决方案。在使用决策树进行测试后,我尝试了一个三维数组来定义规则的条件。我还使用命令模式来定义激活规则时需要执行的操作。

简单来说:

1) 枚举来定义变量:

public enum TypeOfContract { ... }
public enum GymClasses { ... }
public enum PhysicalCondition { ... }
Run Code Online (Sandbox Code Playgroud)

所有可能的条件都会被放入枚举中。

2)定义操作的命令接口

public interface Command {
    public void execute(Map<String, Object> parametersMap);
}
Run Code Online (Sandbox Code Playgroud)

每一个行动都是命令的执行。Map 参数将用于将运行时上下文传递给方法。

3) 一个程序类,用于保存每个条件所需的操作。

public class Procedures {

    private List<Command> actionsToExecute = new LinkedList<Command>();

    public static final Procedures NO_ACTIONS_TO_EXECUTE = new Procedures();

    private Procedures() {}

    public Procedures(Command... commandsToExecute) {

        if (commandsToExecute == null || commandsToExecute.length == 0) {
            throw new IllegalArgumentException("Procedures must have at least a command for execution.");
        }

        for (Command command : commandsToExecute) {
            actionsToExecute.add(command);
        }
    }

    public List<Command> getActionsToExecute() {
        return Collections.unmodifiableList(this.actionsToExecute);
    }   
}    
Run Code Online (Sandbox Code Playgroud)

procedures 类代表需要执行的命令。它有一个命令的链表,以确保命令按所需的顺序执行。

如果三个变量的组合不存在,它会发送 NO_ACTIONS_TO_EXECUTE 而不是 null。

4)RulesEngine类,用于注册规则及其命令

public class RulesEngine {

    private static final int NUMBER_OF_FIRST_LEVEL_RULES = TypeOfContract.values().length;
    private static final int NUMBER_OF_SECOND_LEVEL_RULES = GymClasses.values().length;
    private static final int NUMBER_OF_THIRD_LEVEL_RULES = PhysicalCondition.values().length;

    private static final Procedures[][][] RULES =
            new Procedures[NUMBER_OF_FIRST_LEVEL_RULES]
                    [NUMBER_OF_SECOND_LEVEL_RULES]
                    [NUMBER_OF_THIRD_LEVEL_RULES];

    { //static block
        RULES
            [TypeOfContract.PLATINUM_MEMBERSHIP.ordinal()]
            [GymClasses.PERSONAL_TRAINING.ordinal()]
            [PhysicalCondition.OVER_65.ordinal()] =
                new Procedures(new RequireMedicalApproval(), 
                               new RequireSignedForm() );

        RULES
            [TypeOfContract.GOLD_MEMBERSHIP.ordinal()]
            [GymClasses.PERSONAL_TRAINING.ordinal()]
            [PhysicalCondition.OVER_65.ordinal()] =
                new Procedures(new RequireMedicalApproval(), 
                               new RequireSignedForm(), 
                               new AddExtraMonthlyFee() );

        ...             

    }

    private RulesEngine() {}

    public static Procedures loadProcedures(TypeOfContract TypeOfContract, 
            GymClasses GymClasses, PhysicalCondition PhysicalCondition) {
        Procedures procedures = RULES
                                [TypeOfContract.ordinal()]
                                [GymClasses.ordinal()]
                                [PhysicalCondition.ordinal()];
        if (procedures == null) {
            return Procedures.NO_ACTIONS_TO_EXECUTE;
        }
        return procedures;
    }

}
Run Code Online (Sandbox Code Playgroud)

(为了本网站的可视化目的,进行了不寻常的代码格式化)

这里变量的有意义的关联是在 RULES 三维数组中定义的。

这些规则是通过使用相应的枚举来定义的。

对于我给出的第一个示例,PLATINUM_MEMBERSHIP + PERSONAL_TRAINING + OVER_65,适用以下内容:

RULES
    [TypeOfContract.PLATINUM_MEMBERSHIP.ordinal()]
    [GymClasses.PERSONAL_TRAINING.ordinal()]
    [PhysicalCondition.OVER_65.ordinal()]
Run Code Online (Sandbox Code Playgroud)

(需要ordinal()返回与枚举位置对应的int)

为了表示需要执行的操作,关联了一个过程类,包装了要执行的操作:

new Procedures(new RequireMedicalApproval(), new RequireSignedForm() );
Run Code Online (Sandbox Code Playgroud)

RequireMedicalApproval 和 RequireSignedForm 都实现 Command 接口。

定义这个变量组合的整行代码是:

RULES
        [TypeOfContract.PLATINUM_MEMBERSHIP.ordinal()]
        [GymClasses.PERSONAL_TRAINING.ordinal()]
        [PhysicalCondition.OVER_65.ordinal()] =
            new Procedures(new RequireMedicalApproval(), 
                           new RequireSignedForm() );
Run Code Online (Sandbox Code Playgroud)

要检查特定组合是否具有与其关联的操作,请loadProcedures调用 ,并传递表示该特定组合的枚举。

5) 使用方法

    Map<String, Object> context = new HashMap<String, Object>();
    context.put("userId", 123);
    context.put("contractId", "C45354");
    context.put("userDetails", userDetails);
    context.put("typeOfContract", TypeOfContract.PLATINUM_MEMBERSHIP);
    context.put("GymClasses", GymClasses.PERSONAL_TRAINING);
    context.put("PhysicalCondition", PhysicalCondition.OVER_65);
    ...

    Procedures loadedProcedures = RulesEngine.loadProcedures(
                                        TypeOfContract.PLATINUM_MEMBERSHIP, 
                                        GymClasses.PERSONAL_TRAINING, 
                                        PhysicalCondition.OVER_65);

    for (Command action : loadedProcedures.getActionsToExecute()) {
        action.equals(context);
    }
Run Code Online (Sandbox Code Playgroud)

操作需要执行的所有信息现在都位于地图内。

由三个枚举表示的条件被传递到规则引擎。

RulesEngine 将评估该组合是否具有关联的操作,并将返回一个 procedures 对象,其中包含需要执行的这些操作的列表。

如果不是(该组合没有与之关联的操作),RulesEngine 将返回一个带有空列表的有效过程对象。

6) 优点

  • 使用代码更加清晰
  • 遗留代码开关中的重复代码现已消失
  • 这些操作现已标准化且定义明确(每个操作都在其自己的类中)
  • 现在使用的规则更容易辨别(开发人员只需查看 RULES 数组即可知道设置了哪些规则以及每个规则会发生什么)
  • 可以轻松添加新规则和操作

7) 缺点

  • 在规则的定义中很容易犯错误,因为它们的声明很冗长并且没有进行语义分析 - 它会接受重复,例如,可能会覆盖以前的定义。
  • 现在我有几个类,而不是 3 个相互嵌套的开关。系统的维护比以前更加复杂,学习曲线也更加陡峭。
  • 程序和规则不是好名字 - 仍在寻找更好的名字;-)
  • 将地图作为参数可能会导致糟糕的编码,从而使大量内容变得混乱。

小智 1

您可以使用决策树并从值元组构建它。

这会简单得多,如果正确实现,甚至比硬编码条件更快,并且还提供了更高的可维护性。