1.2 命令式与声明式

命令式和声明式常用于描述编程范式。两者最直观的区别在于,命令式描述代码的执行步骤通过代码流程控制来实现输入和输出,是一种过程导向的思想;声明式则不直接描述执行步骤,而是描述期望的状态和结果,由程序内部逻辑控制来实现最终状态,是一种结果导向的思想。

举例来说,使用docker run命令运行一个容器,这就是最简单的命令式。

当业务逻辑简单且只有一个单体应用的容器时,项目的启动和开发工作是比较容易的。随着系统越来越复杂及微服务数量的增长,服务之间可能会产生依赖的问题。例如所有服务都依赖于MySQL和RabbitMQ服务,那么在启动其他服务前必须先启动这两个服务,此时很难通过人为记住服务依赖及启动顺序执行docker run来启动所有服务,用户需要有一种能够声明微服务之间的依赖和启动关系的方法。最终声明式的服务编排系统便出现了,例如常见的docker-compose。

持续部署环节也面临着同样的问题,从最初简单的命令式部署方法,例如通过FTP、SCP命令的方式将构建物传输至生产环境来满足部署需求,到用户需要实现更复杂的编排能力。这要求持续部署具有一种能够描述复杂部署编排的方案。随着Ansible、docker-compose等产品的出现和成功,声明式的思想为持续部署提供了新的方向。

1.2.1 简单易用的命令式

在命令式的范式中,通过固定一组命令的运行流程来描述控制流,利用赋值和变量来存储中间状态以便后续的流程使用,并使用流程控制命令(如for循环和while循环),直到某些条件产生变化为止,这是命令式编程范式的思想。

对于实现简单的流程和状态,毫无疑问,命令式是最佳的选择,因为命令式只需要执行特定的一个或一组有序命令,例如使用FTP指令将网站上传到生产环境。

更加复杂的部署需求通常需要使用Shell命令式脚本来实现,例如以下一段部署伪代码包含了部署和回滚功能。

使用命令式完成部署虽然非常简单,但在面对更加复杂的部署场景时需要不断修改命令行脚本,且对于不同开发语言和平台一般需要单独改写脚本。随着程序语言、运行平台和服务越来越多,命令式部署脚本也会变得越来越难以维护。

1.2.2 抽象和归纳的声明式

相比于命令式的编程范式,声明式不直接描述运行过程,而是描述期望结果,推导和中间过程由程序内部逻辑实现,对用户相对透明,对外提供一套声明式的定义模板描述期望的最终状态。

例如,常见的声明式编程语言SQL如下。

这条SQL语句让用户自己定义想要什么数据(即最终期望状态),如何存储数据、如何使用更高效的算法查找数据都由数据库决定,最终返回的数据集则是我们期望的结果。

此外,本章提到的docker-compose也使用声明式的编程范式,例如使用其规定的YAML格式文件定义服务依赖、服务环境变量及服务对外暴露端口。

以上YAML文件定义了3项服务,服务名分别为mysql、redis、api-backend。depends_on、environment和ports字段分别定义了服务依赖、服务环境变量和对外暴露端口。若使用docker run命令式的启动方式,那么需要先启动基础服务mysql及redis,再启动api-backend服务。使用docker-compose声明式的定义则只需要对依赖depends_on进行定义,程序内部逻辑会自动处理服务依赖和启动顺序。最终声明式的定义的期望结果是MySQL、Redis、Api-backend服务都启动成功,并暴露端口对外提供服务。

实际上,这是一种“代码即服务”的声明式思想,工具为我们抽象了运行过程的步骤和算法,对用户提供了一套新的语言模板来简化描述,虽然降低了用户的使用复杂度,但用户需要遵循其描述规范,同时也增加新的学习成本。