Android

关注点分离的艺术

2020-03-04  本文已影响0人  jxcyly1985

关注点分离的艺术

[TOC]

原文

引子

这是一篇2008年文章,距今有十余年了,但是里面对于关注点分离的各种技术,以及各个技术点中的概念,原理,说明,不仅仅有整体的介绍,而且对涉及到的元素和结构都有详细的说明,并且这些技术依然在我们日常的工作中不断的使用,对工作之中系统的技术的整体理解起了很大的帮助。

附录部分对特定的概念加入了自己的理解,因为能力问题,整个翻译过程难免有一些纰漏和错误,希望大家能指正。

介绍

在软件工程里,关注点分离是指为了实现系统内部的秩序,对软件元素进行的划分联系

通过正确的关注点分离,复杂性变的可以管理。

这篇文章的目标是提升对关注点分离原则的认识和提供一系列基础概念,以此来辅助软件工程师开发可持续维护的系统。

关注点分离的原则

关注点分离的原则规定系统元素应该具有独有单一的目的。

这意味着,没有系统元素应该去分担另一个元素的职责,以及包含不相关的职责。

关注点分离是通过确立边界来完成的。

边界是指划分指定的一系列职责的任何逻辑或者物理的约束

以下是边界的一些例子[1]

尽管实现关注点分离的过程常常涉及到一系列职责的划分,但是目标不是为了减少系统不可分割的部分,而是朝着

职责内聚不重叠的元素去组织系统。用爱因斯坦的话说,“让任何事情尽可能简单,而不是为了更简单”。

关注点分离的本质是关乎秩序[2]。

关注点分离的整体目标是建立一个良好组织的系统,使其各个部分能履行有意义,易理解直观的角色,同时实现适应变化的最大能力。

关注点分离的价值

在软件设计中应用关注点分离原则可以带来很多的额外的收益。

关注点分离原则应用在商业组织同样具有收益。

在大型公司,确保分配团队和子组织独特的一系列内聚职责,减少团队必要的协作和最大限度的提升每一个团队聚焦他们集体的职责和核心竞争力,从而帮助促进整个商业目标的达成。

关注点分离原则同样可以改进整个企业范围内系统的问题解决能力。

当职责被合适的划分,问题定位变的更加容易,解决方案变的更快,个人责任感增强。这些的每一个方面有助于改进质量控制过程。

不管是人员组织还是软件系统,应用关注点分离原则可以通过消除不必要的重复和合理的职责分配在管理复杂性上获取帮助。

在下一节中,我们将讨论各种应用设计内完成关注点分离的技术。

水平分离

水平关注点分离是指将应用按照在应用内执行同样角色划分功能逻辑层的过程。

一个图形用户交互界面的常见分离过程是划分为表现层业务层资源访问层

这种分类涵盖了大部分应用需要的主要关注点类型,也体现对应用内最小依赖级别关注点的组织方式。

图一描述了这种典型的三层应用:

horizontalLayers.png

表现层涵盖了关于应用用户界面相关的流程[3]和组件

它包括了定义应用视觉显示的组件,也包括像控制器,主持人,展现模型的高级的设计思想。

表现层最主要的目标是封装所有用户界面的相关的内容从而实现应用程序域[4]可以独立的变化。

表现层应该包含应用程序内所有仅仅和和视觉显示相关的的组件和流程,并剔除掉其他无关的组件和流程。

这样才能实现应用程序的其他层可以独立与显示关注点的改变。

业务层涵盖所有和应用程序域相关的流程和组件。

它包括了定义对象模型,业务逻辑管理,控制系统工作流的组件。

业务层可以通过特定的表示工作流,业务流程[5],和应用程序内使用的实体,或者传统面向对象的封装了数据和行为的领域模型的描述。

业务层的最主要目标是封装应用程序的核心业务相关的内容,但不包括数据和行为如何暴露,数据如何获取。

业务层应该包含所有仅仅和应用程序业务领域相关的组件和流程,并剔除其他无关的组件和流程。

资源访问层封装访问外部信息相关的流程和组件。

它包括和本地存储数据和远程服务交互的组件。

资源访问层的目标是提供围绕数据访问细节的抽象层。

