写软件的十一条原则:先数据,后代码
作者列出自己构建软件时遵循的一组原则,主线是让系统更简单:让非法状态无法表示、保证数据一致性、先设计数据再写代码、测量之后再优化。文中用一个数据库例子说明不一致的代价——必须保持 x=y 的两个布尔变量一旦拆到不同库、无法原子更新,数据就多出两种状态,toggle 函数在这些状态下没有正确答案。作者认为一致性是当下最被低估的工程原则,多数问题本质上都是数据不符合预期;他同时主张代码一致性优先于局部「正确」,学习应聚焦 concepts(关系模型、代数数据类型、borrow checker、Curry-Howard 同构)而非 React、Kubernetes 的表层细节。适合做后端与数据系统设计、正在权衡微服务拆分和 schema 取舍的工程师。
17 September 2020
These are my personal principles for building software. I hope to frequently update them as my views change. There can be valid reasons for breaking them (they are principles, not laws), but in general I believe following them works out well.
2020 年 9 月 17 日
以下是我个人在构建软件时遵循的原则。随着看法变化,我希望能时常更新它们。打破它们可能有正当理由(它们是原则,不是法律),但总体而言,我相信照着做会有好结果。
Most of them revolve around making the system simpler in some way. It’s my belief that simpler systems are more reliable, easier and quicker to modify, and generally easier to work with.
Make Invalid States Unrepresentable
Data Consistency Makes Systems Simpler
Design “Data First”
Measure Before You Cut
Avoid Trading Local Simplicity for Global Complexity
Recognise Intrinsic Complexity
Fewer Technologies Result in Simpler Systems
Focus Your Learning on Concepts, not Technologies
Code Consistency is Important
Shared Principles are Important
这些原则大多围绕同一个目标:想办法让系统更简单。我相信,更简单的系统更可靠,改起来更快更省力,整体上也更好驾驭。
让非法状态无法表示
数据一致性能让系统更简单
“数据优先”的设计
先测量再动手
不要用全局复杂度换取局部简单
承认固有复杂度
技术越少,系统越简单
学习要聚焦概念,而非技术
代码一致性很重要
共同的原则很重要
Make Invalid States Unrepresentable
I have put this first because I think it is one of the most important and most powerful principles.
You may have heard this phrase in relation to designing your program’s types, but the principle applies everywhere you represent data – for example database design.
Not only does this reduce the number of states your system can be in (and thus make it simpler), but it reduces the number of invalid states, which is even better! Your system does not have to handle these states because they literally cannot be represented in your program.
This is not just a minor convenience, it can drastically simplify your system and prevent entire classes of bugs from occurring.
I have put together some examples.
让非法状态无法表示
我把它放在第一位,因为我认为它是最重要、也最有力的原则之一。
你可能在设计程序类型时听过这句话,但这条原则适用于所有表示数据的地方——比如数据库设计。
它不仅减少了系统可能处于的状态数量(从而让系统更简单),还减少了非法状态的数量,这一点更为关键!系统根本不必处理这些状态,因为它们在程序里压根无法被表示出来。
这不是一点小便利,它能大幅简化系统,并挡住一整类 bug。
我整理了一些例子。
Data Consistency Makes Systems Simpler
Consistency enforces rules on your data, and so reduces the number of states your system needs to handle. This follows on from the “make invalid states unrepresentable” principle.
Definition
I am using consistency here in a general sense: that your data adheres to certain rules, and that it always obeys those rules at every point in time. This definition relates to ACID consistency, so don’t confuse it with CAP consistency.
The rules can be any pretty much anything; for example, that your credit should never be able to go negative, or that private posts should not be visible to others. It is not restricted to foreign keys or unique indexes, although they are also valid examples.
As well as your database, consistency may be enforced by your application utilising ACID transactions. It is preferable to enforce them at the database level, but this is not common practice for anything more complex than simple checks for practical reasons.
数据一致性能让系统更简单
一致性会给数据加上规则,从而减少系统需要处理的状态数量。这是“让非法状态无法表示”原则的延伸。
定义
这里说的是一般意义上的 consistency:数据遵守某些规则,并且在任何时刻都始终遵守。这个定义对应的是 ACID 里的一致性,别和 CAP 里的一致性混为一谈。
这些规则几乎可以是任何东西;比如,余额永远不能变成负数,或者私密帖子不该被别人看到。它不限于外键或唯一索引,虽然那些也是正当的例子。
除了数据库,应用也可以借助 ACID 事务来保证一致性。在数据库层面强制执行更为理想,但出于现实考虑,只要比简单校验复杂一点,这种做法就不常见了。
Practical Advice
Anything which restricts or compromises consistency results in complexity. This leads to the following practical advice:
It is simpler to have:
Fewer databases (ideally one)
Normalised, less redundant data
A ‘good’ database design (big topic)
ACID transactions
More data constraints
It is more complex to have:
Multiple databases
Redundant or denormalised data
A poor database design
Fewer (or no) data constraints
Of course, there are valid reasons to make your system more complex, and I don’t intend complexity to be a dirty word, but see “measure before you cut”.
I consider this principle to be one of the most undervalued in software engineering today. Consistency issues often go unrecognised. Many problems, I daresay most problems, are consistency issues at an essential level – data that does not conform to some expectation.
实践建议
任何限制或牺牲一致性的做法,都会带来复杂度。由此可以得出下面这些实践建议:
更简单的做法是:
数据库更少(最好只有一个)
数据规范化,冗余更少
“好”的数据库设计(这是个大话题)
使用 ACID 事务
更多的数据约束
更复杂的做法是:
多个数据库
冗余或反规范化的数据
糟糕的数据库设计
更少(甚至没有)数据约束
当然,让系统更复杂也可能有正当理由,我并不想把复杂度当成贬义词,但请先看“先测量再动手”。
我认为,这条原则是当今软件工程中最被低估的原则之一。一致性问题常常无人察觉。很多问题——我敢说大多数问题——本质上都是一致性问题:数据不符合某种预期。
Design “Data First”
What is more likely to be around in 10 years: your code or your data?
Code can be thrown away and re-written, but this is rarely the case with data.
Data is more important than code. The purpose of code is to transform data.
When designing a new system, it’s best to start with your database and your data structures and build your code on top of that. Consider the constraints you can place on your data and enforce them, ideally by the way your represent your data.
Code design flows naturally from data design. The simpler and more consistent your data model is, the simpler your code will be.
Show me your flowcharts and conceal your tables, and I shall continue to be mystified. Show me your tables, and I won’t usually need your flowcharts; they’ll be obvious
Bad programmers worry about the code. Good programmers worry about data structures and their relationships.
“数据优先”的设计
十年之后,更可能还在的是你的代码,还是你的数据?
代码可以扔掉重写,数据几乎不能。
数据比代码更重要。代码的存在目的就是转换数据。
设计一个新系统时,最好从数据库和数据结构入手,再把代码搭在上面。想想你能给数据加上哪些约束,并把它们落实下去——最好是通过数据的表示方式来落实。
代码设计会自然而然地跟随数据设计。数据模型越简单、越一致,代码就越简单。
把你的流程图给我看,却把表藏起来,我依然会一头雾水。把你的表给我看,我通常就不需要流程图了——它们不言自明。
差的程序员操心代码。好的程序员操心数据结构以及它们之间的关系。
Measure Before You Cut
This is the most common mistake made by software developers. It’s responsible for many self-inflicted problems.
The principle is to ensure that empirical evidence backs the need for a trade-off.
Common mistakes:
Trying to build a complex “scalable” system that scales to a size you’ll never need.
Making services as small as possible without considering need or cost.
Adding inconsistency or complexity for performance in a part of the system that is not a performance bottleneck.
Advice:
Start with the simplest, most correct system possible.
Measure performance.
Do not pay complexity costs or violate the other principles until it solves an actual problem, not an imaginary one.
先测量再动手
这是软件开发中最常见的错误,许多自找的麻烦都源于它。
这条原则要求:任何取舍都要有实证证据支撑它的必要性。
常见错误:
去搭一个复杂的“可扩展”系统,而它要撑起的规模你永远用不到。
不考虑需求和成本,硬把服务拆到最小。
在系统里根本不是性能瓶颈的地方,为了性能引入不一致或复杂度。
建议:
从尽可能简单、尽可能正确的系统开始。
测量性能。
在它能解决真实问题(而不是想象中的问题)之前,不要为它付出复杂度代价,也不要违背其他原则。
Some optimisations can be made without measurement, because they have little or zero cost. For example, using the correct data structures that support favourable performance for the operations you want to perform.
It’s true that sometimes experience alone can tell you if you’re making the correct trade-off. It’s still better if you can prove it.
When you have to choose, prefer correctness and simplicity over performance.
In some cases correct and simple code is the best performing code!
The real problem is that programmers have spent far too much time worrying about efficiency in the wrong places and at the wrong times; premature optimization is the root of all evil (or at least most of it) in programming.
有些优化不必测量就可以做,因为它们几乎没有成本。比如,为你打算执行的操作,选用性能表现合适的正确数据结构。
确实,有时候光靠经验就能判断取舍对不对。但如果你能拿出证据,那就更好。
必须二选一时,优先选正确和简单,而不是性能。
在某些情况下,正确且简单的代码就是性能最好的代码!
真正的问题在于,程序员花了太多时间,在错误的地方、错误的时机操心效率;过早优化是编程中万恶之源(至少是大部分恶的根源)。
Avoid Trading Local Simplicity for Global Complexity
i.e. avoid making a part of the system simpler in exchange for making the system as a whole more complex.
This trade is usually not an even one; chasing after local simplicity can cause and order of magnitude increase in global complexity.
For example, smaller services can make those services simpler, but the reduction in consistency and the need for more inter-process communication makes the system as a whole much more complicated.
不要用全局复杂度换取局部简单
也就是说,不要为了简化系统的某一部分,而让整个系统变得更复杂。
这种交换通常并不对等:一味追求局部简单,可能让全局复杂度涨上一个数量级。
举个例子,把服务拆得更小,这些服务本身是简单了,但一致性下降、进程间通信变多,整个系统反而复杂得多。
Recognise Intrinsic Complexity
Sometimes things are just complicated. You cannot make problems simpler than they are.
Any attempt to do so will ironically make your system more complex.
承认固有复杂度
有些事情本身就是复杂的。你没办法把问题变得比它本身更简单。
讽刺的是,任何试图这么做的努力,只会让系统更复杂。
Fewer Technologies Result in Simpler Systems
It is better to understand fewer technologies deeply than many technologies at a surface level. Fewer technologies mean fewer things to learn, and less operational complexity.
技术越少,系统越简单
与其浮于表面地了解很多技术,不如深入吃透少数几门。技术越少,要学的东西越少,运维复杂度也越低。
Focus Your Learning on Concepts, not Technologies
Do not concern yourself too much with intricate details of the software you use – you can always look them up. Learn the underlying fundamental concepts.
Technologies change, concepts are eternal. The concepts you learn will help with newer technologies, and you will be able to learn them much quicker.
For example, do not concern yourself so much with the surface level details of React, Kubernetes, Haskell, Rust, etc.
Focus on learning:
Pure functional programming
The relational model
Formal methods
Logic programming
Algebraic data types
Typeclasses (in general and specific ones)
The borrow checker (affine/linear types)
Dependant Types
The Curry-Howard Isomorphism
Macros
Homoiconicity
VirtualDOM
Linear regression
etc.
学习要聚焦概念,而非技术
别太纠结所用软件的细枝末节——那些随时都能查。要学的是底层的根本概念。
技术会变,概念长存。你学到的概念对更新的技术同样管用,而且让你学得更快。
比如,不必在 React、Kubernetes、Haskell、Rust 等技术的表层细节上花太多心思。
值得专注学习的有:
纯函数式编程
关系模型
形式化方法
逻辑编程
代数数据类型
typeclass(通用的与具体的)
借用检查器(仿射/线性类型)
依赖类型
Curry-Howard 同构
宏
同像性(homoiconicity)
VirtualDOM
线性回归
等等。
Code Consistency is Important
Sometimes writing the consistent thing is more important than writing the “correct” thing. If you want to change the way something works in your codebase, change all instances of it. Otherwise, try to stick with it.
The readability of your code has more to do with consistency than it does with any notion of simplicity. People understand code by pattern recognition, so repeat (and document) patterns!
Shared Principles are Important
The more principles you have in common with your teammates, the better you will work together, and the more you will enjoy working together.
代码一致性很重要
有时候,写一致的代码比写“正确”的代码更重要。如果你想改变代码库里某处的做法,那就把所有出现的地方都改掉;否则,就尽量沿用现有写法。
代码可读性更多来自一致性,而不是某种简单性的理念。人理解代码靠的是模式识别,所以要把模式重复下去(并写进文档)!
共同的原则很重要
你和队友共有的原则越多,协作就越顺,一起干活也越愉快。
Appendix A: Inconsistency Results in Complexity
This is the simplest example I can think of to illustrate this principle. I hope it doesn’t require too much imagination to relate to realistic problems.
Consider a database with two Boolean variables x and y. Your application has a rule that x = y, and it can enforce this rule by using a transaction to atomically change both variables.
If this rule is correctly enforced, your data can only be in two states: (x = True, y = True) or (x = False, y = False).
Writing the function ‘toggle’ with this rule in place is straightforward. You atomically read one of the values and set both values to the negation.
附录 A:不一致会带来复杂度
这是我能想到的、用来说明该原则的最简单例子。希望把它对应到现实问题不需要太多想象力。
设想一个数据库里有两个布尔变量 x 和 y。你的应用有一条规则:x = y,而它可以用事务原子地同时修改这两个变量来保证规则成立。
如果规则被正确执行,你的数据只可能处于两种状态:(x = True, y = True) 或 (x = False, y = False)。
在规则成立的前提下,写 toggle 函数很直接:原子地读出其中一个值,再把两个值都设成它的取反。
Now consider what happens if you split those variables into their own databases and they can no longer be atomically changed together.
Because you can no longer consistently ensure that x = y, your data can be in two more states: (x = True, y = False) or (x = False, y = True).
Which value should you use if your system is in one of these states?
What should your ‘toggle’ function do in one of these states?
How do you ensure that both writes are successful when writing a new value?
There are no correct answers to these questions.
Of course, if we’d followed the “make invalid states unrepresentable” principle in the first place, there would only be one variable! :)
现在设想把这两个变量拆进各自的数据库,它们再也无法被原子地一起修改。
由于你无法再始终保证 x = y,数据会多出两种状态:(x = True, y = False) 或 (x = False, y = True)。
如果系统正处于这两种状态之一,你该用哪个值?
你的 toggle 函数在这种状态下该怎么做?
写入新值时,你怎么保证两次写入都成功?
这些问题都没有正确答案。
当然,如果一开始就遵循“让非法状态无法表示”的原则,那就只会有 一个变量!:)