揭示常见的重构误区

2023-10-28

 作者 Danijel Arsenovski译者 张逸

公正地说,.NET社区对于重构技术的研究起步太晚。直到今天,.Net开发的旗舰产品Visual Studio仍然无法在C#中突破重构的界限(http://www.martinfowler.com /articles/refactoringRubicon.html)。Visual Basic以及最新的C++情况略好,但却需要你下载和安装一个免费的重构插件Refactor!,它是Developer Express为VB或C++开发的。


之后的所有替代品都不再是免费的晚餐。虽然这些产品完全配得上你的投入,然而当我们开始关注那些诸如“代码质量”等虽非必要却极为深奥的要素,并达 成一致意见时,这些产品却难以成为开发者的主流工具。即使不使用工具,你仍然可以进行重构,但手工方式会由于太过复杂而会将开发者拒之门外。无怪 乎.Net社区对重构的引入会大大地滞后,因为我们对于重构的所有问题及其作用,依旧混乱不堪。

本文试图列出一些我经常遇到的使用重构的误区。这些误区与某些传统的对编程的偏执一样,总是会成为吸取技术精华的壁垒。紧接着,我还会列举某些先入 为主的误解,试图阐释其起源,并给出有力的证据驳斥这些论点。我希望本文能为每个人澄清对重构本质的怀疑,让他们学会成为一个重构者,或者在他的团队中建 立并推广这种实践。

“如果没有坏,就不要修复它”

这句话老少相传,可谓工程智慧的真实写照,然而,“如果没有坏,就不要修复它”只会滋生得过且过的情绪。重构有足够充分的理由来摒弃这种思想。

在你的编程生涯的早期,一定明白“千里之堤,溃于蚁穴”的道理,并为之而付出过深深地代价。即使一个细微的变化,都会导致软件在最糟糕的时刻以令人 莫名惊 诧的方式而停止运转。一朝被蛇咬,十年怕井绳,故而你害怕任何变化的发生,只要变化不是必须的。然而这只能够苟延残喘维持一时。一旦形势急转直下,出现的 错误不得不解决,新的功能需求也不能一拖再拖了。此时你面对的代码即使是相同的,但实际却已养成了大患。

那些接受了“如果没有坏,就不要修复它”信条的开发人员认定重构是可有可无的,甚至会干扰既定的开发目标。实际上,这种试图维持“现状”的顺从姿态,来源于某种心理,他们认为对代码的恐惧,以及无法掌控的事实都是合理的。

许多经验丰富的程序员之所以认可这种观点,是因为对一些不必要工作“偷工减料”,乃人之常情,是合情合理的。譬如说,如果应用程序已经具备优越的性 能,就无需为了性能对处理器周期耗费心机。类似的投机性设计,通常会用来搪塞那些具有前瞻性的编程观点,诸如“我们可能会在将来的某一天需要这一特性”。

从这个意义上讲,重构需要随时进行。在重构时,你需要消除冗余代码,避免投机性设计或预先优化。

而对于重构的老手来讲,这样的软件完全是“金玉其外,败絮其中”。如果设计有瑕疵,例如拙劣的代码和糟糕的结构,那么在软件外部是看不到这些问题 的。然而 即使应用程序在某个时候能够正常运行,我们仍然需要对此进行重构,对设计进行优化。从这个意义上讲,重构坚持在一些不太明显,但却具有决定性作用的特征上 作文章,例如设计、简单性,以及改善源代码的可读性,便于理解。

重构可以帮助你赢回对代码的支配权。这对于那些业已脱离控制的代码库而言并非易事,如果不诉诸于重构,那么唯一的解决之道就是彻底地重写。

重构不是新生事物

换而言之,这种误解可以这样说:“重构无非就是一种新瓶装旧酒的说辞罢了。”这意味着你对于如下种种早已熟谙于心:编写好的代码,面向对象设计,编码风格,最佳实践,如此等等,不一而足。重构无非是某些人的故作玄虚之语,编造出来用以兜售自己的新书,如此而已。

