去年十二月, 重读时, 输出了几篇博文, 主要几章重构技巧梳理 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流程. 在平时, 就有意识地控制代码质量