1. 第一组重构
1 提炼函数
1.1 提炼函数的动机
- 何时重构:需要花时间才能弄懂一段代码
对于api,我们不需要去弄明白代码如何实现,只需要知道功能,则不需要重构
- 大量短小函数会让程序跑的更快,更容易被缓存
- 当函数超过10行就应该考虑提炼函数
1.2 做法
- 创造新函数,根据意图进行命名(”做什么“来命名,而不是“怎么做”命名)
- 将待提炼代码复制到新的函数中
- 若提炼的代码引用了作用域限于源函数,则以参数的形式传递给新函数
- 在源函数中,将被提炼代码段替换为对目标函数的调用
- 测试
- 若其他代码与被提炼代码有相似处,使用以函数调用来取代内敛代码
import datetime
class Invoice(object):
def __init__(self,orders,name):
self.orders=orders
self.customer=name
def printOwing(invoice:Invoice)->None:
outstanding=0
# 打印账单头
print("*******************************")
print("****Customer Owes ****")
print("*******************************")
# 计算总欠款
for o in invoice.orders:
outstanding+=o
# 计算还款时间
today=datetime.datetime.now().date()
invoice.dueDate=today+datetime.timedelta(days=30)
# 打印账单详情
print("name: ",invoice.customer)
print("amount: ",outstanding)
print("due: ",invoice.dueDate)
if __name__ == '__main__':
orders=[1,2,3,4,4]
invorice=Invoice(orders,"zhangsan")
printOwing(invorice)
- 打印账单头:没有局部变量
- 计算总欠款:引用了源函数的变量
- 计算还款时间:引用了源函数的变量
- 打印账单详情:引用源函数的变量,还引用了计算总欠款处理后的变量
import datetime
class Invoice(object):
def __init__(self,orders,name):
self.orders=orders
self.customer=name
def printOwing(invoice:Invoice)->None:
printBanner()
outstanding = calculateOutstanding(invoice)
recordDueDate(invoice)
printDetails(invoice, outstanding)
def printDetails(invoice, outstanding):
print("name: ", invoice.customer)
print("amount: ", outstanding)
print("due: ", invoice.dueDate)
def recordDueDate(invoice):
today = datetime.datetime.now().date()
invoice.dueDate = today + datetime.timedelta(days=30)
def calculateOutstanding(invoice):
outstanding=0
for o in invoice.orders:
outstanding += o
return outstanding
def printBanner():
print("*******************************")
print("****Customer Owes ****")
print("*******************************")
if __name__ == '__main__':
orders=[1,2,3,4,4]
invorice=Invoice(orders,"zhangsan")
printOwing(invorice)
2. 内联函数
即将函数调用替换成代码段
2.1 动机
- 函数的内部代码和函数名称一样清晰易读
- 对于一群组织不合理的函数,可以先将它们内联到大型函数中,再以我们喜欢的方式重写提炼成小函数
2.2 做法
- 首先检查函数,确定它不具有多态性(
如果一个函数属于一个类,并且有子类继承了这个函数,那么就无法内联
) - 找出这个函数的所有调用点
- 将这个函数的所有调用点都替换成函数本体
- 每次替换后,进行测试(
小步小步这样好测试
) - 删除函数的定义
class Customer(object):
def __init__(self,name,location):
self.name=name
self.location=location
def gatherCustomerData(out:dict,acustomer:Customer):
out["name"]=acustomer.name
out["location"]=acustomer.location
def reportLines(aCustomer:Customer):
lines={}
gatherCustomerData(lines,aCustomer)
return lines
if __name__ == '__main__':
aCustomer=Customer("zhangsan","hanan")
lines = reportLines(aCustomer)
for line in lines:
print(lines[line])
- 收集数据可以内联
class Customer(object):
def __init__(self,name,location):
self.name=name
self.location=location
def reportLines(aCustomer:Customer):
lines={}
lines["name"]=aCustomer.name
lines["location"]=aCustomer.location
return lines
3. 提炼变量
引入解释性变量
3.1 动机
- 表达式复杂难懂
3.2 做法
- 确认要提炼的表达式有没有副作用
- 声明一个不可修改的变量,把你想要提炼的表达式复制一份,并将其赋值给这个变量。
- 用这个变量来取代原来的表达式
- 测试
class Order(object):
def __init__(self,itemPrice,quantity):
self.itemPrice=itemPrice
self.quantity=quantity
def price(order:Order):
return order.quantity*order.itemPrice-\
max(0,order.quantity-500)*order.itemPrice*0.05+\
min(order.quantity*order.itemPrice*0.1,100)
if __name__ == '__main__':
order=Order(9,888)
amout = price(order)
print(amout)
class Order(object):
def __init__(self,itemPrice,quantity):
self.itemPrice=itemPrice
self.quantity=quantity
def price(order:Order):
basePrice=order.quantity*order.itemPrice
quantityDiscount=max(0,order.quantity-500)*order.itemPrice*0.05
shipping=min(order.quantity*order.itemPrice*0.1,100)
return basePrice-quantityDiscount+shipping
if __name__ == '__main__':
order=Order(9,888)
amout = price(order)
print(amout)
4. 内联变量
4.1 动机
- 当变量名并不比表达式本身更具有表现力时,我们可以进行内联变量
4.2 做法
- 确认变量复制语句的右侧的表达式没有副作用
- 如果变量没有被声明为不可修改,则先将其变为不可修改,并执行测试(
确保该变量只被赋值一次
) - 找到第一处使用该变量的地方,替换成表达式
- 测试
- 重复前面两步,直到所有该变量处都被替换
- 删除变量声明和赋值语句
- 测试
5. 改变函数声明
改变函数签名
5.1 函数最重要的是什么
- 函数名字:好的函数名字,就是通过函数名就知道函数的用途
- 参数列表:增加函数应用范围,改变连接模块条件,去除耦合
反例
class Person(object):
def __init__(self,phone):
self.phone=phone
def format_phone_number(person:Person):
"""
将个人电话号码转换成特定格式
"""
- 方法跟Person耦合,有必要吗。
正例
class Person(object):
def __init__(self,phone):
self.phone=phone
def format_phone_number(phone:str):
"""
将电话号码转换成特定格式
"""
- 只依赖需要的内容。
5.2 为何更改函数申明
- 为了使函数名更好的解释函数用途
- 参数列表时函数的上下文,为了使得参数列表更合理
5.3 做法
5.3.1 简单做法
- 修改函数参数
- 修改函数声明
- 替换与测试
5.3.2 迁移式做法
- 有必要先对函数体内部加以重构,使得后面的提炼步骤易于开展
- 使用提炼函数将函数体提炼成一个新的函数(可以起一个临时的名字)
- 如果提炼出的函数需要新增参数,用前面的简单做法添加即可
- 测试
- 对旧函数使用
内联函数
- 如果新函数使用了临时名字,再次使用
改变函数声明
将其改回原来的名字 - 测试
如果需要改名的函数时一个对外界提供的API,则在提炼出新函数之后就可以暂停重构了,可以将原来的函数声明标位不推荐使用(deprecated)。这样给客户端一点时间转用新的函数。
5.4 case1
class Book(object):
def __init__(self)->None:
self._reservations=[]
# 接收用户预订
def add_reservation(self,customer):
self._reservations.append(customer)
if __name__ == '__main__':
a=Book().add_reservation("zhangsan")
- 添加参数重构
class Book(object):
def __init__(self)->None:
self._reservations=[]
# 接收用户预订
def add_reservation(self,customer):
"""
不推荐使用
"""
self.add_reservation_v2(customer,False)
# 新的接收用户预订方法
def add_reservation_v2(self,customer,is_priority):
# 根据优先级进行一系列处理,省略。。。。
self._reservations.append(customer)
if __name__ == '__main__':
a=Book().add_reservation("zhangsan")
5.5 case2
# 判断一个人是否来自新西兰
def in_new_england(a_customer):
return a_customer.address.state in ["MA","CT","ME","VT","NH","RI"]
# 判断一个人是否来自新西兰
def in_new_england(a_customer):
"""
已废弃
"""
state_code=a_customer.address.state
return in_new_enland_v2(state_code)
# 新版本
def in_new_enland_v2(state_code):
return state_code in ["MA","CT","ME","VT","NH","RI"]
6. 引入参数对象
6.1 动机
- 当一组变量总是结伴而行,出没于一个有一个函数。可以考虑将其构造参数对象
6.2 做法
- 如果没有何时的数据结构,就创建一个
- 测试
- 使用改变函数申明给原来的函数新增一个参数,类型是新建的数据结构
- 测试
- 调整所有调用者,传入新数据结构的适当实例,每修改一处,执行测试
- 用新数据结构的每项元素,逐一取代参数列表中与之对应的参数项,然后删除原来的参数,测试
7. 函数组合成类
7.1 动机
- 当一组函数形影不离地操作统一块数据,那么这时候就该考虑建立一个类了。
- 类能明确地给这些函数提供一个共用的环境,在对象内部调用这些函数可以少传许多参数。
7.2 做法
- 将多个函数共用的数据记录加以封装成一个数据结构,然后作为类的属性
- 将使用这个数据记录的每个函数,都搬移到这个新建类中
- 用以处理数据记录的逻辑可以用提炼函数提炼出来,加入新类中
8. 函数组合成变换函数
8.1 动机
- 在软件设计时,通常需要把数据喂给一个程序,让它计算出各种派生信息,而且这些派生信息可以在多处被用到。
- 此时可以将这些派生数据和源数据一起收集起来。
- 做法就是,利用源数据创建一个类,这个类包含源数据和源数据的派生信息。
8.2 做法
- 创建一个变换函数,它输入是源数据,输出是带有附加信息的数据
- 在变换函数中,调用了计算附加信息的函数。
9. 拆分阶段
9.1 动机
- 当一段代码在同时处理两件不同的事情,就应该想着把它拆分成各自独立的模块
- 如构造与表示分离
9.2 做法
- 将第二阶段的代码提炼成独立的函数(比如第二阶段是表示数据)
- 测试
- 引入一个中转数据结构,将其作为参数添加到提炼出的新函数的参数列表中
- 测试
- 逐一检查提炼出的”第二阶段函数“的每个参数,如果某个参数被第一阶段用到,则就将其移动到中转数据结构中。每次搬移都要进行测试。
- 对第一阶段的代码运用提炼函数,让提炼出的函数返回中转数据结构
def priceOrder(product,quantity,shippingMethod):
# 基本价格
basePrice = product.basePrice * quantity
# 折扣
discount = max(quantity - product.discountThreshold, 0) * \
product.basePrice * product.discountRate;
# 最终价格
price = applyShipping(basePrice, shippingMethod, quantity, discount)
return price
def applyShipping(basePrice, shippingMethod, quantity, discount):
# 每件运费
shippingPerCase = shippingMethod.discountedFee \
if basePrice > shippingMethod.discountThreshold \
else shippingMethod.feePerCase
# 总运费
shippingCost = quantity * shippingPerCase
# 最终价格
price = basePrice - discount + shippingCost
return price
def calculatePricingData(product, quantity):
priceData = PricingData()
# 基本价格
priceData.basePrice = product.basePrice * quantity
# 折扣
priceData.discount = max(quantity - product.discountThreshold, 0) * \
product.basePrice * product.discountRate;
# 数量
priceData.quantity=quantity
return priceData
def applyShipping(priceData, shippingMethod):
# 每件运费
shippingPerCase = shippingMethod.discountedFee \
if priceData.basePrice > shippingMethod.discountThreshold \
else shippingMethod.feePerCase
# 总运费
shippingCost = priceData.quantity * shippingPerCase
# 最终价格
price = priceData.basePrice - priceData.discount + shippingCost
return price
def priceOrder(product,quantity,shippingMethod):
# 构造计算所需要的数据
priceData = calculatePricingData(product, quantity)
# 计算总价
return applyShipping(priceData, shippingMethod)