对,重构从一诞生之初,就从未放言像面向对象或面向方面编程那般会成为一种划时代的全新模式。它仅仅是从根本上改变你编码的方式:它定义了一些规 则,使得利用工具(例如点击按钮)完成代码的复杂转换成为可能。你不应将代码看作是不易修改的僵化的结构。相反,你应该看到自己有能力维持代码总是恰当好 处,有效地应对新的挑战,而无需害怕修改代码。

重构是高科技

编程并不易为。这是一项复杂活动,需要大量的知识积累。某些知识可能很难掌握。VB程序员如果要掌握Visual Basic .NET,必须具备熟练运用面向对象语言的能力。对于多数人而言,这是一大困惑。而其好处则在于学习一门新的技能绝对是物有所值的。

重构的伟大之处就在于它的简单。只需了解很小的一套简单规则,你就可以“盛装出发”了。再加上一个好的工具,迈开重构的第一步简直就是轻而易举。与 当前一 个高级程序员应该了解的其他技术相比,例如UML或设计模式,我得说重构有着最简单的学习曲线,就像VB和其他编程语言相比一样。

学习重构很快就会迎来你的收获季节。当然,与世间万事万物相同,知识的学习总是“一份耕耘,一份收获”的。

重构会导致性能低下

复杂点儿的说法是“因为重构通常会引入大量的细粒度元素(如方法和类),这种间接的设计会导致性能的损失。”

让我们把时钟往回调一小段,你会发现这样的观点似曾相识,当初在质疑面向对象编程时,就发出过类似奇怪的声音。

事实的真相是代码结构重构与否,在性能上的区别微乎其微,所以常常可以忽略不计,除非是某些特别的系统。

经验证明,性能总是受制于某一段确定的代码。在优化阶段修复它们,可以获得你需要的性能等级。能够轻易地识别关键代码是重中之重。减少代码的重复与数量,从而使得代码易于理解,一旦发生变化,也只会影响到单独的模块,这样的重构极大地改善了优化的过程。

当我们发现在一段日子里,CPU好似上足了马力一般,使用率不停上升,而代码的其他特性,例如可维护性、质量、可伸缩性以及可靠性又使得我们不得不将性能置诸脑后。而现在,我们再也不能以这样的理由为编写出性能糟糕的代码寻求托辞了,当然,我们也不能矫枉过正。

面临这样的境况,你可以看看本文提供的一些数据,也算是我的一点小小经验。我会使用两个代码示例。第一个例子代码结构简陋,且只有一个单独的 Main方 法。第二个例子的Main方法则被放到一个模块中,其中定义的一个类Circle包含了几个细粒度的方法。最初,我使用这些示例仅仅是为了演示非结构化代 码与结构化代码的编码风格,因而缺乏一些用于量化的代码。

我计算了执行一个简单几何公式(求圆周长)的时间,为了避免编写一些涉及计算敏感应用的代码,我增加了一些数据库查询代码。为了量化值的准确性,我 将执行放入到一个循环中,重复执行10000次。由于我并没有打算获得极端精确的值,因此我使用了 System.Diagnostics.Stopwatch类用以捕捉耗时值,毕竟在这个案例中,Stopwatch的值已经足够精确了。

代码示例1:非结构化代码

Option Explicit On
Option Strict On

Imports System.Diagnostics
Imports System.Data.SqlClient

Namespace RefactoringInVb.Chapter9

Structure Point
Public X As Double
Public Y As Double
End Structure

Module CircleCircumferenceLength

