计算机中的权衡(trade-off)
计算机中一切都是权衡(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
就会好很多。不过由于团队中并不是所有人都能记住,所以综合下来使用 http.StatusOK
是一个更好的
选择,如果团队能够达成一致,那么无论哪个选择都是OK的。
再例如之前我读Go的源码时,发现源码中到处都是使用 “linux”, “windows”, “drawin” 三个字符串,而不是抽出来成为const, 我就去提了个 issue 问作者们为啥不抽成常量然后导入。
作者的回答很有道理,因为大家都懂这几个词的意思,他们其实也是常量。而导入一个新的模块更加麻烦,这也是一种取舍。
抽象泄漏(abstraction leaky)
抽象,这是CS的代名词。无论是语言,还是数据,这些年来,我们都是冲着抽象一步一步发展。正所谓,计算机中没有什么事情是加一个 中间层解决不了的,如果有,那就加两个。举个例子,我们从最开始的汇编语言,为了屏蔽各种汇编语言的细节,我们提供了高级语言 这层抽象。在高级语言中,为了屏蔽多个语句的实现细节,我们提供了函数这层抽象。
完全不抽象(暴露所有实现细节)和过度抽象正是我们最上面所画的A和B两个极端。完全不抽象的代码,会导致非常低的可读性,以为 它会把我们带入细节的深渊,试想,你无法了解一个系统的全局,怎么可能全面掌握它呢?
而过度抽象则是另外一个反面,每一次抽象都意味着细节的丢失,过度抽象之后,你只知道一个全貌,却无法了解细节,会导致自己 陷入一个似懂非懂的状态,例如当你阅读Go代码时,碰到一个接口(interface),但是你无法找到具体是哪一个实现,这里你就丢失了 很大一块细节,为了掌握整个系统,你需要了解实现细节,但是现在细节却丢失了。
从细节到抽象,和从抽象到细节,其实就是思维过程中,自底向上和自顶向下的两种方式,我们要掌握这两种方式,并且来回切换,才能 更快更好的了解一个东西,自顶向下帮助你掌握轮廓,自底向上帮助你掌握细节,同时运用两种方式才能让你真正的掌握一个系统。
说回正题,抽象泄漏,就是想要做到完美的抽象,却发现做不到,比如ORM,想要屏蔽所有数据库的实现细节,但却发现为了使用数据库 独有的功能,又必须在公有组件上加上每种数据库独有的功能支持。
函数到底拆多细
这是最近和群友们讨论的一个话题(进技术交流群见右侧二维码),函数到底拆多细?如果你感受过一个函数几百行,翻几屏都翻不完的 那种恐惧,大概率以后你自己再也不会写那样的代码了。
而当你见过另一种极端,几乎每一行代码都被拆散成一个函数时,你会了解到另一种痛苦。
人类的思维过程是比较适合一溜顺的,也就是说,你的思维能从头到尾不打断,而不是在各种环境中切换来切换去跳来跳去的。一个 函数中有很多代码时,符合这一点,你可以从头看到尾,然而它有一个很严重的问题,可维护性非常低,并且当长度达到一定程度时, 你需要记忆的上下文(例如前面定义的变量、内部函数等等)变得越来越多,会达到一个点,超过这个点之后,非常难理解这份代码。
拆的过细的函数会导致另一个问题,每次遇到一个函数时,相当于在你的思维里打一个断点,告诉自己,这里需要执行另一个分支。 同样当函数多到一定程度时,就会达到你的人脑上下文内存上限。
所以函数到底拆多细,怎么拆,这个问题是非常主观的一个问题,但是可以肯定的是,它是一种取舍。
这里得提一句,一个好的函数名可以减少这种痛苦。
总结
计算机中,到处都是权衡取舍。
更多文章
- socks5 协议详解
- zerotier简明教程
- 搞定面试中的系统设计题
- frp 源码阅读与分析(一):流程和概念
- 用peewee代替SQLAlchemy
- Golang(Go语言)中实现典型的fork调用
- DNSCrypt简明教程
- 一个Gunicorn worker数量引发的血案
- Golang validator使用教程
- Docker组件介绍(二):shim, docker-init和docker-proxy
- Docker组件介绍(一):runc和containerd
- 使用Go语言实现一个异步任务框架
- 协程(coroutine)简介 - 什么是协程?
- SQLAlchemy简明教程
- Go Module 简明教程