Avoid Public Functions Calling Other Public Functions

Avoid Public Functions Calling Other Public Functions

A small-scale rule of thumb. This article will land much better with a concrete example, but I am drawing a blank here, so you gotta just blindly trust my say so! When implementing a software module, it’s often a case that you are writing a public function foo, and at least part of what foo does is already handled by another public function bar. The DRY instinct encourages you to call bar() from within foo. This usually creates problems down the line.

这是一个小规模的经验法则。如果能用具体的例子来阐述,这篇文章的效果会好得多,但我现在一时想不起来,所以你只能盲目地相信我所说的!在实现软件模块时,经常会出现这种情况:你正在编写一个公共函数 foo,而 foo 所做的一部分工作已经由另一个公共函数 bar 处理了。DRY(不要重复自己)的本能会促使你在 foo 内部调用 bar()。这通常会在后续开发中引发问题。

Instead, create a new private function and refactor both foo and bar to call that shared helper: // BAD pub fn foo() void { return bar(.{ .frobnicate = true }); } pub fn bar(options: struct { frobnicate: bool = false, }) void { // … } // GOOD pub fn foo() void { return helper(.frobnicate); } pub fn bar() void { return helper(.transmogrify); } fn helper(mode: enum { frobnicate, transmogrify }) void { // … }

相反,你应该创建一个新的私有函数,并重构 foo 和 bar,让它们都去调用那个共享的辅助函数: // 糟糕的做法 pub fn foo() void { return bar(.{ .frobnicate = true }); } pub fn bar(options: struct { frobnicate: bool = false, }) void { // … } // 推荐的做法 pub fn foo() void { return helper(.frobnicate); } pub fn bar() void { return helper(.transmogrify); } fn helper(mode: enum { frobnicate, transmogrify }) void { // … }

Rationale: the goal of a public API is to provide an interface that works best at the call site. If parts of public API are re-used to implement the rest of the API, then there’s a tension between making the interface convenient for the outside user and making it convenient to use from within abstraction itself. Namely, functions tend to grow optional parameters that should never be set by the user. Crucially, the parameter growth and interface degradation can happen after the original code sharing.

其基本原理是:公共 API 的目标是提供一个在调用点使用效果最佳的接口。如果公共 API 的部分内容被重用来实现 API 的其余部分,那么在“方便外部用户使用”和“方便在抽象内部使用”之间就会产生冲突。具体来说,函数往往会增加一些本不该由用户设置的可选参数。关键在于,参数的增加和接口的退化可能会在最初的代码共享之后发生。

By proactively making the shared implementation private, the tension is defused. You still might need to tinker with helper signature until it does everything it needs to do, but, because the helper is private, this is a cheap, local change. A notable exception to this rule of thumb are “shortcut” APIs, where a public function is defined as a commonly used chain of calls to other public functions. There, the rule is the opposite — no private calls are allowed, to make sure that it is a genuine shortcut, and not a midlayer mistake.

通过主动将共享实现设为私有,这种冲突就化解了。你可能仍然需要调整辅助函数的签名,直到它能完成所需的一切工作,但由于该辅助函数是私有的,这只是一个低成本的局部修改。这一经验法则的一个显著例外是“快捷方式”API,即一个公共函数被定义为对其他公共函数的一系列常用调用。在这种情况下,规则恰恰相反——不允许进行私有调用,以确保它是一个真正的快捷方式,而不是中间层的错误。

P.S. While researching if anyone wrote a better blog post already, I stumbled upon a fun stack overflow question, asking, paraphrasing: I’ve heard that it is bad when a public function calls another public function. Is this really so? The most upvoted answer explains that it is indeed bad, because this makes mocking to test a single method in isolation more complicated. Which is, imo, a natural, but horrible consequence of misguided approach to testing. You shouldn’t be mocking your methods, you should mock your IO.

附注:在研究是否已经有人写过更好的博文时,我偶然发现了一个有趣的 Stack Overflow 问题,大意是:我听说公共函数调用另一个公共函数是不好的。真的是这样吗?得票最高的回答解释说这确实不好,因为它使得为了隔离测试单个方法而进行的模拟(mocking)变得更加复杂。在我看来,这是测试方法误入歧途所导致的自然但糟糕的结果。你不应该模拟你的方法,而应该模拟你的 IO。

And, similarly, you shouldn’t test methods, but rather software functionality (unless you are building a public library with a stable API, which is a fun and important, but very narrow fraction of software development). The accepted answer says that this recommendation makes no sense if it is not backed by an industry wide or an organizational standard. Which somehow manages to make even less sense — you shouldn’t follow the guideline rules because they are written in sanctified ink! Rather, you should apply each particular rule because of the underlying reasons that motivate the rule. Guidelines and best practices articles can concentrate lots of good reasoning in a single place, but they don’t absolve you from thinking!

同样,你不应该测试方法,而应该测试软件功能(除非你正在构建一个具有稳定 API 的公共库,这虽然有趣且重要,但只是软件开发中非常狭窄的一部分)。被采纳的回答称,如果这一建议没有得到行业标准或组织标准的支持,那么它就毫无意义。这反而更让人费解——你不应该仅仅因为准则被写在“神圣的墨水”里就去盲目遵循!相反,你应该应用每一条特定的规则,是因为其背后的动机。指南和最佳实践文章可以将许多好的推理集中在一处,但它们并不能免除你思考的责任!