规则定义与抽象:规则接口设计(IRule)

好,咱们进入第三章。这一章,我打算聊聊规则的定义与抽象。

你想想看,一个风控系统里,规则可能有成百上千条。有简单的黑白名单,有复杂的设备指纹评分,还有基于机器学习的模型输出。如果每条规则都自己玩自己的,那系统就乱套了。所以,我们需要一个统一的抽象。

这个抽象,就是规则接口。我习惯叫它 IRule

规则接口设计(IRule)

接口是什么?说白了,就是一份合同。它规定了一条规则「必须」做什么,以及「可以」提供什么信息。

我个人习惯,一个最简的规则接口,至少包含三个核心方法:

  • 评估(evaluate):这是规则的核心。给它一个上下文(比如用户请求、设备信息),它返回一个决策结果(通过、拒绝、人工审核)。
  • 获取元数据(getMetadata):返回规则的描述信息,比如名字、版本、优先级。这方便我们做日志、监控和动态管理。
  • 初始化(initialize):有些规则需要加载配置、建立数据库连接。这个方法就是干这个的。

来看一个简单的 Python 接口定义:

from abc import ABC, abstractmethod
from typing import Any, Dict

class IRule(ABC):
    """规则接口,所有规则都必须实现这个接口。"""

    @abstractmethod
    def initialize(self, config: Dict[str, Any]) -> None:
        """初始化规则,加载配置。"""
        pass

    @abstractmethod
    def evaluate(self, context: Dict[str, Any]) -> str:
        """
        评估规则。
        参数 context: 包含请求上下文,如用户ID、IP、设备指纹等。
        返回值: 'pass', 'reject', 'review'
        """
        pass

    @abstractmethod
    def get_metadata(self) -> Dict[str, Any]:
        """返回规则的元数据。"""
        pass

嗯,这里要注意。接口设计得越简单,扩展性就越强。我在项目中遇到过,有人把规则接口设计得特别「胖」,塞了几十个方法进去。结果新加一条规则,就得实现一堆用不上的方法,痛苦得很。

规则元数据:规则的名字、版本、优先级

元数据,就是「关于数据的数据」。对于规则来说,元数据就是它的身份证。

我一般会包含这几个字段:

字段 类型 说明
name str 规则名称,用于日志和监控。比如「IP_黑名单_检查」
version str 规则版本号,用于灰度发布和回滚。比如 "1.2.0"
priority int 优先级,数值越小越优先。用于规则排序
description str 规则描述,方便运维同学理解
enabled bool 是否启用。动态开关规则就靠它

为什么版本和优先级这么重要?

我曾经踩过一个坑。有一次上线新规则,没注意版本号,结果旧规则和新规则混在一起执行,导致线上误杀了一大批正常用户。从那以后,我强制要求每条规则必须带版本号,并且在加载时做版本校验。

优先级呢?说白了就是规则执行的顺序。比如「白名单」规则优先级最高,一旦命中直接放行,后面的规则就不用跑了。这能大幅提升性能。

来看一个元数据类的实现:

from dataclasses import dataclass, field
from typing import Any, Dict

@dataclass
class RuleMetadata:
    """规则元数据"""
    name: str
    version: str = "1.0.0"
    priority: int = 100
    description: str = ""
    enabled: bool = True

    def to_dict(self) -> Dict[str, Any]:
        return {
            "name": self.name,
            "version": self.version,
            "priority": self.priority,
            "description": self.description,
            "enabled": self.enabled
        }

你看,用 dataclass 定义,简洁又清晰。默认优先级设为 100,这样新加的规则默认排在后面,不会影响已有的高优规则。

规则条件与动作的抽象

一条规则,本质上就是「如果条件满足,就执行动作」。所以,把条件和动作抽象出来,是复用规则逻辑的关键。

我习惯把条件抽象成一个接口 ICondition,动作抽象成 IAction

条件接口

class ICondition(ABC):
    @abstractmethod
    def evaluate(self, context: Dict[str, Any]) -> bool:
        """判断条件是否满足。返回 True 表示条件成立。"""
        pass

动作接口

class IAction(ABC):
    @abstractmethod
    def execute(self, context: Dict[str, Any]) -> None:
        """执行动作,比如记录日志、发送告警、拒绝请求。"""
        pass

有了这两个抽象,我们就可以组合出各种规则。比如:

  • 条件:IP 在黑名单中、用户注册时间小于 7 天、设备指纹异常。
  • 动作:拒绝请求、标记为高风险、发送短信验证码。

你想想看,这样做的好处是什么?

条件可以复用。比如「用户注册时间小于 7 天」这个条件,既可以用在「新用户注册风控」规则里,也可以用在「新用户支付风控」规则里。动作也可以复用。比如「拒绝请求」这个动作,几乎所有规则都能用。

来看一个完整的规则实现示例:

class IpBlacklistCondition(ICondition):
    def __init__(self, blacklist: set):
        self.blacklist = blacklist

    def evaluate(self, context: Dict[str, Any]) -> bool:
        ip = context.get("ip", "")
        return ip in self.blacklist

class RejectAction(IAction):
    def execute(self, context: Dict[str, Any]) -> None:
        print(f"拒绝请求: {context.get('ip')}")
        # 实际项目中,这里会调用风控决策引擎的拒绝接口

class IpBlacklistRule(IRule):
    def __init__(self):
        self.metadata = RuleMetadata(
            name="IP黑名单检查",
            version="1.0.0",
            priority=10,
            description="检查请求IP是否在黑名单中"
        )
        self.condition = None
        self.action = None

    def initialize(self, config: Dict[str, Any]) -> None:
        blacklist = set(config.get("blacklist_ips", []))
        self.condition = IpBlacklistCondition(blacklist)
        self.action = RejectAction()

    def evaluate(self, context: Dict[str, Any]) -> str:
        if self.condition.evaluate(context):
            self.action.execute(context)
            return "reject"
        return "pass"

    def get_metadata(self) -> Dict[str, Any]:
        return self.metadata.to_dict()

你看,这条规则的结构非常清晰。条件负责判断,动作负责执行,规则本身只负责编排。这就是抽象的力量。

核心要点:规则 = 条件 + 动作。把条件和动作抽象成接口,就能实现规则的灵活组合和复用。
我的小技巧:在设计条件接口时,我习惯让 evaluate 方法返回一个「置信度」而不是简单的布尔值。比如 0.8 表示「很可能命中」。这样在规则引擎里可以做更精细的决策,比如多个条件加权求和。
注意:条件和动作的实现,一定要保证线程安全。因为规则引擎通常是多线程并发执行的。我见过有人用全局变量存储条件状态,结果线上出现数据竞争,规则评估结果时对时错。血的教训。

本章小结

这一章,我们聊了规则的定义与抽象。核心就三件事:

  1. 规则接口(IRule):定义了规则的生命周期方法,包括初始化、评估、获取元数据。
  2. 规则元数据:给规则一个身份,包括名称、版本、优先级等。这是动态加载和灰度发布的基础。
  3. 条件与动作的抽象:把规则拆成「条件」和「动作」两个独立的部分,实现逻辑复用。

说白了,好的抽象,能让你的规则系统像搭积木一样灵活。每块积木都很简单,但组合起来能应对各种复杂的业务场景。

下一章,我们会聊聊如何把这些规则动态加载到系统中。嗯,那才是真正有意思的部分。


公众号:蓝海资料掘金营,微信deep3321