关注分离的艺术

jopen 9年前

关注分离的艺术

前言

在软件工程中,关注的分离是指在系统中为达到目的对软件元素的划分与对比。通过适当的关注分离,将复杂的东西变成可管理的。这篇文章的目的是促进对关注分离的原则的理解,以及提供一个基础概念的设置来帮助软件工程师开发出可维护的系统。

关注点分离原则

关注点分离原则所描述的是系统的元素应该表现出互不相干的目的。也就是说,没有会分担另外一个元素职责的,或者其 它不相干职责的元素。关注点的分离是通过明确边界来达成的。边界是任何逻辑或者物理的限制,它能给一组特定的职责划定界限。一些边界的例子包括在一个应用 程序中定义了系统核心行为的方法,对象,组件以及服务;针对源代码组织结构的项目,方案,以及目录的层级结构;用于处理组织的应用程序层级或分片结构;以 及用于产品发布组织结构的版本库和安装文件。尽管达成关注点分离的处理经常涉及一组职责的分离,但其目标并不是将系统限制成一个不可分割的部分,而是去将 系统组织成互不重复的高凝聚的责任单位。正如阿尔伯特爱因斯坦所言 “让一切尽可能简约而不简单。”本质上讲,关注点分离关乎于秩序。关注点分离的全局目标是建立一个组织良好的系统,其每个部分都满足一个有意义和直白的作 用,从而最大化其适应改变的能力。

关注点分离的价值

将关注点分离原则应用于软件设计有许多的好处。首先,单个组件少了重复并且目的单一,会使得整个系统更易于维护。其次,作为提高了可维护性的副产品,系统 整体变得更加稳定。第三,这一需要确保每个组件只把关注点放在一组单一明确职责的策略,常常可以得到天然的可扩展性。第四,由组件专注于单一目标所形成的 解耦,使得在其它系统中的复用,或者在同一个系统中的不同上下文的容 留,变得更加的容易。关注点分离原则在应用于商业组织机构时也可以带来好处。在大公司中,确保组织及其子机构分配到独立明确的职责,将有助于通过最小化团 队之间的协调工作,并最大化每个团队专注于其各自职责的潜能,来促进整体目标的达成。关注点分离原则同样能促进企业级规模的系统性问题的解决。当责任被正 确的分配,问题的定位就会变得更加容易,解决起来更加快速, 个人的职责能力也得到了增加。这些领域中的每一个都可以回过头来使得质量控制过程得到促进。无论所面对的是有人构成的组织,还是软件系统,关注点分离原则 都能通过消除不必要的重复和分配适当的责任来帮助对于复杂性的管理。在下一节,将讨论在应用程序的设计中实现关注点分离,可以使用的各种技术。

水平分离

关注点的水平分离指的是将一个应用程序分离成满足应用程序中相同角色逻辑功能层级的过程。一个通行的针对图形用户界面应 用程序的分离就是对展现层、业务层和资源访问层进行分离的过程。这些类别涵盖了大部分应用程序所需要的主要关注点,并呈现出一种最小化了应用程序中依赖层 次的组织结构。图 1 就描述了一个典型的三层级应用程序:

关注分离的艺术

    图 1

表示层包含与应用程序用户界面相关的处理和组件。这包括组件定义一个应用程序的可视化显示,并可能包括先进的设计理念,如控制器,演示层,或表示模 型。表示层的主要目标是封装所有用户界面的关注,以便允许应用程序域独立变化。表示层应该包括所有组件和过程,只涉及到应用程序的可视化显示需求,并应排 除其他所有组件和流程。这允许在应用程序中的其他层不同于他的显示关系。业务层包含与应用程序域相关的流程和组件。这包括定义对象模型、管理业务逻辑、控 制系统工作流程的组件。业务层可以通过专门的组件为代表的工作流程,为业务流程,并在应用程序使用的实体,或通过传统的面向对象的域模型封装了数据和行 为。业务层的主要目标是封装核心业务关注的应用程序的数据和行为如何被曝光,或如何具体地获得数据。业务层应该包括所有的组件和流程,只涉及到应用程序的 业务域,并且应该排除所有其他组件和流程。资源访问层包含相关的外部信息的访问过程和组件。这包括与本地数据存储或远程服务接口的组件。资源访问层的目标 是提供一个围绕数据访问特定的细节的抽象层。这包括建立数据库和服务连接的任务,维护数据库模式或存储过程的知识,服务协议的知识,以及服务实体和业务实 体之间的数据的传送。资源访问层应该包括所有的组件和过程,只涉及到系统访问数据的外部,并且应该排除所有其他组件和过程。面向服务的应用程序使用的另一 个公共部门是将应用程序划分为服务接口层、业务层和资源访问层,如图 2 所示:

