Active Record 模式是一种可以把数据库中的行与对象进行映射的数据持久化模式,在 Rails、Hibernate 和其他很多 ORM 工具中都得到了广泛应用。但是用 Bob Martin 的话说,它正是引起混乱的根源。“表与类,列与字段之间的 1 对 1 映射”是以假设表与对象之间十分相似为基础的,但事实却是,面向数据和面向对象的编程风格之间有着本质上对立的特性。
对象封装数据并暴露行为,因为公共接口是围绕方法来构造的。数据结构暴露数据且没有行为,因为它们通常都不包含业务规则。由此可见,对象和数据结构并不具备同样的“免疫力”和弱点。对象对于添加新的类型而言是透明的——因为在 OOP 中,算法中不会包含任何有关它们所操作的对象的信息——但是对象对于添加新的功能则非如此:
一个旧例子 shape.draw(); 就可以说明这一点。调用者不知道所绘制的到底是哪一种 shape。实际上,如果我添加了新的 shape 类型,调用 draw() 的算法也不会意识到这种变化,不需要重新构建、重新测试,或是重新部署。……
但在同样的情况下,如果我向 shape 类中添加了新的方法,那所有 shape 的派生类就都要进行修改。
使用数据结构的算法则恰恰相反,对于添加新的功能是透明的,而添加新的类型则会引起变化:
现在考虑一个使用数据结构的算法。 switch(s.type) {
case SQUARE: Shape.drawSquare((Square)s); break;
case CIRCLE: Shape.drawCircle((Circle)s); break;
} ……假设我们要添加一些新的功能进去,比如 Shape.eraseXXX()。既有的代码都不会受到影响。不需要重新构建、重新测试,或是重新部署。
……
但在同样的情况下,如果我添加了一个新的 shape 类型,我必须要找出所有的算法,并且向对应的 switch 语句中加入新的 shape。
按照 Bob Martin 的观点,使用 Active Record 模式就会带来一个非常经典的后果——“关系数据库与面向对象语言之间的阻抗失衡(impedance mismatc)” 。他提出质疑说,虽然“Active Record 看起来是一个对象”,但它“是被打算作为数据结构来使用的”。他说的没错,由于 Active Record 类常常会包含业务规则方法,所以它会向外暴露行为。但同时它并没有真的对数据进行封装,因为“几乎所有 ActiveRecord 的派生类都要通过访问器(accessors)和修改器(mutators)来暴露数据库的列数据” 。
“问题在于 Active Record 确实是数据结构。把业务规则放到里面去并不能让它们变成真正的对象。到最后,那些使用了 Active Records 的算法就变得很脆弱,容易因为 schema 或是类型的变化而引起改变。 ……
所以构建在 ActiveRecord 上的应用实际上是构建在数据结构的基础上的。而构建在数据结构基础上的应用是过程式的——不是面向对象的。当我们以 Active Record 来搭建应用的时候,我们就错过了使用面向对象设计的机会。”
不过这并不是意味着人们不应该使用 Active Records。Bob Martin 相信,实际上“很多优秀设计的根本就在于如何把不同编程风格所具备的优点和劣势进行混合匹配”。不过他还是提倡,不要把它当作“应用程序的组织原则”。他认为,Action Record 模式应该属于“将数据库与应用程序分离的层”,扮演一种数据库表的数据结构和应用程序对象之间“有效转换机制”的角色。通过这种方式,我们可以依据开放封闭原则来设计“暴露行为并封装数据的对象导向”的应用程序,并且在添加新的对象类型时,可以保证足够的灵活性。
查看英文原文: Preserving flexibility while using Active Record pattern
评论