Designing a Rule Engine for Multi‑Condition Decision Scenarios
The article explains why long chains of if‑else are hard to maintain, proposes a rule‑engine architecture with abstract BaseRule, concrete rule classes, and a RuleService that supports AND/OR short‑circuit execution, and evaluates its advantages and drawbacks with Java code examples.
Business scenario
Original logic used a series of if‑else checks with short‑circuit behavior to decide whether a user is overseas, a fraud user, an unpaid user out of service period, or a referred/paid/internal‑recommended user. The requirements demanded a short‑circuit mechanism and highlighted maintainability problems of the monolithic if‑else chain.
Rule abstraction
A rule engine is built around three core types: RuleDto – a data‑transfer object that carries the input fields (e.g., address, age, name, subject). BaseRule – an interface defining a single method boolean execute(RuleDto dto). AbstractRule – an abstract class implementing BaseRule. It provides a generic convert helper that by default returns the original DTO and delegates execution to an abstract executeRule method that concrete rules override.
@Data
public class RuleDto {
private String address;
private int age;
private String name;
private String subject;
}
public interface BaseRule {
boolean execute(RuleDto dto);
}
public abstract class AbstractRule implements BaseRule {
protected <T> T convert(RuleDto dto) { return (T) dto; }
@Override
public boolean execute(RuleDto dto) { return executeRule(convert(dto)); }
protected <T> boolean executeRule(T t) { return true; }
}Concrete rule examples
Two concrete rules illustrate the pattern.
public class AddressRule extends AbstractRule {
@Override
public boolean execute(RuleDto dto) {
System.out.println("AddressRule invoke!");
return dto.getAddress().startsWith(RuleConstant.MATCH_ADDRESS_START);
}
}
public class NationalityRule extends AbstractRule {
@Override
protected <T> T convert(RuleDto dto) {
NationalityRuleDto nrDto = new NationalityRuleDto();
if (dto.getAddress().startsWith(RuleConstant.MATCH_ADDRESS_START)) {
nrDto.setNationality(RuleConstant.MATCH_NATIONALITY_START);
}
return (T) nrDto;
}
@Override
protected <T> boolean executeRule(T t) {
System.out.println("NationalityRule invoke!");
NationalityRuleDto nrDto = (NationalityRuleDto) t;
return nrDto.getNationality().startsWith(RuleConstant.MATCH_NATIONALITY_START);
}
}Constant values are defined in RuleConstant:
public class RuleConstant {
public static final String MATCH_ADDRESS_START = "北京";
public static final String MATCH_NATIONALITY_START = "中国";
}Rule engine (RuleService)
The engine stores rule lists keyed by logical operators: AND = 1 and OR = 0. Fluent methods and(...) and or(...) add rule lists to an internal map. The execute method iterates over the map entries, dispatches to private and or or helpers, and returns false immediately when a short‑circuit condition fails.
public class RuleService {
private Map<Integer, List<BaseRule>> hashMap = new HashMap<>();
private static final int AND = 1;
private static final int OR = 0;
public static RuleService create() { return new RuleService(); }
public RuleService and(List<BaseRule> ruleList) { hashMap.put(AND, ruleList); return this; }
public RuleService or(List<BaseRule> ruleList) { hashMap.put(OR, ruleList); return this; }
public boolean execute(RuleDto dto) {
for (Map.Entry<Integer, List<BaseRule>> item : hashMap.entrySet()) {
List<BaseRule> ruleList = item.getValue();
switch (item.getKey()) {
case AND:
System.out.println("execute key = " + AND);
if (!and(dto, ruleList)) return false;
break;
case OR:
System.out.println("execute key = " + OR);
if (!or(dto, ruleList)) return false;
break;
default:
break;
}
}
return true;
}
private boolean and(RuleDto dto, List<BaseRule> ruleList) {
for (BaseRule rule : ruleList) {
if (!rule.execute(dto)) return false;
}
return true;
}
private boolean or(RuleDto dto, List<BaseRule> ruleList) {
for (BaseRule rule : ruleList) {
if (rule.execute(dto)) return true;
}
return false;
}
}Engine usage example
A JUnit test assembles the engine, creates a RuleDto with sample data (age 5, name "张三", address "北京", subject "数学"), chains and and or calls, executes the rule set, and prints the result.
public class RuleServiceTest {
@org.junit.Test
public void execute() {
AgeRule ageRule = new AgeRule();
NameRule nameRule = new NameRule();
NationalityRule nationalityRule = new NationalityRule();
AddressRule addressRule = new AddressRule();
SubjectRule subjectRule = new SubjectRule();
RuleDto dto = new RuleDto();
dto.setAge(5);
dto.setName("张三");
dto.setAddress("北京");
dto.setSubject("数学");
boolean ruleResult = RuleService.create()
.and(Arrays.asList(nationalityRule, nameRule, addressRule))
.or(Arrays.asList(ageRule, subjectRule))
.execute(dto);
System.out.println("this student rule execute result :" + ruleResult);
}
}Observations
Advantages : simple structure; each rule is independent; clear separation of rule logic, data, and executor; the convert method in AbstractRule allows rule‑specific data transformation.
Drawbacks : all rules share a single RuleDto, creating data coupling; modifying the shared DTO directly is considered undesirable, suggesting that data objects should be constructed ahead of rule execution.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
IT Niuke
Focused on IT technology sharing, original and innovative content. IT Niuke, we grow together.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