关注分离的艺术

Figure 2

这部分,业务和资源访问层服务于先前讨论的相同的目标,暴露的服务封装成一个服务接口层。这层包括服务接口,如业务流程的曝光,通过各种协议服务的 约定和数据类型的管理。服务接口层的主要目标是封装所有服务接口,以便允许应用程序域独立地改变。服务接口层应该包括所有的组件和过程,只涉及到应用程序 的曝光服务,并且应该排除所有其他组件和过程。分组处理涉及基于他们的应用程序中的作用,提高整个系统的可管理性。这些好处包括易于维护,通过一致的体系 结构和隔离的过程,增加了绝缘的变化的影响,增加适应性变化,并增加了潜在的重用。

垂直分离

垂直分离指的是将应用程序划分为与子系统功能相似的功能模块。垂直分离将整体应用的特点,在一个单一的边界内,将任何接口、业务和资源访问问题关联。图 3 描述了一个应用程序分成三个模块:

关注分离的艺术

Figure 3

将一个应用程序的功能分离出来,阐明了每个功能的责任和依赖关系,可以帮助测试和整体维护。从逻辑上讲,边界可以定义为帮助组织,或物理上,独立的 开发和维护。逻辑边界意味着模块化的存在,虽然用于表示分离的方法可能与应用程序的实际部署或运行时行为无关。这对于提高应用程序的可维护性是有益的,因 为在物理上分离功能的未来的努力是有益的。图4描述了一个包含逻辑边界的应用程序:

关注分离的艺术

Figure 4

物理边界通常被用在开发插件或者组合式应用程序这些场景中, 可以使得其功能特性由不同的开发团队来管理. 支持模块插件技术的应用程序经常采用诸如自动发现,或者基于一个外部配置源来对模块进行初始化的技术。图 5 所展示的托管框架就包含了由不同的开发团队开发的模块:

关注分离的艺术

    图 5

而垂直的分离,会基于总体上能实现一个应用程序中的特定功能,对彼此相关的一个关注点集合进行组合, 而这并不排除对其它关注点分离策略的使用。例如,每个模块其自身可能会被设计成使用层级来圈定模块内部组件各自的角色。图 6 展示了一个同时使用垂直和水平关注点分离策略的应用程序:

关注分离的艺术

图 6

切片式分离

关注点的切片式分离,更有名的对应术语是面向切面编程, 指的是将跨域关注点从应用程序的核心关注点中分离出来的过程。跨域关注点,或者说切片,就是那些会穿插在应用程序中多个边界中的关注点。日志就是一个会跨 越多个系统组件执行的动作示例。图 7 展示了一个包含一些跨域关注点的应用程序:

关注分离的艺术

图 7

由于其广泛分布于整个应用程序的多个位置,所以跨域关注点很难被维护。将它们同核心的关注点混在一起,就会变得更加复杂起来,这导致应用程序更难于 被维护。通过分理出这些关注点,使得核心关注点和这些跨域的关注点都更易于管理。切片分离不同于其他的分离技术,它依赖于一种编译时或者运行时的预处理, 以同应用程序跨域关注点分离策略相融合。融合两个关注点的过程过去被比喻为“编排”。 通过使用不同的策略,跨域关注点可以被分离,并为了保持运行时的聚合,会被编排回到应用程序之中。切片分离工具在大范围的编程语言中都是可用的。更多有关 这个主题的信息,包括设置切片分离的工具集合,可以看看这里的《面向切面编程》。

依赖方向

