简述
去model化这个说法其实有点儿难听,model化就是使用数据对象,去model化就是不使用数据对象。所以这篇文章主要讨论的问题就是:数据传递时,是否要采用数据对象?这里的数据传递并不是说类似RPC的场景,而是在单个工程内部,各对象之间、各组件之间、各层之间的数据传递。
所谓数据对象
,就是把不同类型的数据映射到不同类型的对象上,这个对象仅用于表达数据,数据通过对象的property来体现。瘦Model、贫血模型就属于这一类。
去Model化
,就是不使用特定对象迎合特定数据的映射的方式,来表达数据。比如我们可以使用NSDictionary,或者其他手段例如reformer、virtual record,来避免这种数据映射对象。
关于这个问题的讨论涉及以下内容:
- 如何理解面向对象思想
- 为什么不使用数据对象
- 去Model化都有哪些手段
通过以上三点,我希望能够帮助大家建立对面向对象的正确理解,让大家明白如何权衡是否要采用对象化的设计。以及最终当你决定不采用对象化思想而采用非对象化思想时,应该如何进行架构设计。
如何理解面向对象思想
面向对象的思想简单总结一下就是:将一个或多个复杂功能封装成为一个聚合体,这个聚合体经过抽象后,仅暴露少部分方法,这些方法向外部获取实现功能所需要的条件后,就能完成对应功能
。传统的面向过程只针对功能的实现做了封装,也就是函数。经过这层封装后,仅暴露参数列表和函数名,用于外部调用者调用并完成功能。
我们可以推导出:函数封装了实现功能所需要的代码,因此对象实质上就是再次对函数进行了封装。将函数集合在一起,形成一个函数集合,面向对象思想的提出者把这个函数集合称之为对象
,把对象
的概念从理论映射到实际的工程领域,我们也可以叫它类
。
然而我们很快就能发觉,只是单纯地把函数集合在一起是不够的,这些函数集有可能互相之间需要共用参数或共享状态。因此面向对象的理论设计者让对象自己也能够提供属性(property),来满足函数集间共用参数和共享状态的需求。这个函数集现在有了更贴切的说法:领域
。因此当这个领域中的个别函数不需要共用参数或共享状态,仅仅是提供功能时,这些相关函数就可以体现为类方法。当领域里的函数需要共用参数或共享状态时,这些函数的体现就是实例方法。
这里补充一下,领域
的概念我们更多会把它理解得比较大,比如多个相关对象形成一个领域。但一个对象自身所包含的所有函数也是一个领域,是大领域里的一个子领域。
以上就是一个对面向对象思想的朴素理解。在这个理解的基础上,还衍生出了非常多的概念,不过这并不在本文的讨论范围中。
总之,一个对象事实上是对一个较小领域的一种封装。对应到本文要讨论的问题来看,如果拿一个对象去表达一套数据而非一个领域,这在一定程度上是违背面向对象的设计初衷的。你看着好像是把数据对象化了,就是面向对象编程了,然而事实上并非如此。Martin Fowler早年也在他的《Anemic Domain Model》中提出了一样的看法:
The fundamental horror of this anti-pattern is that it's so contrary to the basic idea of object-oriented design; which is to combine data and process together. The anemic domain model is really just a procedural style design, exactly the kind of thing that object bigots like me (and Eric) have been fighting since our early days in Smalltalk. What's worse, many people think that anemic objects are real objects, and thus completely miss the point of what object-oriented design is all about.
为什么不使用数据对象
根据上一小节的内容,我们可以把对象抽象地理解为一个函数集,一个领域。在这一节里,我们再进一步推导:如果这些函数集里的所有函数,并不都是处在同一个问题领域内,那么面向对象的这种实践是否依旧成立?
答案是成立的,但显然我们并不建议这么做。不同领域的函数集如果被封装在了一起,在实际工程中,这种做法至少会带来以下问题:
- 当需要维护某个问题领域内的函数时,如果修改到某个需要被共用的参数或者需要被共享的对象,那么其他问题领域也存在被影响的可能。牵一发而动全身,这是我们不希望看到的。
- 当需要解决某个问题时,如果引入了某个充满了各种不同问题领域的函数集,这实质就是引入了对不同问题领域解决方案的依赖。当需要做代码迁移或复用时,就也要把不相关的解决方案一并引入。拔出萝卜带出泥,这也是我们不希望看到的。
当需要维护某个问题领域内的函数时,如果修改到某个需要被共用的参数或者需要被共享的对象,那么其他问题领域也存在被影响的可能。牵一发而动全身,这是我们不希望看到的。
我们在进行对象化设计时,必须要分割好问题域,才能保证设计出良好的架构。
业界所谓的各种XX建模、XX驱动设计、XXP,大部分其实都是在强调合理分割
这一点,他们提供不同的方法论去告诉你应该如何去做分割的事情,以及如何把分割出来的部分再进一步做封装。然而这些XX概念要成立的话,就都一定需要具备这样一个前提条件:一个被封装出来的对象的活动领域,必须要小于等于当前被分割出来的子问题领域
。如果不符合这个前提条件的话,一个大的问题领域即使被强行分割成各种小的问题领域,这些小的问题领域还是依旧难以被封装成为对象,因为对象的跨领域活动势必就要引入其它领域的问题解决方案,这就使得分割
名不副实。
然而,一个被封装出来的对象的活动领域,必须要小于等于当前被分割出来的子问题领域
这个前提在实际业务场景实践中,是否一定成立呢?如果一定成立的话,那么这种做法和这些方法论就是没问题的。如果在某些场景中不成立,对象化设计在这些场景就有问题了。
事实上,这个前提在实际业务场景中,是不一定成立的。在实际业务场景中,一个数据对象被多个业务领域使用是非常常见的。一个数据对象在不同层、不同模块中被使用也是非常常见的。所以,如果两个业务对象之间需要传递的仅是数据,在这个场景下就不适合传递对象化的数据。
当需要解决某个问题时,如果引入了某个充满了各种不同问题领域的函数集,这实质就是引入了对不同问题领域解决方案的依赖。当需要做代码迁移或复用时,就也要把不相关的解决方案一并引入。拔出萝卜带出泥,这也是我们不希望看到的。
这种场景其实就很好理解了。实际工程中,对象化数据往往不是一个独立存在的对象,而是依附于某一个领域。例如持久层提供的对象化数据,往往依附于持久层。网络层提供的对象化数据往往依附于网络层。当你的业务层某个模块使用来自这些层的对象化数据时,将来要迁移这个模块,就必须不得不把持久层或者网络层也跟着迁移过去。迁移发生的场景之一就是大型工程的组件化拆分,实施组件化时遇到这种问题是一件非常伤脑筋的事情。
小结
所以,在数据传递时,我不建议采用对象化设计,尤其是数据传递的两个实体是跨层实体或者跨模块实体时,对象化设计对架构的伤害非常大。
从实际而非理论的角度上讲,数据对象的使用主要存在这些问题:
- 数据对象并不符合面向对象的设计初衷
- 数据对象有变为支持多领域对象的可能
- 数据对象使得领域间依赖性变强
去Model化都有哪些手段
字典流
这种做法是最原始最简单的做法,我就不多说了。
reformer
reformer是这样的工作原理:
------------------ ------------------
| | | |
.| Reformer_A | .... | View_A |
. | | | |
. ------------------ ------------------
.
------------------ . ------------------ ------------------
| |. | | | |
| APIManager |......| Reformer_B | .... | View_B |
| |. | | | |
------------------ . ------------------ ------------------
.
. ------------------ ------------------
. | | | |
.| Reformer_C | .... | View_C |
| | | |
------------------ ------------------
APIManager提供了来自网络层的数据。Reformer_A,Reformer_B,Reformer_C,分别代表不同的领域。View_A,View_B,View_C,分别就是各领域对不同的数据应用之后产生的结果。在讲网络层的文章中,我设计了reformer的方式来实现非对象化。更详细的讲述和实际的Demo文章里都有,我在这里就不多说了。
Virtual Record
Virtual Record事实上把reformer和某个领域相关对象集合在了一起。Virtual Record和reformer的区别在于:reformer更加有利于单数据对应多对象的场景,Virtual Record更加有利于多数据对单对象的场景
------------------ ------------------
| | | |
| DataCenter_A | .....| VirtualRecordA |.
| | | | .
------------------ ------------------ .
.
------------------ ------------------ . ------------------
| | | | . | |
| DataCenter_B |......| VirtualRecordB |.......| View_B |
| | | | . | |
------------------ ------------------ . ------------------
.
------------------ ------------------ .
| | | | .
| DataCenter_C | .....| VirtualRecordC |.
| | | |
------------------ ------------------
事实上这幅图有个地方画的不太贴切,Virtual Record其实只是View_B的一个protocol,它并不是一个实例,所以才Virtual。关于Virtual Record的详细解释和案例,在讲持久层的文章里有。
总结
将数据对象化事实上是一个不符合面向对象思想的做法。
这种说法看起来很反直觉,但事实上如果你对面向对象有深入的理解,就能够明白其中的原因。这种不符合面向对象思想的做法,也导致了工程实践上代码的高耦合和组件难以复用的情况,这都是我们不希望看到的。我在这篇文章里提供了几种去Model化的做法,但看起来这应该不是所有的手段,很有可能还有其它方法。未来如果我遇到了其他场景想到了其它方法的话,会对它进行补充。如果各位读者还有不同的方法或其它的问题,也欢迎在评论区一起交流。