Design and Implementation of a Java Rule Engine with AND/OR Logic
This article explains how to refactor a simple if‑else based user‑application rule check into a modular Java rule engine that supports AND/OR short‑circuit evaluation, detailing the design, abstract and concrete rule classes, a fluent service builder, and a test example.
The author introduces a real‑world requirement to extend existing trial‑user application rules and shows a basic if‑else snippet that highlights the need for short‑circuit evaluation when combining multiple conditions.
To address this, a rule‑engine architecture is proposed. A V1 design (illustrated by an image) separates the data transfer object ( RuleDto ), a BaseRule interface, and an abstract template ( AbstractRule ) that provides a convert method and delegates execution to executeRule .
// 业务数据
@Data
public class RuleDto {
private String address;
private int age;
}
// 规则抽象
public interface BaseRule {
boolean execute(RuleDto dto);
}
// 规则模板
public abstract class AbstractRule implements BaseRule {
protected
T convert(RuleDto dto) {
return (T) dto;
}
@Override
public boolean execute(RuleDto dto) {
return executeRule(convert(dto));
}
protected
boolean executeRule(T t) {
return true;
}
}
// 具体规则- 例子1
public class AddressRule extends AbstractRule {
@Override
public boolean execute(RuleDto dto) {
System.out.println("AddressRule invoke!");
if (dto.getAddress().startsWith(MATCH_ADDRESS_START)) {
return true;
}
return false;
}
}
// 具体规则- 例子2
public class NationalityRule extends AbstractRule {
@Override
protected
T convert(RuleDto dto) {
NationalityRuleDto nationalityRuleDto = new NationalityRuleDto();
if (dto.getAddress().startsWith(MATCH_ADDRESS_START)) {
nationalityRuleDto.setNationality(MATCH_NATIONALITY_START);
}
return (T) nationalityRuleDto;
}
@Override
protected
boolean executeRule(T t) {
System.out.println("NationalityRule invoke!");
NationalityRuleDto nationalityRuleDto = (NationalityRuleDto) t;
if (nationalityRuleDto.getNationality().startsWith(MATCH_NATIONALITY_START)) {
return true;
}
return false;
}
}
// 常量定义
public class RuleConstant {
public static final String MATCH_ADDRESS_START = "北京";
public static final String MATCH_NATIONALITY_START = "中国";
}The RuleService class acts as a fluent builder that stores rule lists in a map keyed by AND (1) or OR (0). It provides and() and or() methods to register rule collections and an execute() method that iterates over the map, applying short‑circuit logic via private and() and or() helpers.
public class RuleService {
private Map
> 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
ruleList) {
hashMap.put(AND, ruleList);
return this;
}
public RuleService or(List
ruleList) {
hashMap.put(OR, ruleList);
return this;
}
public boolean execute(RuleDto dto) {
for (Map.Entry
> item : hashMap.entrySet()) {
List
ruleList = item.getValue();
switch (item.getKey()) {
case AND:
System.out.println("execute key = " + 1);
if (!and(dto, ruleList)) {
return false;
}
break;
case OR:
System.out.println("execute key = " + 0);
if (!or(dto, ruleList)) {
return false;
}
break;
default:
break;
}
}
return true;
}
private boolean and(RuleDto dto, List
ruleList) {
for (BaseRule rule : ruleList) {
boolean execute = rule.execute(dto);
if (!execute) {
return false;
}
}
return true;
}
private boolean or(RuleDto dto, List
ruleList) {
for (BaseRule rule : ruleList) {
boolean execute = rule.execute(dto);
if (execute) {
return true;
}
}
return false;
}
}A test class demonstrates usage: instantiate concrete rules, build a RuleDto with sample data, and invoke the engine with a chained call that first applies an AND group (nationality, name, address) and then an OR group (age, subject), finally printing the overall result.
public class RuleServiceTest {
@org.junit.Test
public void execute() {
// 1. Define rules
AgeRule ageRule = new AgeRule();
NameRule nameRule = new NameRule();
NationalityRule nationalityRule = new NationalityRule();
AddressRule addressRule = new AddressRule();
SubjectRule subjectRule = new SubjectRule();
// 2. Create DTO
RuleDto dto = new RuleDto();
dto.setAge(5);
dto.setName("张三");
dto.setAddress("北京");
dto.setSubject("数学");
// 3. Build and execute
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);
}
}In the concluding section the author lists the advantages of this approach—simplicity, modularity, independent rules, and a reusable conversion method—and the disadvantages—tight coupling to a shared DTO, which may require careful data preparation.
Code Ape Tech Column
Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.cn
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.