[译] Android 架构:Part 2 —— 介绍 Clean Architecture

在本系列的第一部分,我们介绍了我们在寻找可行架构的道路上所犯过的错误。在这部分,我们将介绍传说中的 Clean Architecture。

当你在谷歌搜索 "clean architecture" 时,你看到的第一张图片是:

它也被称为洋葱架构,因为图看起来象个洋葱(你会意识到你需要写样板代码写到哭);或者是端口和适配器,因为你可以看到右图的一些端口。六角架构是另一个相似的架构。

Clean Architecture 是前面提到的 Uncle Bob 的心血结晶,他是 《代码整洁之道》的作者。这种方法的要点是,业务逻辑(也称为 domain),是宇宙的核心。

掌控你的领域(domain)

当你打开项目时,你应该已经知道这个 app 是做什么的,与技术无关。其它一切都是实现细节。譬如,持久化就是一个细节。定义接口,创建一个快速的粗糙的内存内(in-memory)实现,不要想太多,直到完成业务。然后你可以决定怎样真正地持久化数据。数据库,网络,两者结合,文件系统 —— 或者仍然保留在内存中,或者结果你根本不需要持久化。总之一句话:内层包含业务逻辑,外层包含实现细节。

话说回来, Clean Architectue 有一些特性使这成为可能:

  1. 依赖规则
  2. 抽象
  3. 层与层之间的通信

I.依赖规则

依赖规则可以用下图解释:

外层应该依赖内层。那三个在红色框框内的箭头表示依赖。与其使用“依赖”,也许使用“看见”、“知道”、“了解”这类术语更好。在这些术语中,外层看见,知道,了解内层,但内层看不见,也不知道,更不了解外层。正如我们先前所说,内存包含业务逻辑,外层包含实现细节。遵循依赖规则,业务逻辑既看不到,也不知道,更不了解实现细节。这正是我们努力想要做到的。

如何实现依赖规则取决于你。你可以把它们放到不同的包,但小心“内层的”包不要使用“外层的”包。然而,如果有人不知道依赖规则,没有什么可以阻止他破坏规则。一个更好的方法是把层分离到不同的 Android 模块(modules,即子项目),并在构建文件(build.grale)中调整依赖,这样内层就无法依赖外层。

还有值得一提的是,虽然没人可以阻止你跨层依赖,譬如蓝色的层的组件使用红色的层的组件,但我强烈建议你只访问相邻的层的组件。

II.抽象

抽象原则之前已有所暗示。也就是说,当你朝图中间移动时,东西变得更抽象。 这是有道理的:正如我们所说内层包含业务逻辑,而外层包含实现细节。

甚至可以在多个层之间划分相同的逻辑组件,如图所示。 内层定义更抽象的部分,外层定义更具体的部分。

举个例子说清楚些。我们可以定义一个 “Notifications” 的抽象接口,并将其放到内层,这样你的业务逻辑需要时可以使用它来向用户显示通知。另一方面,我们可以这样来实现该接口,即使用 Android NotificationManager 显示通知来实现,并把该实现放到外层。

以这种方式,业务逻辑可以使用这样的功能 —— 通知(在我们的例子中)—— 但它不了解实现细节:实际的通知是如何实现的。此外,业务逻辑甚至不知道实现细节的存在。来看下面这张图片:

当将抽象规则和依赖规则组合在一起时,结果是使用通知的抽象业务逻辑既不会看到,也不会知道,更不会了解使用 Android NotificationManager 的具体实现。这很好,因为我们可以在业务逻辑毫不知情的情况下切换具体实现。

让我们把这种规则组合和标准的三层架构简单对比下,看看它们各自的抽象和依赖是怎样的以及如何工作的。

在图中,你可以看到,标准三层架构的所有依赖最终都传到数据库。也就是说,抽象和依赖并不匹配。在逻辑上,业务层应该是 app 的中心,但它却不是,因为依赖朝向数据库。

业务层不应该知道数据库,应该反过来。在 Clean Architecture 中,依赖朝向业务层(内层),并且抽象也提升到业务层,因此它们很好地匹配。

这是重要的,因为抽象是理论,依赖是实践。抽象是 app 的逻辑布局,依赖关系是(组件)如何实际组合在一起。在 Clean Architecture 中,这两者是匹配的。而在标准三层架构中则不然,如果你不小心,很容易导致各种逻辑上的不一致和混乱。

