面向对象中的主体客体思维

01 为什么面向对象难以理解?

面向对象是应用软件设计比较好的方式,可以指导用计算机解决现实中的业务问题,因此是软件开发中的一种主流方式。

不过,用好面向对象则比较困难,即使有数年经验的软件工程师也难说能很好驾驭。大多数人往往是照猫画虎,没有理解软件开发的 “骨相”。

背后的原因为面向对象是对现实业务的抽象,需要使用者对现实有深入的理解,于是面向对象带有一定的哲学认识论的色彩。

实际上,现代英语、现代法律、面向对象编程都和近代哲学有关,而近代哲学被称为“主体性哲学”,“主体” 概念和主客体关系是非常重要的内容。

现代英语、现代法律、面向对象编程看似三个无关的领域,背后的逻辑却惊人的一致。

在现代英语(古代英语除外)的主要语法是:主语 + 谓语 + 宾语 + 修饰语。想象一个你在一家餐厅吃饭,你点了一份三明治,用一般现在时就是:I order a large sandwich。在这套逻辑关系中,主体就是你自己,客体是三明治,行为是点餐,其他的内容都是修饰成分。

假定你和餐厅出现了纠纷,餐厅忘记给你上了餐,却说已经上了。你发起了诉讼,让餐厅赔给你三明治。在诉讼的逻辑关系中,这是一起民事纠纷,你是民事主体,民事客体就是三明治,诉讼内容是赔偿行为。

而如果软件工程师需要编写一个软件用来处理订单,实现一个收银机功能。可能他会写一个 OrderService 来实现。伪代码如:

class OrderService {
    public Order createOrder(User user, Product product) {
        Order order = new Order();
        ……
        return order;
    }
}

这段代码可能会被认为不符合面向对象,因为某些教科书中,Order 是需要自己来完成业务的。实际上,在这段代码中,当我们认识到主客体关系时,一切豁然开朗。OrderService 是我们的业务主体,Order、User、Product 不过都是业务客体。和民事行为一样,业务逻辑也应该发生在业务主体中,这样就容易理解了。

既然主体、客体思维可以让面向对象更容易理解,我们来严肃的说下这些概念。哲学可能会有一些无聊,不过值得去了解它们。

主体、客体在哲学中的定义是什么呢?按照主流的哲学教科书,可以看到类似下面的描述:

  • 主体是有认识能力和实践能力的人,或者,是在社会实践中认识世界、改造世界的人。
  • 客体是实践和认识活动所指向的对象,是存在于主体之外的客观事物。

这里需要修正下,随着科学技术的发展,主体可以不只是普通的 “人” 了,可以是一个具有集体意识的团体、网络虚拟世界的一个形象,比如 xxx 公司、初音未来也可以是主体。与之相对的普通人是 “自然人”。

主体、客体思维从笛卡尔时期就开始出现,在康德时期又进一步发展。在主体、客体英文分别是 Subject、Object,它们都是实体,主体是具有行为、感知和思想的一类。

需要注意的是,主体、客体是相对的、动态的。比如用户,在做出一些操作的时候它是主体,当被管理员或者系统操作的时候,它又是客体,这点尤为重要。

关于更详细的主体、客体的知识,可以参考逻辑学、哲学书籍,这里不再展开。当我们理解到主体和客体的思维后,就可以用它了分析和指导我们的实践,也就是面向对象的编程了。

02 指导业务分析

用主体、客体思维可以分析我们需要设计的软件需求,软件需求一般由产品经理、BA参与。

如果按照主体、客体思维的来看,不同角色的用户就是主体,我们设计的软件就是客体,无论这个软件运行在服务器还是客户端。

一直以来,产品经理的交付物没有一个明确的标准,因此业务输出物的质量也参差不齐。用主体、客体思维可以检验产品经理的交付物,也可以改进其设计。

例如,在有一个项目中,产品经理把代理商、站点后台系统设计到一起,并只提供了原型图,告诉我们这只是权限的不同,又补充了一份权限说明。在这个例子中,业务上陷入了一个逻辑问题,导致在后期的系统开发时陷入比较大的麻烦。拿具体的场景来说,代理商、站点后台系统都有一个相似的页面 —— 产品列表,因为它们非常相似,产品经理当做一个用例来处理。其真实的业务如下:

  1. 代理商系统的产品列表,显示当前登录的代理商管理员所属的代理商下的产品,在这个场景下,其主体是代理商管理员,客体是所属的代理商下的产品。
  2. 站点后台系统的产品列表,显示的是整个站点的产品列表。在这个场景下,其主体是系统管理员,客体是所有的产品。

这两种管理的逻辑、权限、审计、搜索方式都不一样,只是页面比较类似罢了,列表页都有删除操作,但是这里的两种删除逻辑绝不一样。

主体、客体思维对业务分析的指导地方还很多,一些常见的经验如下:

  1. 主体 + 行为 + 客体组成一个用例(use case),即使同样的行为主体、客体变了,也是不同的用例。
  2. 流程图的绘制注意粒度,以主体 + 行为 + 客体的用例为粒度绘制比较合适。
  3. 权限是主体的行为限制,和用例一一对应。
  4. 不同主体的区别可以用角色来描述,同一主体下的权限区分使用用户组。比如代理商管理员需要被授予不同的权限,这里的区分不是指代理商管理员和后台管理员这样的角色,实际是用户组的不同。
  5. 如果出具原型图,以每个角色来独立设计,复用的问题可以在实现时考虑。

03 指导业务建模

用主体、客体思维可以帮助我们建立领域模型。

业务建模是我们开发人员比较熟悉的部分了,无论是使用 E-R 模型直接对数据库建模,还是使用 DDD 领域建模,设计与存储无关的模型。建模都是非常重要的概念,模型驱动开发是面向对象重要的思维。

