Chapter7 可修改性

本文探讨了软件可修改性的重要性,涉及模块大小、内聚性、耦合减少、延迟绑定等策略,以及这些策略在控制修改成本和时间上的作用。文章强调了在设计过程中考虑变化的可能性、时间点和成本,以实现更灵活且经济的系统架构。
摘要由CSDN通过智能技术生成

目录

可修改性的一般情景

可修改性策略

减小模块的大小

增加内聚性

减少耦合

延迟绑定

在编译时或构建时绑定值的策略包括:

在部署时绑定值的策略包括:

在启动或初始化时绑定值的策略包括:

在运行时绑定值的策略包括:

用于可修改性设计的检查表

总结


适应或灭亡,正如自古以来一直是自然的无情要求。 - H.G.威尔斯

变化是不可避免的。

研究表明,典型软件系统的大部分成本都发生在最初发布之后。如果变化是宇宙中唯一不变的因素,那么软件的变化不仅是不断的,而且无处不在。变化发生是为了增加新功能,修改或淘汰旧功能。变化发生是为了修复缺陷,加强安全性,或改善性能。变化发生是为了提升用户体验。变化发生是为了采用新技术,新平台,新协议,新标准。变化发生是为了使系统能够协同工作,即使它们从未被设计为如此。可修改性与变化有关,我们对其感兴趣主要集中在变化的成本和风险上
要规划可修改性,架构师必须考虑四个问题:
1. 什么可以发生变化?系统的任何方面都可能发生变化:系统计算的功能、平台(硬件、操作系统、中间件)、系统运行的环境(必须与之互操作的系统、与世界其他部分通信的协议)、系统展现的特性(性能、可靠性,甚至未来的修改)、以及容量(支持的用户数量、同时操作的数量)。
2. 变化的可能性如何?无法为系统的所有潜在变化制定计划,否则系统永远无法完成,或者如果完成了,成本将过于昂贵,而且可能在其他方面出现质量问题。尽管任何事情都可能发生变化,但架构师必须就哪些变化是可能的,因此哪些变化应得到支持,哪些不应得到支持做出艰难的决策。
3. 变化何时发生,由谁发起?过去最常见的是对源代码进行变更。也就是说,开发人员必须进行变更,然后进行测试,然后在新版本中部署。然而,现在,变更何时发生与由谁进行变更的问题交织在一起。终端用户更改屏幕保护程序显然是对系统的某个方面进行变更。同样明显的是,它与更改系统以便在网络上而不是在单一机器上使用不在同一类别中。可以在实施时(通过参数设置、插件等),还可以通过开发人员、终端用户或系统管理员进行变更。
4. 变更的成本是多少?使系统更易修改涉及两种类型的成本:
    - 引入使系统更容易修改的机制的成本
    - 使用机制进行修改的成本
例如,使变更的最简单机制是等待变更请求,然后更改源代码以适应请求。引入机制的成本为零;使用它的成本是更改源代码和重新验证系统的成本。在谱系的另一端是应用程序生成器,比如用户界面生成器。生成器以直接操作技术产生的设计师用户界面的描述作为输入,并生成(通常)源代码。引入机制的成本是构建UI生成器的成本,这可能很大。使用机制的成本包括生成供生成器使用的输入的成本(成本可能很大或可以忽略不计),运行生成器的成本(大约为零),以及对结果执行的任何测试的成本(通常远小于通常情况)。
对于N个类似的修改,引入变更机制的简化理由是:
N x 不使用机制进行变更的成本 < 引入机制的成本 + (N x 使用机制进行变更的成本)
N是预期将使用可修改性机制的修改数,但N是一种预测。如果实际发生的变更比预期少,那么昂贵的修改机制可能不合适。此外,创建可修改性机制的成本可以用于其他地方,包括添加功能、改善性能,甚至用于非软件投资,如购买技术股票。此外,这个方程式不考虑时间。从长远来看,建立一个复杂的变更处理机制可能更经济,但你可能等不及那个时机。