关注分离的一个好的特性是依赖方向的完美建立。一个完美的依赖方向确立了消费者和依赖的角色,因此依赖的角色是被最有可能被可复用的实体占用。用一个简单的例子来说明依赖方向的概念是业务组件和功能组件之间的一般关系。假设提供一个命令审查流程的系统要求高效的缓存频繁请求的数据。为了提高命令审查的缓存,一个缓存功能组件也许是从命令审查流程的剩余部分中分离出来的。图8表示这个项目被分成两个组件:

关注分离的艺术

8

因为缓存功能是不查询处理更加通用的功能,所以两者之中缓存组件具有更高的复用潜力。因此,该场景中最佳的依赖指向是从业务组件指向工具组件。这一依赖显示在了图 9 中:

关注分离的艺术

图 9

这样的一种关系可以让缓存组件不用去理会查询处理如果进行,使其可以为以后的处理过程复用。

数据关注

采用关注分离策略来使数据转变为合适的模块化的信息,从而让系统将信息管理起来。当数据模型采用面向对象和数据库设计时,由于数据库的基本需求往往需要遵循特定的功能,因此这里所说的主要是面向对象的数据关注。

当 在对象模型里组织数据的时候,对外的信息应该是表示实体所固有的信息。例如,给一个销售产品给客户的系统,定义一个产品的对象应该不包含客户的相关信息。 这是由于产品不应该包含购买这个产品的用户的信息。一个较好的解决方法应该是生成一个组合客户和产品信息的订单对象。这可以让产品对象在未来被其他不关心 客户信息的程序复用。

除了考虑到数据模型在不同的环境中复用外,当维护较复杂的系统时数据组织的直观性也是很有帮助的。例如,如果 一个新的开发者负责对已存在产品对象的指定的部分组成的序列号的访问, 开发者可能会先试着找出产品指定的部分,然后在这些部分寻找“序列号”或者相似名字的属性。开发者一般不会想到查找诸如“产品号转换为序列号”专业术语之 类的东西去定位产品对象,因为这不是数据自然的表现。这类似于通过人们已有序列号的腕表来生成一个人的序列号。

然而,有时当数据的自然组织方式不能为信息提供一个有效的查询机制。例如,一个极其复杂的产品对象,这个对象需要频繁的检测获取所有铜元素的总数量,这通过检查或者对象的组成元素之间的交叉引用就不是那么有效了。

在 这个案例中,当自然模型不能满足自身需求时,一个对象模型的完整性可以通过补充概念类型去满足特殊的需求去维护。例如,如果产品的组成保留了一个静态变 量,一个概念性的模块表示由同级的产品信息组成的产品的贵金属信息(例如“ProductPreciousMetalManifest“) 。如果产品的组合频繁的改变,并且组合的流程是集中的,那么贵重金属信息或许可以作为这个流程的一部分而被更新。此外,一个专用的模块 (如"PreciousMetalDetector")能便利去动态返回产品的贵重金属信息。

就像第一个例子那样,分离概念从其他自然模型得到的好处是模块被另一个程序复用时不会引起非固有属性的额外开销,和保持易维护性。

行为关注

行为分离涉及到系统流程在逻辑上,管理上,代码单元的可复用上的分离。这表示关注分离的最基本的类别。

在面向对 象系统内,细腻的行为使用方法分离,而宽泛的行为通过对象,组件,应用,和服务分离。就像数据分离,封装行为应该也包含边界。例如。一个命名 为"CreateCustomer()"的方法就应该只包含创建一个新客户的行为。而不能包含为新客户下订单的行为。 类似的,一个命名为"ProductAssembler" 组件的就应该只包含产品组装的数据和行为,为不能包含客户的数据和行为。

为了达到好的行为分离,这通常是一个反复的过程。一个系统的基本行为在设计阶段只是一般的设想,但是在实现系统的时候,设计经常会被反复的重构直到细节部分慢慢的呈现出来。

当组织行为时,你的目标应该是寻求:1.没有重复的功能。2.限制行为的范围达到最小化。3.限制行为的范围在含有边界的描述以内。4.限制行为的范围在含有边界的应有的行为以内。5.最小化外部依赖。6最大可能的复用。

扩展关注

