早些年我负责一块挺重复的功能,好几种差不多的东西,都要走同一套「收请求、取数据、转格式、回结果」的流程。我嫌麻烦,不想给每种东西都重写一遍,就琢磨了个偷懒的法子,做一个通用的骨架,把流程里不变的那截固定住,把每种东西不一样的地方用一个开关推开。
骨架长这样(下面是等价改写,名字都换过了):
// 骨架:写一遍,几种对象共用
internal abstract class ProviderTemplate<TList, TModel, TCriteria, TService>
{
public abstract OperationResult Retrieve(IProviderContext ctx);
}
// 变体:一种对象一个类,开关值写在类头上
[Handles(StoreObjectType.Log)]
internal sealed class LogProvider : ProviderTemplate<...> { ... }
新来一种东西,只要把开关拨到对应位置,骨架就自动知道该走哪条路。那时候我对「省事」的理解很朴素,能不写第二遍的绝不写第二遍,当时心里还挺得意。
我没意识到的是,这个开关一旦立起来,就不再只为我服务了。后面的人接手新功能,第一件事就是去拨这个开关、按它规定的姿势写。我图的那点省事,变成了所有人必须遵守的写法。
转机出现在一次排错。有个功能跑得不对,新人来问我,我顺着那个开关一路查,才发现它牵连的东西比我想的多得多。它串起了好几个层级,每一层都指望着开关给出正确的信号,开关一拨错,好几层一起歪。
具体到代码,那个拨动只发生在一处,但它管着后面所有的事:
public static RetrieveResponse DispatchRequest(RetrieveRequest request)
{
var objectType = request.ObjectType;
// 就这一查表,决定后面每一层走哪条路
if (!ProviderRegistry.Factory.TryGet(objectType, out var provider))
return FailureResponse(Codes.NotSupported, "query not supported.");
return provider.Retrieve(SenderFor(request));
}
那一刻我明白,当年那个「省事的小开关」已经长成了整个体系的枢纽。它确实让加功能变快了,但也意味着谁想动这块,都得先弄懂它,一动就可能影响所有人。
所以我现在的习惯是,做这种通用机制,先把「它会不会变成隐形的规矩」想清楚。会的话,就在最显眼的地方写清它的边界,什么能拨、什么不能拨、拨错了会怎样,别让它变成一个只有你懂、别人只能照猫画虎的黑盒。省事没错,可省下来的工夫得拿一部分出来,给后来的接手人留路标。