它包括建立建立数据库连接和服务连接,维护数据库模式以及存储过程的相关知识,有关服务协议的知识,和服务实体和业务实体的编组(序列化)的任务。

资源访问层应该包含所有仅仅和系统外部数据访问相关的组件和流程,并剔除掉其他无关的组件和流程。

另一个对面向服务的应用程序是把应用程序分离成服务器接口层业务层资源访问

horizontalLayers2.png

在这种分离方式下,业务层和资源访问层和之前讨论的目的是一样的,而服务暴露关注点被封装为服务接口层。

服务接口层涵盖服务接口的内容,例如通过各种协议对业务流程的暴露,以及服务约定和数据类型的管理。

服务接口层的最主要目标是封装所有的服务接口内容,以允许应用程序域独立的变化。

服务接口层应该包含所有仅仅和应用暴露为服务的组件和流程,并剔除其他无关的组件和流程。

通过对基于应用内角色的进行关注点分组,可以获取很多改进整体系统可管理性的益处。

这些益处包括通过一致的体系架构和流程间隔带来的易于维护,增强对变更修改影响的隔离,增强变更的适应性,和增加潜在可重用性。

垂直分离

垂直分离是把应用程序根据应用内同样特性[6]或者子系统划分成功能模块的过程。

垂直分离是对一个应用中的特性进行整体的划分,并关联所有使用到的接口业务资源访问内容在一个单独的边界。

verticalLayers.png

对应用的特性分离成模块阐明了每个特性的职责和相互的依赖,这有助于测试和整体的维护。

边界可以通过逻辑的定义辅助组织,或者通过物理的定义从而能够独立开发以维护。

逻辑边界就意味着模块化的存在,虽然通过函数方式表示隔离也许对应用程序实际的发布和运行时行为没有影响。

但是逻辑边界对于改进应用程序的维护性和减轻未来物理上的特性分离的成本。图4描述了一个包含逻辑边界的而应用程序。

verticalLayers2.png

物理边界通常用在插件程序或者组合程序的开发场景,从而能够通过完全不同的开发小组管理特性。

支持插件化的应用程序通常采用类似自动发现,或者基于外部配置源的初始化功能模块的技术。

图5描述了一个由不同的开发小组开发的包含不同模块的容器框架。

verticalLayers3.png

虽然垂直分离是在一个应用程序内基于一个特定功能的整体实现的相关性对一系列的关注点进行分组,但是那不意味着排除使用其他的关注点分离策略。

例如,每一个模块内部会使用层来划分组件的角色进行设计。图6描述了应用程序同时使用了水平和垂直的关注点分离策略。

horizontalAndVerticalLayers.png

切面分离

关注点切换分离,被人更熟知是面向切面编程,是指从应用程序的核心关注点中分离出交叉的关注点的过程。

交叉关注点,或说切面,是指在一个应用程序内穿插(或者说散布)在多个边界内的关注点。

日志是一个遍及在许多系统组件中运行的活动的示例。

图7描述了一个具有多个交叉关注点的应用

aspect1.png

交叉关注点通常因为广泛贯穿整个应用的放置从而变的难以维护。

交叉关注点和核心关注点的混合也同样增加了复杂性,致使应用变成更难以维护。

通过分离这些关注点,核心关注点和交叉关注点都变成更容易管理。

切面分离和其他的分离技术不同在于它依赖于预处理编译时运行时把交叉关注点和应用程序结合在一起。

这个把两种关注点重新结合在一起的过程,我们称为编织

通过使用各种策略,交叉关注点能够被分离和运行时衔接方式织入应用程序中。

切面分离工具在广泛的编程语言中都能获取到。关于方便切面分离的可用工具这个话题的更多信息可以查看这个

面向切面编程

依赖方向

良好的关注点分离的一个特征是确立理想的依赖方向。

一个理想的依赖方向是确立消费者的角色(客户)和依赖关系,这样被实体占用的依赖的角色[7]就具有最高的重用潜力。

一个简单的例子阐明依赖方向的概念,即业务组件和通用组件的一般关系。

我们考虑一个提供订单查询处理的系统,为了处理更高效需要缓存高频请求的数据。

为了订单查询的缓存更容易,可以开发一个缓存通用组件用来从订单查询处理的其余部分中分离缓存关注点。

图8用两个组件描述了这个系统

