书接上回,重构从现在开始 一文中我们讨论了重构的含义、意义以及时机,并说明了测试的重要性。从本文开始将介绍重构具体的技巧,首先登场的是一些通用型技巧。
提炼函数应该是最常见的重构技巧。我们都不希望把一大堆东西塞在一个函数里,使得一个函数做了一大堆事,这样无论是阅读还是修改都会很累(我曾经在字节跳动就见过一个600+行代码的功能函数)。
理论上函数应该是专事专办,每个函数只做一件事。基于这个思路,我们可以对一个函数依据其代码段的特性和功能进行划分和提炼,以嵌套函数的方式进行调用。当然,划分粒度可以自己决定,在《重构》一书中作者认为函数超过 6 行代码就会散发臭味。
重构前:
import datetime
class Invoice():
def __init__(self, orders, customer):
self.orders = orders
self.customer = customer
self.dueDate = ""
def printOwing(invoice):
outstanding = 0
print("***********************")
print("**** Customer Owes ****")
print("***********************")
# calculate outstanding
for o in invoice.orders:
outstanding += o["amount"]
# record due date
now = datetime.datetime.now()
invoice.dueDate = now + datetime.timedelta(days=30)
# print details
print(f'name: {invoice.customer}')
print(f'amount: {outstanding}')
print(f'due: {datetime.datetime.strftime(invoice.dueDate, "%Y-%m-%d %H:%M:%S")}')
invoice = Invoice(
[{"amount": 1}, {"amount": 2}, {"amount": 3}],
"zhangsan"
)
printOwing(invoice)
重构后:
import datetime
class Invoice():
def __init__(self, orders, customer):
self.orders = orders
self.customer = customer
self.dueDate = ""
def printBanner():
print("***********************")
print("**** Customer Owes ****")
print("***********************")
def printOwing(invoice):
outstanding = 0
printBanner()
# calculate outstanding
for o in invoice.orders:
outstanding += o["amount"]
# record due date
now = datetime.datetime.now()
invoice.dueDate = now + datetime.timedelta(days=30)
# print details
print(f'name: {invoice.customer}')
print(f'amount: {outstanding}')
print(f'due: {datetime.datetime.strftime(invoice.dueDate, "%Y-%m-%d %H:%M:%S")}')
invoice = Invoice(
[{"amount": 1}, {"amount": 2}, {"amount": 3}],
"zhangsan"
)
printOwing(invoice)
我们对静态打印语句进行了提取,同样,我们也可以提取 print detail 的部分,不过这一块是有参数的,可以将参数作为提炼的函数入参
每进行一次重构的修改,一定要重新进行测试
import datetime
class Invoice():
def __init__(self, orders, customer):
self.orders = orders
self.customer = customer
self.dueDate = ""
def printBanner():
print("***********************")
print("**** Customer Owes ****")
print("***********************")
def printDetails(invoice, outstanding):
print(f'name: {invoice.customer}')
print(f'amount: {outstanding}')
print(f'due: {datetime.datetime.strftime(invoice.dueDate, "%Y-%m-%d %H:%M:%S")}')
def printOwing(invoice):
outstanding = 0
printBanner()
# calculate outstanding
for o in invoice.orders:
outstanding += o["amount"]
# record due date
now = datetime.datetime.now()
invoice.dueDate = now + datetime.timedelta(days=30)
# print details
printDetails(invoice, outstanding)
invoice = Invoice(
[{"amount": 1}, {"amount": 2}, {"amount": 3}],
"zhangsan"
)
printOwing(invoice)
同样,record due date 也可以以相同的手法取出
import datetime
class Invoice():
def __init__(self, orders, customer):
self.orders = orders
self.customer = customer
self.dueDate = ""
def printBanner():
print("***********************")
print("**** Customer Owes ****")
print("***********************")
def printDetails(invoice, outstanding):
print(f'name: {invoice.customer}')
print(f'amount: {outstanding}')
print(f'due: {datetime.datetime.strftime(invoice.dueDate, "%Y-%m-%d %H:%M:%S")}')
def recordDueDate(invoice):
now = datetime.datetime.now()
invoice.dueDate = now + datetime.timedelta(days=30)
def printOwing(invoice):
outstanding = 0
printBanner()
# calculate outstanding
for o in invoice.orders:
outstanding += o["amount"]
# record due date
recordDueDate(invoice)
# print details
printDetails(invoice, outstanding)
invoice = Invoice(
[{"amount": 1}, {"amount": 2}, {"amount": 3}],
"zhangsan"
)
printOwing(invoice)
中间 calculate outstanding 一段是对 outstanding 局部变量的赋值,这块要怎么提炼呢?
很简单,只需要将 outstanding 移动到操作它的语句旁边,然后将其变为临时变量,在函数中将其以临时变量的身份处理后返回即可
import datetime
class Invoice():
def __init__(self, orders, customer):
self.orders = orders
self.customer = customer
self.dueDate = ""
def printBanner():
print("***********************")
print("**** Customer Owes ****")
print("***********************")
def printDetails(invoice, outstanding):
print(f'name: {invoice.customer}')
print(f'amount: {outstanding}')
print(f'due: {datetime.datetime.strftime(invoice.dueDate, "%Y-%m-%d %H:%M:%S")}')
def recordDueDate(invoice):
now = datetime.datetime.now()
invoice.dueDate = now + datetime.timedelta(days=30)
def calculateOutstanding(invoice):
outstanding = 0
for o in invoice.orders:
outstanding += o["amount"]
return outstanding
def printOwing(invoice):
printBanner()
# calculate outstanding
outstanding = calculateOutstanding(invoice)
# record due date
recordDueDate(invoice)
# print details
printDetails(invoice, outstanding)
invoice = Invoice(
[{"amount": 1}, {"amount": 2}, {"amount": 3}],
"zhangsan"
)
printOwing(invoice)
至此,我们将原先的一个函数 printOwing,拆分成了 5 个函数,每个函数只执行特定的功能,printOwing 是他们的汇总,此时其逻辑就显得非常清晰。
相对于提炼函数而言,在某些情况下我们需要反其道而行之。例如某些函数其内部代码和函数名称都清晰可读,而被重构了内部实现,同样变得清晰,那么这样的重构就是多余的,应当去掉这个函数,直接使用其中的代码。
重构前:
def report_lines(a_customer):
lines = []
gather_customer_data(lines, a_customer)
return lines
def gather_customer_data(out, a_customer):
out.append({"name": a_customer["name"]})
out.append({"location": a_customer["location"]})
print(report_lines({"name": "zhangsan", "location": "GuangDong Province"}))
重构后:
def report_lines(a_customer):
lines = []
lines.append({"name": a_customer["name"]})
lines.append({"location": a_customer["location"]})
return lines
print(report_lines({"name": "zhangsan", "location": "GuangDong Province"}))
当一段表达式非常复杂难以阅读时,我们可以用局部变量取代表达式进行更好的表达。你可能会认为增加一个变量显得冗余不够精简且占据了一部分内存空间。但实际上这部分的空间占用是微不足道的。虽然使用复杂表达式会让代码显得非常精简,但却难于阅读,这样的代码依旧是具有坏味道的。
重构前:
def price(order):
# price is base price - quantity discount + shipping
return order["quantity"] * order["item_price"] - \
max(0, order["quantity"] - 500) * order["item_price"] * 0.05 + \
min(order["quantity"] * order["item_price"] * 0.1, 100)
print(price({"quantity": 20, "item_price": 3.5}))
《重构》一书中提到,如果你觉得需要添加注释时,不妨先进行重构,如果重构后逻辑清晰,读者能够通过代码结构和函数名就理清逻辑,便不需要注释了。
这里便是如此,臃肿的计算表达式在不打注释的前提下很难理解到底为什么这样算。
重构后:
def price(order):
# price is base price - quantity discount + shipping
base_price = order["quantity"] * order["item_price"]
quantity_discount = max(0, order["quantity"] - 500) * order["item_price"] * 0.05
shipping = min(order["quantity"] * order["item_price"] * 0.1, 100)
return base_price - quantity_discount + shipping
print(price({"quantity": 20, "item_price": 3.5}))
当在类中时,我们可以将这些变量提炼成方法
重构前:
class Order(object):
def __init__(self, a_record):
self._data = a_record
def quantity(self):
return self._data["quantity"]
def item_price(self):
return self._data["item_price"]
def price(self):
return self.quantity() * self.item_price() - \
max(0, self.quantity() - 500) * self.item_price() * 0.05 + \
min(self.quantity() * self.item_price() * 0.1, 100)
order = Order({"quantity": 20, "item_price": 3.5})
print(order.price())
重构后:
class Order(object):
def __init__(self, a_record):
self._data = a_record
def quantity(self):
return self._data["quantity"]
def item_price(self):
return self._data["item_price"]
def base_price(self):
return self.quantity() * self.item_price()
def quantity_discount(self):
return max(0, self.quantity() - 500) * self.item_price() * 0.05
def shipping(self):
return min(self.quantity() * self.item_price() * 0.1, 100)
def price(self):
return self.base_price() - self.quantity_discount() + self.shipping()
order = Order({"quantity": 20, "item_price": 3.5})
print(order.price())
相对于提炼变量,有时我们也需要内联变量
重构前:
base_price = a_order["base_price"]
return base_price > 1000
重构后:
return a_order["base_price"] > 1000
一个好的函数名能够直观的表明函数的作用,然而我们在工作中经常能遇到前人所写乱七八糟的函数名并不打注释。这种情况下我们就需要进行函数名重构。
一种比较简单的做法就是直接修改函数名,并将调用处的函数名一并修改
另一种迁移式做法如下:
重构前:
def circum(radius):
return 2 * math.PI * radius
重构后:
def circum(radius):
return circumference(radius)
def circumference(radius):
return 2 * math.PI * radius
调用 circum 处全部修改为指向 circumference,待测试无误后,再删除旧函数。
还有一种特殊情况,就是重构后的函数需要添加新的参数
重构前:
_reservations = []
def add_reservation(customer):
zz_add_reservation(customer)
def zz_add_reservation(customer):
_reservations.append(customer)
重构后:
_reservations = []
def add_reservation(customer):
zz_add_reservation(customer, False)
def zz_add_reservation(customer, is_priority):
assert(is_priority == True || is_priority == False)
_reservations.append(customer)
通常,在修改调用方前,引入断言确保调用方一定会用到这个新参数是一个很好的习惯
函数的迁移较为容易,但数据麻烦的多。如果把数据搬走,就必须同时修改所有引用该数据的代码。如果数据的可访问范围变大,重构的难度就会随之变大,全局数据是大麻烦的原因。
对于这个问题,最好的办法是以函数的形式封装所有对数据的访问
重构前:
default_owner = {"first_name": "Martin", "last_name": "fowler"}
space_ship.owner = default_owner
# 更新数据
default_owner = {"first_name": "Rebecca", "last_name": "Parsons"}
重构后:
default_owner = {"first_name": "Martin", "last_name": "fowler"}
def get_default_owner():
return default_owner
def set_default_owner(arg):
default_owner = arg
space_ship.owner = get_default_owner()
# 更新数据
set_default_owner({"first_name": "Rebecca", "last_name": "Parsons"})
好的命名是整洁编程的核心,为了提升程序可读性,对于前人的一些不好的变量名称应当进行改名
如果变量被广泛使用,应当考虑运用封装变量将其封装起来,然后找出所有使用该变量的代码,逐一修改。
我们会发现,有一些数据项总是结伴出现在一个又一个函数中,将它们组织称一个数据结构将会使数据项之间的关系变得清晰。同时,函数的参数列表也能缩短。
重构之后所有使用该数据结构都会通过同样的名字来访问其中的元素,从而提升代码的一致性。
重构前:
station = {
"name": "ZB1",
"readings": [
{"temp": 47, "time": "2016-11-10 09:10"},
{"temp": 53, "time": "2016-11-10 09:20"},
{"temp": 58, "time": "2016-11-10 09:30"},
{"temp": 53, "time": "2016-11-10 09:40"},
{"temp": 51, "time": "2016-11-10 09:50"},
]
}
operating_plan = {
"temperature_floor": 50,
"temperature_ceiling": 54,
}
def reading_outside_range(station, min, max):
res = []
for info in station["readings"]:
if info["temp"] < min or info["temp"] > max:
res.append(info["temp"])
return res
alerts = reading_outside_range(station, operating_plan["temperature_floor"], operating_plan["temperature_ceiling"])
print(alerts)
重构 min 和 max 的方法比较简单的一种就是将其封装为一个类,同时,我们也能在类中添加一个方法用于测试 reading_outside_range
重构后:
station = {
"name": "ZB1",
"readings": [
{"temp": 47, "time": "2016-11-10 09:10"},
{"temp": 53, "time": "2016-11-10 09:20"},
{"temp": 58, "time": "2016-11-10 09:30"},
{"temp": 53, "time": "2016-11-10 09:40"},
{"temp": 51, "time": "2016-11-10 09:50"},
]
}
operating_plan = {
"temperature_floor": 50,
"temperature_ceiling": 54,
}
class NumberRange(object):
def __init__(self, min, max):
self._data = {"min": min, "max": max}
def get_min(self):
return self._data["min"]
def get_max(self):
return self._data["max"]
def contains(self, temp):
return temp < self._data["min"] or temp > self._data["max"]
def reading_outside_range(station, range):
res = []
for info in station["readings"]:
if range.contains(info["temp"]):
res.append(info["temp"])
return res
range = NumberRange(50, 54)
alerts = reading_outside_range(station, range)
print(alerts)
有一种情景,一组函数操作同一块数据(通常将这块数据作为参数传递给函数),那么此时我们可以将这些函数组装为一个类。类能明确地给这些函数提供一个共用的环境,在对象内部调用这些函数可以少传许多参数,从而简化函数调用,并且这样一个对象也可以更方便地传递给系统的其他部分
重构前:
reading = {
"customer": "ivan",
"quantity": 10,
"month": 5,
"year": 2017,
}
def acquire_reading():
return reading
def base_rate(month, year):
return year/month
def tax_threshold(year):
return year / 2
def calculate_base_charge(a_reading):
return base_rate(a_reading["month"], a_reading["year"]) * a_reading["quantity"]
a_reading = acquire_reading()
base_charge = base_rate(a_reading["month"], a_reading["year"]) * a_reading["quantity"]
a_reading = acquire_reading()
base = base_rate(a_reading["month"], a_reading["year"]) * a_reading["quantity"]
taxable_charge = max(0, base - tax_threshold(a_reading["year"]))
a_reading = acquire_reading()
basic_charge_amount = calculate_base_charge(a_reading)
print(base_charge)
print(taxable_charge)
print(basic_charge_amount)
重构后:
reading = {
"customer": "ivan",
"quantity": 10,
"month": 5,
"year": 2017,
}
class Reading(object):
def __init__(self, data):
self._customer = data["customer"]
self._quantity = data["quantity"]
self._month = data["month"]
self._year = data["year"]
def get_customer(self):
return self._customer
def get_quantity(self):
return self._quantity
def get_month(self):
return self._month
def get_year(self):
return self._year
def base_rate(self):
return self._year / self._month
def get_calculate_base_charge(self):
return self.base_rate() * self._quantity
def tax_threshold(self):
return self._year / 2
def acquire_reading():
return reading
raw_reading = acquire_reading()
a_reading = Reading(raw_reading)
base_charge = a_reading.get_calculate_base_charge()
base = a_reading.get_calculate_base_charge()
taxable_charge = max(0, base - a_reading.tax_threshold())
basic_charge_amount = a_reading.get_calculate_base_charge()
print(base_charge)
print(taxable_charge)
print(basic_charge_amount)
对于 9 的问题还有一种重构方案,那就是将函数组合成变换。简单来说就是放弃将函数组装成类,而是组装到一个函数中。在这个函数里对组装进来的函数进行增强和变换。
重构前:
reading = {
"customer": "ivan",
"quantity": 10,
"month": 5,
"year": 2017,
}
def acquire_reading():
return reading
def base_rate(month, year):
return year/month
def tax_threshold(year):
return year / 2
def calculate_base_charge(a_reading):
return base_rate(a_reading["month"], a_reading["year"]) * a_reading["quantity"]
a_reading = acquire_reading()
base_charge = base_rate(a_reading["month"], a_reading["year"]) * a_reading["quantity"]
a_reading = acquire_reading()
base = base_rate(a_reading["month"], a_reading["year"]) * a_reading["quantity"]
taxable_charge = max(0, base - tax_threshold(a_reading["year"]))
a_reading = acquire_reading()
basic_charge_amount = calculate_base_charge(a_reading)
print(base_charge)
print(taxable_charge)
print(basic_charge_amount)
重构后:
reading = {
"customer": "ivan",
"quantity": 10,
"month": 5,
"year": 2017,
}
def acquire_reading():
return reading
def base_rate(month, year):
return year/month
def tax_threshold(year):
return year / 2
def calculate_base_charge(a_reading):
return base_rate(a_reading["month"], a_reading["year"]) * a_reading["quantity"]
def enrich_reading(original):
original["base_charge"] = calculate_base_charge(original)
original["taxable_charge"] = max(0, original["base_charge"] - tax_threshold(original["year"]))
return original
raw_reading = acquire_reading()
a_reading = enrich_reading(raw_reading)
base_charge = a_reading["base_charge"]
taxable_charge = a_reading["taxable_charge"]
basic_charge_amount = calculate_base_charge(a_reading)
print(base_charge)
print(taxable_charge)
print(basic_charge_amount)
如果有一段代码在同时处理多件不同的事,那么我们就会习惯性地将其拆分成各自独立的模块。这样到了需要修改的时候,我们可以单独处理每个主题。
重构前:
def price_order(product, quantity, shipping_method):
base_price = product["base_price"] * quantity
discount = max(quantity - product["discount_threshold"], 0) * product["base_price"] * product["discount_rate"]
shipping_per_case = shipping_method["discounted_fee"] if base_price > shipping_method["discount_threshold"] else shipping_method["fee_per_case"]
shipping_cost = quantity * shipping_per_case
price = base_price - discount + shipping_cost
return price
这段代码里前两行根据 product 信息计算订单中与商品相关的价格,随后两行根据 shipping 信息计算配送成本。因此可以将这两块逻辑拆分。
def price_order(product, quantity, shipping_method):
base_price = product["base_price"] * quantity
discount = max(quantity - product["discount_threshold"], 0) * product["base_price"] * product["discount_rate"]
price = apply_shipping(base_price, shipping_method, quantity, discount)
return price
def apply_shipping(base_price, shipping_method, quantity, discount):
shipping_per_case = shipping_method["discounted_fee"] if base_price > shipping_method["discount_threshold"] else shipping_method["fee_per_case"]
shipping_cost = quantity * shipping_per_case
price = base_price - discount + shipping_cost
return price
接下来我们可以将需要的数据以参数形式传入
def price_order(product, quantity, shipping_method):
base_price = product["base_price"] * quantity
discount = max(quantity - product["discount_threshold"], 0) * product["base_price"] * product["discount_rate"]
price_data = {"base_price": base_price, "quantity": quantity, "discount": discount}
price = apply_shipping(price_data, shipping_method)
return price
def apply_shipping(price_data, shipping_method):
shipping_per_case = shipping_method["discounted_fee"] if price_data["base_price"] > shipping_method["discount_threshold"] else shipping_method["fee_per_case"]
shipping_cost = price_data["quantity"] * shipping_per_case
price = price_data["base_price"] - price_data["discount"] + shipping_cost
return price
最后,我们可以将第一阶段代码独立为函数
def price_order(product, quantity, shipping_method):
price_data = calculate_pricing_data(product, quantity)
return apply_shipping(price_data, shipping_method)
def calculate_pricing_data(product, quantity):
base_price = product["base_price"] * quantity
discount = max(quantity - product["discount_threshold"], 0) * product["base_price"] * product["discount_rate"]
return {"base_price": base_price, "quantity": quantity, "discount": discount}
def apply_shipping(price_data, shipping_method):
shipping_per_case = shipping_method["discounted_fee"] if price_data["base_price"] > shipping_method["discount_threshold"] else shipping_method["fee_per_case"]
shipping_cost = price_data["quantity"] * shipping_per_case
return price_data["base_price"] - price_data["discount"] + shipping_cost