27

计算机中的权衡(trade-off)

 4 years ago
source link: https://jiajunhuang.com/articles/2020_01_11-paradigm.md.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

计算机中一切都是权衡(trade-off)。所谓权衡,即在两个极端中取一个相对中间的位置,不是非黑即白,而是根据实际情况作出取舍, 也许偏向A多一点,从而偏向B少一点,也许是反过来。

A <------------------------> B
   \                      /
          trade-off

我们来聊几个常见的例子。

(关系型)数据库范式和反范式

而常见的两个极端,就是范式和反范式。所谓范式,就是一定的套路,是一种大家都遵循的行为准则,有时候业界也会称之为最佳实践。 反范式,就是不按套路出牌,反其道行之。如果你是工作了几年的人,细心回想一下,就会发现日常工作中到处都是范式和反范式, 同时到处都是权衡(trade-off)。

数据库设计中的范式和反范式我想大家都应该有所耳闻( 可以参考此处 )。

如果你仔细读一下 3NF 就会发现,它是冲着数据的低冗余去的,首先通过1NF把数据拆散成最小可拆分粒度,然后通过2NF确定唯一性, 通过3NF消除冗余。加入我们遵守3NF,实际设计中,就会发现为了消除冗余,数据库需要大量使用外键,而外键有这么一个特点,每次 修改数据时,都会去检查另一张表中的数据,从而确保外键的约束。对于传统的数据库使用来说,这没有什么太大的问题,然而放在 互联网中,一旦并发上上来(虽然大部分所谓的互联网公司仍然达不到这个量),外键将迅速成为数据库性能瓶颈。因此,常见的互联网 公司设计使用中,一般都会使用反范式,即主动冗余一份数据到所需要的,并发非常高的表中(更常见的做法是加缓存,但是不在此文 范畴内)。

而至于实际用途中,遵守多少范式,又遵守多少反范式,是一个度的问题,我们需要根据实际情况进行权衡取舍。

常量

经常在代码中看到这么两种设计:

return c.JSON(http.StatusOK, {})

或者是

return c.JSON(200, {})

我们大家都知道,RESTful中会使用HTTP状态码来表示一些返回状态,例如200就是成功,400就是参数错误等等,而我们几乎所有人都 听说过,代码需要高内聚,低耦合,不要使用魔数(magic number)等等,因此很多情况下我们会使用 http.StatusOK 来表示200, 仔细想想,其实这样做有两个坏处,那就是所有地方都要导入定义 StatusOK 这个常量的模块;阅读并理解 StatusOK 的速度没有 200快,而且实际情况中我们常用的80%状态码就那么几个:200, 301, 302, 400, 401, 404, 405等。

当然,从另一方面,当你遇到不熟悉的状态码时,文字的含义和可读性比数字高,例如 402 到底是什么意思?如果使用 StatusPaymentRequired 就会好很多。

再例如之前我读Go的源码时,发现源码中到处都是使用 “linux”, “windows”, “drawin” 三个字符串,而不是抽出来成为const, 我就去提了个 issue 问作者们为啥不抽成常量然后导入。

作者的回答很有道理,因为大家都懂这几个词的意思,他们其实也是常量。而导入一个新的模块更加麻烦,这也是一种取舍。

抽象泄漏(abstraction leaky)

抽象,这是CS的代名词。无论是语言,还是数据,这些年来,我们都是冲着抽象一步一步发展。正所谓,计算机中没有什么事情是加一个 中间层解决不了的,如果有,那就加两个。举个例子,我们从最开始的汇编语言,为了屏蔽各种汇编语言的细节,我们提供了高级语言 这层抽象。在高级语言中,为了屏蔽多个语句的实现细节,我们提供了函数这层抽象。

完全不抽象(暴露所有实现细节)和过度抽象正是我们最上面所画的A和B两个极端。完全不抽象的代码,会导致非常低的可读性,以为 它会把我们带入细节的深渊,试想,你无法了解一个系统的全局,怎么可能全面掌握它呢?

而过度抽象则是另外一个反面,每一次抽象都意味着细节的丢失,过度抽象之后,你只知道一个全貌,却无法了解细节,会导致自己 陷入一个似懂非懂的状态,例如当你阅读Go代码时,碰到一个接口(interface),但是你无法找到具体是哪一个实现,这里你就丢失了 很大一块细节,为了掌握整个系统,你需要了解实现细节,但是现在细节却丢失了。

从细节到抽象,和从抽象到细节,其实就是思维过程中,自底向上和自顶向下的两种方式,我们要掌握这两种方式,并且来回切换,才能 更快更好的了解一个东西,自顶向下帮助你掌握轮廓,自底向上帮助你掌握细节,同时运用两种方式才能让你真正的掌握一个系统。

说回正题,抽象泄漏,就是想要做到完美的抽象,却发现做不到,比如ORM,想要屏蔽所有数据库的实现细节,但却发现为了使用数据库 独有的功能,又必须在公有组件上加上每种数据库独有的功能支持。

函数到底拆多细

这是最近和群友们讨论的一个话题(进技术交流群见右侧二维码),函数到底拆多细?如果你感受过一个函数几百行,翻几屏都翻不完的 那种恐惧,大概率以后你自己再也不会写那样的代码了。

而当你见过另一种极端,几乎每一行代码都被拆散成一个函数时,你会了解到另一种痛苦。

人类的思维过程是比较适合一溜顺的,也就是说,你的思维能从头到尾不打断,而不是在各种环境中切换来切换去跳来跳去的。一个 函数中有很多代码时,符合这一点,你可以从头看到尾,然而它有一个很严重的问题,可维护性非常低,并且当长度达到一定程度时, 你需要记忆的上下文(例如前面定义的变量、内部函数等等)变得越来越多,会达到一个点,超过这个点之后,非常难理解这份代码。

拆的过细的函数会导致另一个问题,每次遇到一个函数时,相当于在你的思维里打一个断点,告诉自己,这里需要执行另一个分支。 同样当函数多到一定程度时,就会达到你的人脑上下文内存上限。

所以函数到底拆多细,怎么拆,这个问题是非常主观的一个问题,但是可以肯定的是,它是一种取舍。

这里得提一句,一个好的函数名可以减少这种痛苦。

总结

计算机中,到处都是权衡取舍。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK