Tired of Endless if‑else? Try a Rule Engine for Cleaner Logic
The article shows how a growing list of if‑else checks for user eligibility becomes hard to maintain, then walks through designing and implementing a Java rule engine—defining rule abstractions, building an AND/OR executor, and demonstrating usage with concrete examples, while discussing its advantages and drawbacks.
Business Scenario
We received a request to extend the eligibility rules for trial users. The original logic consisted of a series of if‑else checks such as:
if (isOverseasUser) { return false; }
if (isFraudUser) { return false; }
if (!paid && outOfServicePeriod) { return false; }
if (referralUser || paidUser || internalReferral) { return true; }The author notes that the process is based on AND/OR relationships and that a short‑circuit evaluation is required. Maintaining a long list of conditions quickly becomes unreadable and hard to extend.
Rule Engine
To address the problem the author designed a rule‑engine (V1) that separates rule definitions, data transfer objects, and the executor.
Rule abstraction
// Business data
@Data
public class RuleDto {
private String address;
private int age;
}
// Rule abstraction
public interface BaseRule {
boolean execute(RuleDto dto);
}
// Abstract template
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 – example 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;
}
}
// Concrete rule – example 2
public class NationalityRule extends AbstractRule {
@Override
protected <T> T convert(RuleDto dto) {
NationalityRuleDto nrDto = new NationalityRuleDto();
if (dto.getAddress().startsWith(MATCH_ADDRESS_START)) {
nrDto.setNationality(MATCH_NATIONALITY_START);
}
return (T) nrDto;
}
@Override
protected <T> boolean executeRule(T t) {
System.out.println("NationalityRule invoke!");
NationalityRuleDto nrDto = (NationalityRuleDto) t;
if (nrDto.getNationality().startsWith(MATCH_NATIONALITY_START)) {
return true;
}
return false;
}
}
// Constants
public class RuleConstant {
public static final String MATCH_ADDRESS_START = "北京";
public static final String MATCH_NATIONALITY_START = "中国";
}Engine construction
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 = " + 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<BaseRule> ruleList) {
for (BaseRule rule : ruleList) {
boolean execute = rule.execute(dto);
if (!execute) {
return false;
}
}
return true;
}
private boolean or(RuleDto dto, List<BaseRule> ruleList) {
for (BaseRule rule : ruleList) {
boolean execute = rule.execute(dto);
if (execute) {
return true;
}
}
return false;
}
}Engine usage
public class RuleServiceTest {
@org.junit.Test
public void execute() {
// Define rules
AgeRule ageRule = new AgeRule();
NameRule nameRule = new NameRule();
NationalityRule nationalityRule = new NationalityRule();
AddressRule addressRule = new AddressRule();
SubjectRule subjectRule = new SubjectRule();
// Build DTO
RuleDto dto = new RuleDto();
dto.setAge(5);
dto.setName("张三");
dto.setAddress("北京");
dto.setSubject("数学");
// Chain and/or composition 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);
}
}Conclusion
Pros
Simple structure; each rule is independent and can be combined flexibly.
Separation of rule, data, and executor makes the caller code tidy.
The convert method in the abstract template allows rule‑specific data preparation.
Cons
All rules share the same DTO, creating hidden data coupling; building the DTO beforehand is recommended.
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.
Architect's Guide
Dedicated to sharing programmer-architect skills—Java backend, system, microservice, and distributed architectures—to help you become a senior architect.
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.
