去model化和数据对象

原文

简述

去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.

为什么不使用数据对象

根据上一小节的内容,我们可以把对象抽象地理解为一个函数集,一个领域。在这一节里,我们再进一步推导:如果这些函数集里的所有函数,并不都是处在同一个问题领域内,那么面向对象的这种实践是否依旧成立?

答案是成立的,但显然我们并不建议这么做。不同领域的函数集如果被封装在了一起,在实际工程中,这种做法至少会带来以下问题:

  1. 当需要维护某个问题领域内的函数时,如果修改到某个需要被共用的参数或者需要被共享的对象,那么其他问题领域也存在被影响的可能。牵一发而动全身,这是我们不希望看到的。
  2. 当需要解决某个问题时,如果引入了某个充满了各种不同问题领域的函数集,这实质就是引入了对不同问题领域解决方案的依赖。当需要做代码迁移或复用时,就也要把不相关的解决方案一并引入。拔出萝卜带出泥,这也是我们不希望看到的。

当需要维护某个问题领域内的函数时,如果修改到某个需要被共用的参数或者需要被共享的对象,那么其他问题领域也存在被影响的可能。牵一发而动全身,这是我们不希望看到的。

我们在进行对象化设计时,必须要分割好问题域,才能保证设计出良好的架构。

业界所谓的各种XX建模、XX驱动设计、XXP,大部分其实都是在强调合理分割这一点,他们提供不同的方法论去告诉你应该如何去做分割的事情,以及如何把分割出来的部分再进一步做封装。然而这些XX概念要成立的话,就都一定需要具备这样一个前提条件:一个被封装出来的对象的活动领域,必须要小于等于当前被分割出来的子问题领域。如果不符合这个前提条件的话,一个大的问题领域即使被强行分割成各种小的问题领域,这些小的问题领域还是依旧难以被封装成为对象,因为对象的跨领域活动势必就要引入其它领域的问题解决方案,这就使得分割名不副实。

然而,一个被封装出来的对象的活动领域,必须要小于等于当前被分割出来的子问题领域这个前提在实际业务场景实践中,是否一定成立呢?如果一定成立的话,那么这种做法和这些方法论就是没问题的。如果在某些场景中不成立,对象化设计在这些场景就有问题了。

事实上,这个前提在实际业务场景中,是不一定成立的。在实际业务场景中,一个数据对象被多个业务领域使用是非常常见的。一个数据对象在不同层、不同模块中被使用也是非常常见的。所以,如果两个业务对象之间需要传递的仅是数据,在这个场景下就不适合传递对象化的数据。

当需要解决某个问题时,如果引入了某个充满了各种不同问题领域的函数集,这实质就是引入了对不同问题领域解决方案的依赖。当需要做代码迁移或复用时,就也要把不相关的解决方案一并引入。拔出萝卜带出泥,这也是我们不希望看到的。

这种场景其实就很好理解了。实际工程中,对象化数据往往不是一个独立存在的对象,而是依附于某一个领域。例如持久层提供的对象化数据,往往依附于持久层。网络层提供的对象化数据往往依附于网络层。当你的业务层某个模块使用来自这些层的对象化数据时,将来要迁移这个模块,就必须不得不把持久层或者网络层也跟着迁移过去。迁移发生的场景之一就是大型工程的组件化拆分,实施组件化时遇到这种问题是一件非常伤脑筋的事情。

小结

所以,在数据传递时,我不建议采用对象化设计,尤其是数据传递的两个实体是跨层实体或者跨模块实体时,对象化设计对架构的伤害非常大。

从实际而非理论的角度上讲,数据对象的使用主要存在这些问题:

  1. 数据对象并不符合面向对象的设计初衷
  2. 数据对象有变为支持多领域对象的可能
  3. 数据对象使得领域间依赖性变强

去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化的做法,但看起来这应该不是所有的手段,很有可能还有其它方法。未来如果我遇到了其他场景想到了其它方法的话,会对它进行补充。如果各位读者还有不同的方法或其它的问题,也欢迎在评论区一起交流。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容

  • iOS网络架构讨论梳理整理中。。。 其实如果没有APIManager这一层是没法使用delegate的,毕竟多个单...
    yhtang阅读 5,174评论 1 23
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,799评论 25 707
  • 1、通过CocoaPods安装项目名称项目信息 AFNetworking网络请求组件 FMDB本地数据库组件 SD...
    阳明先生_X自主阅读 15,969评论 3 119
  • 把Java对象转换为字节序列,并存储至一个储存媒介的过程。 什么是反序列化? 把字节序列恢复为Java对象的过程。...
    芒鞋胜马阅读 667评论 0 0
  • 变异凯撒 密文 afZ_r9VYfScOeO_UL^RWUc 分析 既然是凯撒,本质还是移位密码,但是要注意区别,...
    V0W阅读 2,499评论 0 0