Refactory in Python: 使用受控类型而不是Hash和tuple

《Clean Code》中提到了函数传递的参数不应该超过3个,如果超过三个推荐将它们变为符合数据结构。
在Python的应用中,对于这个场景,我看到一般的做法就是使用Hash。其实Hash就是一种Key-Value store,是一种弱类型的结构,好比一个没有shema的object。这样做起初看来是很舒服的,因为它自然的让你可以访问到一个复合数据结构。可是据我观察这一般都是Bad smell。缺点在于:

  • 你不知道这个Hash中有哪些key,经常会造成需要翻看很多代码才能知道key有哪些,从哪里来的。
  • 由于Hash是弱数据类型,你很有可能把Key写错,如有的地方用了”User”有的地方是”user”,就有可能发生由于误解造成的bug。
  • 因为Hash是一个开放数据结构,所以你无法控制它被get和set,这容易造成你的hash在多次传递过程中被意外的覆盖,这是非常危险的一种bug。
  • 如果使用了Hash,随着时间的推移它很有可能成为垃圾桶。就是一个全局的Hash被传递很多层,大家都依赖它并向里面插入自己的数据。由于这样的Hash根本不可能知道自己的数据属于哪些领域模型,所以它可能会承载多个领域模型的部分数据结构。这对复杂度来说是一个灾难,由于数据和行为分离,它会鼓励系统模块间函数的拷贝和粘贴,最后造成一团存在大量重复的乱麻。如果你的代码发展到这个地步,那它基本上是维护和重构的地域。
  • python中hash的访问方式不好看,你需要hash[“user”]这样去访问。:D

对于以数据为中心的系统,你看到一个Hash被传递于超过一次函数调用,那么你最好对它马上进行重购。将它处理为一个可控的数据类型(如Class和Namedtuple)。
非受控类型还有另外一种表现形式,那就是tuple。tuple让python变得强大,尤其是它被用在平行赋值的情况下,如:

def fucntion_a():
    a, b = 10, 20
    return a, b

def function_b():
    price, amount = function_a()

这种情况下它有很不错的表现力。但是在非平行赋值的情况下使用tuple传递数据结构就是一种Bad smell了。如:

def function_a():
     my_tuple = ('tin', 'male', 28)
     return my_tuple

def function_b():
     a_tuple = function_a()
     name = a_tuple[0]
     age = a_tuple[2]
     .....

这种情况的邪恶是使用了顺序来约定数据结构,它比起hash来说明显的问题就是僵化、不容易阅读。僵化是说当你发现需要传递更多数据的时候你需要在顺序中添加新的元素,此时如果你像插入在前面几个元素之间,你会面临大量的index修改操作,非常容易造成bug。不容易阅读是,如果不在同一个文件中,你怎么知道0, 1, 2分别是哪个属性的index,为此你肯定花大量力气写注释,而且这个注释需要随你的修改而修改,非常恼人,遗漏了就是bug。所以它们那是明显的bad smell。
上面两种情况属于同样一种情况,那就是使用非受控数据结构表示结构化数据。那么如何“使用受控类型而不是Hash和tuple”呢?一般来说很简单。一种是写一个数据结构的Class,这种情况是你发现这里的数据结构是潜在的领域模型的时候非常有用:

class BusinessLogData:
    date = datetime.now()
    username = 'Unknow user'
    business_type = ''
    .....

好处是当你发现系统中围绕这个数据结构的算法或者说行为的时候你马上就有一个地方存放这些逻辑了,会让你很舒服。而且逐渐的你的领域模型就会清晰了,这对没有使用领域模型驱动的系统进行重构的时候很有用。另外一种情况,你可能知道没有什么逻辑在数据结构上,它就是给算法访问的纯数据type。那么可以使用namedtuple。它实际上是一种元编程。官方的例子是这样的。

Point = namedtuple('Point', 'x y', verbose=True)
p1 = Point(11, y=22)
p2 = Point._make((11, 22))

我个人认为namedtuple非常方便,很象ruby中的struct。它对于已经使用tuple传递数据的系统的重构非常有效,因为_make这样使用tuple创建namedtuple实例的方法非常适合重构,可以马上消除0, 1, 2这样的index magic number。而且在使用sql的时候它也是非常有用的工具,官方例子:

EmployeeRecord = namedtuple('EmployeeRecord', 'name, age, title, department, paygrade')

import csv
for emp in map(EmployeeRecord._make, csv.reader(open("employees.csv", "rb"))):
    print emp.name, emp.title

import sqlite3
conn = sqlite3.connect('/companydata')
cursor = conn.cursor()
cursor.execute('SELECT name, age, title, department, paygrade FROM employees')
for emp in map(EmployeeRecord._make, cursor.fetchall()):
    print emp.name, emp.title

OK,到这里这个重构实践就阐述完毕了。下面我会继续讨论Refactory in Python这个话题。