软件开发中的可维护性策略与实践
在软件开发领域,追求高效、高质量且易于维护的代码是每个开发者和项目管理者的目标。现代工具的发展为我们提供了更多实现这一目标的途径,同时,我们也需要遵循一系列的原则和方法来确保软件的可维护性。
1. 一次性设计方法的优势
现代计算机辅助软件工程(CASE)工具使得一次性设计方法比以往更具吸引力。这些工具支持快速迭代,在过去完成两个设计的时间内,现在可以完成三到四个设计。作为项目管理者,在项目开始前,需要能够预见一次性设计在哪些方面会发挥作用。这就要求研究初步的项目计划或概念,找出需要优化的关键部分,并设定目标来评估和改进设计。
使用一次性设计方法,我们可以在创建软件的过程中学习。虽然这可能会让我们的自尊心受到一些打击,但最终能得到更好的软件。为此,我们首先要有谦逊的态度,因为只有承认自己并非无所不知,才能真正学习。其次,要摒弃一些随意的习惯,更全面地规划项目。我们必须审视整个项目,识别出可能困难甚至致命的部分,并预留必要的时间。
无论是将其称为原型设计还是软件风险管理,本质上都是一种简单的方法:边做边学,抛弃不好的软件,然后第二次把事情做对。
2. 可维护的面向对象编程
面向对象程序理论上可以消除脆弱的代码和维护噩梦,但前提是从一开始就使程序具有可维护性。然而,如今许多面向对象程序员正在编写未来难以维护的应用程序。尽管面向对象编程促进了可扩展性和可修改性,使程序更易于理解和增强,但这并不意味着面向对象程序就一定易于维护,良好的代码需要付出努力。
许多组织刚开始采用面向对象开发,往往只关注当前的开发问题,而忽略了未来的维护问题。这很令人费解,一方面,很多组织转向面向对象编程是因为维护现有遗留代码存在重大问题,按常理他们应该担心新编写的面向对象程序的维护问题;另一方面,不可维护的应用程序并不能带来工作保障,这些应用程序很快会被外包或重写,导致许多维护程序员失业。
组织需要采用自下而上的方法来实现可维护性:可维护的代码产生可维护的方法,可维护的方法产生可维护的类,可维护的类产生可维护的应用程序。以下是编写易于维护的面向对象程序的一些简单技巧。
2.1 可维护的面向对象代码
编写可维护的面向对象程序所涉及的问题与编写过程式代码的问题差异不大,有三条基本法则:
- 每行代码只执行一个命令。
- 使用括号指定操作执行的顺序。
- 使用有意义的变量名。
例如,下面的 C++ 代码:
x = y++;
这行代码做了两件事:先将 y 的值赋给 x,然后递增 y。但也可能是先递增 y 再将新值赋给 x,这就需要查阅 C++ 的操作顺序规则。这行代码的关键问题是它做了不止一件事,虽然在 20 世纪 70 年代早期,为了在穿孔卡片上最大化功能,这种编码风格被认为是好的,但对于现在不使用穿孔卡片编程的我们来说,这样只会让代码比实际需要的更复杂。
将代码改为每行只做一件事会更易于维护:
y++;
x = y;
这样不仅更容易理解,而且由于操作顺序不再是问题,更有可能按我们的预期工作。
使用括号指定操作顺序也是提高代码可维护性的方法。例如:
x = (y++);
这样我们至少知道操作的执行顺序,增加了代码的可理解性,进而提高了可维护性。添加括号多花的几秒钟,可能会为维护程序员节省数小时追踪难以捉摸的错误的时间,甚至可能从一开始就避免错误。
变量名也很重要,像
x
和
y
这样的变量名很难让人理解其含义,几个月后更难回忆起来。对于 Smalltalk,应该使用完整的英文名称,如
numberOfPeople
和
employeeName
;对于 C++,建议使用匈牙利命名法,将变量类型的指示与完整的英文名称连接起来,例如
iNumberOfPeople
和
pEmployeeName
。
2.2 可维护的方法
方法是面向对象编程中与过程或函数等价的概念。有五个因素影响方法的可维护性:
- 方法应该有良好的文档。
- 方法应该有段落结构。
- 方法应该有实际作用。
- 方法应该只做一件事。
- 方法应该能在 30 秒内被理解。
以一个 Smalltalk 方法为例:
withdraw: amount
| fileStream dateTime |
(balance < amount) ifTrue: [
^ 'Not enough funds in account'.
].
balance := balance - amount.
dateTime := (Date today) + (Time now).
fileStream := File open: 'c:\L2000.TXT'.
fileStream notNil ifTrue: [
fileStream append: accountNumber.
fileStream append: dateTime.
fileStream append: 'W'.
fileStream append: amount.
fileStream append: Cr.
fileStream close.
].
这段代码虽然简短,但很难立即看出它的作用和原因。添加注释和空白行可以改善这种情况:
withdraw: amount
" Debit the provided amount from the account and post a transaction.
Passed: A positive amount of money (of class Float).
Answers: The receiver when the withdrawal is successful, other−
wise a string describing the problem encountered. "
| fileStream dateTime |
" Don't allow the account to become overdrawn"
(balance < amount) ifTrue: [
^ 'Not enough funds in account'.
]
" Debit the account by the amount of the withdrawal "
balance := balance - amount.
" Post a transaction so that we have a record of this transaction (for statements, ...)"
fileStream := File open: 'c:\L2000.TXT'.
fileStream notNil ifTrue: [
fileStream append: accountNumber.
dateTime := (Date today) + (Time now).
fileStream append: dateTime.
fileStream append: 'W'.
fileStream append: amount.
fileStream append: Cr.
].
注释不仅记录了传递给方法的参数、它们的含义和使用方式,还记录了方法的返回值。同时,空白行将代码分成易于理解的小部分,提高了可读性和可维护性。
段落结构也很重要,通过缩进代码可以使代码更易于阅读和维护:
withdraw: amount
"Withdraw.... "
| fileStream dateTime |
" Don't allow the account to become overdrawn"
(balance < amount) ifTrue: [
^ 'Not enough funds in account'.
].
" Debit the account by the amount of the withdrawal "
balance := balance - amount.
" Post a transaction so that we have a record of this transaction (for statements, ...)"
fileStream := File open: 'c:\L2000.TXT'.
fileStream notNil ifTrue: [
fileStream append: accountNumber.
dateTime := (Date today) + (Time now).
fileStream append: dateTime.
fileStream append: 'W'.
fileStream append: amount.
fileStream append: Cr.
fileStream close.
].
缩进的方式可以使用制表符或 2 - 4 个空格,关键是要保持一致。
方法应该只做一件事,例如
withdraw:
方法既从账户中扣除金额,又记录交易,从维护的角度来看,将记录交易的代码提取到一个单独的方法中会更好:
withdraw: amount
" Withdraw.... "
| fileStream dateTime |
" Don't allow the account to become overdrawn" (balance < amount) ifTrue: [
^ 'Not enough funds in account'.
].
" Debit the account by the amount of the withdrawal "
balance := balance - amount.
" Post a transaction so that we have a record of this transaction (for
statements, ...)"
self postTransaction: 'W' amount:amount.
postTransaction: transactionType amount: amount
" Post transaction to a flat−file so that we can maintain a record of the activities on the
fileStream := File open'c:\L2000.TXT'.
fileStream notNil ifTrue: [
fileStream append: accountNumber.
dateTime := (Date today) + (Time now).
fileStream append: dateTime.
fileStream append: 'W'.
fileStream append: amount.
fileStream append: Cr.
fileStream close.
].
方法还应该有实际作用,像下面的代码:
aMethod: aParameter
^ AnotherClass doSomething: aParameter
这个方法只是将任务传递给另一个类,没有实际价值,还会使程序变得复杂。更好的做法是让原始对象直接向
AnotherClass
发送消息。
最后,另一个程序员应该能够在不到 30 秒内查看你的方法并完全理解它的作用、原因和实现方式。如果做不到,代码就太难维护,需要改进。
2.3 可维护的类
类的可维护性在很大程度上取决于访问器方法的适当使用。访问器方法有两种类型:设置器(setters)和获取器(getters)。设置器用于修改变量的值,获取器用于获取变量的值。访问器方法的名称与它们访问的变量相同。
例如,以下是 Smalltalk 和 C++ 中账户号码的访问器方法示例:
accountNumber
^ accountNumber.
accountNumber: aNewNumber
accountNumber := aNewNumber.
accountNumber()
{
return (accountNumber);
}
accountNumber( int aNewNumber)
{
accountNumber = aNewNumber;
}
虽然访问器方法会增加一些开销,但它们有助于隐藏类的实现细节。通过最多从两个控制点(一个设置器和一个获取器)访问变量,可以减少需要更改的点,从而提高类的可维护性。
考虑银行账户号码的实现,如果银行将账户号码存储方式从一个大数字改为由四位分支 ID 和六位在分支内唯一的 ID 组成,没有访问器的话,这个简单的更改通常会产生巨大影响,每个访问账户号码的代码都需要更改;而使用获取器和设置器,只需要做一些小的更改,例如:
accountNumber
" Answer the unique account number, which is the concatenation of the branch
id with the internal branch account number "
^ (self branchID) * 1000000 + (self branchAccountNumber).
accountNumber: aNewNumber
self branchID: (aNewNumber div:1000000).
self branchAccountNumber: (aNewNumber mod: 100000).
使用访问器方法还可以避免一些错误。例如,在获取器方法中记录重要的业务规则,这样程序员就不需要依赖过时的外部文档。同时,对于常量也应该使用访问器方法,以减少错误的可能性并提高系统的可维护性。
对于布尔型变量的获取器方法,也有一些技巧。例如:
isPersistent
" Indicates whether or not this object occurs in the database "
^ isPersistent.
当
isPersistent
变量未初始化时,这个方法可能会导致比较操作崩溃。更好的做法是:
isPersistent
" Indicates whether or not this object occurs in the database "
^ isPersistent = true.
这样可以确保获取器方法始终返回布尔值。
变量的初始化也很重要,有两种思路:在对象创建时初始化所有变量(传统方法)或在首次使用时初始化。传统方法在 C++ 中使用构造函数,在 Smalltalk 中使用初始化方法,但容易出错,添加新变量时可能会忘记更新。另一种方法是“懒初始化”,例如:
isPersistent
" Indicates whether or not this object occurs in the database"
(isPersistent isNil) ifTrue: [
self isPersistent: false.
].
^ isPersistent.
这种方法虽然增加了初始化检查的开销,但能确保变量始终被初始化,提高了可维护性。
访问器方法的最后一个优点是可以减少子类与其超类之间的耦合。当子类仅通过相应的访问器方法访问继承的属性时,就可以在不影响子类的情况下更改超类中属性的实现。
3. 可维护的应用程序
面向对象应用程序的可维护性受以下因素影响:
-
继承的适当使用
:继承可以建模类之间的“是一个”“类似”或“是一种”关系。
-
聚合的适当使用
:聚合可以建模“是一部分”的关系。
-
关联的适当使用
:关联可以建模类之间的其他关系。
-
多态的适当使用
:多态可以改变对象的类型,使我们可以向不同类型的类(通常在同一类层次结构中)发送相同的消息。
3.1 面向对象的耦合和内聚
耦合衡量两个事物的关联程度。例如,两个类之间的耦合越高,一个类的更改影响另一个类的可能性就越大。遗留应用程序通常存在高耦合问题,修复一个地方的错误可能会在其他地方引发新的错误。
面向对象应用程序除了传统的高耦合来源外,继承还引入了新的来源,子类与超类耦合。如果设计不好,超类实现的更改可能会对其子类产生深远影响。
内聚衡量事物的合理性。方法是否只做一件事?类是否封装了一种对象的行为?应用程序是否执行了一个系统的功能?松散的内聚会导致系统难以维护和增强。
继承的不当使用通常会导致绝大多数面向对象的维护问题。从错误的超类继承的类往往最终会覆盖或重新实现现有功能,这不是一个好的维护状态。
一个简单的经验法则是,“子类是/是一种/类似于超类”这句话应该有意义。例如,“客户是一个人”和“支票账户类似于储蓄账户”是合理的,但“账户是客户”或“客户是账户”则不合理,所以
Customer
类不应该从
Account
类继承,反之亦然。
对于聚合关系也有类似的经验法则,“一个类是另一个类的一部分”这句话应该有意义。例如,“发动机是飞机的一部分”是合理的,但“储蓄账户是客户的一部分”则不合理。人们常常在应该使用继承时使用聚合,或者反之。
类之间的关联(也称为“对象/实例关系”)也可能被误用。策略是先应用继承的句子规则,如果不行再尝试聚合规则。如果两个句子都没有意义,那么这两个类之间可能存在关联(前提是它们之间确实有关系)。
最后,多态的误用或未使用会使代码难以维护。这个问题通常表现为基于对象类的
CASE
语句(在 C++ 中通过
switch
命令实现,在 Smalltalk 中需要编写级联的
if
语句)。例如:
withdraw: amount
" Withdraw .....""
| type |
type := self class.
" Answer the class of the receiver "
(type iskindOf: SavingsAccount) ifTrue: [
" Some code....."
] ifFalse: [
(type iskindOf: CheckingAccount)
ifTrue: [
" Some code "
] ifFalse: [
" etc.... "
].
].
当添加新类型的账户时,需要修改这个方法以及采用相同策略的其他方法,这是一个潜在的维护噩梦。更好的方法是为每个账户类定义
withdraw:
方法,当需要取款时,直接向账户对象发送
withdraw:
消息。
综上所述,通过遵循上述原则和方法,我们可以提高软件开发的可维护性,减少未来的维护成本和风险。无论是项目管理者还是程序员,都应该重视这些方面,以确保软件的长期健康发展。
软件开发中的可维护性策略与实践
4. 关键因素对可维护性的影响总结
为了更清晰地理解各个因素对软件可维护性的影响,我们可以通过以下表格进行总结:
| 影响因素 | 具体内容 | 对可维护性的作用 |
| — | — | — |
| 一次性设计方法 | 现代 CASE 工具支持快速迭代,项目管理者需提前规划,边做边学、抛弃不良设计 | 提高软件质量,减少后期维护成本 |
| 可维护的面向对象代码 | 每行代码只执行一个命令、使用括号指定操作顺序、使用有意义的变量名 | 增强代码的可读性和可理解性 |
| 可维护的方法 | 良好文档、段落结构、有实际作用、只做一件事、30 秒内可理解 | 便于代码的理解和修改 |
| 可维护的类 | 适当使用访问器方法(设置器和获取器),处理好变量初始化和布尔型获取器 | 隐藏类实现细节,减少更改点,降低耦合 |
| 可维护的应用程序 | 适当使用继承、聚合、关联和多态 | 合理建模类间关系,降低维护复杂度 |
5. 案例分析:银行账户系统
为了更好地说明上述原则和方法在实际中的应用,我们以一个银行账户系统为例。假设该系统包含储蓄账户、支票账户等不同类型的账户,并且需要处理取款、存款等业务。
5.1 初始设计与问题
最初的设计可能存在一些问题,例如取款方法使用了基于对象类的
CASE
语句:
withdraw: amount
" Withdraw ....."
| type |
type := self class.
" Answer the class of the receiver "
(type iskindOf: SavingsAccount) ifTrue: [
" Some code....."
] ifFalse: [
(type iskindOf: CheckingAccount)
ifTrue: [
" Some code "
] ifFalse: [
" etc.... "
].
].
这种设计在添加新类型的账户时,需要修改多个方法,维护成本高。
5.2 改进方案
-
多态的应用
:为每个账户类定义
withdraw:方法,利用多态性,当需要取款时,直接向账户对象发送withdraw:消息。
" 储蓄账户类的 withdraw: 方法 "
SavingsAccount>>withdraw: amount
" 储蓄账户取款逻辑 "
" ... "
" 支票账户类的 withdraw: 方法 "
CheckingAccount>>withdraw: amount
" 支票账户取款逻辑 "
" ... "
这样,当添加新类型的账户时,只需要为新账户类定义
withdraw:
方法,而不需要修改其他代码。
- 访问器方法的使用 :对于账户号码的存储和访问,使用访问器方法。假设账户号码存储方式发生变化,从一个大数字改为由四位分支 ID 和六位在分支内唯一的 ID 组成,通过访问器方法可以轻松应对。
accountNumber
" Answer the unique account number, which is the concatenation of the branch
id with the internal branch account number "
^ (self branchID) * 1000000 + (self branchAccountNumber).
accountNumber: aNewNumber
self branchID: (aNewNumber div:1000000).
self branchAccountNumber: (aNewNumber mod: 100000).
- 方法的优化 :确保每个方法只做一件事,并且有良好的文档和段落结构。例如,将取款和记录交易的逻辑分开:
withdraw: amount
" Withdraw.... "
| fileStream dateTime |
" Don't allow the account to become overdrawn" (balance < amount) ifTrue: [
^ 'Not enough funds in account'.
].
" Debit the account by the amount of the withdrawal "
balance := balance - amount.
" Post a transaction so that we have a record of this transaction (for
statements, ...)"
self postTransaction: 'W' amount:amount.
postTransaction: transactionType amount: amount
" Post transaction to a flat−file so that we can maintain a record of the activities on the
fileStream := File open'c:\L2000.TXT'.
fileStream notNil ifTrue: [
fileStream append: accountNumber.
dateTime := (Date today) + (Time now).
fileStream append: dateTime.
fileStream append: 'W'.
fileStream append: amount.
fileStream append: Cr.
fileStream close.
].
6. 实施可维护性策略的流程
为了在软件开发项目中有效实施可维护性策略,可以遵循以下流程:
graph LR
A[项目启动] --> B[研究初步计划]
B --> C{识别关键部分}
C -->|是| D[设定优化目标]
C -->|否| B
D --> E[采用一次性设计方法]
E --> F[编写可维护代码]
F --> G[设计可维护方法]
G --> H[构建可维护类]
H --> I[创建可维护应用程序]
I --> J[持续评估和改进]
J -->|需要改进| F
J -->|无需改进| K[项目完成]
- 项目启动 :明确项目目标和需求。
- 研究初步计划 :分析项目的初步计划或概念,了解项目的整体架构和功能。
- 识别关键部分 :找出需要优化的关键部分,例如性能瓶颈、复杂的业务逻辑等。
- 设定优化目标 :为关键部分设定具体的优化目标,例如提高代码的可读性、降低耦合度等。
- 采用一次性设计方法 :利用现代 CASE 工具进行快速迭代,尝试多个设计方案,选择最优方案。
- 编写可维护代码 :遵循每行代码只做一件事、使用括号指定操作顺序、使用有意义的变量名等原则。
- 设计可维护方法 :确保方法有良好的文档、段落结构,只做一件事,并且能在 30 秒内被理解。
- 构建可维护类 :适当使用访问器方法,处理好变量初始化和布尔型获取器。
- 创建可维护应用程序 :合理使用继承、聚合、关联和多态,确保应用程序的耦合度和内聚度合理。
- 持续评估和改进 :在项目开发过程中,不断评估代码的可维护性,及时发现问题并进行改进。
7. 总结与建议
通过本文的介绍,我们了解到在软件开发中,可维护性是一个至关重要的方面。为了提高软件的可维护性,我们需要从项目的各个阶段入手,采用一次性设计方法,编写可维护的代码、方法、类和应用程序,合理使用面向对象的特性。
对于开发者来说,要养成良好的编程习惯,注重代码的可读性和可理解性,遵循基本的编程原则。同时,要不断学习和掌握新的技术和方法,提高自己的编程能力。
对于项目管理者来说,要在项目开始前充分规划,识别一次性设计的应用场景,合理分配资源,确保项目的顺利进行。并且要关注项目的可维护性,鼓励开发者采用可维护性策略,为项目的长期发展奠定基础。
总之,提高软件的可维护性需要开发者和项目管理者的共同努力,只有这样,才能开发出高质量、易于维护的软件,满足用户的需求,降低软件的维护成本和风险。
提升软件可维护性的策略
超级会员免费看

被折叠的 条评论
为什么被折叠?