direction1.png

因为缓存的功能是一个比订单查询处理更通用的行为,在两者间缓存组件就具有更高重用的潜力。

因此,在这个用例中最好的依赖方向会是从业务组件到通用组件。依赖关系如图9描述

direction2.png

这种关系可以使得缓存组件保持对业务查询处理的不可知(即不知道业务查询处理),从而使它被未来的处理中可以重用。

数据关注点

对数据应用关注点分离原则涉及到对系统管理的信息进行恰当地建模。

虽然数据建模的各个方面适用于面向对象和数据库设计,但是本讨论重点在面向对象数据关注点,因为数据的主要需求通常根据功能设计表单。

在对象模型中组织数据时,公开的信息应该是被表示的实体所固有的。

例如,考虑一个给消费者销售产品的系统,定义产品的对象不应该包含消费者相关的信息。

这是因为产品本身并不关心谁可能购买产品。一个更好的方案是创建一个由消费者和产品信息构成的订单概念的对象。

这样产品对象将来可以在与消费者信息无关的其他流程中重用。

除了考虑在各种不同的上下文中数据模型的重用潜力之外,在维护高度复杂的系统时,直观的数据组织也是有用的。

例如,如果一个新开发者被委派访问由已经存在内存中的产品对象构成的特殊部件的序列号,那这名开发者可能首先尝试定位到特殊产品部件,然后检查这个部件的“序列号”或者类似名字的属性。

同时他很可能不会考虑在这个产品对象上查找类似“产品编号->序列号”的字典,因为这个不是数据的一个自然表达。这就好比因为人们戴了一块拥有序列号的腕表就认为一个人也具有了序列号。

有时候当数据的自然组织不能提供信息的有效查询机制时候,情况会不一样。

例如,一个需要频繁的检查以获取铜元素的总数量的异常复杂的产品对象,可能不能通过检查和它组成的元素的交叉引用处理。

在这种情况下当自然建模不够充分时,可以补充概念类型[8]来满足特定的需求,从而保持对象模型的完整性。

例如,当产品的成分一旦在组装后就保持静态,那么表示产品贵金属信息("ProductPreciousMetalManifest"[产品贵重金属清单])的概念模型可以在产品信息的同等级别上组合。

如果产品的成分会频繁的改变并且成分的处理是集中的,那么贵重金属信息可以作为这个处理过程的一部分被更新。

否则,可以构思一个特定的组件("PreciousMetalDetector"[贵重金属探测器])动态的返回产品贵重金属信息。

和第一个例子一样,从其他自然模型中分离出概念需求的好处是对于其他流程可以重用模型,而不会引起非固有特征的开销,并且模型也能易于维护。

行为关注点

分离行为涉及到划分系统的流程成逻辑的,可管理,可重用的代码单元。

行为分离代表着最基本的关注点分离类型。在面向对象系统内部,细粒度的行为使用方法分离,而粗粒度行为使用对象,组件,应用程序,服务进行分离。

和数据分离一样,封装的行为应该是包含它的边界内所固有行为。

举例,有一个命名为CreateCustomer的方法。它不应该被预期,例如,在新消费者内放入一个订单。同样的,一个命名ProductAssembler 的组件应该预期包含与产品装配的数据和行为,而不应该预期包含消费者的数据和行为。

实现良好的行为分离常常是一个迭代的过程。

系统的主要行为通常是在设计阶段构思的,然而随着细颗粒度的关注点变的更加清晰,系统设计的具体实现常常要求多次重构的迭代。

当我们组织行为时,应该追求以下的目标:

扩展关注点

扩展是一种能够向已存在的关注点集添加新行为的关注点分离策略。

扩展用来增强那些已有的关注点集,这些关注点集期望的行为不是系统固有的行为而无法在目标系统添加,或者是作为系统的核心特征集的一部分包含在其中是不切实际的。

图10,描述了目标系统 的一个扩展的依赖关系

extensions1.png

扩展通过响应按键事件的通知和目标系统交互。

扩展提供的行为示例包括显示新的视图和控件,改变正常的流程,或者改变目标系统内的数据。

扩展通常采用托管加载项的方式,并且通常采用配置和动态发现过程进行注册。

