¿Cuál es el patrón/estilo de diseño efectivo para diseñar un motor de reglas en Java?
class design-patterns (5)
Creo que has agregado al menos una capa de código innecesario. Recuerde que enum
también puede implementar interfaces e incluso puede tener métodos abstractos.
/**
* I don''t like `Object` so I will adjust.
*/
public interface Rule {
public boolean pass(String s);
}
/**
* All pass country codes.
*/
public enum ValidCountry {
US, USA;
public static Set<String> all = new HashSet<>();
static {
for (ValidCountry c : ValidCountry.values()) {
all.add(c.name());
}
}
}
public enum NlpRule implements Rule {
CITY_NOT_NULL {
@Override
public boolean pass(String location) {
return location.split(",")[0] != null;
}
},
STATE_NOT_NULL {
@Override
public boolean pass(String location) {
return location.split(",")[1] != null;
}
},
COUNTRY_US {
@Override
public boolean pass(String location) {
return ValidCountry.all.contains(location.split(",")[2]);
}
},
STATE_ABBREVIATED {
@Override
public boolean pass(String location) {
return location.split(",")[1].length() == 2;
}
};
/**
* You can even make Sets of them.
*/
static Set<NlpRule> isValidLocation = EnumSet.of(CITY_NOT_NULL, STATE_NOT_NULL, COUNTRY_US, STATE_ABBREVIATED);
}
public void test() {
String test = "Santa Barbara,CA,USA";
for (Rule r : NlpRule.isValidLocation) {
boolean pass = r.pass(test);
System.out.println(r + "(/"" + test + "/") - " + (pass ? "passes" : "FAILS"));
}
}
Estoy implementando un motor de reglas en Java. Mi motor de reglas predefine una lista de reglas independientes y conjuntos de reglas. Una regla aquí es simplemente una pieza de lógica. Y un conjunto de reglas combina estas reglas simples en un conjunto ordenado.
Soy un desarrollador de Java decente pero no un Gurú. Mi colega me sugirió dos diseños para este propósito. No estoy satisfecho con ambos diseños, de ahí esta pregunta.
Ejemplo de una regla en mi proyecto: diga que las entradas son ubicaciones en EE. UU. Para, por ejemplo, Santa Bárbara, California, EE. UU. U OH, EE. UU. Entonces puedo tener algunas reglas de la siguiente manera:
REGLA 1: Ciudad no nula
REGLA 2: Estado no nulo
REGLA 3: País es igual a EE.UU. o EE.UU.
REGLA 4: La longitud del estado es igual a 2
Ejemplo de un RuleSet en mi proyecto:
REGLAMENTO: ubicación válida Este conjunto de reglas es un conjunto ordenado de las reglas definidas anteriormente.
Las dos plantillas de diseño que he implementado son las siguientes:
Diseño 1: Usando Enum con clases internas anónimas
Rule.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);
}
}
RuleSet.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;
}
}
RuleSets.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;
}
}
Main.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();
}
}
}
Diseño 2: Uso de la clase abstracta
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;
}
};
}
RuleSet.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;
}
}
RuleSets.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;
}
}
Main.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();
}
}
}
¿Mejor diseño de enfoque / patrón? Como puede ver, el diseño 2 elimina la interfaz y la enumeración. En su lugar, utiliza una clase abstracta. Todavía me pregunto si hay un mejor patrón / enfoque de diseño para implementar el mismo.
Creación de instancias utilizando bloques de inicialización:
Ahora en el caso de los dos diseños anteriores. Por ejemplo, si necesito crear una instancia de una clase externa para usarla dentro de mi lógica de aplicación, entonces me veo obligado a usar bloques de inicialización, de los cuales no estoy totalmente consciente de si es una buena práctica. Vea el ejemplo de tal escenario a continuación:
Diseño 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;
}
});
...
Diseño 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;
}
};
...
Expertos en Java, por favor, arrojar algo de luz! También indíquelo si encuentra algún defecto en los dos diseños anteriores. Necesito saber las ventajas y desventajas asociadas con cada uno de los diseños para ayudarme a tomar la decisión correcta. Estoy buscando lambdas, predicados y varios otros patrones como lo sugieren algunos usuarios en los comentarios.
Esta es una pregunta interesante con muchas respuestas posibles. Hasta cierto punto, la solución dependerá de las preferencias personales. A menudo me he encontrado con problemas similares y tengo las siguientes recomendaciones. Tenga en cuenta que esto funciona para mí, pero puede que no se ajuste a sus necesidades.
Utilizar
enum
A largo plazo, creo que tienen muchas ventajas sobreprivate static
miembrosprivate static
en cuanto a la comprobación de errores y los contenedores útiles (EnumSet, etc.) que pueden usarlos de manera eficiente.Usa interfaces sobre clases abstractas. Antes de Java 8 había razones útiles para usar clases abstractas. Con
default
miembrosdefault
ahora no hay buenas razones (solo mi opinión, estoy seguro de que otros no estarán de acuerdo). Un enum puede implementar una interfaz.En Java 8, la lógica asociada con cada ''regla'' se puede incrustar en una expresión lambda, lo que hace que el código de inicialización para sus enumeraciones sea más claro.
Mantenga las lambdas muy cortas: solo uno o dos comandos como máximo (y preferiblemente una expresión sin bloque). Esto significa dividir cualquier lógica compleja en métodos separados.
Use enumeraciones separadas para clasificar sus reglas. No hay una buena razón para ponerlos todos en uno y al dividirlos, puede hacer que los constructores sean simples al tener exactamente las expresiones lambda relevantes para su dominio. Vea mi ejemplo a continuación para ver lo que quiero decir.
Si tiene jerarquías de reglas, use el patrón de diseño compuesto. Es flexible y robusto.
Así que poniendo esas recomendaciones juntas sugeriría algo como:
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);
}
}
Estos pueden ser utilizados como:
AnyPassRule anyPass = new AnyPassRule();
anyPass.addRule(ValidValueRule.STATE_NOT_NULL);
anyPass.addRule(new NegateRule(StateSize.IS_SMALL_STATE));
anyPass.isValid(location);
Y así.
Interfaz con campos estáticos:
public interface NlpRule
{
Object apply(Object object);
NlpRule CITY_NOT_NULL = object ->
{
String location = (String) object;
String city = location.split(",")[0];
return ...true/false;
};
// etc.
Algunos pueden preferir métodos sobre objetos funcionales
public interface NlpRule
{
Object apply(Object object);
static boolean cityNotNull(Object object) // java8: static method in interface
{
String location = (String) object;
String city = location.split(",")[0];
return ...true/false;
};
// etc.
}
// use method reference as functional object
NlpRule rule = NlpRule::cityNotNull;
ruleset.add( NlpRule::cityNotNull );
O podrías tener tanto método como campo.
public interface NlpRule
{
Object apply(Object object);
NlpRule CITY_NOT_NULL = NlpRule::cityNotNull;
static boolean cityNotNull(Object object)
{
...
};
Las reglas de ejemplo son todas String->boolean
, sin estar seguro de por qué NlpRule es Object- Object->Object
. Si las reglas realmente pudieran aceptar / devolver diferentes tipos, probablemente debería generar NlpRule<T,R>
.
El CustomParser se puede almacenar en una clase auxiliar privada de paquete
class NlpRuleHelper
{
static final CustomParser parser = new CustomParser();
}
--
public interface NlpRule
...
NlpRule STATE_ABBREVIATED = object ->
{
...
location = NlpRuleHelper.parser.parse(location);
Otra posible respuesta es usar un analizador DSL para validar su regla, en el lenguaje de programación de funciones, hay una cosa llamada combinador de analizador que podría generar un analizador más grande (conjunto de reglas) a partir de un analizador básico diferente (regla). El punto positivo de este modo es la flexibilidad, las desventajas son que cada vez que desee cambiar su conjunto de reglas, debe volver a codificar.
Ya hay muchos motores de reglas de Java (de código abierto), visite http://java-source.net/open-source/rule-engines & http://drools.org/
Puede comenzar con el uso / examen de la fuente de uno de esos (tomando nota de que no cumple con sus requisitos) y continuar desde allí.