扩展是一种关注分离的策略,它可以将新的动作附加到现有的关注集合。扩展被用来增强现有的关注,需要的动作不会被添加到目标系统,它不是系统的固有动作, 也不会不切实际地被纳入作为系统核心功能集合的一部分。图 10 显示了一个扩展跟一个目标系统的依赖关系:

关注分离的艺术

    图10

扩展在对关键事件的通知进行的响应中与目标系统发生互动。扩展所提供的行为示例,包括新视图和空间的显示,改变正常进行的处理过程,还有目标系统中 的数据修改。扩展一般采用托管加载的形式,一般通过配置或者一个动态发现过程进行注册。当一个企业中有多个客户端应用程序正在被维护,你可能会发现一个客 户端所提供的扩展或许也能被其他的客户端使用。如果扩展是针对目标应用程序进行了高度定制的,并且提供了某种相对简单的性能,在一个新的应用程序中能采取 的最佳措施就是复制该功能.。反之,如果扩展的行为规模较大且复杂度较高,那么一般通过一个企业服务来提供其行为或许是更好的办法。如果由服务所提供的行 为仍然不适宜作为主系统的一个核心依赖,那么就可以针对它开发一个新的扩展来同代表了应用程序的服务进行交互。

委派关注

委派关注指的是将责任分派给能满足该行为的一个子组件的过程。这一策略从执行那里将责任的关注点分离,并且有利于设计出 根据外部条件表现出不同行为细节的组件。使用这一策略, 组件代理针对另外一个组件的某些或者全部请求和方法调用被设计成以一种特殊的方式满足请求。例如,组件会被设计成需要返回的分配给当前用户的角色清单,以 请求的形式分发给一个或者更多个被设计成从本地 XML 文件,数据库甚或一个远程服务获取角色信息的子组件。图 11 描述了将授权关注向针对特定不同数据源的组件的委派策略:

关注分离的艺术

    图 11

委派可以使用策略模式Plug-in 模式Microsoft Provider 模型 做进一步发展,而其他的模式变体则可以使组件的行为在外部被实现。

反向关注

反向关注,亦称作控制反转,指的是将关注移到既定边界之外的过程。一些对反向关注的使用包括影响面分离,非必要过程最少化,组件从特殊抽象策略处解耦,还 有搬迁基础设施组件的职责。一些特定的应用程序包括减轻有关硬件的交互,工作流管理,注册过程以及获得依赖。图12 显示了一个分享关注的过程,其中展现组件和域组件都已经将关注移动到了基础设施级别的组件:

关注分离的艺术

图 12

可以看到对关注进行反转的一个示例就是对 模板方法 模 式的使用。这种模式被用来对一个过程的行为进行规约化,以此来通过集成实现行为的各种变化。当把这一模式应用到一个现有的组件上的时候,所需处理过程应该 执行的步骤就是将其规约并封装到一个基类之中。然后现有的组件会要继承自这个新的几类,组件内部只保留特殊的行为。其他类型的组件则可以自由选择是否继承 自这个几类来提供不同的实现。这应该就是一种算法序列的关注反转的示例。 另外一个反转关注的示例还可以在将一个交互式控制台应用程序转化成一个 GUI 应用程序时被观察到 一个典型的交互式控制台应用程序提供了一个能先用户显示信息并等待数据被输入的主循环。当要被转成 GUI 应用程序时,程序的主要控制逻辑部分一般由基础设施组件来提供,而用户交互的提醒机制则使用 观察者模式 来 完成。这就转而使用户计划的处理主要控制逻辑去依赖于应用程序的基础设施,或者这些关注点的托管环境。依赖注入是一个被应用到有关于从外部获取依赖的关注 反转的术语。依赖注入的目标是将对于依赖如何获取的关注从一个边界内的核心关注分离。这样的通过根据上下文来对组件进行不同的依赖供应,提高的可重用性。 依赖注入的过程一般涉及到容器、依赖和接收者这些角色。由一个组件所占据的容器角色,它所负责是想一个接收者组件分配依赖。容器一般使用 工厂 模 式来进行实现,以实现接收者和依赖组件的创建。依赖的角色由将要被提供给接收者的资源所占据。容易常常被实现为让现有的对象可以被注册成为一个依赖来使 用, 或者在需要时创建出新的实体。接收者的角色有从容器那里接收依赖的组件所占据。接收者需要使用有容器所提供的某种策略来声明所需要的依赖。识别依赖信息的 策略一般采用反射的运用来检查接收者一个或者更多个成员的类型,而且可能还需要利用属性/注解来隐式或显式的指定依赖。图13中的一个序列图展示这些角 色。