当企业内部有多个客户端程序需要维护时,你也许会发现其中一个客户端扩展提供的行为对于加入另一个客户端也非常有价值。

如果这个扩展是对某个目标应用高度定制,并且提供相对简单的行为,那么最好的做法是在新应用程序重新生成这个功能。

然而,如果这个扩宽的行为在大小和复杂度上是非常量大的,通常通过企业服务提供行为是更合适的。

如果通过企业服务提供的行为依然不合适作为主机系统的核心依赖,那么可以开发一个新的扩展代表应用程序和企业服务交互(做适配层,只扩展需要的核心功能)。

委派关注点

委派关注点指的是将履行行为的职责分配给下级组件的过程。

这个策略是从执行中分离出职责关注点,并且对于实现细节可能依赖于外部条件而改变的组件设计是有利的。

使用这个策略,组件将代理另一个设计在特定情况下满足请求的组件的一些或者全部的数据请求和方法调用。

例如,一个设计用来返回分配给当前用户的角色列表的组件,也许会委派这个请求给一个或多个下级组件,那些下级组件设计用来从本地的XML文件,数据库,或者远程服务中获取角色。

图11阐述了委派授权关注点给指定不同数据源的组件。

delegation1.png

可以使用策略模式、插件模式、Microsoft提供程序模型和其他使组件的部分行为能够在外部实现的变体模式来简化委派。

反转关注点

反转关注点,更为人熟知是反转控制,是指把一个关注点移动到确立的边界外的过程。

反转关注点的一些用途包括切面分离最小化非必要流程从特定抽象策略分离组件,或者重分配职责到基础组件。在一些特定的应用里用途也包括减轻与硬件相关的交互,工作流程管理,注册过程,和获取依赖相关的责任。

图12描述了关注点反转过程,其中表现层和领域层的都有一些关注点转移到基础设施级别的组件里。

IoC1.png

关注点反转常见的一个例子是在模板模式的使用上。

这个模式用来泛化处理的行为以便允许通过继承可以改变。

当在已有的组件上应用这个模式,期望的处理流程的执行步骤将会被泛化,并被封装在一个基础类中。已有组件然后将继承这个新的基类,仅仅维护特定的行为。然后其他的类型将自由的从这个基类继承,并提供各种不同的实现。这会是算法序列专注点反转的一个例子。

另一个可以观察到的反转关注点的例子是在从控制台交互应用转换为图形用户界面应用。

一个典型的控制台交互应用提供主循环,它提交用户输入信息,并等待数据输入。

然而转换为图形用户界面应用时,程序的主要控制通常是由基础设施的组件提供,用户交互的通知使用观察者模式来完成。图形用户界面方式通过依赖对用户交互教程主要控制流程关注的应用基础设施和宿主环境,反转了用户交互的主要控制流程。

依赖注入是一个用于反转与获得外部依赖相关关注点的术语。

这个术语是为了更清晰的描述一类关注这种反转形式(获取外部依赖的反转)的框架有关行为。

依赖注入的目标是将一个依赖项如何获取的关注点和边界的核心关注点[9]分离出来。

这样通过能够为组件根据不同的上下文提供变化的依赖项从而提高重用性。

依赖注入的过程通常涉及到容器依赖项接收者这些角色。

容器的角色由负责将依赖项分配给接收者组件的组件占有。容器通常用工厂模式实现,以允许创建接收者和依赖项组件。

依赖项的角色由提供给接收者的资源占有。容器通常实现成允许已有对象作为依赖项使用注册进来,或者在需要时创建一个新实例。

接收者由从容器接收一个依赖项的组件占用。接收者要求使用容器支持的策略声明依赖项。确定依赖项信息的策略通常使用反射检查一个或多个接收者的成员类型,并且可能需要使用属性/注解来隐式的或显式的指定依赖项。

我们使用图13的时序图来表示这些角色。

IoC2.png

一个使用依赖注入的例子是不使用工厂模式或者工厂方法模式从业务组件中抽象缓存功能。

通过使用依赖注入给接收者提供缓存组件,接收者不需要和一个特定的工厂耦合从而保持了对缓存组件的松耦合。

另一个例子是使用依赖注入而不使用单例模式控制缓存组件的数量。这使应用确保在整个应用的生命周期仅仅使用一个实例,或者控制哪个接收者接收缓存组件的哪个实例。

