Javaでルールエンジンを設計するための効果的な設計パターン/スタイルは何ですか? 質問する

Javaでルールエンジンを設計するための効果的な設計パターン/スタイルは何ですか? 質問する

私は Java でルール エンジンを実装しています。私のルール エンジンは、独立したルールとルール セットのリストを事前定義します。ここでのルールは単なるロジックです。そして、ルール セットは、これらの単純なルールを順序付けられたセットに組み合わせます。

私はそこそこの Java 開発者ですが、達人ではありません。同僚がこの目的のために 2 つのデザインを提案してくれました。どちらのデザインにも満足していないので、この質問をしました。

私のプロジェクトのルールの例:入力が米国内の場所、たとえば米国カリフォルニア州サンタバーバラや米国オハイオ州であるとします。これは通常、都市、州、国フィールドを含む明確に定義された形式です。その場合、次のようなルールを設定できます。

ルール1:市区町村がnullではありません
ルール2:状態が null ではありません
ルール3:国が米国またはアメリカ合衆国に等しい
ルール4:状態の長さは2

私のプロジェクトのルールセットの例:

ルールセット:有効な場所 このルールセットは、上記で定義されたルールの順序付けられたセットです。

私が実装した 2 つのデザイン テンプレートは次のとおりです。

デザイン 1: 匿名内部クラスで Enum を使用する

ルール.java

public interface Rule {
    public Object apply(Object object);
}

NlpRule.java

public enum NlpRule {
    CITY_NOT_NULL(new Rule() {

        @Override
        public Object apply(Object object) {
            String location = (String) object;
            String city = location.split(",")[0];
            if (city != null) {
                return true;
            }
            return false;
        }

    }),

    STATE_NOT_NULL(new Rule() {

        @Override
        public Object apply(Object object) {
            String location = (String) object;
            String state = location.split(",")[1];
            if (state != null) {
                return true;
            }
            return false;
        }

    }),

    COUNTRY_US(new Rule() {

        @Override
        public Object apply(Object object) {
            String location = (String) object;
            String country = location.split(",")[2];
            if (country.equals("US") || country.equals("USA")) {
                return true;
            }
            return false;
        }

    }),

    STATE_ABBREVIATED(new Rule() {

        @Override
        public Object apply(Object object) {
            String location = (String) object;
            String state = location.split(",")[1];
            if (state.length() == 2) {
                return true;
            }
            return false;
        }

    });

    private Rule rule;

    NlpRule(Rule rule) {
        this.rule = rule;
    }

    public Object apply(Object object) {
        return rule.apply(object);
    }
}

ルールセット.java

public class RuleSet {
    private List<NlpRule> rules;

    public RuleSet() {
        rules = new ArrayList<NlpRule>();
    }

    public RuleSet(List<NlpRule> rules) {
        this.rules = rules;
    }

    public void add(NlpRule rule) {
        rules.add(rule);
    }

    public boolean apply(Object object) throws Exception {
        boolean state = false;
        for (NlpRule rule : rules) {
            state = (boolean) rule.apply(object);
        }
        return state;
    }
}

ルールセット.java

public class RuleSets {
    private RuleSets() {

    }

    public static RuleSet isValidLocation() {
        RuleSet ruleSet = new RuleSet();
        ruleSet.add(NlpRule.CITY_NOT_NULL);
        ruleSet.add(NlpRule.STATE_NOT_NULL);
        ruleSet.add(NlpRule.COUNTRY_US);
        ruleSet.add(NlpRule.STATE_ABBREVIATED);
        return ruleSet;
    }
}

メイン.java

public class Main {
    public static void main(String... args) {
        String location = "Santa Barbara,CA,USA";
        RuleSet ruleSet = RuleSets.isValidLocation();
        try {
            boolean isValid = (boolean) ruleSet.apply(location);
            System.out.println(isValid);
        } catch (Exception e) {
            e.getMessage();
        }
    }
}

デザイン2: 抽象クラスの使用

NlpRule.java

