重构技战术(一)——通用型重构技巧

书接上回,重构从现在开始 一文中我们讨论了重构的含义、意义以及时机,并说明了测试的重要性。从本文开始将介绍重构具体的技巧,首先登场的是一些通用型技巧。

1 提炼函数

提炼函数应该是最常见的重构技巧。我们都不希望把一大堆东西塞在一个函数里,使得一个函数做了一大堆事,这样无论是阅读还是修改都会很累(我曾经在字节跳动就见过一个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 是他们的汇总,此时其逻辑就显得非常清晰。

2 内联函数

相对于提炼函数而言,在某些情况下我们需要反其道而行之。例如某些函数其内部代码和函数名称都清晰可读,而被重构了内部实现,同样变得清晰,那么这样的重构就是多余的,应当去掉这个函数,直接使用其中的代码。

重构前:

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"}))

3 提炼变量

当一段表达式非常复杂难以阅读时,我们可以用局部变量取代表达式进行更好的表达。你可能会认为增加一个变量显得冗余不够精简且占据了一部分内存空间。但实际上这部分的空间占用是微不足道的。虽然使用复杂表达式会让代码显得非常精简,但却难于阅读,这样的代码依旧是具有坏味道的。

重构前:

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())

4 内联变量

相对于提炼变量,有时我们也需要内联变量
重构前:

base_price = a_order["base_price"]
return base_price > 1000

重构后:

return a_order["base_price"] > 1000

5 改变函数声明

一个好的函数名能够直观的表明函数的作用,然而我们在工作中经常能遇到前人所写乱七八糟的函数名并不打注释。这种情况下我们就需要进行函数名重构。

一种比较简单的做法就是直接修改函数名,并将调用处的函数名一并修改
另一种迁移式做法如下:
重构前:

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)

通常,在修改调用方前,引入断言确保调用方一定会用到这个新参数是一个很好的习惯

6 封装变量

函数的迁移较为容易,但数据麻烦的多。如果把数据搬走,就必须同时修改所有引用该数据的代码。如果数据的可访问范围变大,重构的难度就会随之变大,全局数据是大麻烦的原因。

对于这个问题,最好的办法是以函数的形式封装所有对数据的访问
重构前:

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"})

7 变量改名

好的命名是整洁编程的核心,为了提升程序可读性,对于前人的一些不好的变量名称应当进行改名

如果变量被广泛使用,应当考虑运用封装变量将其封装起来,然后找出所有使用该变量的代码,逐一修改。

8 引入参数对象

我们会发现,有一些数据项总是结伴出现在一个又一个函数中,将它们组织称一个数据结构将会使数据项之间的关系变得清晰。同时,函数的参数列表也能缩短。

重构之后所有使用该数据结构都会通过同样的名字来访问其中的元素,从而提升代码的一致性。

重构前:

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)

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,
}

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)

10 函数组合成变换

对于 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)

11 拆分阶段

如果有一段代码在同时处理多件不同的事,那么我们就会习惯性地将其拆分成各自独立的模块。这样到了需要修改的时候,我们可以单独处理每个主题。

重构前:

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
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值