可修改性的一般情景

  1. 刺激源这部分指定谁进行变更:开发人员、系统管理员或终端用户。
  2. 刺激这部分指定要进行的变更。变更可以是添加功能、修改现有功能或删除功能。对于这种分类,我们认为修复缺陷是更改功能,这可能是由于缺陷导致功能无法正常工作。变更也可以涉及系统的性能:使其更具响应性、提高可用性等等。系统的容量也可能发生变化。适应更多同时用户的需求是常见的。最后,变更可能会发生以适应某种新技术,其中最常见的是将系统移植到不同类型的计算机或通信网络。
  3. 工件这部分指定要进行变更的是什么:具体的组件或模块、系统的平台、用户界面、环境,或者与其互操作的另一个系统。
  4. 环境这部分指定何时可以进行变更:设计时间、编译时间、构建时间、启动时间或运行时。
  5. 响应进行变更、进行测试,并部署变更。
  6. 响应度量。所有可能的响应都需要时间和金钱;时间和金钱是最常见的响应度量。尽管两者听起来很容易衡量,但事实并非如此。你可以测量日历时间或员工时间。但你是否要测量变更通过配置控制委员会和批准机构的审批所需的时间(其中一些可能在你的组织之外),或者仅测量工程师进行变更所需的时间?成本通常意味着直接支出,但也可能包括您的员工在变更上工作而不是其他任务的机会成本。其他度量包括变更的范围(受影响的模块或其他工件的数量)或变更引入的新缺陷数量,或对其他质量属性的影响。如果变更是由用户进行的,您可能希望衡量所提供的变更机制的有效性,这在某种程度上与可用性度量重叠(请参阅第11章)。

图7.1展示了一个具体的可修改性情景:开发人员希望通过在设计时间修改代码来更改用户界面。这些修改在三小时内完成,没有副作用。

Table 7.1 列举了表征可修改性的一般情景的各个要素:

可修改性策略

控制可修改性的策略旨在控制修改的复杂性以及进行修改所需的时间和成本。图7.2展示了这种关系。

为了理解可修改性,我们从耦合性(coupling)和内聚性(cohesion)开始。
模块有责任。当一项变更导致模块被修改时,它的责任以某种方式发生变化。通常情况下,影响一个模块的变更比影响多个模块要容易和便宜。但是,如果两个模块的责任在某种程度上重叠,那么一个变更可能会影响它们两个。我们可以通过衡量一个模块的修改传播到其他模块的概率来衡量这种重叠,这称为耦合,高耦合度是可修改性的敌人

内聚度衡量模块的责任有多密切相关。非正式地,它衡量模块的“目的一致性”。目的一致性可以通过影响模块的变更场景来衡量。模块的内聚度是一个变更场景影响某一责任的概率也会影响其他(不同的)责任。内聚度越高,给定变更会影响多个责任的概率越低。高内聚度是好的,低内聚度是不好的。该定义允许两个目的相似的模块各自具有内聚性。

在这个框架下,我们现在可以识别用于激发可修改性策略的参数:

  • 模块的大小。分割模块的策略将减少对正在分割的模块进行修改的成本,只要分割被选择以反映可能进行的变更类型。
  • 耦合性。降低两个模块A和B之间耦合的强度将减少影响A的任何修改的预期成本。减少耦合的策略是将各种类型的中介放置在模块A和B之间。
  • 内聚度。如果模块A的内聚度较低,那么可以通过删除不受预期变更影响的责任来改善内聚度。

最后,我们需要考虑变更在软件开发生命周期的何时发生。如果忽略为修改准备架构的成本,我们希望变更尽可能晚地绑定。只有在体系结构适当准备好以容纳这些变更的情况下,才能在生命周期的后期成功地进行变更(即以最低的成本和最快的速度)。因此,可修改性模型的第四个参数是:

  • 修改的绑定时间。一个适当准备好以容纳生命周期后期的修改的体系结构,平均成本将低于迫使相同修改更早进行的体系结构。系统的准备意味着某些情况下,生命周期后期的修改成本将为零,或者非常低。然而,这忽略了为晚期绑定准备架构的成本。

现在我们可以理解策略及其对前述参数的影响:减少模块的大小、增加内聚度、减少耦合性和延迟绑定时间。这些策略在图7.3中显示。

减小模块的大小

  • 分割模块。如果正在修改的模块具有大量功能,那么修改的成本可能会很高。将这个模块拆分成几个较小的模块应该会降低未来变更的平均成本。

增加内聚性

有几种策略涉及将责任从一个模块转移到另一个模块。将责任从一个模块移动到另一个模块的目的是降低副作用影响原始模块中其他责任的可能性。

  • 增加语义一致性。如果模块中的责任 A 和 B 不具有相同的目的,它们应该放在不同的模块中。这可能涉及创建一个新的模块,也可能涉及将责任移动到现有模块中。一种识别要移动的责任的方法是假设可能会影响模块的变更。如果某些责任不受这些变更的影响,那么这些责任可能应该被移除。

减少耦合