public abstract class NlpRule {

    public abstract Object apply(Object object);

    public final static NlpRule CITY_NOT_NULL = new NlpRule() {
        public Object apply(Object object) {
            String location = (String) object;
            String city = location.split(",")[0];
            if (city != null) {
                return true;
            }
            return false;

        }

    };

    public final static NlpRule STATE_NOT_NULL = new NlpRule() {
        public Object apply(Object object) {
            String location = (String) object;
            String city = location.split(",")[0];
            if (city != null) {
                return true;
            }
            return false;

        }

    };

    public final static NlpRule COUNTRY_US = new NlpRule() {
        public Object apply(Object object) {
            String location = (String) object;
            String country = location.split(",")[2];
            if (country.equals("US") || country.equals("USA")) {
                return true;
            }
            return false;

        }

    };

    public final static NlpRule STATE_ABBREVIATED = new NlpRule() {
        public Object apply(Object object) {
            String location = (String) object;
            String state = location.split(",")[1];
            if (state.length() == 2) {
                return true;
            }
            return false;
        }

    };

}

ルールセット.java

public class RuleSet {
    private List<NlpRule> rules;

    public RuleSet() {
        rules = new ArrayList<NlpRule>();
    }

    public RuleSet(List<NlpRule> rules) {
        this.rules = rules;
    }

    public void add(NlpRule rule) {
        rules.add(rule);
    }

    public boolean apply(Object object) throws Exception {
        boolean state = false;
        for (NlpRule rule : rules) {
            state = (boolean) rule.apply(object);
        }
        return state;
    }
}

ルールセット.java

import com.hgdata.design.one.NlpRule;
import com.hgdata.design.one.RuleSet;

public class RuleSets {
    private RuleSets() {

    }

    public static RuleSet isValidLocation() {
        RuleSet ruleSet = new RuleSet();
        ruleSet.add(NlpRule.CITY_NOT_NULL);
        ruleSet.add(NlpRule.STATE_NOT_NULL);
        ruleSet.add(NlpRule.COUNTRY_US);
        ruleSet.add(NlpRule.STATE_ABBREVIATED);
        return ruleSet;
    }
}

メイン.java

public class Main {
    public static void main(String... args) {
        String location = "Santa Barbara,CA,USA";
        RuleSet ruleSet = RuleSets.isValidLocation();
        try {
            boolean isValid = (boolean) ruleSet.apply(location);
            System.out.println(isValid);
        } catch (Exception e) {
            e.getMessage();
        }
    }
}

より良い設計アプローチ/パターン?ご覧のとおり、デザイン 2 ではインターフェースと列挙型が削除されています。代わりに抽象クラスが使用されています。同じものを実装するためのより優れたデザイン パターン/アプローチがあるかどうか、まだ疑問に思っています。

初期化ブロックを使用したインスタンス化:

さて、上記の両方の設計の場合。たとえば、外部クラスをインスタンス化して適用ロジック内で使用する必要が生じた場合、初期化ブロックを使用する必要が生じますが、これが良い方法であるかどうかはよくわかりません。このようなシナリオの例を次に示します。

デザイン1:

...
STATE_ABBREVIATED(new Rule() {
        private CustomParser parser;

        {
            parser = new CustomParser();
        }

        @Override
        public Object apply(Object object) {
            String location = (String) object;
            location = parser.parse(location);
            String state = location.split(",")[1];
            if (state.length() == 2) {
                return true;
            }
            return false;
        }

    });
...

デザイン2:

...
public final static NlpRule STATE_ABBREVIATED = new NlpRule() {
        private CustomParser parser;

        {
            parser = new CustomParser();
        }
        public Object apply(Object object) {
            String location = (String) object;
            location = parser.parse(location);
            String state = location.split(",")[1];
            if (state.length() == 2) {
                return true;
            }
            return false;
        }

    };
...

