介绍
理解函数的方式。
如果您来自函数式编程背景,我将要说的内容对您来说可能并不新鲜。但是,我知道有些开发人员不习惯强类型,当他们想到函数时,Hindley-Milner 的表示法并不是他们脑海中首先浮现的东西。我把这篇文章写给那些人。
Input/Output 模式
我们知道函数接受参数并返回一个值。我们有时会忽略的是,这个特性是一个非常强大的设计模式,我们可以用它来编写更好的函数。
首先尝试通过回答以下两个问题来编写任何函数:
这个函数的输入是什么(它接受什么参数)?
这个函数的输出是什么(它返回什么数据)?
这样做可以让您将技术细节放在一边,专注于作为 input/output 操作的函数,事实上,它确实如此。您给出的答案可能暗示了某些实现细节,但最重要的是,它们在编写任何实际代码之前很久就定义了对函数职责的明确约束。
你可以对你的答案使用抽象类型。例如,一个函数可以接受一个苹果列表并返回一只快乐的狐狸。这种类型抽象进一步将 call 签名与 implementation 分离。
让我们将其付诸实践。比如,你需要编写一个验证表单字段的函数。影响字段验证的因素有很多,但你可以把这些放在一边,先回答 Input/Output 问题:
我的函数接受一个字段;
我的函数返回验证结论 ()。boolean
写下来后,这些答案表示函数的调用签名:
function validate(field: Field): boolean;
此时您可能不知道类型可能是什么,但您知道它代表什么。总的来说,该函数也可以这样说:无论哪些因素影响字段的有效性,您最终都必须解析为布尔值。定义的输入和输出起到限制作用,防止我们的函数在截止日期驱动的开发过程中变得过于智能。这确保了我们编写的 logic 位于 function 的职责范围内,并且保持简单,同时满足 single responsibility 和 KISS 原则。Fieldvalidate
采用此模式并不意味着您应该立即使用 TypeScript 或任何其他强类型语言重写代码。首先,它是思考函数的方式。将输入和输出记录在 JSDoc 块或 Sketchbook 中是可以的。从改变您的思维方式开始,工具就会随之而来。
类似于在制作适当的 UX 之前将用户的需求放在首位 decisions 中,您需要考虑函数接受和返回哪些数据 为了建立其未来实施的边界。
按比例排列
考虑使用这种模式的函数固然很好,但是通常由多个函数组成并表示更复杂的逻辑的实际操作呢?
事实是,即使是最复杂的函数也可以写成一组连续执行的较小函数。当您以这种方式处理任务时,您可以专注于一次设计每个单独的函数。但是,将函数隔离在一起是危险的,因为您最终可能会得到多个无法组合在一起的拼图。有一个函数组合规则可以避免这个问题:
如果两个函数的输出可以用作另一个函数的输入,则两个函数是可组合的。
知道了这一点,让我们实现一个相当复杂的操作,该操作接受用户并返回其所有帖子下的点赞数。为了防止复杂性,我们可以将此操作描述为一系列步骤:
获取用户 → 获取用户的帖子 → 获取帖子的点赞数量
这些步骤中的每一个都是一个函数,我们可以应用 Input/Output 模式来设计其调用签名,同时牢记组合原则。
const getUser = (id: string) => User const getPosts = (user: User) => Post[] const getTotalLikes = (posts: Post[]) => number
这种功能链的高级概述使您能够不受干扰地跟踪数据流,并突出显示 logic 中的潜在问题。此外,这只是一个有趣的练习。
最后,函数是关于转换数据的,因此请使用所有方法 可用于确保转换的连贯性和高效性。
还有一件事!
关于 Input/Output 方法还有一个隐藏的宝石。假设你用 “my function accept a list of strings and return a number” 来回答这些主要问题。恭喜,您刚刚为您的函数编写了一个单元测试!
expect(myFunc(["a", "b"])).toEqual(2);
此模式的结果可能会反映在测试方案中,从而使用实际的单元测试来支持函数设计决策。这鼓励 TDD(测试驱动开发)和 BDD(行为驱动开发),因为我们通过描述函数的输入和输出来表达函数的意图。
总结
关注函数的 input 和 output types 定义了该函数的简明规范:
函数的调用签名;
该函数的最小单元测试。
根据规范实现事物是一种愉快且安全的体验,我绝对建议您习惯。
后记。
如果你想了解更多实用的模式和功能设计的方法,请点赞这篇文章,并在评论中告诉我。