Rails 的单表继承和多态关联

浏览: 111 发布日期: 2017-07-04 分类: ruby

单表继承

在使用面向对象开发时,经常会用到类和继承.如应用程序中有几个模型相似的东西,我们假设是不同角色的人员(People):顾客(Customer)、员工(Employee)和经理(Manager)等等。在rails应用开发者中我们要对其进行建模.应该怎么做呢?

一般的设计思路: Single Class with conditionals (one class, one table)一个类, 一个表
在上述中的模型其中有一些属性是共有的,另一些属性是特有的。因此,创建模型:Customer类和Employee类,且都是People的子类,Manager类是Employee的子类。子类会继承父类的所有属性和行为。

从理解上讲这个是我们最熟悉和最常用的做法.但是从我们的开发规范来讲,建模的类需要考虑到他的单一职责,程序设计又有着DRY(don`t repeat yourself)的原则,如果我们建模时,用几个不同的表来分别承载这些类的话,这与我们ruby语言的优雅和程序设计的规范是不符合的.

所以我们在程序设计中就引入了一种叫做单表继承(STI Single Table Inheritance)的思路:

Single Table Inheritance (separate classes, one table) 单表继承,一张表, 分多个类使用
在上面描述的问题中,我们可以这样来设计: 建一张名为people的表并以此作为类,里面的字段包含了人的基本属性和一个type字段,用来标记每个实例的类型.当各个模型之间只有细微的差别时,就可以考虑使用单表继承(STI)

使用单表继承时有什么需要注意的?

当我们决定如何设计数据模型的时候,我们首先要问自己几个问题

  1. 是所有的对象都继承自一个类吗?
    比如,汽车,卡车,摩托车都可以认为是机动车的子类, 但是如果现在加入了自行车,手推车呢,似乎就不合理了,也就是说不要为了共享属性,比如轮子的个数,颜色等属性而使用单表继承,而且要揣摩父类的命名如何能更合理

  1. 是所有对象都有相同的属性,但是不同的行为吗?
    完全的数据归一化往往并不是最好的设计.数据库中有多少列是各个类所共享的,如果每个类有过多的特殊属性,那么就不建议使用单表继承,因为使用单表继承会导致数据库中冗余的信息过多

rails 里使用单表继承的代码

# model的定义
## 基础类(人), 承载了人的基本属性
class People < ActiveRecord::Base

end

## 人的子类 顾客
class Customer < People

end

## 人的子类 员工, 承载了员工的属性,例如 属于某一个老板的员工.
class Employee < People    

end

## 员工的子类 经理
class Manager < Employee

end

这是类的结构,rails里有 migration 的机制,是可以直接修改操作数据库表结构的能力,以下是migration的代码

class CreatePeople < ActiveRecord::Migration
  create_table :people, :force => true do |t|
    ## 其中type 字段是必须有的,是STI机制的必要字段
    t.string :type
    t.string :name
    t.timestamps
    ...
  end
end
# 添加人员
empl = Employee.new({name: 'David',  dept: 2})
user = Customer.create({name: 'Jim', balance: 100})

由上面的代码可以看出类定义中使用了继承,而对于不同角色的人员,均保存于people表,people表存储了顾客和员工的属性字段,对于empl对象,都是没有user的balance属性的,即这个对象的balance字段是null,而对于user对象的dept和reports_to字段同样是null,这样就实现了一个单表继承,相比于我们使用各种其它方法会简单很多。但是问题是:
单表继承中所有的属性都存在于一张表中,这样真的好吗?如果子类存在的差异较大,且属性数据较大,如果仍然存在于同一张表,就会产生很多问题
再有,这样的模型设计也有一点不够合适, 例如

virtual = Person.create({name: 'virtual'})

我们会惊讶的发现virtual 对象的type 字段是null.对于这个情况,我们的思考和方案:

  1. 在People中实现一个名为abstract_class?的类方法,并使其返回true,这样,就可以达到目的了。
    不过它带来的问题是:ActiveRecord永远不会尝试寻找对应于抽象类的数据库表,这是对我们有利的,2.抽象类的子类会被当作各自独立的ActiveRecord模型类,即各自映射到一张独立的数据库表。这就达不到我们对公共属性抽取的目的了。这种方法不完美

  2. 使用Ruby模块来包含这些需要共享的功能,然后将模块混入所谓的子类。这是书中提供的方法,也感觉不完美,没有了继承的感觉

  3. 能否在父类中添加什么使其只可读不可写呢?

多态关联

Rails模型中的关系有一对一,一对多还有多对多,这些关联关系都比较直观,除此之外Rails还支持多态关联
所谓的多态关联其实可以概括为一个模型同时与多个其它模型之间发生一对多的关联。并且在实际的应用中这种关系也十分普遍,比如可以应用到站内消息模块,评论模块,标签模块等地方,其实多态关键就是一个表关联到多个表上。
就如Comment(评论)表吧,一个Topic应该有Comment(一个帖子应该有许多的评论),除此之外Micropost(微博)也可能有很多的Comment。然后一个网站中既有Topic的论坛功能,又有Micropost的功能,我们怎么处理Comment表呢?
当然我们可以建两个独立的表比如TopicComment和MicropostComment,再分别关联到Topic和Micropost上,但这不是一种好的选择,我们可以只建一个表,然后去关联这两个表,甚至多个表。这也就实现了多态的能力。
那么.多态关联是一个什么样的工作过程呢?
拿上面描述的场景来举例,我们只有一张comments的表,表的每一列都是一个评论,然后有两个字段来标记这条评论是来评论哪个主题文章的,这连个字段可以是commentable_type(用来标记评论类型), commentable_id(用来标记具体某个评论主题).这样,我们只要根据类型和id就可以定位关联到具体的某个评论主题.

rails 里使用多态关联的代码

首先,在数据库表层面要支持这两个关键字段

class CreateComments < ActiveRecord::Migration
  def change
    create_table :comments do |t|
      t.text :content
      #这里指明了多态,这样会生成commentable_id和commentable_type这两个字段的
      t.belongs_to :commentable, :polymorphic => true 
      t.timestamps
    end
  end
end

模型层面也要有标记关联关系

class Comment < ActiveRecord::Base
  belongs_to :commentable, :polymorphic => true  
end

多态魔法就在这里,commentable_typle字段用于指明comment所关联的表的类型,如topic或micropost等,而comment_id用于指定那个关联表的类型对象的id。
如:可以把一个comment关联到第一篇topic上,那么commentable_type字段为topic,而commentable_id为对应topic对象的id 1,同理这样就可以关联到不同表了,从而实现多态的关联。
而在模型层面.这里的comment belongs_to没有写topic,micropost等,而写了commentable,因为commentable中有type和id两个字段,可以指定任何其他model对象的,从而才能实现多态,如果这里写belongs_to topic的话就没办法实现多态了。然后我们看看topic和mocropost的model该如何写。

class Topic < ActiveRecord::Base
  has_many :comments, :as => :commentable

end

class Micropost < ActiveRecord::Base
  has_many :comments, :as => :commentable

end

有了这样的has_many和belongs_to的关系,我们可以轻易的来操作模型的关联关系了.
我们可以轻易的为topic添加评论

topic = Topic.find(1)
topic.comments.create(content: '评论')
mocropost = Micropost.find(1)
mocropost.comments.create(content: '评论')

我们也可以从评论找到他属于哪个主题

comment = Comment.find(1)
## 这里comment belongs_to commentable 所以
comment.commentable ## topic

这样就实现了多态关联.

小结

ruby事面向对象的语言,在面向对象编程的世界里,设计模型主要依据的三要素有: 封装,继承,多态

  1. 建模的类需要考虑到他的单一职责(类的封闭性),也就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。

  2. 程序设计的DRY,合理使用继承(类的开放性),它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展

  3. 支持多态,是允许你将父对象设置成为和一个或更多的他的子对象相等的技术,赋值之后,父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作

封装可以隐藏实现细节,使得代码模块化;继承可以扩展已存在的代码模块(类);它们的目的都是为了——代码重用。而多态则是为了实现另一个目的——接口重用!多态的作用,就是为了类在继承和派生的时候,保证使用“家谱”中任一类的实例的某一属性时的正确调用。

返回顶部