内容简介:重构 - 读书笔记(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流程. 在平时, 就有意识地控制代码质量
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。