写在前面
本文是《重构,有品位的代码》系列第五篇文章,前面文章主要介绍的重构手法是关于如何新建、移除或重命名程序的元素。当然,不只是只有这些手法,还有类型的重构也是很重要的,主要是在不同上下文间搬移元素。可以通过搬移函数手法在类与其他模块之间搬移函数,同样的也有搬移字段手法,还有其它手法将在本文中将逐一介绍...
前情回顾:
- 《重构,有品位的代码 01──走上重构之道》
- 《重构,有品位的代码 02──构建测试体系》
- 《重构,有品位的代码 03──常用重构手法》
- 《重构,有品位的代码 04──封装》
常见的搬移特性手法
在平时开发中,经常会在代码中使用到搬移特性,但是并不知道是做了什么搬移特性,现在我们将常用的搬移特性手法进行总结如下:
- 搬移函数
- 搬移字段
- 搬移语句到函数
- 搬移语句到使用者
- 以函数调用取代内联代码
- 移动语句
- 拆分循环
- 以管道取代循环
- 移除死代码
1. 搬移函数
模块化能够确保我们的代码块间的联系易于查找、直观易懂,能够保证相互关联的软件要素集中在一块,便于我们理解和管理。与此同时,随着对代码的理解加深,了解到那些软件要素如何组织最为恰当,此时需要通过不断地搬移元素进行重新模块化。
函数是存活在上下文中的,这个上下文可能是全局的,也有可能是当前所在模块进行提供的。而类即为主要的模块化手段,作为函数的上下文,此外通过函数嵌套的方式,外层函数也可为内层函数提供上下文。简而言之,模块可以为函数提供存活的上下文环境。
由于在某些代码频繁引用其他上下文中的元素,即与其他上下文的元素关系紧密,而对于自身上下文中的元素关心甚少,此时就可以考虑将联系密切的元素进行归纳,取得更好的封装效果。那么有以下情况,你可以进行搬移函数的操作:
- 某段代码需要频繁调用别处函数
- 在函数内部定义帮助函数在别处也有调用
- 在类中定义函数
通常的,首先检查函数在当前上下文中引用的所有程序元素(包括变量和函数),考虑是否需要将它们进行搬移,并对待搬移函数是否具有多态性进行检查。将函数复制一份到目标上下文中,调整函数使得适应新的上下文。执行静态检查,设法从源上下文中正确引用目标函数,修改源函数,使之成为一个纯委托函数。
原始代码:
class Account{
constructor(){
....
}
get bankCharge(){
let result = 4.5;
if(this._daysOverdrawn> 0) result += this.overdraftCharge;
}
get overdraftCharge(){
if(this.type.isPremium){
const basecharge = 10;
if(this.dayOverdrawn <= 7){
return baseCharge;
}else{
return baseCharge + (this.daysOverdrawn - 7) * 0.85;
}
}else{
return this.daysOverdrawn * 1.75;
}
}
}
重构代码:
class Account{
constructor(){
...
}
get bankcharge(){
let result = 4.5;
if(this._daysOverdrawn> 0) result += this.overdraftCharge;
}
get overdraftCharge(){
return this.Type.overdraftCharge(this);
}
}
class AccountType{
constructor(){
...
}
overdraftCharge(account){
if(this.isPremium){
const basecharge = 10;
if(account.dayOverdrawn <= 7){
return baseCharge;
}else{
return baseCharge + (account.daysOverdrawn - 7) * 0.85;
}
}else{
return account.daysOverdrawn * 1.75;
}
}
}
2. 搬移字段
在开发中你是否会遇到一些糟糕的代码,使用了糟糕的数据结构,代码的逻辑并不清晰条理,更多的是各种纠缠不清,代码很多令人费解的无用代码。因此,通常可以做些预先的设计,设法获取最恰当的数据结构,而具备驱动设计方面的经验和知识,将有助于你设计数据结构。
当然,即使经验丰富、技能熟练,也会在设计数据结构的时候犯错,但是随着对问题理解的深入,对业务逻辑的熟悉,便会考虑到更深更全面。在过程中发现数据结构不适应需求,便要及时进行修缮,如果容许瑕疵存在便会导致代码复杂化,问题累积。
在你每次进行调用函数时,在传入一个参数时,总是需要伴随另外的字段作为参数传入,即修改一条记录同时需要修改另一条记录,那么意味着此处的字段位置放置错误。另外的,假设你更新某个字段,同时需要在多个结构中做出改变,那么就意味着你需要将此字段进行正确的搬移。
具体的,确保源字段已经进行良好封装,在目标对象上创建字段(及对应的访问函数)并执行静态检查,确保源对象里能够正常引用目标对象,即调整源对象的访问函数能够使用目标对象的字段。最后,移除源对象上的字段。
原始代码:
class User{
constructor(name,age,getName){
this._getName = getName;
this._age = age;
this._name = name;
}
get getName(){
return this._getName;
}
}
class UserType{
constructor(firstName){
this._firstName = firstName;
}
}
重构代码:
class User{
constructor(age,name){
this._age = age;
this._name = name;
}
get getName(){
return this._name.getName;
}
}
class UserType{
constructor(firstName,getName){
this._firstName = firstName;
this._getName = getName;
}
get getName(){
return this._getName;
}
}
3. 搬移语句到函数
在重构代码时有几条黄金准则,其中最重要的就是要“消除重复”代码,对重复语句进行抽象到函数中,通过调用函数来实现复杂代码的运行。
4. 搬移语句到调用者
作为搬砖码农的指责就是设计结构一致、抽象合宜的程序,而函数就是抽象的制胜法宝。当然所有的手段都并非放之四海而皆准的法则,随着系统能力的演变,最初设计的抽象边界逐渐向外扩散变得模糊,从原先单独整体、聚焦唯一点,分化成多个不同关注点。
而函数边界发生偏移,意味着之前多个地方调用的行为,现在需要会在不同点表现出不同的行为。这样,我们可以把不同表现行为从函数中挪出,将其搬移到调用处。
printHtml(outData,onData.html);
function printHtml(outData,html){
outData.write(`<p>title:${html.title}</p>`);
outData.write(`<p>content:${html.content}</p>`);
}
即:
printHtml(outData,onData.html);
outData.write(`<p>content:${onData.html.content}</p>`);
function printHtml(outData,html){
outData.write(`<p>title:${html.title}</p>`);
}
5. 以函数调用取代内联代码
使用函数可以将相关行为进行打包,提升代码的表达能力,清晰的解释代码的用途和作用,有助于消除重复的代码。如果某段内联代码是对已有函数进行重复,那么可以使用一个函数调用来取代内联代码,可以实现业务逻辑的抽象。
let flag = false;
for(const color of colors){
if(color === "yellow") flag = true;
}
即:
let flag = colors.includes("yellow");
6. 移动语句
如果有几行代码使用了相同的数据结构,那么可以使其关联使用,使得代码更易理解,而不是夹在其他数据结构中间。那么在我们写完代码后,需要进行审读,将关联性强的代码移动语句进行聚集。通常,移动语句作为其他重构代码的先提重构手段。
const pricingPlan = rePricingPlan();
const order = reOrder();
let charge;
const chargePerUnit = ricingPlan.uint;
重构代码:
const pricingPlan = rePricingPlan();
const chargePerUnit = ricingPlan.uint;
const order = reOrder();
let charge;
7. 拆分循环
在常规的开发中,会在一次循环中做多件事情,意图让其避免过高的时间复杂度。有的时候,在一次循环中代码过多、逻辑混乱,反而不便于我们日常理解。因此可以根据情况合理拆分循环,使其每次循环只做一件事情,更便于阅读使用。
let averagePrice = 0;
let totalCount = 0;
for(const p in goods){
averagePrice += p.price;
totalCount += p.count;
}
averagePrice = averagePrice / totalCount;
重构代码:
let averagePrice = 0;
for(const p in goods){
averagePrice += p.price;
}
let totalCount = 0;
for(const p in goods){
totalCount += p.count;
}
averagePrice = averagePrice / totalCount;
是不是看起来有点傻,当你在复杂代码中阅读会发现很清晰。
8. 以管道取代循环
在过去进行数组、对象遍历时,通常做法是使用循环进行迭代,当然也可以使用更好的语言结构———”集合管道“来处理迭代(map和filter等)。集合管道允许使用一组运算来描述集合迭代过程,其中每种运算都是一个集合。
通常做法:创建一个用于存放参与循环过程的集合的新变量,从c循环顶部开始,将循环内的每块行为依次搬移。在创建的集合变量中用管道运算进行替换,直到循环内的全部行为进行搬移完毕,最后将循环进行删除。
const users = [];
for(const item in arrs){
if(item.age === 20) users.push(item.name);
}
//重构代码
const users = arrs
.filter(item=>item.age === 20)
.map(item=>item.name);
9. 移除死代码
在将项目部署在生产环境中,可能会因为代码量太大而造成更大的内存开销,无用代码会拖累系统的运行速度,导致项目进程缓慢。当然,多数的现在编译器会自动将无用代码进行移除,但是在你阅读理解代码逻辑和原理时,会让你花费时间去思索,耗费精力。在代码不再使用时,应当立即删除,当你突然又想使用时可以通过版本控制回滚。
if(false){
...
}
这是一句无用代码,应当立刻删除。
小结
在本文中,主要介绍了搬移字段、搬移函数等搬移手段,也有单独对语句搬移、调整顺序的,也可以调整代码的位置,对循环进行拆分、使用管道替换等方法。
参考文章
《重构──改善既有代码的设计(第2版)》
写在最后
我是前端小菜鸡,感谢大家的阅读,我将继续和大家分享更多优秀的文章,此文参考了大量书籍和文章,如果有错误和纰漏,希望能给予指正。
更多最新文章敬请关注笔者掘金账号一川萤火和公众号前端万有引力。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!