Sub Main()
Dim center As Point
Dim pointOnCircumference As Point
'read center coordinates
Console.WriteLine("Enter X coordinate" + _
"of circle center")
center.X = CDbl(Console.In.ReadLine())
Console.WriteLine("Enter X coordinate" + _
"of circle center")
center.Y = CDbl(Console.In.ReadLine())
'read some point on circumference coordinates
Console.WriteLine("Enter X coordinate" + _
"of some point on circumference")
pointOnCircumference.X = CDbl(Console.In.ReadLine())
Console.WriteLine("Enter X coordinate" + _
"of some point on circumference")
pointOnCircumference.Y = CDbl(Console.In.ReadLine())
'calculate and display the length of circumference
Console.WriteLine("The lenght of circle" + _
"circumference is:")
'calculate the length of circumference
Dim radius As Double
Dim lengthOfCircumference As Double
Dim i As Integer
'use stopWatch to measure transcurred time
Dim stopWatch As New Stopwatch()
stopWatch.Start()
'repeat calculation for more precise measurement
For i = 1 To 10000
'add some IO
Dim connection As IDbConnection = New SqlConnection( _
"Data Source=TESLATEAM;" + _
"Initial Catalog=RENTAWHEELS;" + _
"User ID=RENTAWHEELS_LOGIN;" + _
"Password=RENTAWHEELS_PASSWORD_123")
connection.Open()
Dim command As IDbCommand = New SqlCommand( _
"SELECT GETDATE()")
command.Connection = connection
Dim reader As IDataReader = command.ExecuteReader()
reader.Read()
reader.Close()
connection.Close()
radius = ((pointOnCircumference.X - center.X) ^ 2 + _
(pointOnCircumference.Y - center.Y) ^ 2) ^ (1 / 2)
lengthOfCircumference = 2 * 3.1415 * radius
Next
stopWatch.Stop()
Console.WriteLine(stopWatch.Elapsed)
Console.WriteLine(lengthOfCircumference)
Console.Read()
End Sub
End Module
End Namespace

代码示例2:结构化代码

Option Explicit On
Option Strict On

Imports System.Data.SqlClient

Namespace RefactoringInVb.Chapter11
Public Structure Point
Public X As Double
Public Y As Double
End Structure

Module CircleCircumferenceLength

Sub Main()
Dim circle As Circle = New Circle
circle.Center = InputPoint("circle center")
circle.PointOnCircumference = InputPoint( _
"point on circumference")
Console.WriteLine("The length of circle " + _
"circumference is:")
Dim circumference As Double
Dim i As Integer
'use stopWatch to measure transcurred time
Dim stopWatch As New Stopwatch()
stopWatch.Start()
'repeat calculation for more precise measurement
For i = 1 To 10000
circumference = circle.CalculateCircumferenceLength()
Next
stopWatch.Stop()
Console.WriteLine(stopWatch.Elapsed)
Console.WriteLine(circumference)
WaitForUserToClose()
End Sub

Public Function InputPoint(ByVal pointName As String) As Point
Dim point As Point
Console.WriteLine("Enter X coordinate " + _
"of " + pointName)
point.X = CDbl(Console.In.ReadLine())
Console.WriteLine("Enter Y coordinate " + _
"of " + pointName)
point.Y = CDbl(Console.In.ReadLine())
Return point
End Function

Private Sub WaitForUserToClose()
Console.Read()
End Sub
End Module
Public Class Circle
Private centerValue As Point
Private pointOnCircumferenceValue As Point
Public Property Center() As Point
Get
Return centerValue
End Get
Set(ByVal value As Point)
centerValue = value
End Set
End Property
Public Property PointOnCircumference() As Point
Get
Return pointOnCircumferenceValue
End Get
Set(ByVal value As Point)
pointOnCircumferenceValue = value
End Set
End Property

Public Function CalculateCircumferenceLength() As Double
QueryDatabase()
Return 2 * 3.1415 * CalculateRadius()
End Function

Private Function CalculateRadius() As Double
Return ((Me.PointOnCircumference.X - Me.Center.X) ^ 2 + _
(Me.PointOnCircumference.Y - Me.Center.Y) ^ 2) ^ (1 / 2)
End Function

Private Sub QueryDatabase()
Dim connection As IDbConnection = New SqlConnection( _
"Data Source=TESLATEAM;" + _
"Initial Catalog=RENTAWHEELS;" + _
"User ID=RENTAWHEELS_LOGIN;" + _
"Password=RENTAWHEELS_PASSWORD_123")
connection.Open()
Dim command As IDbCommand = New SqlCommand( _
"SELECT GETDATE()")
command.Connection = connection
Dim reader As IDataReader = command.ExecuteReader()
reader.Read()
reader.Close()
connection.Close()
End Sub
End Class
End Namespace

经过几次执行,可以看到两个例子的耗时相当接近。在我的机器上,它们的值在2.2到2.4秒之间。值得称许的一点细微差别是:非结构化示例的最小耗时为1.9114800,而结构化示例的则为2.0398497。在我看来,二者并无太大差异。

重构破坏好的面向对象设计