关注分离的艺术

    图13

一个对依赖注入的使用示例就是将缓存功能从一个业务组件中抽象出来,替换使用工厂(Factory )或者工厂方法(Factory Method)模式。通过使用依赖注入来想一个接收者供应缓存组件,无需特定工厂的耦合增加,接收者就可以同缓存组件保持松耦合。另外一个例子就是使用依赖注入来控制缓存组件创建的数量,替换之以 单例模式(Singleton Pattern) 的使用。这样就能让应用程序在整个应用程序的生命周期中都只使用一个实体,或者是去控制决定哪一个接收者接收到哪一个组件的实体。这也提升了通过模拟依赖 的供应,隔离出依赖一对组件进行测试的能力,而不需要所需的单例跟随它们的使用者同时被测试。组件使用诸如工厂,工厂方法,抽象工厂,单例,插件,服务定 位器,这些模式来抽象对象创建或者依赖获取的关注,是考虑用依赖注入进行补充或者替换的一种良好选择。通过使用依赖注入代替、或者与这些或者其它这样的模 式合作,使用的组件可以从抽象策略那里完全抽象出来。

夸张练习

现实的情况常常是一次设计上的选择,特别是那些针对灵活性和可重用性的设计,其负面影响往往只会在系统变得稳定之后才会 变得明显起来。灵活和可重用性上的问题其根源往往是没有遵守关注点分离原则。有一个过程可以对关注点的优化起到辅助作用,那就是考虑将设计应用到比较夸张 的场景环境下。通过这项练习,夸大系统的使用条件,使其超过了对系统能力的已知预期,从而揭示出一个设计方法中潜在的薄弱之处。例如,当要设计一个被两个 现有系统所使用的对象模型时, 人们应该考虑当对象模型被 50 个系统共享时会产生的后果。通过夸大设计的使用条件,缺乏组织的责任将变得很容易被识别。为了展示这项练习,考虑如下针对创建一个新的综合客户关系管理系 统的关注: 在一个企业中,一个 IT 部门被要求创建一个定制的 CRM 应用程序,它可以让不同的开发团队贡献功能的特定模块。企业中主要的客户关系环节将是营销,结算和技术支持,贯穿其中可能就是被分配用来支持每个环节的功 能的多个开发团队。应用程序也被要求能显示一个主屏界面,可以通过姓名,地址,电话号码,订购编码和其它已经注册过的产品编码,等等查询条件,来对客户进 行查询。而结果视图和工作流则应该依据制定的时间采用了那种业务功能来决定。例如,在提交客户的查询条件时,销售用户看到的显示应该是有关于过去的购买趋 势,信用等级分数以及建议的销售上限视图;而向结算用户所展示的则可能是客户的支付记录,纠纷历史,和突出的余额。初步分析表明,五个不同的搜索条件域会 导致总共有三种工作流程的变化,而所有环节需要获取的信息将会涉及到三个后台系统。因为变化程度比较低,所以做出的选择就是在将主视图,客户搜索功能和工 作流初始化放到一个搜索模块中。增加新的搜索条件和工作流程被理解为需要由搜索模块开发组来进行修改,但相信这些修改需求不会太频繁。而将夸张练习应用到 这一设计决定,考虑如果业务环节或者后台系统的数量被增加到 50 个,会有什么后果接踵而至。将搜索功能和工作流的初始化集中到一起的后果就是,增加这些关注的范围的同时,也将增加搜索模块开发组的责任和工作压力。这会 包含对所有功能特性和变更请求的有关关注点的范围界定,分析,设计,编码和测试。反过来这可能会导致搜索模块组对于增加开发资源数量的需求。也还有可能导 致这些关注点合并的同一个初始假设也会影响到其它针对搜索模块所作出的设计决定。这可能已经导致设计决定可能不太容易适应这种责任的增加,而需要做某些程 度上的内部设计来处理新的关注。通过这一练习可以关注到的一个突出问题就是需要由搜索组付出的工作量跟由该系统所支持的业务环节和工作流程的数量成比例相 关。初始方案不能随着需求的增加而扩展,这一事实所表明的可能就是关注被不恰当的分布在整个系统中了。加强搜索功能的决定及其所导致的把工作流做到搜索模 块中的选择,是识别每个用例之间相似处所导致的。所没有给与同等考虑的则是每个用户应该以特别的方式看待这一事实,还有责任的数量没有真正的边界这一事 实。以为搜索屏幕为许多模块提供了集中的功能呢,就可以说它拥有了固有的基础设施级的关注。然而,每个情况中的细节并不是一样的,而且可以被认为是每个对 应模块的固有关注。随着对于固有职责的识别,可选的方法应该包括一个用来巩固共同关注的框架的开发,但可以分出每个领域特定的关注。这样一种方式可以通过 要求每个模块提供一个可以注册可用搜索条件,并提供调用对应工作流程的处理能力的附件来完成。一个框架应该来负责显示搜索条件的统一视图,并提供一个通用 的基础设施,来讲每个搜索条件同对应的工作流处理器相关联,通过使用这种方式,搜索模块可以被设计成能容纳无限数量的用例. 而夸张练习在创建高灵活性的设计中很有用,这仅仅是其实现关注分离最佳方案这一原始目标的一个副产品。夸张联系可能可以同获取一个病人的体温的医疗实践进 行类比,这项医疗实践测试的是可能的潜在问题所对应的症状。这一练习尝试去通过检验潜在设计设计的可扩展性这一方面,来确定关联到关注分离上的问题。这可 能被类比于图 14 所示的用放大镜去观察细节。