III.层与层之间的通信

现在我们将 app 分模块,将所有内容分开,将业务逻辑放在我们 app 的中心,并在外层实现细节,一切看起来都很棒。 但是你可能很快遇到一个有趣的问题。

如果你的 UI 是一个实现细节,网络是一个实现细节,业务逻辑在中间,那么我们如何从互联网获取数据,经过业务逻辑,然后发送到界面?

业务逻辑在中间,应该协调网络和界面,但它甚至不知道两者的存在。这是一个关于通信和数据流的问题。

我们希望数据能够从外层流向内层,反之亦然,但依赖规则不允许。 让我们举个最简单的例子。

我们只有两层,绿色和红色的。绿色的是外层,它知道红色的,红色的是内层,它只知道自己。我们希望数据从绿色流向红色,然后折回绿色。该解决方案先前已经暗示过了,看下图:

图的右边部分显示了数据流。数据源于 Controller,经过 UseCase(或者替换成你选择的组件)的输入端口,然后通过 UseCase 本身,最后通过 UseCase 输出端口发送到 Presenter。

图的主要部分(左边)的箭头表示组合和继承 —— 组合用实心箭头表示,继承用空心箭头表示。组合也被称作 has-a 关系,继承被称作 is-a 关系。圆圈中的 “I” 和 “O” 表示输入和输出端口。可以看到,定义在绿色层中的 Controller,拥有一个(has-a)定义在红色层中的输入端口。UseCase(齿轮,业务逻辑,现在不重要)是一个(is-a)(或实现)输入端口,并且拥有一个(has-a)输出端口。最后,定义在绿色层中的 Presenter 实际上是一个(is-a)定义在红色层的输出端口。

现在,我们可以将其与数据流匹配。Controller 拥有一个输入端口 —— 拥有一个指向它的引用。它调用输入端口的一个方法,这样数据就从 Controller 流到输入端口。但输入端口是一个接口,而它的实际实现是 UseCase。也就是说,它调用 UseCase 的一个方法,这样数据就流向了 UseCase。UseCase 执行某些操作,并希望将数据发送回来。它拥有输出端口的一个引用 —— 输出端口定义在同一层 —— 因此它可以调用上面的方法。因此,数据流向输出端口。最后 Presenter 是,或者实现了输出端口,这是魔法的一部分。因为它实现了输出端口,数据实际上流到它那了。

巧妙的是,UseCase 只知道它的输出端口,世界在此停止(意指数据流到此结束)。Presenter 实现了它(输出端口),实际上它可以被任何对象实现,因为 UseCase 不知道或不关心这些,它只清楚其层内的一亩三分地。可以看到,通过结合组合和继承,我们可以使数据流向两个方向,尽管内层并不知道它们在和外部世界通信。瞄一眼下图:

可以看到,和依赖箭头一样,has-a 和 is-a 箭头也指向中间。这是符合逻辑的。根据依赖规则,这是唯一可行的方法。外层可以看到内层,但不能反过来。唯一复杂的部分是,is-a 关系尽管指向了中间,却反转了数据流。

请注意,定义输入和输出端口是内层自己的职责,因此外层可以使用它们与其建立通信。我说过,这个解决方案先前已经暗示过,而且已经有了。那个讲解抽象的通知例子,也是这种通信的一个例子。我们在内层定义了一个通知接口,业务逻辑可以用来向用户显示通知,但是我们在外层也定义一个实现。在这种情况下,通知接口是业务逻辑的输出端口,用来和外部世界(在本例中,就是和具体的实现)通信。你不需要把你的类命名为 FooOutputPort 或者 BarInputPort,我们命名端口只是为了解释理论。

总结

那么,它是过度复杂,过度费解的过度工程吗?好吧,当你习惯了,它就简单。并且这是必要的。它允许我们使得好的抽象/依赖实际匹配真实世界的通信和工作。也许这一切都提醒你不过是空中楼阁:美丽,理论上优雅,但过于复杂,我们仍然不知它是否有效,但在我们的案例中,它确实有效。

这就是本系列的第二部分。最后,第三部分,毕竟我们已经了解了理论和架构,将讲解所有你需要了解的那些图上的标签。换句话说,分离的组件。我们将向你展示一个真实的应用于 Android 的 Clean Architecture。

原文

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

推荐阅读更多精彩内容