重构 - 读书笔记(Python示例)
去年十二月, 重读时, 输出了几篇博文, 主要几章重构技巧梳理 6/7/8/9/10/11, 这周重读时, 从另一个角度总结一下
我们总是想着, 找个时间重构, 额, 其实, 重构更应该放在平时, 每一次去变更代码时处理. 毕竟, 所谓的重构契机有时候太过遥远; 而如果不做重构, 痛苦的是每时每刻维护代码的自己
如果你发现自己需要为程序添加一个特性, 而代码结构使你无法很方便地达成目的, 那就先重构那个程序, 使特性的添加比较容易进行, 然后再添加特性
另外, 如果可能, 尽量加单元测试, 哪怕一次只增加一两个, 一段时间后, 你会发现, 你会感谢过去的自己
原则
- 小步前进, 频繁测试
- 隔离变化
- 控制可见范围, 让变量/常量/函数/类等, 在最小的范围内可见. 例如设为私有变量/私有函数, 移除不必要的设值函数
- 重构时, 不要关注性能. 到性能优化阶段, 再关注性能. 不同阶段关注点不一样, 不要过早优化. 很多时候, 性能并不是瓶颈, 可读性和可维护性更重要
- 任何时候, 都不要拷贝代码, 拷贝类, 甚至拷贝源码文件
1. 命名
- 好的名字, 清晰表达其含义. 命名至关重要
- 好的代码应该清楚表达出自己的功能, 变量名称是代码清晰的关键
- 如果为了提高代码的可读性, 需要修改某些名字, 大胆去改!
- IDE/单元测试/好的查找替换工具
- 建议读
编写可读代码的艺术
这本书.
2. 常量和临时变量
提取常量
你有一个字面数值, 带有特别含义. 创建一个常量, 根据其意义为它命名, 并将上述字面数值替换为这个常量
def potential_energy(mass, height):
return mass * 9.81 * height
to
GRAVITATIONAL_CONSTANT = 9.81
def potential_energy(mass, height):
return mass * GRAVITATIONAL_CONSTANT * height
任何时候, 都不要拷贝常量, 当你发现要改一个数据, 要到非常多的文件去改字面值时, 你就需要意识到, 该提取常量了
加入: 引入解释性变量
一个复杂的表达式, 将复杂表达式或其中一部分放入临时变量, 以变量名称来解释表达式用途
if "MAC" in platform.upper() and "IE" in browser.upper() and was_initialized() and resize > 0:
#do something
to
is_macos = "MAC" in platform.upper()
is_ie_browser = "IE" in browser.upper()
was_resized = resize > 0
if is_macos and is_ie_browser and was_initialized() and was_resized:
# do something
分解: 分解临时变量
某个临时变量被赋值超过一次, 非循环变量, 也不用于收集计算结果.每次赋值, 创砸一个独立, 对应的临时变量
单一职责原则
tmp = 2 * (height * width)
print tmp
tmp = height * width
print tmp
to
perimeter = 2 * (height * width)
print perimeter
area = height * width
print area
去除: 移除临时变量
临时变量仅被一个简单表达式赋值一次, 可以去除这个临时变量
临时变量, 简单表达式, 另外, 需要考虑使用次数, 如果仅使用一次, 可以去除, 如果多次, 则需谨慎考虑对可读性的而影响
best_price = order.base_price()
return best_price > 1000
to
return order.base_price > 1000
移除: 控制标记
在一系列布尔表达式中, 某个变量带有"控制标记"(control flag)的作用. 以break语句或return取代控制标记
def dosomething():
is_success = False
if xxx:
is_success = True
if yyy:
is_success = False
...
return is_success
to
def dosomething():
if xxx:
return True
if yyy:
return True
...
return False # 一定不要忘记
注意力相关.
这类逻辑中, 很痛苦的是, 你必须无时无刻关注这些控制标记的值, 追踪
变量在每一个逻辑之后的变化, 会带来额外的思考负担, 从而让代码变得不易读.
3. 函数
拆分: Extract Method提炼函数
你有一段代码可以被组织在一起并独立出来, 将这段代码放进一个独立函数中, 并让函数名称解释该函数的用途
def print_owing(double amount):
print_banner()
// print details
print "this is the detail: "
print "amnount: %s" % amount
to
def print_details(amount):
print "this is the detail: "
print "amnount: %s" % amount
def print_owing(double amount):
print_banner()
print_details(amount)
去除: Inline Method内联函数
一个函数的本体与名称同样清楚易懂, 在函数调用点插入函数本体, 然后移除该函数
小型函数, 函数太过简单了, 可能只有一个表达式, 去除函数!
def is_length_valid(x):
return len(x) > 10
print 'the length is %s' % ('valid' if is_length_valid(x) else 'invalid')
to
print 'the length is %s' % ('valid' if len(x) > 10 else 'invalid)
合并: 合并多个函数, 使用参数
若干函数做了类似的工作. 但在函数本体中却包含了不同的值. 建立单一函数, 以参数表达那些不同的值
def five_percent_raise():
pass
def ten_percent_raise():
pass
to
def percent_raise(percent):
pass
副作用: 函数不应该有副作用
某个函数既返回对象状态值, 又修改对象状态. 建立两个不同函数, 一个负责查询, 一个负责修改.
单一职责原则, 一个函数不应该做两件事, 函数粒度尽量小.
4. 表达式
guard(注意力相关)
过多的条件逻辑, 难以理解正常的执行路径. 在python中的特征是, 缩进太深
coolshell中曾经讨论过的问题 如何重构“箭头型”代码, 而在python中的现象是, 缩进嵌套层级太深, 有时候甚至有十几层缩进, 整体难以理解
而减少嵌套缩进的方式是, 使用guard
语句, 尽早返回,
注意力相关, 尽早return
, 你也就不用关心已经过去的逻辑了, 只需关注后面代码的逻辑.
if _is_dead:
result = dead_amount()
else:
if _is_separated:
result = separated_amount()
else:
if _is_retired:
result = retired_amount()
else:
result = normal_payamount()
return result
to
if _is_dead:
return dead_amount()
if _is_separated:
return separated_amount()
if _is_retired:
return retired_amount()
return normal_payamount()
合并: 合并条件表达式
你有一系列条件测试, 都得到相同结果. 将这些测试合并成一个条件表达式, 并将这个条件表达式提炼成为一个独立函数
if _seniority < 2:
return 0
if _months_disabled > 10:
return 0
if _is_part_time:
return 0
to
if is_not_eligible_for_disability:
return 0
分解: 分解复杂条件表达式
你有一个复杂的条件语句(if-then-else). 从if, the, else三个段落中分别提炼出独立函数
if date < SUMMER_START) or date > SUMMER_END:
charge = quantity * _winter_rate + _winter_servioce_charge
else:
charge = quantity * _summer_rate
to
if not_summber(date):
charge = winter_charge(quantity)
else:
charge = summber_charge(quantity)
提取: 合并重复的条件片段
在条件表达式的每个分支上有着相同的一段代码. 将这段重复代码搬移到条件表达式之外
if is_special:
total = price * 0.95
send()
else:
total = price * 0.98
send()
to
if is_special:
total = price * 0.95
else:
total = price * 0.98
send()
这是维护系统, 特别是中后期很容易忽略的问题. 很容易在代码中出现, 特别是遇到那种加需求
的地方, 通常, 会选择不动原来的代码, 加个分支, 复制代码下来改. 但这样的后果是, 逐步地, 会发现每个分支中都有重复代码.
5. 参数及返回值
参数和返回值: 提取对象
如果参数/返回值是一组相关的数值, 且总是一起出现, 可以考虑提取成一个对象.
def get_width_height():
....
return width, height
def get_area(width, height):
return width, height
to
class Rectangle(object):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
def get_shape():
....
return Rectangle(height, width)
类似的还有: start_time/end_time -> TimeRange
/
减少参数
对象调用了某个函数, 并将所得结果作为参数, 传递给另一个函数. 而接受该参数的函数本身也能调用前一个函数. 让参数接收者去除该参数, 并直接调用前一个函数
base_price = quantity * item_price
discount_level = get_discount_level()
final_price = discounted_price(base_price, discount_level)
to
base_price = quantity * item_price
final_price = discounted_price(base_price)
6. 类
搬移: 函数/字段
- 搬移函数: 某个函数与所在类之外的另一个类有更多的交互, 调用或被调用(例如: 使用另一个对象的次数比使用自己所在对象的次数还多). 即, 跟另一个类更相关. 则搬移过去
- 搬移字段: 某个字段被其所在类之外的另一个类更多地用到
拆分: 拆分类
某个类做了应该由两个类做的事. 类太大/太臃肿. 建立一个新类, 将相关字段和函数从旧类版移到新类
特征: 类中某些字段是有关系的整体, 或者有相同的前缀
class Persion(object):
def __init__(self, name, age, office_area_code, office_number):
self.name = name
self.age = age
self.office_area_code = office_area_code
self.office_number = office_number
def get_phone_number(self):
return "%s-%s" % (self.office_area_code, self.office_number)
to
class Person(object):
def __init__(self, name, age, office_area_code, office_number):
self.name = name
self.age = age
self.phone_number = PhoneNumber(office_area_code, office_number)
def get_phone_number(self):
return self.phone_number.get_number()
class PhoneNumber(object):
def __init__(self, area_code ,number):
self.area_code = area_code
self.number = number
def get_number(self):
return "%s-%s" % (self.area_code, self.number)
去除
一个类没有做太多的事情, 不再有独立存在的理由.
7. 模式
原则:
- 慎用
- 只使用你理解的模式
- 只在符合的业务场景使用对应模式
adapter
你需要为提供服务的类增加功能, 但是你无法修改这个类.
使用组合(推荐, 持有对象)/继承(加子类), 持有该对象, 增加对应附加功能
adapter思维.
使用场景: 使用一些第三方库处理外部依赖, 例如依赖一个系统, 业务A
(requests)/es
(Elasticsearch)/redis
(redispy), 但是, 基于第三方系统, 你需要有自己业务相关的统一处理逻辑, 此时, 你可以建立一个XXClient
, 持有第三方组件底层调用逻辑, 同时封装自身业务逻辑, 在上层直接调用
facade
适配模式中举的例子, 也有facade
的思想, 将复杂的东西, 统一封装, 对外提供相对简单清晰地接口
template method
出现的次数也很高
装饰器
python中最常用
其他
根据使用场景, 应用策略/桥梁/工厂/观察者等等, 具体看业务场景
举例
重构一个相对较大的django
项目
- 明确业务对象, 对象概念, 对象边界
- 明确分层
- 明确代码目录结构, 划分模块, 明确每个模块可以放入的东西
- 粗粒度重构: 移动模块/类/函数, 根据前几步的划分, 将模块/类/函数等, 移动到对应模块中, 同时, 修改
import
和调用点 - 中粒度重构: 根据
django
项目本身划分, 移动函数 - 中粒度重构: Extract Method. 读具体函数代码, 遇到
重复代码 / 过长函数 / 过大的类 / 超大的if-else或switch / 包含大段注释的代码
等, 思考, 提炼函数, 放入对应模块 - 细粒度重构: 提取常量 / 提取枚举 / 修改模块名类名函数名变量名
举例:
- 对于
django
项目, 原则fat models, helper modules, thin views, stupid templates
fat model
, 将对象本身相关的, 尽量放入models
, 这个对象相关的, 可以加入补充一系列porperty
/classmethod
/staticmethod
, 可以有效地降低使用这个对象时调用处的代码复杂度. 例如, 每次取兑现改一个字段都需要进行转换, 则搞个property
替换每次都需要的转换逻辑. (找拿到model
对象后的处理逻辑代码中那些反复出现的, 重复的)- 将对象查询相关的, 全部迁移到
manager
中, 需要先通过Model.objects
查询然后做各种事情的, 迁移放入到manager
中 utils
, 将业务逻辑无关的工具函数等, 统一归入utils
模块中; 将业务有关但多个application
共用的utils
放入到common.utils
模块中, 而将appication
依赖的局部utils
, 放入到application.utils
中constants
, 同上, 区分通用, 还是某个applications
中使用thin view
, 业务逻辑, 尽量瘦小简短stupid template
, 模板, 尽量傻瓜, 不要包含复杂计算/判断逻辑, 将复杂迁移到后端代码
其他
善用工具, 有方案设计评审, 平时通过pull request
, 走code review
, 有代码风格自动检查, 要求单元测试, 走cicd流程. 在平时, 就有意识地控制代码质量