整洁的架构
小结:
1、User Case
用例是架构设计中非常重要的一环,架构也是基于这些用例进行设计的,如果缺失了用例,就无从谈起架构设计了。
一文读懂架构整洁之道 https://mp.weixin.qq.com/s/XAm1MO4RQYtkj3ay-2jT7A
一文读懂架构整洁之道
我在之前的文章《谈谈什么是好的代码》中谈了一些自己对整洁代码的感悟,代码并不是独立存在的,成百上千个类的系统在企业应用中非常常见,如何将代码进行有效的组织,保持高可读性,高可维护性,则是一个好的架构需要考虑的事情。本文从原则切入,聊聊组件的分层和解耦,浅谈下Bob大叔提出的整洁架构,感兴趣的同学也可以发表下自己的看法。
原则
原则属于做事情的指导方针,在讨论架构前,先来看看相关的一些原则。有适用于代码层面的原则,有适用于再高一层级——组件的原则。
▐ 代码原则
代码的原则有SOLID,迪米特法则,组合复用原则等等,我在谈谈什么是好的代码中也列出来过,关于这些原则讨论的文章非常之多,大家也比较熟悉,本文主要谈架构方面的,这里就不展开了。
▐ 组件原则
组件是一组代码的集合,拿盖房子来打比方,代码原则指导如何使用砖块建造房间,而组件原则指导如何将房间构建成高楼大厦。组件的构建要遵循一些原则,否则即使墙砌的再好,房间修建的再漂亮,不按照规范建造,房子可能就歪歪扭扭,整栋楼房的质量也堪忧。
组件原则包括组件内的关系(组件聚合)以及组件间的关系(组件耦合)。
组件聚合
组件聚合方面的原则有以下几个:
-
REP:复用/发布等同原则
-
CCP:共同闭包原则
-
CRP:共同复用原则
REP是组件聚合总的指导原则,表示组件内的类和模块是彼此紧密相关的。CCP和CRP是对REP的补充,CCP可以看做是组件级别的单一职责原则(SRP):由于相同原因修改,并且需要同时修改的东西放一起;不同原因修改,并且不同时修改的东西分开。CRP可以看做是组件级别的接口隔离原则(ISP):不要依赖不需要的东西。
可以看出通过这三个原则构建的组件,拥有以下几个特点:
-
组件内的类和模块紧密相关,需求变更时通常需要同时修改;
-
需求变更时,需要进行的修改只涉及很少的组件甚至在一个组件内;
-
复用这个组件时,通常组件内的功能均是用户需要的,而不是有一些不相关的功能;
这里贴下书中的张力图:
项目的初期,更多关注的是维护性而牺牲复用性,随着项目逐渐成熟,项目重心会逐渐倾向于复用性。
组件耦合
组件耦合方面的原则有以下几个:
-
ADP:无依赖环原则
-
SDP:稳定依赖原则
-
SAP:稳定抽象原则
组件间的依赖如果存在环,则维护性将大大降低,我们应该避免组件间的循环依赖。
组件间的依赖关系应该是指向更稳定的方向,每个组件的稳定性都低于其依赖的组件稳定性。组件越稳定,则其抽象程度需要更高;组件越不稳定,则其抽象程度需要更低,越具体。
有时候,可能会新引入一个引用,导致一个稳定的组件依赖了一个不稳定的组件,此时就可以使用依赖倒置原则(DIP)将依赖关系反转,保持稳定依赖。
分层与解耦
谈了组件相关的原则,现在来谈谈组件间的分层和隔离的方式。通过有效的分层手段,可以有效隔离不同功能的组件。
▐ 水平分层
得益于MVC模式的普及,水平分层在我们的系统中已经非常普及了,通常有以下几层:
-
表现层/UI层:负责系统的界面展示,通常包括Controller和VO等;
-
领域层/业务逻辑层:负责处理系统的业务逻辑,通常包括Service,Manager和DTO等;
-
数据层:负责与DB等底层数据存储介质通信,通常包括Mapper和DO等;
▐ 垂直分层
但是,仅仅做到水平分层是不够的。当业务逻辑比较复杂时,涉及的用例会非常多,这些用例之间的变更原因几乎肯定是不同的,所以还要进行垂直分层,将变更原因不一样的用例切分开。
例如,很多系统的修改操作和删除操作的变更原因和变更频率是不一样的,这时候可以考虑将修改和删除进行解耦。
不论是水平分层还是垂直分层,其核心目的都是将更新频率不同的代码给分开,放入不同的组件中。
▐ 解耦方式
分层后的解耦方式有多种:
-
源码层次:通过控制源代码模块间的依赖关系进行解耦,部署时仍然一起部署,适用于项目早期刚起步时;
-
部署层次:通过控制部署单元(例如jar包等)之间依赖进行解耦,当系统对部署和开发方面有更高的要求时,部分组件需要独立出去形成新的部署单元;
-
服务层次:通过将组件的依赖关系降低到数据结构级别,然后通过服务进行通信来解耦,当系统足够大的时候,就需要服务层次的解耦了;
但需要注意的是,不论通过哪种解耦方式进行代码的隔离,并不意味着这样就万事大吉,拥有良好可扩展和可维护性了。这也是Bob在《架构整洁之道》中提到的“横跨型变更”(虽然作者是在服务的这一章提出的,但我认为也适用于其他两种解耦层次)。
请看下面的出租车调度系统的服务架构图
图中可以看出,Taxi UI依赖 Taxi Finder查找符合条件的出租车,依赖Taxi Selector进行出租车调度。而Taxi Finder通过多个Taxi Supplier服务获取车辆信息,Taxi Selector依赖Taxi Dispatcher进行最终的派单。
各个组件都是服务化的。可以看到,各个组件都是具体的类,虽然各个组件隔离部署,但其实他们之间是强耦合的,并没有真正的解耦:加入现在出租车公司准备推出运送猫咪的服务,则所有的组件都需要进行更改,同时有些同学的更改方式就是在原有的类中增加if...else,这显然是不可取的。
正确的做法应该是在组件最初的设计中,就应该考虑抽象化和多态,如下图,使用策略模式或者模板方法进行解耦:
注意看我红框标出来的,除了服务间的隔离外,在组件内部其实也存在隔离,而这个的隔离更加重要。这也就是书中讲的:
服务边界并不能代表系统的架构边界,服务内部的组件边界才是。
架构
从代码和组件原则到组件分层和解耦,我们逐渐对系统底层的一些元素有了比较深入的了解,那么上层的架构到底是什么呢?
▐ 什么是架构
并没有非常明确的定义,这里引用书中的一些描述,大家应该有一些体会:
软件架构的实质就是规划如何将系统切分成组件,并安排好组件之间的排列关系,以及组件之间互相通信的方式。
软件架构的终极目标是,用最小的人力成本满足构建和维护该系统的需求
需要指出的是,架构和框架并不是相同的东西:
-
架构一定是业务相关的,包含了业务属性,并且这个业务属性是系统的核心价值;
-
框架一般都是业务无关的,是我们编码实现架构的的工具,属于实现细节。
最初设计系统架构时,并不需要过多考虑使用什么框架,而更多的是关注自身业务。
此外,很多人可能对架构有些误解:设计那么多有什么用,代码不还照样得写?
是的,代码还得写,架构并不能让你不写代码了(有时可能还会让你多写代码)。但是,好的架构会让写代码变得更容易了。
容易不一定是体现在需要变更的代码量多少上,好的架构可以让你更快速的找出需要变更的范围,并且很容易的就修改掉——这对于一个运行维护了多年的系统尤为重要。大家可以回想下这样的场景是不是很熟悉:明明是一个看起来很正常很合理的需求,看起来变更范围也不大,但真的去撸代码时发现需要改的地方好多,甚至无从下手去改。这个时候可能就要去看看之前的架构设计是不是不够合理,有哪些需要优化改进的。
六边形架构
六边形架构,又名端口适配器架构(我更喜欢这个名字,因为六边形老让人感觉有六个什么东西跟它对应),DDD极力推崇该架构。系统的领域模型是系统最为重要的部分,而其他的诸如DB,UI,缓存,消息队列等等均通过适配器与领域层进行通信,也就是依赖关系是由外到内的(依赖倒置)。之前在商家规模化运营项目中也尝试过使用DDD来进行架构设计。
▐ 整洁架构
Bob综合六边形架构和其他几个架构的特点提出了整洁架构,它具有以下几个特点:
-
独立于框架
-
可被测试
-
独立于UI
-
独立于数据库
-
独立于任何外部机构
架构最内部是业务实体,代表了系统关键业务逻辑。
再外一层是用例——特定应用场景下的业务逻辑,在Bob看来,用例是架构设计中非常重要的一环,架构也是基于这些用例进行设计的,如果缺失了用例,就无从谈起架构设计了。
紧接着是控制器,网关和展示器,是一层接口适配器。
最外层则是框架和驱动程序,这里面包含了数据库,web,工具等等,这一层一般只包含了一些通信的黏合性代码。
也并不一定只有四层,真实的情况可能会超过四层,但总的原则不变:外层依赖内存,最内部是最通用、最高层的策略,最外层是最具体的实现细节。
外层的是底层组件,内层的是高层组件,底层组件作为高层的插件存在,换句话说外层的这个组件是可以被其他的组件像插件一样替换掉的。
这里需要明确的一点是:有时候,软件运行的方向与依赖的方向是不同的(这个很多同学可能会没有注意到)。比如业务实体从数据库取数据,运行方向是业务实体->数据库,而通过依赖反转(DI,业务实体定义查询接口,数据库层实现),我们的依赖关系是业务实体<-数据库。这里讲到的保持底层组件对高层组件的依赖,高层组件的稳定性和抽象性,也就是前面讲的SDP和SAP。
总结
本文从代码和组件的原则讲起,讲到组件内和组件间的关系,以及如何进行组件的分层和隔离,接着引出了架构相关的讨论,列举了六边形架构和整洁架构,并谈了一些自己的理解。
后续会结合自身做过的项目,谈一谈具体的一些架构模式。
参考:
《架构整洁之道》 Robert C. Martin
《领域驱动设计》 Eric Evans
《企业应用架构模式》Martin Fowler
什么是整洁的架构 - xybaby - 博客园 https://www.cnblogs.com/xybaby/p/11729354.html
目录
正文
看完了clean code -- 代码整洁之道,那么接下来就应该读读其姊妹篇:clean architecture -- 架构整洁之道。不过对我而言,代码是实实在在的,看得见,摸得着;而架构虽然散发着光芒,但好像有点虚,似乎认知、思考还比较少。本文主要记录《clean architecture》的主要内容以及自己的一点思考。
本文地址:https://www.cnblogs.com/xybaby/p/11729354.html
架构的存在意义
clean architecture的作者是一位从事软件行业几十年的架构大师,参与开发了各种不同类型的软件,在职业生涯中发现了一个规律:那就是,尽管几十年来硬件、编程语言、编程范式发生了翻天覆地的变化,但架构规则并没有发生变化。
The architecture rules are the same!
我想读过clean code之后,应该都达成了以下共识
getting it work is easy
getting it right is hard
right make software easy to maintain、change
上升到架构层面来说,问题同样存在,而且更加明显,因为架构的影响面远大于代码。作者举了一个例子,展示了随着代码量增加、团队人员增加、release版本增加,导致的新增代码代价的激增以及程序员生产力的下降。
从可以看到,随着时间的推移,每一行代码的代价(成本)都在逐渐上升。
从另一个角度来看
单个程序员的产出随着 release急剧 下降,即使为了一个小小的feature,也不得不到处修修改改,容易牵一发而动全身
moving the mess from one place to the next
这样的经历,我想大家都有或多或少的同感,尤其在项目后期,或者团队人员几次轮换之后,代码就变得难以维护,以至于没有人敢轻易改动。出现这样的问题,不能仅仅归咎于code -- code这个层面关注的是更为细微具体的东西(比如命名、函数、注释),更多的应该是设计出了问题,或者说架构出了问题。
因此说,软件架构的目标是为了减少构造、维护特定系统的人力成本
The goal of software architecture is to minimize the human resources required to build and maintain the required system.
behavior vs architecture
行为和架构是软件系统的两个价值维度,行为是指软件开发出来要解决的问题,即功能性需求;而架构则算非功能性需求,比如可维护性、扩展性。很多程序员迫于各种压力,可能觉得只要实现功能就行了;殊不知,非功能性需求也是技术债务,出来混,迟早是要还的。
怎么看待二者的关系呢,这里祭出放之四海而皆准的艾森豪威尔矩阵:
behavior: 紧急,但不总是特别重要
architecture:重要,但从来不紧急
了解过时间管理或者目标管理的话,都知道重要但不紧急的事情反而是需要我们特别花时间去处理的。
而架构设计就是让我们在支撑功能的同时,保证系统的可维护性、可扩展性。
design level
软件开发和修房子一样,在实施角度来看都是从low-level到high-level的过程,比如房子是由砖块(brick)到房间(room),再由房间到房子(house)。作者的类比如下
software | building |
---|---|
programming paradigms | brick |
module rule(solid) | room |
component rule | house |
在我看来,clean code中强调的变量名、函数、排版更像是软件开发中最基础的单位,不同的programming paradigms遵循的思想是不同的,但代码质量(整洁代码)是独立于编程语言的。
module rule(solid)
module(模块)一般的定义即单个源文件,更广义来说,是一堆相关联的方法和数据结构的集合。
关于这部分,在clean architecture中讲得并不是很详细,于是我结合了《敏捷软件开发》(Agile Software Development: Principles, Patterns, and Practices)一书一起学习。
SOLID是一下几个术语的首字母缩写
- SRP(Single responsibility principle):单一职责原则,一个module只有一个原因修改
- OCP(Open/closed principle):开放-关闭原则,开放扩展,关闭修改
- LSP(Liskov substitution principle):里氏替换原则,子类型必须能够替换它们的基类型
- ISP(Interface segregation principle):接口隔离原则,你所依赖的必须是真正使用到的
- DIP(Dependency inversion principle):依赖导致原则,依赖接口而不是实现(高层不需要知道底层的实现)
SRP
module级别的SRP很容易和函数的单一职责相混淆。函数的单一职责是一个函数只做一件事 -- 这件事通过函数名就可以看出来。而SRP则是指一个module仅仅对一个利益相关者(actor)负责,只有这个利益相关者有理由修改这个module。
违背SRP,会导致不相关的逻辑的意外耦合,如下面这个例子
Employee这个类里面包含了太多的功能:
save
是给CTO调用CalculatePay
是给CFO使用- 而COO则关心
reportHours
。
问题在于,CalculatePay
也依赖ReportHours
,如果CFO因为某些原因修改了ReportHours
,那么就会影响到COO。
这个例子也表明,一个类是对什么东西的抽象并不是最重要的,而在于谁使用这个类,如何使用这个类。
解决方法之一是使用Facade模式,如下所示
Facade模式保证了对外暴露同样的三个接口,但其职责都委托给了三个独立的module,互不影响。
LSP
对于继承而言,子类的实例理论上是满足基类的所有约束的,比如Bird extend Animal,那么Animal的所有行为bird都应该满足。
但上面也描述过,类的有效性取决于类的使用方式,并不能用人类的认识去判断。比如正方形是否应该继承自长方形(square is a rectangle?),按照正常人的认知来说肯定是的,但对于某些使用方式就会存在问题, 比如下面这个函数
def g(Rectangle &r)
{
r.setW(5);
r.setH(2);
assert(r.area() == 10);
}
上述的代码表明,g
函数的编写者认为存在一种约束:修改rectangle的长不会影响宽。但 这个对于squre是不成立的,因此square违背了某种(隐式的)契约,这个契约是关于如何使用rectangle这个类的。
如何传达这个契约呢,有两种方式,第一是单元测试;第二是DBC(design by contract)。
详见讨论: 你会怎样设计长方形类和正方形类?
ISP
接口隔离原则解决的是“胖”接口问题,如下图所示:
OPS
所提供的三个接口是给三个不同的actor使用的,但与SRP要解决的问题不同,在这里并不存在因公用代码导致的耦合。真正的问题是 Use1对op1
的使用导致OPS的修改,导致User2 User3也要重新编译。
解决方法是引入中间层,如下所示
当然,静态语言之间的源码依赖才会导致 recompilation and redeployment; 而对于动态语言(如python)则不会有这个问题。
ISP is a language issue, rather than an architecture issue.
不过,不要依赖你不需要的东西,这个原则总是好的。
DIP
DIP(Dependency inversion principle)是架构设计中处理依赖关系的核心原则,其反转的是依赖关系。比如一个应用可能会使用到数据库,那么很自然的写法就是
Business rule依赖Database的问题在于,database的选择是一个细节问题,是易变的,今天是mysql,明天就可能会换成Nosql,这就导致Business rule也会收到影响。所以需要依赖反转,就是让database去依赖Business rule
Business rule依赖抽象接口,而database实现了这个抽象接口,接口一般是稳定的,因此即使替换DB的实现,也不会影响到Business rule。
这也提供了某种暗示:对于java C++等静态类型语言,import include应该只refer to 接口、抽象类,而不是concrete class。
OCP
OCP是下面两个短语的缩写
- open for exrension: 当应用的需求变更时,我们可以对模块进行扩展,使其满足新需求
- close for mofifacation: 对模块进行扩展时,无需改动模块的源代码或者二进制文件
很容易想到,有两种常见的设计模式能实现这样的效果,就是Strategy与Template Method。
要实现OCP,至少依赖于SRP与DIP,前者保证因为不同原因修改的逻辑不会耦合在一起,后者则保证是逻辑上的被使用者依赖使用者,从Strategy模式的实现也可以看出。
其实我觉得OCP应该是比其他几个module rule抽象层级更高的原则,甚至高于后面会提到的component rule,软件要可维护性、可扩展性强,那么就最好不要去修改(影响)已有的功能,而是添加(扩展)出新的功能。这是不证自明的。
component rule
什么是component呢,component是独立开发、独立部署的基本单元,比如一个.jar、.dll,或者python的一个wheel或者egg。
component rule主要解决两个问题,第一是哪些module可以形成一个component,即component cohesion,组件的内聚问题;另一个则是不同的component之间如何协作的问题,即component coupling
component cohesion
哪些module或者类应该放在一起作为独立部署的最小实体呢,取决于以下几个规则
REP:THE REUSE/RELEASE EQUIVALENCE PRINCIPLE
The granule of reuse is the granule of release.
复用/发布等同原则:即软件复用的最小粒度等同于其发布的最小粒度。
这是从版本管理的角度来思考软件复用的问题,通过版本追踪系统发布的组件包含了每个版本修改的bug、新增的feature,才能让软件的使用者能够放心的选择对应的版本,达到软件复用的效果。
CCP:THE COMMON CLOSURE PRINCIPLE
共同闭包原则:如果一些module因为同样的原因做修改,并且改变次数大致相同,那么就应该放在一个component里面。这个是其实就是将单一职责原则(SRP)应用到component这个level
This minimizes the workload related to releasing, revalidating, and redeploying the software
可见,CCP的目标是较少发布、验证、部署的次数,那么是倾向于让一个component更大一些。
CEP:THE COMMON REUSE PRINCIPLE
共同复用原则: 总是被一起复用的类才应该放在一个component里面。这个是接口隔离原则(ISP)在component level的应用
Thus when we depend on a component, we want to make sure we depend on every class in that component
与CCP的目标不同,CEP要求总是一起复用的类才放在一起,那么是倾向于让一个component更小一些。
component coupling
组件之间要相互协作才能产生作用,协作就会导致依赖。
比如组件A使用到组件B(组件A中的某个类使用到了组件B中的某个类),那么组件A就依赖于组件B。在这样的依赖关系里面,被依赖者(组件B)的变更会影响到依赖者(组件A),在Java,C++这样的静态类型语言里面,就体现为组件A需要重现编译、发布、部署。
架构设计的一个重要原则,就是减少由于组件之间的依赖导致的rebuild、redeploy,这样才能减低开发、维护成本,最大化程序员的生产力。
ADP: Acyclic Dependencies Principle
无环依赖原则:就是在组件依赖关系图中不应该存在环。
上图中右下角Interactors
,Authorizer
,Entities
三个组件之间就形成了环装依赖。环装依赖的问题是,环中的任何一个组件的修改都会影响到环中的任何组件,导致很难独立开发部署。另外,Database
组件本身是依赖Entities
的,现在Entities
在一个环中,那就相当于Database
依赖整个环。也就是说,对外而言一个环中的所有组件事实上形成了一个更大的组件。
如何解环呢?
一种方法是使用依赖倒置原则DIP,改变依赖顺序
另一种方法是抽象出新的通用component
SDP: Stable Dependencies Principle
稳定依赖原则
Any component that we expect to be volatile should not be depended on by a component that is difficult to change. Otherwise, the volatile component will lso be difficult to change
其实就是说,让易变(不稳定)的组件去依赖稳定的组件。这里的稳定性指变更的成本,如果一个组件被大量依赖,那么这个组件就没法频繁变更,事实上也就变得稳定(或者说僵化)了。
比如在逻辑上,应用层相对UI是可稳定的,UI发生修改的变大大得多,但如果应用层依赖UI,那么为了稳定,UI的修改也得非常小心谨慎。
解决的方案也是依赖反转原则
SAP: Stable Abstractions Principle
稳定抽象原则
A component should be as abstract as it is stable.
越稳定应该越抽象,稳定意味着会被依赖,如果不抽象,那么一旦修改,影响巨大。这个时候就可以考虑OCP,对于稳定的模块,要关闭修改,开放扩展,而抽象保证了便于扩展。
按照component cohesion规则形成的组件,再加上组件之间的耦合、依赖关系,就形成了一个架构,接下来就讨论什么是整洁的架构。
architecture
一个好的架构需要支持一些功能
- The use cases and operation of the system.
- The maintenance of the system.
- The development of the system.
- The deployment of the system.
但很多时候,很难搞清用户要怎么使用系统,要怎么运维、如何部署。而且,随着时间推移,这一切都在变化中,说不定今天是集中式部署,明天就要服务化,后天还要上云。如何应对这些可能的变化,同时又不过度设计,有两条可遵循的原则:
- well-isolated components
- dependency rule
上一章节已经提到,应该让不稳定的组件去依赖稳定的组件,那么什么组件稳定,什么组件不稳定呢。
稳定的应该是业务逻辑,policy、business rule、use case。不稳定的应该是业务逻辑的周边系统,detail、UI、db、framework
keep option open with boundary
理清楚组件之间的依赖关系,可以帮助我们推迟有关detail的决定
The longer you leave options open, the more experiments you can run, the more things you can try, and the more information you will have when you reach the point at which those decisions can no longer be deferred.
书中作者列举了自己开发Fitnesse的例子。
项目开始之初,作者就知道需要一个持久化的功能,可能就是一个DB。
遵循依赖倒置原则,DB应该依赖于business rule,所以作者在这二者之间引入了一个interface,如下所示
上图中红色的boundary line其实就是两个组件的分割,可以看到Database Interface和Business Rules在同一个组件中。通过依赖翻转,database事实上成为了business rule的一个插件(plug-in),既然是插件,那么就很方便替换。
在Fitnesse中,作者将这个DatabaseInterface命令为WikiPage
, 如之前所述,DB是一个detail,是不稳定组件,而且直接使用一个DB会引入许多工作量,对测试也不够友好。于是作者在开发期用了一个MockWikiPage
,直接返回预定义数据给business rule使用;过了一年之后,业务功能不满足mock的数据,使用了基于内存的InMemoryPage
;最终发现基于文件存储的FileSystemWikiPage
是比MySqlWikiPage
更好的选择。
clean architecture
回到架构这个话题上来,作者认为什么样的架构是整洁的呢,尽在下图:
这是一个分层架构,从外环到内环,软件的层级逐渐升高,也如之前所说
- high level policy
- low level detail
那么clean architecture的dependency rule就是:外环(low level)依赖内环(high level)
Source code dependencies must point only inward, toward higher-level policies.
entity vs rule
在上图中,出现了Entities和Use case这两个并没有怎么强调的概念,二者都属于Business rule的范畴
Entity:An Entity is an object within our computer system that embodies a small set of critical busin
比如说在一个银行借贷系统中,Loan就是一个entity,包含一系列属性如principle、rate以及相关操作applyInterest等等,这是业务逻辑的核心,也称之为Critical Business Rules
Use case:A use case is a description of the way that an automated system is used
比如说贷款前的风控系统,如何做风控,跟具体实现有较大关系,因此也称之为 application-specific business rules
不难看出,Use cases依赖于Entities, 相比而言,Entities更加稳定,所以处在环的最中间。
一个典型场景
重点在于上图的右下角, Controller、 Presenter都是第三层的实体,依赖第二层的Use case,上图展示了数据的流向,且没有违背依赖关系。
下面这个Java web系统更加详细、清楚
这个系统架构值得仔细揣摩、学习,在这里值得注意的是:
- controller、presenter 与use case的依赖、交互关系
- use case实现Input接口,声明output接口(Presenter实现)
- 交互使用的data structure,并没有在各个layer之间传递Data对象
references