在一些数据处理场景中,我们经常会设计这样的接口:
byte[] GetImageArgb();
它看起来很自然:调用方法,得到一段结果数据。
但在某些情况下,这类接口会带来一个不容易察觉的问题:
后续的优化空间,被提前限制了。
问题从哪里开始?
这个接口同时做了两件事:
-
表达能力:获取 ARGB 数据
-
引入决策:用 byte[] 承载,并由方法内部完成分配
这两件事本质上属于不同的关注点:
-
前者是"能力表达"
-
后者是"使用体验优化 / 默认策略"
问题在于:
它们在接口层被混在了一起。
当关注点没有分离
一旦能力与策略耦合:
-
默认决策成为唯一路径
-
调用方无法绕开
-
系统行为被提前固定
换句话说:
可选项在接口层被提前收敛了。
而这违背了一个很基本的设计准则:
尽可能保留更多的可选项。
这类问题并不局限于接口
类似的情况,在更大尺度的设计中也很常见:
-
过早绑定某种数据库、图像处理库
-
过早确定系统形态(如 B/S、C/S)
它们的共同点是:
在能力尚未被充分表达之前,就提前锁定了实现路径。
也就是说,本该后置的决策,被前移到了更早的阶段。
回到接口设计
但在这篇文章中,我们只关注一个更基础、也更容易被忽视的层面:
接口设计。
因为一旦在这里没有处理好,这种"过早做决定"的问题,就会以更隐蔽的方式渗透进整个系统,并在后续被不断放大。
在图像处理这类数据密集场景中,这种影响尤为直接:
-
数据被强制写入新的缓冲区
-
内存分配策略无法控制
-
上下游无法复用同一块数据
最终结果是:
零拷贝链路在接口处被打断;离高性能目标更远。
一旦数据在这里被"落地"为一个新对象,
后续即使再优化,也很难恢复原本可以存在的数据流动方式。
更合理的设计方式
更健康的方式是把关注点拆开:
先表达能力本身,再叠加默认使用方式。
例如:
void GetImageArgb(Span<byte> destination);
这个接口只表达能力:
-
不分配内存
-
不决定数据归属
-
只负责写入
在此之上,再提供便利接口:
byte[] GetImageArgb()
{
var buffer = new byte[...];
CopyImageArgbTo(buffer);
return buffer;
}
这样:
-
想简单用 → 有默认路径
-
想优化 → 可以绕开默认路径
写在结尾
在设计接口时,可以问一个问题:
这个接口中的默认决策,是否可以被绕开?
如果可以,它只是优化;
如果不可以,它就开始限制系统的能力。
问题从来不在于是否做了优化,而在于:
优化是否在表达能力时,就已经参与并固定了决策。
一旦如此,可选项就会在接口层被提前收敛。
因此,一个更稳妥的设计目标不是"替调用方做出更方便的决定",而是:
在提供默认路径的同时,仍然保留做出不同选择的可能。