去年四月份,我接手了一个支付网关模块。代码里有一个 PaymentGatewayFactory 抽象工厂,四个子工厂,每个子工厂里还套了两个策略类。为了给对接方一个新渠道,我需要先 new 一个 WeChatPaymentGatewayFactory,再 new 一个 WeChatPaymentStrategy,还必须传入一个 PaymentConfigBuilder。整台电脑都仿佛在替我念紧箍咒。那天我盯着屏幕二十分钟,终于想起一个早就该问的问题:这些东西,到底是为谁服务的?
后来我翻代码历史,看到最初只有两个支付渠道,写代码的人照着书上的'AbstractFactory'来了个大套餐。结果渠道从两个变成八个,这些工厂、策略、模板方法纠缠在一起,每次加渠道都要改四五个类,测试启动时间从2秒涨到18秒——因为所有单例管理器都在初始化连接池。我统计了一下,模块里用了七个不同的单例类,有的单例还要在构造器里加载外部配置。如果换成依赖注入容器,这些单例生命周期交给容器管理,代码能少写三百行,而且每个测试都可以独立跑。
有人会说,设计模式不是面向对象的精华吗?我的观点是:模式是药,不是饭。你去看看策略模式的经典写法:先定义策略接口,再写一堆实现类,然后一个 Context 持有策略。Java 7 之前这确实没办法,只能靠多态。但 Java 8 之后,一个 lambda 表达式就搞定了。比如实现促销折扣,老代码:DiscountStrategy 接口,VipDiscount、NormalDiscount、LoyaltyDiscount 三个类,然后 if (type == VIP) return new VipDiscount(); 这一套下来几十行。而现在只需要一个 Map<String, Function<Double, Double>> strategies = Map.of("VIP", p -> p * 0.8, "NORMAL", p -> p); 就完了。哪种更直观?如果你还在用 Java 7,或者团队里有坚决不写 lambda 的老牛,那就用策略模式;否则,一个函数更值得。
但我不反对设计模式本身。我反对的是模式前置思想。很多年轻人(包括十年前的我)学完模式,下意识在脑子里给每个需求找模式,就像手里拿着锤子看什么都是钉子。我见过一个刚入职的小伙,给一个只有三个字段的User类加了建造者模式,明明直接new User("张三", 20, "男")就完事,他非要 User.builder().name("张三").age(20).sex("男").build()。代码从两行变成八行,组长让他删掉他还不乐意,说以后肯定要加字段。可问题是,加字段时你也可以直接加构造函数参数,或者用setter,根本不需要为了假设的将来牺牲现在的简洁。
怎么判断该不该用一个模式?我现在给自己定了三个问题。第一,代码现在是不是真的让你痛苦?比如你在改需求时不得不复制粘贴一大段,或者两个类之间扯不断理还乱。第二,用了模式后,调用方是更简单了还是更复杂了?很多模式的引入反而是把复杂内部化,可是调用方还得理解上下文。第三,你付出的复杂换取的是不是“以后可能会需要”?如果是,就别用了,因为以后你会变,需求会变,模式很可能变成新的枷锁。另外,能组合就别继承。你看那些经典模式里,策略、装饰器、状态,其实都是组合的思想,而不是强制你建一个类树。用函数去组合,比用继承去分类,在现代编程里更划算。
最后说回那个支付模块。我花了三天把所有工厂和策略包装全拆掉,换成一张支付渠道配置表外加三个普通函数:解析配置、校验参数、发起请求。效果很夸张:代码从两千四百行降到八百二十行,新渠道对接从一天缩短到两小时,测试从十二分钟变成四十秒。当然,我也没把设计模式当垃圾扔掉,只是把它们从'预置框架'挪到了'应急工具箱'里。模式本身从来不是错,错的是你以为不写模式就代表水平不够。很多时候,能用一个函数解决的问题,就别给它盖一座庙。