具有优美结构以及经过重构的代码,在菜鸟的眼里,是笨拙而粗陋的。方法是如此的短小,在他们看来简直言之无物。类显得不够重量级,仅仅包含屈指可数的几个成员。这样的代码看起来简直就等于没有嘛。

像类和方法那样,若要管理大量的元素,则意味着需要处理的复杂度会加大。

这样的观点常会引人误解。事实上,复杂度总是相同的。但重构后的代码会显得条理更清晰,结构更合理。

重构无法提供短期利益

一个占据主流的论调是重构可以使得你的程序更快。迄今为止,就我所知,并没有相关的研究(这是我强烈呼吁的)可以证明我的看法,但我的经验告诉我存 在这样的情形。这是唯一合乎逻辑的。由于你在整体上具有少量的代码,极少的重复以及清晰的意图,因此重构带来的益处很快就能彰显无遗,除非你处理的是一些 可有可无,也无任何实际意义的小规模代码。

重构只适宜敏捷团队

在敏捷方法学中,重构是被频繁提及的其中一项关键技术,因而通常的解释是,重构只有在遵循敏捷原则的团队中才能如鱼得水。

重构是敏捷团队不可或缺的

即使你的团队采取的方法别出蹊径,但在大多数时候,总是由你来负责管理编码方式,这时就是运用重构的时机。 其他的团队成员或管理方式可能会忽略这样一个事实,就是你需要不停地在IDE中使用“Refactor”选项。不管你的团队采取何种方法,都没有任何东西 可以阻止你对代码进行重构。如果你遵循小步重构,并在编码过程中定期执行,就能达到最佳的重构效果。某些实践例如严格的代码所有权或瀑布过程,可能会与重 构背道而驰。如果你能够证明重构从编程的视角来看是有意义的,你就可以开始构建你的支撑库,首先从你的伙伴开始,然后推广到整个团队。

重构可以在开发过程中作为独立的阶段实施,并由独立的团队执行

经理们通常对此深以为然。将重构视为一个独立的阶段,然后将其放在诸如实现阶段和测试阶段期间,从管理学的角度来看,容易给人一种错觉,认为重构就是甘特图的一根线条,可以轻易地将其压缩时间甚至移走。

事实上,为了成功地执行重构,你需要完整地理解整个问题域,了解需求、设计甚至实现阶段的细节。倘若你从一开始就没有将实施重构的人看作团队的一份子,也没有花时间与客户交流,分析需求和思考设计,你将很难改善最初的团队构建的内容。

遵循某种模型,则代码可以通过它在编码之后得到精化,就像工业生产过程中提炼出的某种物质那样,总会带来一些好处。若是不能切实地理解代码的意义, 则你真正能够对重构保有信心的只能是一些细微的改进。若在此种情形下,妄图通过重构使代码焕然一新,结果很有可能是南辕北辙,适得其反。代码若与问题域紧 密相关,就有可能使得事情变得更加糟糕,最终还会为你的应用程序引入bugs。

没有单元测试重构照样能够工作

我想,一些简单的重构可以在没有单元测试的情形下进行。重构工具与编译器自身可以提供一定的安全保障,不至于引入一些简单的人为错误。你也可以采用 传统方 式对代码进行测试,例如使用调试器或者执行功能测试。但这些手动的测试方法却是乏味而不值得信赖的。重构时,代码比以前对修改更为敏感与脆弱。若要避免不 必要的问题,则应添加NUnit单元测试放到项目中。在你执行每一小步重构时,就能够及时发现错误。

不要依赖于注释的观点未必正确

我敢担保这会导致某些看起来有趣的混乱与疑惑。毫无疑问,你已经千百次地被告知,在编写代码时一定要添加注释。作为一种好的编程实践,这种思想会帮 助别人理解你的代码。这通常意味着编码的方式是优秀的、有序的、专业的。因此,如果现在有人居然胆敢告诉你注释未必是一桩好事儿,你一定会大吃一惊。

添加注释的动机通常与代码重构一致。你应该竭力提高代码的可读性。在早期,编程工具受制于标识符的长度,故而注释成为了传达编程涵义的唯一选择。立 足于重 构,则要求代码是自解释性的,即选择正确的方法、类、变量以及其他标识符。同时,你应该避免为了相同的目的使用注释,因为注释不会被执行,且很容易作废。 在每日的编程成为急就章时,总是会忘记更新注释、文档、图示或其他次等级的工件。

