[导语] 重构是在不更改代码功能的情况下提高代码质量的过程。在提交代码之前花一些时间来重构代码可以使代码更具可读性,从而更易于维护。持续执行此操作会发现错误,并从噩梦般的地狱场景添加新功能到公园散步。
理想情况下,单元测试将完全覆盖您的代码,使您可以放心地进行更改。如果不是,那么应该尽可能在重构之前添加测试。但是,如果您无法添加测试,则仍然可以通过使用自动化,安全的重构工具来实现很多目标。
那么从哪里开始呢?本文包含5件简单的事情,您可以立即做,以使您的代码更易读,更美观。
一、删除注释掉的代码
这是最简单的重构,并且可以为您的代码库产生出色的结果。
注释掉的代码令人困惑。它会使代码库膨胀,并使其难以遵循代码的真实执行路径。它也会妨碍真正的注释,而这些注释可能会在大量过时的代码行中丢失。
# lots of
#
# commented out lines of code
do_something_important()
#
# even more
# commented out code
在这样的示例中,由于试图跳过注释,因此在阅读代码时可能会丢失重要信息。
如果您担心注释掉的代码有一天会有用,则将其存储在源代码管理中是正确的方法。如果您需要它,它将在提交历史记录中。只需在删除注释的地方给提交一个合适的名称即可,它们很容易找到。
最重要的是,只要您谨慎删除仅注释掉的行,这种重构就永远不会破坏您的代码。
二、提取数字和字符串
在开发过程中,将字符串文字和数字直接写入代码中通常会更容易。但是,离开他们却是解决问题的良方。
如果数字或字符串以后更改,则需要查找文字的每个实例,检查是否需要更改然后更改它。以这种方式重复更改重复代码是导致错误的主要原因之一,因为很容易错过其中一种用法。用常量替换它们意味着文字存储在一个地方,只需要在那个地方进行更改即可。
另一个问题是,像本例中的数字这样的数字不能告诉您为什么它具有值或用途。
def update_text(text):
if len(text) > 80:
wrap(text)
用常量代替它,让您为它指定一个描述性名称,这使代码更易于阅读和理解。
MAX_LINE_LENGTH = 80
def update_text(text):
if len(text) > MAX_LINE_LENGTH:
wrap(text)
当涉及到实际进行更改时,许多IDE将通过“提取常量”选项来帮助您提取文字。
三、删除重复项
DRY(不要重复自己)是软件开发的基本原则之一。重复使代码更难理解,并且当重复的代码开始产生歧义时,通常会导致错误。
删除重复项的一种简单方法是,如果您发现有提升代码的机会。这是在条件的两个分支上重复执行代码的位置,因此可以在外部使用。
例子01 变量存在很多重复的地方,如下:
- 重构前
def get_bm_page(self, account, proxy, page_id, user_id):
url_owned = f"https://test.com/v8.0/{user_id}/owned_pages?fields=id,name&access_token=Easas..&limit=100"
owner_page= self.check_page_id(url_owned, account, proxy, page_id, user_id)
if not owner_page:
url_client = f"https://test.com/v8.0/{user_id}/client_pages?fields=id,name&access_token=Easas..&limit=100"
owner_page self.check_page_id(url_owned, account, proxy, page_id, user_id)
return owner_page
- 重构后
def get_bm_page_id(account, proxy, page_id, user_id):
base_url = "https://test.com/v8.0/{user_id}/%s?fields=id,name&access_token={access_token}&limit=100".format(bm_id='12',access_token='Easas..')
owner_page = False
for page_type in ['owned_pages', 'client_pages']:
owner_page = check_page_id(base_url % page_type, account, proxy, page_id, user_id)
if owner_page:
break
return owner_page
例子02 更常见的是,代码在函数的不同部分或两个不同的函数中重复。
在这里,您需要将代码提取到另一个函数中,为其指定一个有意义的名称,然后调用它而不是提取的代码。如下:
- 重构前
class Game:
# ...
def was_correctly_answered(self):
if self.not_penalised():
print('Answer was correct!!!!')
self.purses[self.current_player] += 1
print(
self.players[self.current_player]
+ ' now has '
+ str(self.purses[self.current_player])
+ ' Gold Coins.'
)
winner = self._did_player_win()
self.current_player += 1
if self.current_player == len(self.players):
self.current_player = 0
return winner
else:
self.current_player += 1
if self.current_player == len(self.players):
self.current_player = 0
return True
def wrong_answer(self):
print('Question was incorrectly answered')
print(self.players[self.current_player] + " was sent to the penalty box")
self.in_penalty_box[self.current_player] = True
self.current_player += 1
if self.current_player == len(self.players):
self.current_player = 0
return True
仔细看一下,下面是一段清晰的重复代码,它在三个地方弹出:
self.current_player += 1
if self.current_player == len(self.players):
self.current_player = 0
这应该提取到一个函数中。许多IDE可以让您选择代码片段并自动将其提取,而某些PyCharm等也将扫描您的代码,以查看可以通过调用新功能替换哪些部分。让我们提取此方法并调用它next_player,因为它将继续前进current_player到下一个有效值。
- 重构后
class Game:
# ...
def was_correctly_answered(self):
if self.not_penalised():
print('Answer was correct!!!!')
self.purses[self.current_player] += 1
print(
self.players[self.current_player]
+ ' now has '
+ str(self.purses[self.current_player])
+ ' Gold Coins.'
)
winner = self._did_player_win()
self.next_player()
return winner
else:
self.next_player()
return True
def wrong_answer(self):
print('Question was incorrectly answered')
print(self.players[self.current_player] + " was sent to the penalty box")
self.in_penalty_box[self.current_player] = True
self.next_player()
return True
def next_player(self):
self.current_player += 1
if self.current_player == len(self.players):
self.current_player = 0
在此代码上还有很多重构需要完成,但是绝对可以改善。它的读取更清晰,如果next_player()必须更改函数,我们只需要在一个位置而不是三个位置更改代码即可。
随着您进行更多的编码和重构,您将学到越来越多的机会删除重复项。通常,您将需要处理多段代码以使它们更加相似,然后将通用元素提取到函数中。
四、拆分大型功能
功能越长,阅读和理解就越困难。编写良好功能的一个重要方面是,它们应该做一件事,并且要具有一个准确反映其功能的名称-这就是所谓的“单一责任原则”。较长的功能更倾向于做很多不同的事情。
关于应保留多长时间的意见不一,但总的来说越短越好。铸铁的规则是,它绝对应该适合您的代码编辑器的一页。必须上下滚动以查看功能的作用,这会增加理解该功能所需的认知负担。
为了拆分功能,您需要将其部分提取到其他功能中,如上一节所述。
让我们看一些代码,以了解如何进行:
def make_tea(kettle, tap, teapot, tea_bag, cup, milk, sugar):
kettle.fill(tap)
kettle.switch_on()
kettle.wait_until_boiling()
boiled_kettle = kettle.pick_up()
teapot.add(tea_bag)
teapot.add(boiled_kettle.pour())
teapot.wait_until_brewed()
full_teapot = teapot.pick_up()
cup.add(full_teapot.pour())
cup.add(milk)
cup.add(sugar)
cup.stir()
return cup
在这里我们可以看到调用分为三类-分别调用水壶,茶壶和杯子上的方法的那些。根据我们的专业知识(英国人可能会对此有所帮助),这些也对应于制作一杯茶的三个阶段-将水壶煮沸,冲泡茶然后倒入杯子中并食用。
让我们从结尾开始,并提取与倒出一杯茶并上桌有关的内容。您可以手动执行此操作,也可以使用IDE的“提取方法”功能。
def make_tea(kettle, tap, teapot, tea_bag, cup, milk, sugar):
kettle.fill(tap)
kettle.switch_on()
kettle.wait_until_boiling()
boiled_kettle = kettle.pick_up()
teapot.add(tea_bag)
teapot.add(boiled_kettle.pour())
teapot.wait_until_brewed()
full_teapot = teapot.pick_up()
return pour_tea(cup, full_teapot, milk, sugar)
def pour_tea(cup, full_teapot, milk, sugar):
cup.add(full_teapot.pour())
cup.add(milk)
cup.add(sugar)
cup.stir()
return cup
在此阶段,我们的make_tea功能中混合了多个抽象级别-与执行pour_tea功能中涉及的多个步骤相比,打开水壶是一个更简单的操作。理想情况下,我们函数中的每一行都应该处于相似的级别,从而使它们更易于解析为连贯的叙述。为了实现这一点,让我们继续并提取其他两个功能。
def make_tea(kettle, tap, teapot, tea_bag, cup, milk, sugar):
boiled_kettle = boil_water(kettle, tap)
full_teapot = brew_tea(boiled_kettle, tea_bag, teapot)
return pour_tea(cup, full_teapot, milk, sugar)
def boil_water(kettle, tap):
kettle.fill(tap)
kettle.switch_on()
kettle.wait_until_boiling()
return kettle.pick_up()
def brew_tea(boiled_kettle, tea_bag, teapot):
teapot.add(tea_bag)
teapot.add(boiled_kettle.pour())
teapot.wait_until_brewed()
return teapot.pick_up()
def pour_tea(cup, full_teapot, milk, sugar):
cup.add(full_teapot.pour())
cup.add(milk)
cup.add(sugar)
cup.stir()
return cup
查看顶级make_tea功能,您现在可以阅读三个小故事,就像泡茶一样。如果您对任何阶段的细节都感兴趣,则可以深入了解相关方法。
拆分功能时,最好确定逻辑上一致的代码位,这些位可以一起执行某件事。一条经验法则是,如果您可以为新函数想到一个与域名相关的好名称,那么提取它可能是一个好主意。
五、将局部变量声明移至与其用法接近的位置
确保在读取任何特定代码段时必须处理尽可能少的变量非常重要。保持变量声明的紧密性和范围及其用法有助于此。基本上,变量声明得越接近其用法,在以后阅读代码时向上和向下扫描的次数就越少。
def assess_fruit(self, fruit):
happiness = 0
hunger = time_since_breakfast() / size_of_breakfast()
some_other_code()
# work out some things
do_some_other_things()
if is_snack_time() and isinstance(fruit, Apple):
yumminess = fruit.size * fruit.ripeness ** 2
happiness += hunger * yumminess
return happiness
阅读此代码时,您必须hunger从函数的开头到结尾牢记变量,因为可以在任何地方使用或更改它。
def assess_fruit(self, fruit):
happiness = 0
some_other_code()
# work out some things
do_some_other_things()
if is_snack_time() and isinstance(fruit, Apple):
hunger = time_since_breakfast() / size_of_breakfast()
yumminess = fruit.size * fruit.ripeness ** 2
happiness += hunger * yumminess
return happiness
重构以将其移至使用它的作用域即可解决此问题。现在我们只需要hunger在适当的范围内考虑。