设计模式与生活㉑:一句“建议复查”,是好几句话拼出来的

周六,睡到自然醒。

醒的时候,窗帘缝里那条光已经挪到了床脚。没有闹钟,也不用起。躺着听了一会儿楼下的动静------有人遛狗,链子拖在地上响。

起来做饭。冰箱里剩半把青菜、两个鸡蛋。煮了碗面,卧了个蛋,青菜下锅烫了一下。吃完把碗泡在水池里,出门在小区里转了两圈。

太阳很好。健身区有个老人在压腿,动作很慢;快递柜前排着人,一个接一个输取件码。

回到家,手机里躺着一条消息:体检报告已出。

点开,二十几项,一行一行往下翻。箭头有上有下。去年只有一项带箭头,今年三项。

心里沉了一下。

体检这事,查之前想不起来,查完才想起身体这回事。刚才那碗面,忽然显得有点敷衍。

报告最下面是一段话:几项指标异常,建议......

那段话不全是医生一个字一个字敲出来的。报告上的箭头、"偏高""偏低"这些标注,很多是系统按规矩算的:这个数过了上限,记一笔;这一组里超了两项,再记一笔。小规矩套小规矩,最后拼成一句话。

今天聊的解释器模式(Interpreter):把一句判断拆成能算的小句,再一层层拼回去算。系列第二十一篇。

报告先到了手机里

01 一长串 if,把话焊死在判断里

最直接的写法,是把报告上的规矩全塞进一个方法。

java 复制代码
void check(Report r) {
    if (r.get("总胆固醇") > 5.2) {
        System.out.println("总胆固醇偏高");
    }
    if (r.get("低密度脂蛋白") > 3.4) {
        System.out.println("低密度脂蛋白偏高");
    }
    if (r.get("甘油三酯") > 1.7) {
        System.out.println("甘油三酯偏高");
    }
    if (r.get("高密度脂蛋白") < 1.0) {
        System.out.println("高密度脂蛋白偏低");
    }

    if ((r.get("总胆固醇") > 5.2 && r.get("低密度脂蛋白") > 3.4)
            || (r.get("甘油三酯") > 1.7 && r.get("高密度脂蛋白") < 1.0)) {
        System.out.println("血脂这一组,建议复查");
    }
}

第一眼能跑。

毛病有三个。

一是阈值写了两遍。5.2 在上面出现一次,下面又出现一次。哪天机构把上限调了,改了上面忘了下面,一份报告就能自己跟自己打架。

二是判断和话术焊在一起。想改一句提示语,得钻进 if 里;想加一条规矩,也得改这个方法。

三是没有层次。单个指标是"一个数跟一条线比",组合是"几句话拼起来"。这是两种东西,挤在同一层里。指标一年比一年多,这个方法一年比一年长。

一份报告,七八条 if,全堆在一个方法里

02 把判断拆成能算的小句

报告上的判断,拆到底只有两种:某个数高于上限,某个数低于下限。

先定"一条规矩"长什么样。它只干三件事:算不算命中,这句话本身怎么念,带上数值给人看又怎么念。

java 复制代码
interface Rule {
    boolean hit(Report r);   // 算不算命中
    String phrase();         // 这句话本身
    String show(Report r);   // 带上数值,给人看
}

高于上限:

java 复制代码
class Above implements Rule {
    private final String item;
    private final double limit;

    Above(String item, double limit) {
        this.item = item;
        this.limit = limit;
    }

    public boolean hit(Report r) {
        return r.get(item) > limit;
    }

    public String phrase() {
        return item + " 高于 " + limit;
    }

    public String show(Report r) {
        return item + " " + r.get(item) + ",高于 " + limit;
    }
}

低于下限的 Below,把 > 翻成 <,"高于"换成"低于",别的照抄。

为什么要单独留一个:报告上不是所有箭头朝上才叫麻烦。高密度脂蛋白这种,低了才要留意。同样是比大小,方向是反的。

到这儿,一条规矩还只是一个孤零零的小句。让它变成"一句话"的,是连接词。

