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

5

我被设计用来维护一个系统,该系统考虑了三个变量的值以确定它将采取哪些操作。

我想重构它并使用设计模式,但是找不到适合其需求的设计模式。

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

每个健身房用户都有一个合同类型(TYPE_OF_CONTRACT),可以是:

  • 白金会员
  • 黄金会员
  • 银牌会员

健身房有一些健身课(GYM_CLASSES)

  • 举重
  • 平衡训练
  • 踏步操
  • 室内自行车训练
  • 森巴舞
  • 私教

每个健身房用户都有一个体质状况(PHYSICAL_CONDITION)

  • 没有限制
  • 超过65岁
  • 行动不便
  • 有医学状况
  • 18岁以下

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

如果合同类型是白金会员+私教+超过65岁:

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

如果合同类型是黄金会员+私教+超过65岁:

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

如果合同类型是银牌会员+私教+超过65岁:

  1. 拒绝订阅

如果(任意一种会员)+踏步操+有医学状况:

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

如果合同类型是白金会员+举重+行动不便:

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

等等。

这些特征的组合可以有一组操作,这些操作不是互斥的,并且不能保证所有组合都存在。

旧代码使用嵌套开关进行实现。例如:

switch (contractType):

    case PLATINUM_MEMBERSHIP:

        switch (gymClass):            

            case (PERSONAL_TRAINING):

                switch (physicalCondition):            

                    case (OVER_65):

                        requiresMedicalApproval();
                        requiresSignedForm();

...

我的问题是:
  • 有三个条件组合在一起定义了一组规则;
  • 这些规则不一定是唯一的;
  • 并非每个组合都定义了一组规则。

我使用提取方法技术和清理代码进行了重构,但无法摆脱3个switch语句。

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

我想过多态性和策略模式,但找不到使用任何一个的方法。

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

你有什么建议吗?

谢谢。


编辑:

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

简而言之:

1)使用枚举定义变量:

public enum TypeOfContract { ... }
public enum GymClasses { ... }
public enum PhysicalCondition { ... }

枚举将包含所有可能的条件。

2) Command接口用于定义操作。

public interface Command {
    public void execute(Map<String, Object> parametersMap);
}

每个操作都将是Command的一个实现。Map参数将用于将运行时上下文传递给方法。

3)一个Procedures类来保存每个条件所需的操作。

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);
    }   
}    

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

如果三个变量的组合不存在,则发送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;
    }

}

( 为了在本网站中可视化,进行了异常的代码格式设置)

这里使用RULES三维数组定义了变量的有意义的关联。

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

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

RULES
    [TypeOfContract.PLATINUM_MEMBERSHIP.ordinal()]
    [GymClasses.PERSONAL_TRAINING.ordinal()]
    [PhysicalCondition.OVER_65.ordinal()]

ordinal()方法返回枚举常量在枚举中的位置对应的整数值。

为了表示需要执行的操作,会关联一个Procedures类,用于包装要执行的操作:

new Procedures(new RequireMedicalApproval(), new RequireSignedForm() );

RequireMedicalApproval和RequireSignedForm都实现了Command接口。

定义这些变量组合的完整行如下:

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

要检查特定组合是否有相关的操作,需要调用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);
    }

所有执行操作所需的信息现在都在一个Map中。

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

规则引擎将评估组合是否有关联的操作,并返回一个程序对象,其中包含需要执行的这些操作的列表。

如果没有(组合没有与之相关的操作),则规则引擎将返回一个有效的程序对象,其中包含一个空列表。

6)优点

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

7)缺点

  • 在定义规则时很容易出错,因为它们的声明冗长且不进行语义分析-它将接受重复定义,例如可能会覆盖先前的定义。
  • 现在我有几个类,而不是3个嵌套在一起的开关,因此系统的维护比以前略微复杂,学习曲线略微陡峭。
  • 程序和规则不是很好的名称-仍在寻找更好的名称;-)
  • 将Map作为参数可能会促使糟糕的编码,使其混杂着大量内容。

如果它没有出现问题,就不要去修复它,尤其是如果你甚至不知道如何修复它。 - Kayaman
乍一看,我会开始考虑策略模式和装饰器模式的组合。但我的方法是先为现有实现编写单元测试,然后尝试重构其中一个变量。看看它的效果,然后再重复这个过程,直到处理完另外两个变量。可能会发现每个变量需要不同的解决方案。 - dbugger
2
@Kayaman,这是一个简单的开关嵌套。我宁愿不去处理丑陋的代码,而不是因为它没有问题就不管它。随着时间的推移,丑陋的代码往往会扩散,并使应用程序的维护变成一场噩梦。即使我此时不知道如何解决它,也并不意味着我不能尝试寻找解决方法。已经有单元测试了,在重构之前我添加了一些,现在我有了一个安全网。 - Quaestor Lucem
3个回答

2
你会有多少个选项?假设每个类别有8个选项,也许你可以将特定的组合表示为24位数字,每个类别有8位。当你收到一组选项时,将其转换为比特模式,然后与位掩码进行AND操作,以确定是否需要执行某个操作。
这仍然需要进行测试,但至少它们不是嵌套的,如果/当你添加新功能时,只需添加一个新的测试即可。

1
你可以使用决策树,并从值的元组中构建它。这将比硬编码条件更简单,如果正确实现,甚至更快,并提供更高的可维护性。

0

关于设计模式,如果你想要降低复杂度,可以使用抽象工厂。

你可以创建三个类的层次结构。

  1. 合同类型 (AbstractProductA)

    白金会员 (ProductA1)

    黄金会员 (ProductA2)

    白银会员 (ProductA3)

  2. 健身课程 (AbstractProductB)

    举重 (ProductB1)

    身体平衡 (ProductB2)

    踏步课程 (ProductB3)

    室内骑行 (ProductB4)

    尊巴舞 (ProductB5)

    私人训练 (ProductB6)

  3. 身体状况 (AbstractProductC)

    无限制 (ProductC1)

    65岁以上 (ProductC2)

    活动能力有限 (ProductC3)

    有医疗条件 (ProductC4)

    18岁以下 (ProductC5)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接