关注分离的艺术

    图14

它是通过将设计的实际范围扩大,使得小的问题更加容易被识别出来。一旦被识别出来,随后就可以来确定该采取什么样的行动了。

分离焦虑

对关注分离原则进行的应用所涉及到的先进概念和架构,其为应用程序所带来的一定程度上的复杂级别会超过仅仅只解决应用程 序原本关注领域上的复杂度。而对于在这些编程技术上没什么经验的开发者而言,其反应是看到了有将个人关于设计技能的所有本领统统加入的机会,充满了=热情 的兴奋之意,而不好的反应就是对与复杂性的附加两,会导致行动上以尽可能最方便的权宜之计来完成工作。这些技术常常导致缺乏经验且头脑更加战术性的开发者 在他们初次学习遇到挫折时认为这样的设计“过于复杂”或者“设计过度“了,然后就只是在日常基础的层面上来使用这样的架构。更有甚者,常常有永远存在的, 来自于项目经理、产品经理、更高级别的管理者、市场人员或者最终用户的需要即时满足其需求的现实压力,会使得情况趋向于鼓励和奖励权宜之计,而不是深思熟 虑的长远设计。这些条件给开发好的设计所带来的障碍超过了仅仅只需要去解决的技术问题。而那些能促进关注分离的设计常常会增加应用程序的复杂度,所以应该 指出它们其实也帮助去除了一些由于缺乏关注分离而导致的复杂度。对于许多应用程序而言,常常是有序的复杂度和无序的复杂度间的折中。没有表现出一定程度上 的关注分离的应用程序常常由于需在理解部分之前先理解整理而难于学习,而且难于去维护和扩展。高度复杂,而缺乏模块化的应用程序也会导致开发人员的高频率 周转(这往往会使得设计和实现的问题更进一步), 或者吸引那些讨厌改变的人寻求在组织内打造使自己职业不可或缺的”迷宫“。开发团队应该因为复杂而复杂(除非是进入了一个目标比较模糊的竞争之中), 而认为避免先进的设计就等同于避免掉复杂性的这一观点则应该被驱散。

总结

简而言之,关注分离原则的目标就是建立秩序。通过确保系统之中的元素只拥有单个唯一的用途,复杂的系统可以设计得将生产力和可维护性得到最大化。