代码的仪式
我们常能在英文社区看到 coding ceremory 一词,或译为 代码的仪式。Stack Overflow 上有个问题 What does “low ceremony” mean?,作者曾如此提问:
In the Trac Main Features page https://trac.edgewall.org/wiki/TracFeatures, Trac is said to emphasize “ease of use and low ceremony”. Can someone please explain what “ceremony” means in the context of software usage?
low ceremony 与 ease of use 作并列短语,可见在程序开发的语境下,代码的仪式不是一个褒义词——过多的仪式并没有好处。用户 Rowan Freeman 则作此回答:
Low ceremony means a small amount of code to achieve something. It means you don’t need to set up a lot of things in order to get going.
如其所述,代码的仪式是完成一个功能所需要的额外准备。仪式越少,准备工作越简洁,完成起来也越容易。
代码仪式被称为仪式,正如古代祭祀的舞蹈,传统庆典的繁文缛节,其对完成目标贡献甚微,却又是不可或缺的步骤。复杂的仪式冗长而乏味,我们偏偏还得忍受其枯燥,如履薄冰,完成得分毫不差——这也解释了为什么大多数人都不喜欢代码仪式。
不同的代码仪式
依照呈现的形式,代码的仪式可以分为 编写仪式 和 运行仪式 两类。
编写仪式
编写仪式指完成功能我们所需编写的额外代码。其中最基本的一类源自编程语言对程序完整性的要求。每种编程语言会要求用户以特定的方式编写代码,如此方能产生可运行的程序,如 C/C++ 需要定义 main()
函数,旧式 Java 需要声明带有 public static void main()
方法的类等等:
print("Hello world")
#include <stdio.h>
int main() {
printf("Hello world\n");
return 0;
}
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, World");
}
}
main :: IO ()
main = putStrLn "Hello, World!"
SECTION .DATA
hello: db 'Hello world!',10
helloLen: equ $-hello
; Code goes in the text section
SECTION .TEXT
GLOBAL _start
_start:
mov eax,4 ; 'write' system call = 4
mov ebx,1 ; file descriptor 1 = STDOUT
mov ecx,hello ; string to write
mov edx,helloLen ; length of string to write
int 80h ; call the kernel
; Terminate program
mov eax,1 ; 'exit' system call
mov ebx,0 ; exit with error code 0
int 80h ; call the kernel
可见不同编程语言对“程序完整性”的要求有繁有简,短者如 Python 基本没有要求,长者如汇编语言(NASM)需要手动标注代码段与数据段。但这类仪式基本是一次性的,只需在项目伊始完成,较于漫长的开发工作简直不值一提。
编写仪式也可能源于软件框架的规范要求。软件框架简化了大型程序开发的流程,针对特定领域需求构建好了程序的蓝图,事无巨细皆有覆盖。开发者借助框架开发,就好比对照蓝图搭积木,只需编写上层业务相关代码即可。在简化开发的同时,软件框架通常会束缚代码的组织形式。若以复杂性为轴,我们可以绘制一幅软件框架的光谱。框架的对代码组织形式的约束依其在光谱上的位置而大相径庭。
光谱的一端是轻量级框架。它们有着精简的内核与宽松的规范约束。大多数轻量级框架并不强求规范的目录结构,开发者只需添加少量仪式以完成必要的程序初始化。以知名 Web 框架 Flask 为例。作为轻量级框架的代表,Flask 只负责做好与网络协议相关的工作,给予开发者十足的自由决定更上层逻辑的表现。借助 Flask,仅需寥寥数行代码即可构建一个最简单的 Web 程序:
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello_world():
return "<p>Hello, World!</p>"
某种意义上,这类框架可以被看作领域特定语言 1,如 Flask 像是基于 Python 为 Web 开发设计的一门“语言”。以语言作类比使我们能套用前文对编程语言仪式的分析,将此类框架的仪式看作其寄主语言仪式的延伸。上面所示的代码中,from flask import Flask
与 app = Flask(__name__)
等套路化的代码,即是源自虚构的“Flask 语言”对程序完整性的要求。
光谱的另一端是重量级框架。相较于轻量级框架,重量级框架会向上层业务延伸自己的责任边界。它们的设计者通常更精通领域的业务,了解常见问题,通晓最佳实践,并尝试将这些经验融入框架中以服务开发者。
但事实上业务需求是多变的,设计者需要让框架足够灵活、足够可定制,方能满足现实中开发者的需求。灵活可定制,随之而来的代价便是框架日趋复杂。设计者或是抬高框架的抽象程度,应用设计模式使各种模块易于组合拆解;或是提供眼花缭乱的可配置项,迎合每一个潜在的需求点。前者使样板代码(boilerplate code)膨胀,后者使最佳实践难写——这些都是重量级框架仪式的体现。
我们还是以 Python Web 框架举例,但这次是著名的 Django。和 Flask 相比,一个典型的 Django 项目需包含多个文件,并以特定的结构组织在一起。其目录结构和部分文件内容如下:
├── manage.py
├── mysite
│ ├── asgi.py
│ ├── __init__.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
└── polls
├── admin.py
├── apps.py
├── __init__.py
├── migrations
│ └── __init__.py
├── models.py
├── tests.py
└── views.py
3 directories, 13 files
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = "django-insecure-kqi@xv-ay@y!c2es$5m)b&uulbt-l8g28kw-)o2$&w#@2uvx#&"
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
ROOT_URLCONF = "mysite.urls"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]
WSGI_APPLICATION = "mysite.wsgi.application"
# Database
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3",
}
}
# Password validation
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
},
{
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
},
{
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
},
{
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
},
]
# Internationalization
# https://docs.djangoproject.com/en/4.2/topics/i18n/
LANGUAGE_CODE = "en-us"
TIME_ZONE = "UTC"
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.2/howto/static-files/
STATIC_URL = "static/"
# Default primary key field type
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
from django.contrib import admin
from django.urls import path
urlpatterns = [
path("admin/", admin.site.urls),
]
Django 提倡以模块切分业务逻辑,每个模块置于单独的目录,称为一个 app。本例中目录 polls/
便是一个 app,其下的各文件分别定义了所属 app 的数据模型、视图和路由等。Django 奉行所谓的 MTV 模式,即将数据的逻辑和展示分开,使程序更加易于维护。MTV 模式便是 Django 为可维护性引入的额外抽象,其代价是项目结构的复杂化与业务无关代码的增多。而为了更高的灵活性,Django 在 settings.py 中提供了多个配置项。用户可通过调整配置项的值,以满足业务的特定需求。此二者相合便是 Django 开发的编写仪式。
运行仪式
运行仪式是将所写代码执行起来所要付出的额外努力。编写仪式是代码跃于纸上的成本,运行仪式则是代码脱胎成型的代价。 最常见的运行仪式莫过于“程序编译”,这也是人们通常评判运行仪式的焦点。
程序编译即将代码经由某种手段转移为目标代码。编译型语言,如 C/C++、Rust、Go 或 Java 等,其代码须经此工序方能运行。目标代码通常是机器码,但也不尽然,如 Java 是编译为 JVM 的字节码,牺牲一定的效率以换取跨平台能力。
许多人讨厌编译,害怕编译。这一方面是因为编译在代码编写和程序运行中间强插了间隙,加剧调试的延时与焦虑。但最主要的还是编译工序的繁杂与报错的晦涩。C++ 便是个典型例子。其编译的仪式源自语言的设计缺陷——模块概念的缺乏使编译工序冗繁,语法的异常复杂使编译报错晦涩难懂。编译的复杂性还可能源自业务本身的复杂性。如在 Android 开发中,你几乎不可能脱离 Android SDK 完成程序的编译——这是由交叉编译的复杂性所决定的。
程序编译曾是编译型语言的特需,但近年来,解释型语言中也出现了这种需求,其中以 Web 前端开发尤甚。Web 前端程序的运行环境有特殊性。浏览器以 HTML、CSS 以及 Javascript 三种语言的源代码作为最直接、最底层的输入,极大简化了程序的编写。但另一方面,这三种语言结构松散、抽象能力有限,同时在不同平台上标准不一,这迫使人们引入框架和工程化手段以提高大型程序的开发效率和可维护性。人们用表现力更强的“方言”(如 Typescript)编写代码,用更合理的文件结构和抽象程度更高的框架(React、Angular)组织代码,再通过工具编译成为浏览器所能执行的三种语言。这种模式在初期而饱受诟病,后因工具链的不断完善而逐渐被人们接受,直至今日成为了大多数项目的标准配置。
然而,就算是摆脱了编译梦魇的其他解释型语言,在开发过程中也会遇到其他运行仪式。Python 开发者常被依赖安装问题所困。Python 程序免除了程序编译的步骤,却只是把编译期的问题延至运行期。当程序运行时,你需要保证其所依赖的每个模块都被正确安装在正确的位置,并且有着正确的版本号。达成这点并不容易,有时它要求操作系统内要有某个老旧的库,但这个库又被其他程序所依赖,不得轻易修改(早期深度学习开发者应该深有体会)。此种依赖模式还使程序变得难以分发——因为分发对象的机器上也要有对应的依赖。依赖问题催生了 virtualenv、Anaconda 乃至 Docker 等策略的应用,但这愈发加重了 Python 运行仪式的繁杂。
讨论与分析
许多代码仪式其实是迫不得已的产物。这一方面源于业务本身,另一方面源于人。
业务复杂性是代码仪式的最直接成因,决定了其复杂性的下限。如前面所述的 Android 开发必须有 Android SDK 支持,是因为生成一个可执行的 apk 文件就是有这么复杂。另一个例子是 Python 时代前的深度学习开发,人们被各种库的编译与运行时的问题折腾得不轻,是因为 CUDA 编程本身就有这么复杂。
但另一方面,我们还要考虑人的因素。大型程序编写是人分工管理的艺术。人与人的能力不同,效率不同,让项目的有效开发与可维护性成了头疼的问题。此时,框架的设计者或是项目的领导者需提高基础代码的抽象层次,以实现对项目逻辑的拆解,更高效地进行团队协作。这也是为什么大型项目或框架会需要许多样板代码或是胶水代码。重量级框架的意义是积极的,它着眼处理的是大型程序的问题。如果你觉得它们很繁琐,可能是你和它们的目标人群并不完全契合,你需要找寻更轻量级的解决方案。
让人有效协作的另一个手段是压制个性,简单来说便是让所有人都尽量写出质量差不多的代码。这可以从两方面入手。一是使用工具制定代码规范,强迫每处代码遵从一致的风格。Web 前端常用的 ESLint、Python 常用的 pylint 和 autopep8 便是为此服务的。这类工具增加了代码的仪式,但保持了代码的可维护性。另一个方案则是采用尽量简单的编程语言。Java 之所以被企业广泛采用,一部分原因是其语法足够简单,开发者无法写出个性差异巨大的代码,从而能维持项目的一致性。然而,“语法简单”与“抽象能力不足”是一体两面的。语法简单的 Java 在许多时候会显得捉襟见肘,以致需要各种设计模式补偿抽象能力的缺陷,这进而又使代码仪式复杂化了。
在考察代码仪式时,解决方案的灵活性也是个有趣的因素。前文说有着诸多配置项的重型框架会增加代码仪式,那内核紧致的轻型框架又如何呢?轻型框架收缩了责任边界,让社区去构建多种多样的插件以满足上层业务的需求,是否兼具低代码仪式与高灵活性呢?
历史上曾有多种微内核高扩展性的解决方案。Flask 是 Python 后端开发方案中的此类代表。Flask 很简洁,同时有着海量社区插件。但当人们想利用插件构建稍微复杂的程序时,却总会觉得如鲠在喉。一方面是因为插件良莠不齐,总是要斟酌许久才能选中一个特定功能的插件,而且这个插件还不能让人百分百满意。另一方面是各插件之间通常保持着绝对的独立,想在插件间做交互往往要写更多的胶水代码。人们逐渐发现,想利用在野的插件组合出任意需求是很难的,很多时候不如自己从头重写,或是使用一站式的重型框架。与此类似的还有前端的 Webpack——在其鼎盛时期,你在网上可以找到 1000 种编译 ES6 的方案,但每一种都有些小瑕疵。
人们渐渐发现选择众多并不总是一件好事。选择带来争端与标准的割裂,于高效开发是无益的。人们需要一种独断的(opinionated)、大家长式的解决方案,减少选择提高效率。这种解决方案必须瞄准领域的某个最佳实践,有且只能有一个,同时隐藏多数可选项,只提供少数或完全不提供配置项。这种模式看似违背自由竞争的意志,却能切实地减少不必要的代码仪式,提高效率。用 Python 写 Restful API 时,人们更倾向于使用 FastAPI 而不是 Flask;构建前端时,人们发现 Vite 比 Webpack 更省心。这种思潮也深刻影响了近几年开发工具的设计思路。新兴语言如 Go、Rust,其官方都会提供足够好用的包管理器、构建工具以及格式化器等,从源头掐断各种争纷,让用户专注于程序的设计。
尾声
事实上,代码仪式是一个宽泛非正式的概念。人们总是在抱怨某某代码仪式的复杂,但人们没有系统地讨论过。本文无法面面俱到地讨论各种仪式的表现、成因和应对,只是结合现实中的例子粗略地讲解了一下。愿读者能有所收获。
- Domain-Specific Language (DSL)
作者:hsfzxjy
链接:
许可:CC BY-NC-ND 4.0.
著作权归作者所有。本文不允许被用作商业用途,非商业转载请注明出处。
OOPS!
A comment box should be right here...But it was gone due to network issues :-(If you want to leave comments, make sure you have access to disqus.com.