通过可以提供模拟的依赖项而不是需要使用单例和消费者(调用组件)一起测试的方式,提升了隔离依赖项测试组件的能力。[10]

使用工厂工厂方法抽象工厂单例插件服务定位器模式抽象对象创建或者获取依赖项关注点的组件是使用依赖注入进行补充和替换的理想选择。通过使用依赖注入对这些和其他模式替换和协作,可以完全从特定的抽象策略中抽象出使用组件。

夸大练习

通常设计选择的负作用,特别是关于伸缩性和重用性方面,会在系统已经建立很久之后才表现出来。

关于伸缩性和重用的问题通常源于缺少对关注点分离原则的遵守。

一个可以帮助优化关注点的过程是考虑设计应用在夸张环境下的影响。通过这个练习,系统的使用被假设夸大超越系统已知的预期,从而暴露设计方案的潜在弱点。

例如,当设计一个被两个已有系统使用的对象模型,一种假设夸大可能是考虑这个对象模型如果在五十个系统中公用会带来什么后果。

通过夸大的设计的使用,不良的设计职责通常会更容易识别出来。

为了展示这个练习,考虑接下来的例子,关于创建一个新的组合的客户关系管理系统。

在一个企业内部,IT部门要求创建一个允许不同的开发团队贡献特定功能模块的,定制的CRM应用。

公司内部相关部门的主要客户是销售,计费,和技术支持,尽管可能有多个开发团队指派支持每一个细分的功能。

应用被要求展现一个允许通过各种不同的条件,例如名称,地址,手机号,订单号,和任何注册的产品序列号(5个搜索字段),查查询客户的主页。然而,查询结果视图和工作流需要依赖于当前正在执行的哪个业务功能。

例如,在提交客户搜索条件后,销售员应该展现关于客户过去的采购趋势,信誉评分,和建议扩大销售的剧本的视图;而一个记账员应该呈现显示客户支付历史,争议账单历史,未支付余额。

初始的分析透露出将有3个工作流的变更产生于总共5个不同的搜索条件字段,同时将有3个后台系统参与获取所有部门需要的信息。因为变更很小,做出了在搜索模块内部集中主视图,搜索功能,工作流启动的选择。

根据了解,新增搜索条件和工作流需求需要搜索模块开发团队进行修改,然而人们相信这样的需求是很少出现的。

对这个设计选择应用夸大练习也许需要考虑到如果业务部门的数量或者后台系统的数量增加到5个时,会接着发生什么影响。

由于集中了搜索功能和工作流启动,增加了这些关注点的范围将同样增加搜索模块开发团队的职责和工作量。

这些包括对所有新特征和这些关注点需求变更的的问题范围的研究,分析,设计,编码和测试。

反过来,这可能导致增加搜索模块团队需要的开发资源数量。同样还可能导致这些关注点合并的上述最初假设

给搜索模块的其他的设计决策提供了信息。这可能将导致设计选择不能容易的适应职责的增加,需要一定程度的内部重新设计处理这些新关注点。

通过这个练习可以观察到一个突出的问题,搜索团队需要的工作量是和业务部门和系统支持的工作流成比例增加的。事实是初始方案不能随着需求的增加保持伸缩性(可扩展性)指明了关注点在整个系统中分布是不合理的。

在搜索模块内部合并搜索功能和由此产生的工作流决策是识别到每一个用例之间相似性的结果。

我们没有同等考虑的是事实上每一个用例必须用一个特殊的方式进行处理,并且事实上可能性的数量没有准确的边界。

因为搜索屏幕为许多模块提供了中心化的功能,所以搜索可以说具备与生俱来的基础设施关注点。

所以,应该预期搜索模块向所有模块提供通用的行为。然而,每一个用例的细节是不通用的,可以考虑是每个模块固有的关注点。

在识别出固有的职责之后,一种可选择的方案是开发一个框架巩固通用的关注点,但是能够分配每一个领域具体的关注点。

这种方案可以通过要求每个模块提供一个登记可用的搜索条件和调用相应工作流提供处理能力的插件来完成。

然后,框架将负责展现搜索条件的统一视图,并且提供一个通用的基础设施关联每个搜索条件和它对应的工作流处理器。