那么,模型是什么?

广义的模型可以包含的内容可以很多,用一个简单的东西去理解复杂的事物,都可以说是模型,比如地图、教具等实体模型,金字塔、四象限等思维模型。由此我们可以给模型一个广义的定义。

通常,模型是对对象、人或系统的信息表示。它通过较为简单的信息结构来代表我们需要理解的复杂事物或系统。

对于编程来说,模型是业务概念在程序中的一种表达方式,常见的有订单、用户、商品等。

在前文中订单就是一个典型的模型,我们会发现我们通常是对客体在建模,不过有时候也会建立主体模型。

如果对业务主体建模,得到的就是各种 Service,会侧重它们的行为,关心它们的算法和逻辑。

如果对业务客体建模,得到的就是各种实体、以及实体的关系,会侧重他们属性,存储方式。

这样就让建模的目标更清晰了,我们可以通过 UML 类图表达业务主体,表达他们的继承、组合关系等;可以通过非 UML E-R 图表达业务客体,表达他们的关联关系,也可以从容的使用 DDD 中的聚合、实体、值对象的概念,优化模型。

用主体、客体思维建模的一些常见的经验如下:

  1. 对主体建用例,只对客体建模,会让建模更简单。
  2. 识别名词的二义性,避免混淆模型,比如商品和订单中的商品具有二义性,为不同的模型。
  3. 避免对业务客体使用继承,即使是一个属性的不同,意味着这是不同的模型。
  4. 注意集合类名词和单个的名词是不同的客体,有点像公司法人和法人代表之间的关系。

04 指导架构设计

MVC、三层架构、DDD 四层架构怎么选择和理解?用主体、客体思维依然可以帮助理解其中内在逻辑。

我们说过,主体、客体是相对的,现在我们已经打开了系统的黑盒,在这个黑盒中主体、客体关系是怎么样的呢?

如果我们使用 MVC 架构:

  1. C - Controller,负责处理系统输入,Controller 使用 Model 完成业务,并调用 View 能力输出数据到系统外部。我们可以视为主体,他的行为就是用户的每个用例 。
  2. M - Model,就是我们的模型,用来表达数据结构和关系。和上面说的客体模型一致。
  3. V - View,负责处理业务输出,无论是 Rest API 还是渲染模板。在业务输出的场景中,View 是主体,Model 是客体。
  4. 其他未归纳的概念。其实在 MVC 中有一些概念未为被归纳,比如持久化的服务也应该是主体,负责将 Model 写入持久化环境。

三层架构、DDD 四层架构逻辑上一致,只是多了一个 Application 的概念,DDD 四层架构在每一个层都有主体、客体:

  1. UI 层。处理用户输入,负责把用户的请求识别为具体的对象。主体一般由框架实现了,可以是 RequestHandler 之类的,客体是用户请求的数据,并加工成可以理解的对象。

  2. Application 层。这层是为了每个具体的用户设计的,比如管理员、代理商、普通用户,这层的主体是 ApplicationService,客体是用例对象,比如添加用户的 DTO,并转换为领域模型。

  3. Domain 层。这层是为了复用 Application 层的逻辑,保持业务的一致性和避免代码的重复。主体是 DomainService,客体是领域模型。

  4. Infrastructure 层。这层是为了提供技术基础设施,这层的主体种类繁多,比如持久化数据的 Repository、发送邮件的 EmailSender 之类的。这层可以提供对 Domain 层模型的透明持久化,也可能会有自己的对象来处理特定的业务,比如发送邮件的 Message 对象可能只在这一层被定义。

用主体、客体思维指导架构设计的一些常见的经验如下:

  1. 对主体拟人化命名,比如 Controler、Service、Viewer、Handler、Mapper 等。
  2. 对客体拟物化命名,比如 Entity、Model、xxxobject 等。
  3. 类似的概念可能既是主体、也是客体,但逻辑上不同。比如 UserService 和 UserEntity 都是和用户相关,但逻辑不同。
  4. 如果一个对象操作自己的属性,那么主体是其本身,客体是其属性。

05 指导代码实践

具有面向对象能力的高级计算机语言已经和自然语言有一定的相似性了(和自然语言的鸿沟是做为形式语言,必然无法和自然语言进行推理转换)。

如果用主体、客体思维来指导代码实践,指导方法、变量的命名,可以让我们写出规范、诗意的代码。

如果我们在方法命名上参考下面的规则:

  • 参考英语用格式: 主语 + 谓语 + 宾语 + 补语来称述某件事情,计算机也可以用格式:主体 + 方法名 + 参数构成一次操作。方法名和参数无需再出现主体的名词。比如 OrderService 的 create 方法,无需写为 createOrder()。
  • 坚持原则:给主体赋予行为和能力,给客体赋予属性和数据。
  • 把主体的行为作为契约,以此来抽象接口,但是如无必要,勿加接口。
  • 主体、客体都需要遵守单一职责原则,SOLID 其他实践只用于主体,让客体保持简单。

我曾应用这些原则编写了一个简单的 Tomcat 的配置下发中间件,用于动态更新 Tomcat 的配置,简单来说如下:

  • ConfigMananger 作为主体,用于管理信道,实现配置的应用、拉取、上报。
  • ConfigPosterInterface 作为契约,定义了 pull、post 两个方法
  • ConfigRedisPoster、ConfigHttpPoster 两个实现,作为发送的信道
  • Config 作为客体用于承载配置数据。

结合拟人、拟物的命名风格,写出的代码比较容易理解,毕竟这是人们对现实的一种自然的思考方式。

评论已关闭