匈牙利命名法怎么了?

并不只是Charles Simonyi的母语触发了创造匈牙利命名法的灵感。那些看起来像是匈牙利语的命名,例如a_crszkvc30LastNameCol和 lpszFile,并不会让我感到惊讶。如今,编译器会检查类型安全性,跟踪类型信息。在现代的IDE中,所有必要的类型信息都能被找到,并作为鼠标指针 的提示出现。在诸如C#和VB.Net的语言中,匈牙利命名法已经过时了。

然而,旧有的习惯总是很难改变。匈牙利命名法通常被看作是一种有效的然而却难以掌握的编程实践。难怪并非所有人都乐于看见自己掌握的传统命名规则原来已经过时。

使用那些类似人类的自然语言为变量命名,可以使得代码简明易懂。放弃那些有趣的前缀,改而使用别人能够轻松理解的词语吧。

结论

可以毫不夸张地说,重构是编程的一次变革,它从根本上改变了某些旧有的习惯。它必然会面对许多阻力,让不知所以者感到无比的困惑。

我希望本文能够为你打开重构之门。在你第一次展卷阅读时,不要惊讶于那些困扰你的问题。如果你正在倡导重构,或者试图向别人讲解重构,那么你应该时刻准备提出质疑。面对这种情形,我希望我已经提供了足够的论据,以证明采纳重构的原因与必要性是行得通的。

本文源于《Professional Refactoring in Visual Basic》一书,该书作者为Danijel Arsenovski,已由Wrox出版。

关于作者

Danijel Arsenovski是Wrox出版的《Professional Refactoring in Visual Basic》一书的作者。目前,他就职于Excelsys S.A(该公司为地区内的大量客户设计网上银行解决方案),担任产品和解决方案架构师。他对于重构的最初体验来自于对大型银行系统的整改, 从此,他就迷上了重构。他首创了利用重构完成代码从VB 6到VB.NET的升级。Arsenovski还是多个主要出版商的丛书作者,拥有微软认证解决方案开发专家(MCSD,Microsoft Certified Solution Developer)证书,并在2005年被提名为Visual Basic MVP。你可以通过电子邮件danijel.arsenovski@empoweragile.com与他取得联系,他的博客是 http://blog.vbrefactoring.com


本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

揭示常见的重构误区 的相关文章