Java の専門家の皆さん、どうかご指摘ください。また、上記の 2 つの設計に欠陥が見つかった場合は、その点を指摘してください。適切な決定を下すには、それぞれの設計の長所と短所を知る必要があります。コメントで一部のユーザーが提案したように、ラムダ、述語、その他のいくつかのパターンを検討しています。

ベストアンサー1

これは興味深い質問で、多くの答えが考えられます。ある程度、解決策は個人の好みによって異なります。私は同様の問題に何度も遭遇しており、次のような推奨事項があります。これらは私の場合はうまくいきましたが、あなたのニーズには合わないかもしれません。

  1. を使用しますenum。長期的には、private staticエラー チェックや、それらを効率的に使用できる便利なコンテナー (EnumSet など) の点で、メンバーよりも多くの利点があると感じています。

  2. 抽象クラスよりもインターフェースを使用してください。Java 8 より前は、抽象クラスを使用するのに便利な理由がありました。defaultメンバーの場合、今では良い理由はありません (これは私の意見であり、他の人は同意しないと思います)。列挙型はインターフェースを実装できます。

  3. Java 8 では、各「ルール」に関連付けられたロジックをラムダ式に埋め込むことができるため、列挙型の初期化コードがより明確になります。

  4. ラムダは非常に短くしてください。最大でも 1 つか 2 つのコマンド (できればブロックのない 1 つの式) にします。つまり、複雑なロジックを別々のメソッドに分割するということです。

  5. ルールを分類するには、別々の列挙型を使用します。すべてを 1 つにまとめる理由はないので、それらを分割することで、ドメインに関連するラムダ式を正確に持つことで、コンストラクターをシンプルにすることができます。私の言っていることを理解するには、以下の例を参照してください。

  6. ルールが階層化されている場合は、複合設計パターンを使用します。これは柔軟性と堅牢性を備えています。

したがって、これらの推奨事項をまとめると、次のようなものを提案します。

interface LocationRule{
    boolean isValid(Location location);
}

enum ValidValueRule implements LocationRule {
    STATE_NOT_NULL(location -> location.getState() != null),
    CITY_NOT_NULL(location -> location.getCity() != null);

    private final Predicate<Location> locationPredicate;
    ValidValueRule(Predicate<Location> locationPredicate) {
        this.locationPredicate = locationPredicate;
    }

    public boolean isValid(Location location) {
        return locationPredicate.test(location);
    }
}

enum StateSizeRule implements LocationRule {
    IS_BIG_STATE(size -> size > 1000000),
    IS_SMALL_STATE(size -> size < 1000);

    private final Predicate<Integer> sizePredicate;
    StateSize(Predicate<Integer> sizePredicate) {
        this.sizePredicate = sizePredicate;
    }
    public boolean isValid(Location location) {
        return sizePredicate.test(location.getState().getSize());
    }
}

class AllPassRule implements LocationRule {
    private final List<LocationRule > rules = new ArrayList<>();
    public void addRule(LocationRule rule) {
        rules.add(rule);
    }
    public boolean isValid(Location location) {
        return rules.stream().allMatch(rule -> rule.isValid(location));
    }
}

class AnyPassRule implements LocationRule {
    private final List<LocationRule > rules = new ArrayList<>();
    public void addRule(LocationRule rule) {
        rules.add(rule);
    }
    public boolean isValid(Location location) {
        return rules.stream().anyMatch(rule -> rule.isValid(location));
    }
}

class NegateRule implements LocationRule {
    private final Rule rule;
    public NegateRule(Rule rule) {
        this.rule = rule;
    }
    public boolean isValid(Location location) {
        return !rule.isValid(location);
    }
}

たとえば、場所が小さくない都市または州にある必要があるというルールを実装するには、次のようにします。

AnyPassRule cityOrNonSmallState = new AnyPassRule();
cityOrNonSmallState.addRule(ValidValueRule.CITY_NOT_NULL);
cityOrNonSmallState.addRule(new NegateRule(StateSize.IS_SMALL_STATE));
return cityOrNonSmallState.isValid(location);

おすすめ記事