使用这种方案,搜索模块可以设计成容纳无限的用例。

虽然夸大练习对于创建高伸缩性的设计是有用的,但这只是实现关注点最佳分离设计这个主要目标的副产品。

夸大练习可以和测量病人体温检查是否存在潜在问题的症状的医学实践对照。

这个练习通过检查潜在设计的可伸缩性方面识别关于关注点分离的问题。这和图14使用放大镜查看设计的细节对照。

magnify.jpg

通过夸大设计实际范围,小问题才会被放大,从而容易识别。一旦识别,然后就可以确定使用采取什么行动方式。

分离苦恼

应用关注点分离原则通常涉及到高级概念和构想,而不仅仅只是设法解决应用的领域关注点,这给应用带来一定程度的复杂度。

对缺乏这些编程技术的经验的开发者来说,他们一方面的反应是对增加设计技能机会表现出激情四射的兴奋,另一个方面不利的反应是使用最方便的方式完成工作中涉及到的额外复杂度。这些技术常常导致缺乏经验和战术思维的开发者基于他们在第一次学习上遇到的挫折,把关注点分离涉及描述成”过于复杂“或”过度设计“,然后,在日常的工作方式中对这种架构从头到尾的进行操控(修改)。

此外,常常自始至终存在来自与项目经理,产品经理,高层管理者,市场,或者终端用户对于即时满足的压力,会倾向于鼓励和奖励最方便的方式而不是深思熟虑的设计。这些情况对于不仅仅解决技术问题,而是开发好的设计带来了阻碍。

提升关注点分离的设计通常会对应用增加复杂度,同样也应该指出的是它也消除了通常与缺乏关注点分离相关的复杂度。

对于很多应用,常常是在有序的复杂性和无序的复杂性之间做出权限。

那些没有展现出合适的关注点分离的应用由于在理解部分之前需要理解整体,而十分难以学习,并且十分难以维护和扩展。

高复杂性,但模块化程度不高的应用也对开发人员的高流动性产生影响(这会使设计和实现问题进一步恶化),或者吸引那些厌恶变化和通过成为组织内不可或缺的“迷宫主人”寻求职业发展的人。

开发团队当然不应该为了复杂度而寻求复杂度(除非进入了一个混淆竞赛),但是也应该消除把避免高级设计概念等同于避免复杂度的看法。

结论

简而言之,关注点分离的目标是秩序。通过确保系统内的元素遵守单一且唯一的目的,就可以设计出最具生产力和可维护性的复杂系统。

附录

[1]应用内的核心行为,源组织,处理组织,发布组织,都是具有划分指定职责的约束,也就是具有某种限制,某种

[2]秩序是有条理地、有组织地安排各构成部分以求达到正常的运转或良好的外观的状态

[3]流程:表现层的流程是指界面展现的顺序,或者多个界面如何完成一个业务的过程

[4]应用程序域是一种机制,本质上也是一种边界,在水平分离,我理解就是一个个独立的不相关的业务单元,这些业务单元之间是相关隔离的,业务单元之间也互不影响

[5]https://sourcemaking.com/uml/modeling-business-systems/business-processes-and-business-systems

[6]产品或解决方案所具有的特征,往往反映在一个具体的功能上

[7]依赖的角色,也就是被依赖者

[8]概念模型与自然模型对应,描述是概念化结构,而不是真实的自然世界事物的抽象

[9]边界的确认意味着关注点的分离,那也就是说边界的核心关注点,就是依赖项如何获取的关注点之外的核心关注点

[10]虽然通过单例和调用组件一起测试,可以通过修改单例的代码针对不同的测试用例提供不同的对象实例输出,这也是模拟依赖项的方式,但这不符合开闭原则,因为涉及到了原有工厂代码的修改,同时更有可能因为对象实例创建方式的改变,可能需要调用组件对工厂调用的代码做一些修改,这也导致了调用组件也不符合开闭原则。

依赖注入的容器虽然也是通过工厂模式或者工厂方法模式实现,但是通常依赖注入框架都实现了对象创建的各种细节的屏蔽,因此它很容易模拟不同的对象实例注入,同时不需要做业务代码上的修改,这些都由依赖注入的框架实现了。

上一篇下一篇

猜你喜欢

热点阅读