2.2 有状态风格和无状态风格
有状态风格和无状态风格是两种相反的软件编写风格,它们都有各自的优缺点。
顾名思义,有状态软件的行为依赖于其内部状态。我们以Web服务为例,如果服务记住了自己的状态,该服务的使用者可以在每个请求中发送更少的数据,因为该服务记住了这些请求的上下文。然而,虽然节省了发送请求大小和带宽数据的开销,但在Web服务方面有一项隐藏的成本。如果用户同时发送很多请求,则服务必须同步这些请求。由于可能会有多个请求同时改变服务状态,没有同步机制可能会导致数据争用。
但是,如果服务是无状态的,那么每个指向它的请求都需要包含成功处理它所需的所有数据。这意味着请求数据将变得更多,消耗更多的带宽,但服务将拥有更好的性能和可伸缩性。如果你熟悉函数式编程,你可能会发现无状态服务很直观。每个请求的处理都可以理解为对纯函数的调用。事实上,无状态编程的许多优点都源于它的函数式编程本质。可变状态是并发代码的大敌。函数式编程依赖于不可变值,即使这意味着需要复制而不是修改现有对象。得益于此,每个线程都可以独立工作,并且不可能产生数据争用问题。
由于没有竞争条件,因此不需要加锁,这就可能带来巨大的性能提升。没有锁也意味着不再需要处理死锁的问题。纯函数意味着代码更容易调试,因为没有任何副作用。反过来,没有副作用对编译器也有帮助,因为优化没有副作用的代码是一项更容易的任务,而且可以更激进地执行。以函数方式编写代码的另一个好处是,编写的源代码往往更简洁、更具表现力,特别是与严重依赖于GoF(Gang of Four,四人组)设计模式的代码相比。
这并不一定意味着如果没有带宽问题,就应该使用无状态风格。这些决策要从单个类或函数到整个应用程序等许多层面考虑。
以类为例,如果你正在建模Consultant(顾问)类,它理应包含诸如顾问姓名、联系人数据、小时费、当前和过去的项目等字段。自然,它是有状态的。现在,假设你需要计算他们的工作报酬。你应该创建一个PaymentCalculator类,还是应该添加一个成员函数或自由函数来进行计算?如果使用创建类的方法,应该将Consultant作为构造函数参数还是方法参数?这个类应该有津贴等属性吗?
添加成员函数来计算工作报酬将破坏单一责任原则(SRP),因为这样的类有两个职责:计算报酬和存储顾问的数据(状态)。这意味着应该优先引入自由函数或单独的类,而不是使用这样的混合类。
在这个类中,首先应该有一个状态吗?我们来讨论另一种方法,即使用PaymentCalculator类。
一种方法是把计算所需的属性设为可公共访问:
这种方法有两个缺点。第一,它不是线程安全的,PaymentCalculator类的实例不能在没有锁的多线程中使用。第二,一旦计算过程变得更加复杂,该类可能会从Consultant类中复制更多的字段。
为了消除代码重复,我们可以重新编写PaymentCalculator类来存储Consultant实例:
请注意,由于不能简单地重新绑定引用,因此我们使用指南支持库(Guideline Support Library,GSL)中的一个辅助类(not_null)将可重新绑定的指针存储在包装器(wrapper)中,它可自动确保不存储空值。
这种方法的缺点也是不具备线程安全性。那么,有没有更好的方法呢?事实上,我们可以通过类的无状态化来使类线程安全:
如果没有需要管理的状态,那无论是创建自由函数(可能在不同的命名空间中)还是类中的静态函数(像上面的代码那样)区别都不大。就类而言,区分值(实体)类型和操作类型是很有用的,因为把它们混在一起可能会违反单一责任原则(SRP)。
无状态服务和有状态服务
上面关于类的原则也可以用于更高级的概念,例如微服务。
有状态服务是什么样的?以FTP为例,如果FTP不是匿名的,则要求用户通过用户名和密码来创建会话。FTP服务器存储这些数据以识别用户是否仍然处于连接状态,因此它要一直存储这些状态。每次用户更改工作目录时,都会更新其状态。用户所做的每一个更改都会反映为状态的变化,包括断开连接。有状态服务意味着,根据不同的状态,两个外观相同的GET请求会返回不同的结果。如果服务器状态丢失了,那么请求就不会被正确处理。
有状态服务还可能存在会话不完整或事务未完成的问题,这增加了问题的复杂性。应该让会话保持多久?如何验证客户端是否已崩溃或断开连接?应该什么时候撤销所做的更改?虽然你可以想出这些问题的答案,但通常更容易的是依靠服务的消费者以一种动态的“智能”方式与服务进行沟通。因为服务的消费者会自己维护某种状态,所以让服务来同时维护状态不仅是不必要的,而且往往是一种浪费。
无状态服务(例如后面描述的REST服务)则采用相反的方式。每个请求必须包含成功处理它所需的全部数据,因此两个相同的幂等请求(如GET)将得到相同的响应结果。它假设存储在服务器上的数据不会改变,但数据与状态不一定是同一个事情。最重要的是,每个请求都是独立的。
无状态服务是现代互联网服务的基础。HTTP是无状态的,同时许多网络服务API(例如Twitter的)也是无状态的。Twitter的API依赖的REST被设计为功能无状态的(functionally stateless)。REST是REpresentational State Transfer(表述性状态转移)的缩写,其背后的思想是,处理请求所需的所有状态都必须随请求一起传输。如果不满足这个规则,就不能说服务是REST服务。然而,实际上,该规则也有一些例外情况。
如果你正在创建一个在线商店,你可能想要存储与客户有关的信息,比如他们的订单历史和收货地址。用户侧的客户端可能会存储身份验证cookie,而服务器可能会在数据库中存储一些用户数据。cookie让我们不需要再管理会话,就像有状态服务一样。
对于服务来说,将会话保持在服务器端是一种不好的方式,原因有几个:这增加了许多本可以避免的复杂性,使bug更难复现,最重要的是,服务无法扩展。如果想将负载转移到另一台服务器,很可能会在复制带有负载的会话以及在服务器之间同步它们时遇到困难。因此,所有的会话信息都应保存在客户端。
也就是说,如果希望使用有状态的架构,就需要有很充分的理由。以FTP为例,它必须在客户端和服务器端都记录这些更改。用户只对单个特定的服务器进行身份验证,以便执行单一状态的数据传输。将其与Dropbox这样的服务进行比较,后者的数据通常在用户之间共享,而文件访问抽象为API,你可以思考一下为什么无状态模型更适合后者这种情况。