聊聊对象
v1.0
李飞于 2016年9月7日
1. PO
PO(Persistent Object)即持久化对象[1],它跟持久层(通常是数据库)的数据结构形成一一对应的映射关系,如果持久层是关系型数据库,那么,数据表中的每个字段(或若干个)就对应 PO 的一个(或若干个)属性。《企业应用架构模式》[2]中提到的行入口模式[3]主要就是讲的 PO。
An object that acts as a Gateway (466) to a single record in a data source. There is one instance per row.
Figure 1. 行入口模式
PO 一般是 POJO/POCO[4],不包含业务逻辑,主要是为数据持久化服务的。
2. DO
DO(Domain Object)即领域对象[5] ,就是从现实世界中抽象出来的有形或无形的业务实体。
《企业应用架构模式》中提到的『领域模型』主要讲的就是 DO,它是合并了行为和数据的领域的对象模型。DO 不仅仅拥有数据还具有行为。
Domain ModelAn object model of the domain that incorporates both behavior and data.
Figure 2. 领域模型
3. DTO
DTO(Data Transfer Object)即数据传输对象[8],这个概念来源于《企业应用架构模式》,目的是为了分布式应用提供粗粒度的数据实体,以减少分布式调用的次数,从而提高分布式调用的性能和降低网络负载。现在多泛指用于展示层与服务层之间的数据传输对象。
Data Transfer ObjectAn object that carries data between processes in order to reduce the number of method calls.
Figure 3. 数据传输对象
随着手机应用的兴起,前后端分工越来越明确以及服务端 SOA、微服务架构的流行,DTO 的承担的职责越来越多,重要性越来越高:
-
App 与服务端程序通信的 API 接口所使用的对象可以认为是一种 DTO;
-
SOA、微服务架构中,各个服务间通信所使用的对象也可以认为是一种 DTO;
DTO 的设计会与 API 接口的风格有关,如套模板式、RESTful、嵌套外键关系、范式&关系风格等,这些 API 设计风格会影响到 DTO 的设计。
-
套模板式风格的 DTO 会与 VO 非常相近,而 RESTful 风格的 DTO 可能就是 DO 的 POJO/POCO。
-
嵌套外键关系的 DTO 其实体之间的关系与范式&关系的风格恰好相反等。
-
SOA、微服务架构中各个服务间的通信接口又是另外一种设计风格,由于服务间 DO 和 DTO 需要频繁的切换,DTO 可以认为贫血的 DO [9]。
4. DO VS PO
上文提到,简单领域模型使用活动记录来组织领域对象,《企业应用架构模式》中对活动记录的定义:一个对象,它包装数据库表或视图中某一行,封装数据库访问,并在这些数据上增加了领域逻辑。
Active RecordAn object that wraps a row in a database table or view, encapsulates the database access, and adds domain logic on that data.
Figure 4. 活动记录
从定义上,可以看到采用活动记录模式组织的领域对象同时具有 DO 和 PO 的功能,它既可以对持久层进行读写,也具有领域逻辑及其行为。
而在复杂领域模型中,数据映射器模式则通过配置/映射的手段,将 PO 隐藏在后面,数据的持久化完全由数据映射器中间件来完成,无需接触再定义和操作 PO。
Data MapperA layer of Mappers (473) that moves data between objects and a database while keeping them independent of each other and the mapper itself.
Figure 5. 数据映射器
4.1. 消失的 PO
时至今日,数据库依然是主要的数据持久化手段,产品研发过程中会大量采用 ORM 中间件来实现数据库的读写。从 2004 年 ROR 兴起之后,几乎所有的主流 Fullstack Web Framework 和 ORM 框架都采用 AR(活动记录)或 DM(数据映射器)作为其 ORM 的主要模式,如:ROR、Django、SQLAlchemy、Hibernate、Yii、Play、Grails 等。
另一方面,领域驱动设计的流行也使得领域模型成为组织领域逻辑的不二之选,表入口、行入口模式只是在 Go、Node、Rust 等 ORM 中间件不完善的技术栈中出现。在这种背景下,DO 与 PO 的界限淡化,DO 同时承担了两种角色,这种转变也使研发过程本身变的简单,由两种对象合并为一种对象,两种对象之间的相互转换也不需要了。
4.2. PO 的回归
然而,PO 并没有消失太久,随着 NoSQL、缓存技术的发展,混合持久化的流行,PO 又回来了,以另外一种姿态回归大家的视野中。PO 不仅仅是那个与数据库表结构一一对应的对象,还是与 NoSQL 组件紧密结合,更偏向业务的各类数据结构。
一个列表、一棵树、一张图都有可能作为 PO 的数据结构,memcache、redis 都会是 PO 的持久化介质。
5. 领域模型基础崩塌
新技术对 DO 也产生了巨大的影响,由于缓存技术和混合持久化的广泛应用,DO 也已经不能再局限在 ORM 中间件的实现了。现在市面上还没有一款支持混合持久化的 ORM 中间件,DO 的生成逻辑将由 ORM 中间件的 AR 转向自研支持混合持久化的 Data Mapper。
举个简单的例子:有两个领域对象:订单(order)和用户(user),订单上有个 user_id
的外键,一个订单『属于』某个用户。
在传统的 ORM 中间件(如 SQLAlchemy、Django)支持下,我们可以通过 order.user
的方式来访问这个订单对象的用户。
这两个领域对象通过中间件构成了一张互通的网,只要对象之间存在关系,我们就可以通过一个对象到达另外一个对象。当然,这个过程依赖数据库的连接,受到数据库事务的制约。
若我们使用缓存技术将订单缓存起来(储存在 memcache 中)或者将订单存储在 NoSQL(mongodb)中,那么从缓存或 NoSQL 中获得的订单对象并不在 ORM 中间件构成的互通的网内(不在
SQLAlchemy 的 session 里),因此上面提到的 order.user
方式已经无法使用。
此时我们发现之前搭建起领域模型的基础已经崩塌,AR 模式下的 DO 能够覆盖的场景越来越少,有的团队甚至干脆抛弃了 AR(ORM),转向裸写 SQL 来完成从 PO 到 DO 的过程。
6. DO 的前世今生
6.1. DTO VS DO
在日益流行的 SOA、微服务架构下,DTO 和 DO 也有着千丝万缕的联系,它们之间相互影响对方。譬如:B 服务调用 A 服务,那么,A 服务的 DTO 将会是 B 服务 DO 的数据源,反之亦然。DO 和 DTO 在服务调用之间不断地进行着转换:DO → DTO → DO → DTO → DO。这种趋势将会推动 DO 从 AR 模式转型,DO 与 DTO 的互转必然成为 DO 的一项基本技能。
AR 技术是我们读写数据库的尖兵利器,缓存技术也是我们不能丢弃的武器,SOA、微服务又是跨团队、跨系统的不二选择,越来越多的外部依赖使我们的 DO 不堪重负。
上图中,DO A 同时出现在 Service A、B、C 三个系统中。Service A 中的 DO A 有的是通过 AR 从数据库里获取的,有的是通过 PO 从 Redis 中获取的,而 Service B、C 中的 DO A 则是通过 DTO 从 RPC 中获取的。DO A 在 Service C 中通过 DTO 以 API 形式给到了 App。
整理一下便得到了下图:
7. DO 的新技能
-
范式化、避免嵌套
由于 DO 需要以多种形式往 PO、DTO 转换,嵌套的 DO 将非常不利于这种转换的操作,因此 DO 应尽量保存范式化。
-
无外部依赖,数据与行为内聚
DO 需要从多种 PO、DTO 而来,不要有依赖,做到内聚。 放弃级联查找,延时加载等技术。
-
将 AR 从 DO 独立出来
AR 提供的级联查找,延时加载技术都非常好用,将 AR 从 DO 独立出来可以继续发挥这些技术的优势。当然这样做的一个坏处是 DO 将不再具有 AR 的一些特性,譬如关系加载等。
-
AR 与 DO 共享某些领域逻辑
譬如
status
字段中第一位用于标记删除,这段逻辑对于 AR 和 DO 是一致的,可以采用一些元编程、继承、组合的技术来实现领域逻辑的复用。 -
写操作可以跳过 DO
大多数的应用都是读多写少,DO 之所以复杂度升高,很大程度上也是因为引入大量读优化导致的,无论是缓存技术、混合持久化还是 微服务。而写操作也不需要在各个系统/服务中传递 DO,一般都会收敛到一个系统/服务中统一操作。 因此,对于写操作,通常是可以绕过 DO,直接通过 AR 执行。
8. VO
VO(View Object)即视图对象,用于展示层,它的作用是把某个指定页面(或组件)的所有数据封装起来。
现代软件开发过程中,专业化程度越来越高,前后端分工越来越明确,过去一个程序员既读写数据库,又写模版、JS 的场面已经不多见了,VO 主要出现在前端(包含客户端)程序的开发过程中,前后端的通信采用 API 接口,VO 主要与 DTO 打交道更多一些。
DO (Server) → DTO (API) → VO (App)
然而,随着前端(客户端)程序的复杂度不断攀升,前端(客户端)程序中也引入了分层架构,也有了自己的 DO。
以当下非常火的 redux 框架为例,它的 store
里一些存储的对象就是 DO,每次 action
被触发后,store
发生变化后,
会调用一个 mapStateToProps
的函数,将 DO 转化为 VO,然后将 VO 交给 React 组件去渲染。
DO 与 VO 的组织结构有巨大的差异,
DO 应该借鉴『Identity Map』模式采用范式的风格来组织,而 VO 则恰恰相反,应该根据组件的布局,采用嵌套的风格来组织。
DO (Server) → DTO (API) → DO (APP) → VO (App)
9. No Object
对象技术发展至今已经几十年,虽然应用广泛,但也并不是能解决所有问题的『银弹』。
举几个简单的例子:
-
带有正文(content、text)字段的对象,如文章,其对象整体获取成本很高。
-
一些 BI、统计的操作,量大但不需要获取整个对象。
-
关系型数据库本身并不是面向对象的。
-
简单的应用并不需要 DO。
在服务化流行的今天,DTO 大行其道,无论是 RPC 还是事件溯源[10],随处可见 DTO 的影子。 像 RESTful、JSON、Thrift、Protocol Buffers 等服务化技术中,大部分都与对象有关,有的甚至名字中就含有对象的字样。
事实上,广义来看,即便是今天,功能最强大、应用最广泛的数据访问协议依然是诞生于 1974 年的 SQL[11]。 在日常工作中,我们用的一些服务复用了 memcache 协议,有些服务复用了 redis 协议。 那么假设有一个数据访问服务是基于 SQL 的(类似于 Hive,但目的不同),其易用性、开放性、扩展性都会大大提升,上面举的几个例子也可以轻松搞定。
9.1. Query Language, GraphQL
GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data. GraphQL provides a complete and understandable description of the data in your API, gives clients the power to ask for exactly what they need and nothing more, makes it easier to evolve APIs over time, and enables powerful developer tools.
GraphQL[12] 是 Facebook 推出的一套基于 QL 的 API 框架,如官网介绍所言,GraphQL 像 SQL 一样,它可以使服务之间传输的数据更灵活,不再受限于 DTO。 在 QL 面前,RESTful 风格、范式&关系风格都显得很渺小,因为它的风格是由调用者定义的(GraphQL 是嵌套风格)。
{ user(id: 4802170) { id name isViewerFriend profilePicture(size: 50) { uri width height } friendConnection(first: 5) { totalCount friends { id name } } } }
Note
|
与 GraphQL 配合的 data flow 框架 Relay,也与传统的 DO 不同。 |
10. 历史的轮回
「With great power comes great responsibility」,QL 也不是银弹,当调用方拥有了更强大、更灵活的能力时,其可控性却在急剧下降。 在未来某个无法容忍的时刻,人们可能又会想起把 QL 的结果转化为对象,甚至放弃 QL 重新引入 DTO。