java 复制代码
abstract class Combine implements Rule {
    protected final Rule[] rules;

    Combine(Rule... rules) {
        this.rules = rules;
    }
}

class All extends Combine {        // 且
    All(Rule... rules) {
        super(rules);
    }

    public boolean hit(Report r) {
        for (Rule rule : rules) {
            if (!rule.hit(r)) {
                return false;
            }
        }
        return true;
    }
}

class Any extends Combine {        // 或
    Any(Rule... rules) {
        super(rules);
    }

    public boolean hit(Report r) {
        for (Rule rule : rules) {
            if (rule.hit(r)) {
                return true;
            }
        }
        return false;
    }
}

phrase 和 show 这两个方法,两个类都照同样的方式拼字符串,这里省了;打印树的方法和 main,也不占篇幅。

关键在一句:All 和 Any 自己也实现了 Rule。 所以 All 里可以塞 Any,Any 里可以再塞 All,套多少层都行。

连接词也是规矩,规矩可以套规矩

03 一层一层,往上算

把这四个小句和两个连接词拼起来:

java 复制代码
Rule tc  = new Above("总胆固醇", 5.2);
Rule ldl = new Above("低密度脂蛋白", 3.4);
Rule tg  = new Above("甘油三酯", 1.7);
Rule hdl = new Below("高密度脂蛋白", 1.0);

Rule lipid = new Any(
        new All(tc, ldl),
        new All(tg, hdl));

文中的 5.2、3.4、1.7、1.0 只是示例,各家机构的参考范围并不一样。

人念这句话,是从左往右:

text 复制代码
((总胆固醇 高于 5.2 且 低密度脂蛋白 高于 3.4) 或 (甘油三酯 高于 1.7 且 高密度脂蛋白 低于 1.0))

括号不是给人看的,是树的形状。

拿一份报告进去跑(Report 只是个装指标的 Map,这里不展开):

java 复制代码
Report first = new Report()
        .put("总胆固醇", 5.8)
        .put("低密度脂蛋白", 3.9)
        .put("甘油三酯", 1.5)
        .put("高密度脂蛋白", 1.1)
        .put("尿酸", 452);

实际打印出来,树的部分是这样:

text 复制代码
✓ 或
  ✓ 且
    ✓ 总胆固醇 5.8,高于 5.2
    ✓ 低密度脂蛋白 3.9,高于 3.4
  ✗ 且
    ✗ 甘油三酯 1.5,高于 1.7
    ✗ 高密度脂蛋白 1.1,低于 1.0
→ 血脂这一组,建议复查

第二支"且"没成立。但"或"只要一支成立就够了,所以整句亮。

报告上其实还有一项尿酸偏高,这棵树里还没有它------正好留到下一步。

现在加一条规矩:尿酸超过 420。

java 复制代码
Rule uric = new Above("尿酸", 420);
Rule whole = new Any(lipid, uric);

只挂了一个节点。原来的四个小句、两个连接词,一个字都不用动。

换一份报告,血脂四项都在范围内,只有尿酸高:

text 复制代码
✓ 或
  ✗ 或
    ✗ 且
      ✗ 总胆固醇 4.6,高于 5.2
      ✗ 低密度脂蛋白 2.8,高于 3.4
    ✗ 且
      ✗ 甘油三酯 1.2,高于 1.7
      ✗ 高密度脂蛋白 1.3,低于 1.0
  ✓ 尿酸 468.0,高于 420.0
→ 建议复查

加一条规矩,是往树上挂一个节点,不是改一段方法。

左边那棵树,就是这份报告判断的算法:两个"且"各管一组,一个"或"给结论

04 为什么叫解释器

这棵树,就是一句话的语法树。

程序算的时候从上往下:先把叶子算成 true 或 false,再一层层合上去,最后合出一句结论。这个"照着语法结构一层层把结果算出来"的过程,就叫解释。

实际项目里的解释器,往往还多一步:把一句话从字符串解析成树。正则表达式、Spring 的 SpEL、MyBatis 动态 SQL 里的 OGNL,都是这么干的。我们这里是手拼的树,省了解析。