我们现在转向减少模块之间耦合的策略。

  • 封装(Encapsulate):封装引入了一个模块的显式接口。这个接口包括应用程序编程接口(API)及其相关的责任,例如,“对输入参数执行语法转换以生成内部表示。”封装可能是最常见的可修改性策略,它降低了一个模块的变更传播到其他模块的可能性。以前针对模块的耦合强度现在转移到模块的接口。然而,这些强度会减小,因为接口限制了外部责任与模块互动的方式(可能通过包装器)。外部责任现在只能通过公开的接口与模块直接互动(然而,间接互动,如对服务质量的依赖,可能保持不变)。旨在提高可修改性的接口应该相对于可能发生变化的模块的细节是抽象的,即它们应该隐藏这些细节。
  • 使用中介(Use an intermediary)打破依赖关系:给定责任 A 和责任 B 之间的依赖关系(例如,首先执行 A 需要执行 B),可以通过使用中介来打破依赖关系。中介的类型取决于依赖关系的类型。例如,发布-订阅中介将消除数据生成者对其消费者的了解。共享数据存储库也将读取数据的读者与写入数据的写入者分开。在服务导向架构中,服务通过动态查找相互发现,目录服务就是一种中介
  • 限制依赖关系(Restrict dependencies):这是一种策略,它限制了一个给定模块与其他模块互动或依赖的方式。在实践中,这个策略是通过限制模块的可见性(当开发人员看不到一个接口时,他们就不能使用它)和授权(只允许授权模块访问)来实现的。这种策略在分层架构中很常见,其中一个层只允许使用较低层(有时只允许使用下一层),并且在使用包装器中也可见,外部实体只能看到(因此依赖于)包装器而不是它包装的内部功能。
  • 重构(Refactor):当两个模块受到相同变更的影响时,可能是因为它们(至少部分地)相互重复。代码重构是敏捷开发项目的一个主要实践,作为一个清理步骤,以确保团队没有生成重复或过于复杂的代码;然而,这个概念也适用于架构元素。常见的责任(以及实现它们的代码)从它们存在的模块中“分解出来”,并分配一个合适的位置。通过使常见责任共同定位,即将它们作为同一父模块的子模块,架构师可以减少耦合
  • 抽象共同服务:在两个模块提供不完全相同但相似的服务的情况下,以更一般(抽象)的形式实现这些服务可能是成本有效的。共同服务的任何修改都需要在一个地方进行,降低了修改成本。引入抽象的常见方式是通过参数化模块活动的描述(和实现)。这些参数可以是关键变量的值,也可以是后续解释的专用语言中的语句。

延迟绑定

由于人的工作几乎总是比计算机的工作更昂贵,因此尽可能让计算机处理变更几乎总是会降低制作该变更的成本。如果我们设计具有内置灵活性的构件,那么利用这种灵活性通常比手动编写特定的变更要便宜。

参数可能是引入灵活性的最为人熟知的机制,这让人想起了抽象共同服务策略。带参数的函数f(a, b)比假定b = 0的相似函数f(a)更一般。当我们在生命周期的不同阶段绑定了一些参数的值时,就是在应用延迟绑定策略。

一般来说,我们能够在生命周期的晚期绑定值,就越好。然而,引入机制以促进这种延迟绑定通常更昂贵,这又是另一种权衡。因此,在第118页的等式进入了其中。我们希望尽可能晚绑定,只要允许它的机制是具有成本效益的。

在编译时或构建时绑定值的策略包括:
  • 组件替换(例如,在构建脚本或 make 文件中)
  • 编译时参数化
  • 切面
在部署时绑定值的策略包括:
  • 配置时绑定
在启动或初始化时绑定值的策略包括:
  • 资源文件
在运行时绑定值的策略包括:
  • 运行时注册
  • 动态查找(例如,用于服务)
  • 解释参数
  • 启动时绑定
  • 名称服务器
  • 插件
  • 发布-订阅
  • 共享存储库
  • 多态性

将为可修改性建立机制与使用机制来进行修改分离,允许不同的利益相关者参与,一个利益相关者(通常是开发者)提供机制,而另一个利益相关者(例如,安装程序或用户)稍后使用它,可能在完全不同的生命周期阶段。安装机制以便其他人可以对系统进行更改而无需更改任何代码有时被称为“外部化变更”。

用于可修改性设计的检查表

表7.2是一个检查表,用于支持可修改性的设计和分析过程。

总结

可修改性涉及变更以及制作变更所需的时间或金钱成本,包括这种修改对其他功能或质量属性的影响程度

变更可以由人员、安装程序或最终用户进行,这些变更需要事先准备。准备变更也有成本,制作变更也有成本。可开发修改性策略旨在为随后的变更做好准备。

降低制作变更成本的策略包括将模块变得更小增加内聚性以及降低耦合延迟绑定也将降低制作变更的成本。

  • 增加内聚性是另一种标准策略,涉及将不具有相同目的的责任分开。
  • 降低耦合是一类标准的策略,包括封装、使用中介、限制依赖关系、共同定位相关责任、重构和抽象共同服务等。
  • 延迟绑定是一类影响构建时间、加载时间、初始化时间或运行时的策略。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值