随机推荐

  • 计算机密码输入正确,Win10输入正确密码却提示“密码不正确”如何解决

    在使用windows10系统过程中 很多用户都会遇到一些奇怪的问题 比如 有用户反馈开机登录win10的时候输入正确的登入密码 却碰到 密码不正确 请确认您的微软账户密码正确 的错误提示 无法登录 是怎么回事呢 在出现该问题的时候首先我们可
  • QT定时器的几种用法

    一 自定义定时器 定义一个定时器对象 绑定自定义槽函数 也可以定义定时器对象指针 这样可以一次性让多个定时器绑定同一个槽函数 头文件中加入 QTimer myTimer void myTimerEvent 自定义定时处理槽函数 在需要启动的
  • TCP的三次握手与四次挥手以及面试常见题

    TCP 是什么 TCP Transmission Control Protocol 传输控制协议 是一种面向连接的 可靠的 基于字节流的传输层通信协议 而且TCP是全双工模式 面向连接 你和你女朋友聊天是面向连接的 只有连接起来才可以通信的
  • 【Jmeter】什么是BeanShell?

    一 什么是BeanShell BeanShell是用Java写成的 一个小型的 免费的 可以下载的 嵌入式的Java源代码解释器 JMeter性能测试工具也充分接纳了BeanShell解释器 封装成了可配置的BeanShell前置和后置处理
  • 花生壳 linux客户端 命令

    phddns start service sshd status phddns status phddns version
  • HJ32密码截取

    有一定难度 难在考虑不周全 最后还是看了别人提到的方法 自己独立实现了一下 可能没有别人的简洁 但是易懂 调试的时候有些小毛病 但自己解决了 原因不是很清楚 最后总结里会提到 目标 在输入的字符串里找到对称的且是最长的那个字符串 思路 参考
  • 【Zabbix实战之部署篇】Zabbix使用SNMP监控Linux系统

    Zabbix实战之部署篇 Zabbix使用SNMP监控Linux系统 一 SNMP协议介绍 1 SNMP协议简介 2 SNMP协议特点 二 实践环境介绍 三 检查Zabbix监控平台环境 1 检查Zabbix相关组件容器状态 2 检查Zab
  • day05:js基础——函数、作用域问题

    js函数 作用域问题 概述 1 函数 1 1 函数概念 函数作用 函数构成 1 2 定义函数 调用函数 1 3 函数参数 1 4 函数返回值 1 5 js中的特殊函数 1 6 函数demo 2 变量作用域 3 js中的预解析 3 1 声明式
  • 用c语言制作一个简单的答题系统

    首先制作一个答题系统需要有一个题库 其次要有完整的出题系统 然后要能够进行答题和判断答案对错 最后就是统计答案正确率了 实现创建一个题库并不难 仅需要使用数组保存题目与标准答案就行了 使用strcpy函数将题目分别输入进题库 部分代码如下
  • Web自动化测试 —— 测试环境搭建 (Selenium+Python)及视频操作

    一 什么样项目适合做web自动化 1 软件需求不会频繁的变更 2 项目周期比较长 3 自动化的脚本能够重复利用 介入点 第一个版本的核心功能确认以后 系统测试 自动化的实施过程 1 可行性分析 2 框架的选择 selenum rf 框架的搭
  • 使用 maven 自动将源码打包并发布

    在pom xml中添加maven source plugin插件 maven生成 jar的同时生成 sources包
  • 用分布式锁和redis实现原子性递增,解决编号重复问题

    import com baomidou mybatisplus core conditions query QueryWrapper import lombok extern slf4j Slf4j import org apache co
  • C语言-二维数组做函数的参数

    文章目录 1 引例 2 观点1 这种使用方法是错误的 3 观点2 根本不需要这么做 4 二维数组做函数参数的方法 4 1 方法1 4 2 方法2 4 3 方法3 5 与Java的不同 1 引例 下面的程序很简单 定义了一个PrintMatr
  • Talib技术因子详解(一)

    talib安装方式 pip install Ta lib Tushare数据获取请参考 金融量化分析基础环境搭建 数据获取代码 import tushare as ts ts set token Tushare的token pro ts p
  • 如何解决从git上下载很慢的问题

    在国内从git上面下载代码的速度峰值通常都是20kB s 这种速度对于那些小项目还好 而对于大一些的并且带有很多子模块的项目来讲就跟耽误时间 虽然有很多提速的方法 但是实际用起来并不稳定 这里提供一套新的方法 下载速度可以至少达到 2MB
  • [1214]基于Python实现视频去重

    文章目录 基于Python实现视频去重 基本原理 实现方法 其它视频去重code 基于Python实现视频去重 基本原理 一款基于Python语言的视频去重复程序 它可以根据视频的特征参数 将重复的视频剔除 以减少视频的存储空间 它的基本原
  • MySQL之字符串函数

    字符串是由零个或多个字符组成的有限序列 一般记为 s a1a2 an n gt 0 通常以串的整体作为操作对象 如 在串中查找某个子串 求取一个子串 在串的某个位置上插入一个子串以及删除一个子串等 假如结果的长度大于 max allowed
  • OKL4 的故事

    转自 弯曲评论 编者注 Gernot 的这篇 blog 介绍了一些 NICTA 和 OK lab 的故事 关于 NICTA 和 OK lab 的来历 读者如果感兴趣 可以阅读我以前写的这篇文章General Dynamics 收购 Open
  • java.util.ConcurrentModificationException

    增强for底层用了迭代器 会导致遍历的时候修改集合中的元素出现java util ConcurrentModificationException 这是因为ArrayList底层维护了一个modCount用于记录list集合修改的次数 每操作
  • 揭示常见的重构误区

    作者 Danijel Arsenovski译者 张逸 公正地说 NET社区对于重构技术的研究起步太晚 直到今天 Net开发的旗舰产品Visual Studio仍然无法在C 中突破重构的界限 http www martinfowler com