别小看那一步。字符串一进来,词法、语法、括号配对、报错位置,全是活。写一个能用的解释器,大部分工夫花在"把话读对"上,不是"把数算对"。

跟组合(Day⑱)再对一句:组合管的是"单个和一组长一个样",树是它装东西的筐;解释器管的是"这棵树怎么算出一个结果",树就是那句话本身。

所以有几条边界:

  • 规矩就一两条,直接 if,别建树。煮一碗面,犯不上先搭一个调料架。
  • 要让人写规则文本,先看现成的表达式引擎(SpEL、OGNL、Aviator 这类),别自己发明一门方言。
  • 解释器只负责"按规矩算"。数从哪来(检验仪器、接口、手工录入),算完拿去干什么(存库、发通知),都不归它管。

再把失真点挑明。

现实里,报告上的规矩是检验机构定死的,你手里没有。代码里的树是你自己拼的;要改一条上限,得重新发版。想做到改规矩不发版,就得把树从代码里挪到配置里,那是另一件事。

还有一处,比技术更要紧:算出来的只是"提示",不是诊断。

箭头不等于病,参考范围也不是健康的分界线。这段代码能给报告加一句"建议复查",给不了"你没事"或"你有事"。真结论,得让医生看。

05 又转了一圈

看完报告,把手机扣在桌上,又出门转了一圈。

这次没带手机,走得比刚才慢。小区里有人在跑步,跑得不快,一圈一圈,从我身边过去。

报告替不了我做什么。它只是把身体早就想说的话,晚几天递到我手上。

那三项我记进日历,标了红,三个月后再查一次。不是为了盯着数字,是想知道,这时候改,还来不来得及。

跟前面二十篇对一句:单例管造几个,工厂管怎么造,模板方法管事情怎么走,策略管这一步用什么做法,装饰器管外面加几层,命令管把请求做成对象,代理管谁能碰到真东西,观察者管变化后通知谁,外观管一堆零件只留一个入口,状态管当前状态下怎么做,责任链管请求由哪一环接住,原型管拿哪个现成对象复制,桥接管抽象和实现怎么独立扩展,享元管把能共享的部分只留一份,备忘录管把某个时刻的状态存下来、再原样恢复,生成器管把复杂对象的装配拆成一步一步、最后一次性成型,访问者管对一批稳定的元素做什么操作由递进来的单子说了算,组合管单个和一组长一个样,适配器管两套对不上的接口中间怎么搭一座桥,中介者管一堆对象互相说话时怎么都先经过一个中间人。本篇解释器,管一句判断怎么拆成能算的小句、再拼回去算。

带走一句话:解释器 = 一句判断,拆成能算的小句,再一层层拼回去算。

箭头是报告给的,日子是自己过的。

那句"建议复查",几行代码就能拼出来;往后的日子,拼不出来,只能一天天过。


参考资料

相关推荐
杯子不吃紫菜1 天前
LangGraph 体系实战(下):中断恢复、流式、子图与设计模式
单片机·嵌入式硬件·设计模式
是2的10次方啊1 天前
设计模式与生活⑳:楼里的事都找前台,你就再没见过师傅
设计模式
是2的10次方啊2 天前
设计模式与生活⑲:投影连不上,别急着换投影仪
设计模式
cjz38993 天前
规则树:把一百行if-else拆成一棵树
设计模式
Adellle3 天前
设计模式指南 (Java 版) ㊁
java·开发语言·设计模式
是2的10次方啊3 天前
设计模式与生活⑱:工资还没焐热,账单已经凑成一摞
设计模式
百思不得小李4 天前
一个复杂的动态表单是如何实现的
前端·设计模式
爱你真是太好了4 天前
上下文组成以及如何压缩
设计模式
freejackman4 天前
【工程化】从“单兵作战”到“多引擎架构”:基于门面模式的重构实战
大数据·数据库·hive·sql·测试工具·设计模式·架构