Stoned's Blog

A startup hacker.

Splitting a Fat Model Into Multiple Files

起因

Rails的应用内,总有一些超级复杂的model,比如User。而正因是胖model,瘦controller这样的指导思想,会导致一些model臃肿不堪,有些model甚至会超过1000行,而一旦超过1000行,一个类的代码就变得难以维护了。所以一段时间内,我们为了不让我们的model太臃肿,我们只能不往User里面写代码,而把一些代码写在其他的model里面。比如写成如下的方式

#exam.rb
class Exam < ActiveRecord::Base
    def add_user(user)
    end
end

但这样的方式Exam.add_user(user)明显没有user.join_exam(exam)直观。

#user.rb
class User < ActiveRecord::Base
    def join_exam(exam)
    end
end

所以为了兼顾直观与保持model可维护性更高,我查找了一些资料,尝试为我们的model瘦身。

解决思路

一个广为流传的办法是Use concerns to keep your models manageable,这是dhh(Creator of Ruby on Rails)大神推荐的方式,已被列入Rails4的规范之中了(Rails4重大设计决策:“胖”Model用ActiveSupport::Concern瘦身)。

但使用concern的根本目的不是为了解决单个model的肥胖问题,而是为了让一些model通用的代码片段存在于一个合适的位置,比如commentable,这样但凡有用到comment的model,只要include commentable就可以实现评论相关的功能,这样的实现方式既简单又直观。

如果为model瘦身,主要的问题是,如何分离你的业务逻辑,因为单纯的将代码发在几个小文件中是没有什么意义的(为什么没有意义?因为会增加你查找的难度和理解的难度),这也是为什么7 Patterns to Refactor Fat ActiveRecord Models

“Any application with an app/concerns directory is concerning.”

的一个重要原因。

所以综合以上思路,最终实践得出了三个可以简化User model的办法

  • 为model抽象一些功能性concern,比如Authenticatable, Genderable。
  • 为model分离一些业务相关模块。比如user_post, 比如 user_event
  • 将特殊的功能模块抽象成class

实践

以下实践均以User model为例

为model抽象一些功能性concern

这样做符合concern本身的意图,比如将用户验证的相关操作和相关scope放在Authenticatable的module,这样User就拥有了可拔插的能力,如果想去掉相关的功能,只需要取消include就可以了。

所以,这部分的代码类似于

#app/models/concerns/authenticatable.rb
# -*- encoding : utf-8 -*-
module Authenticatable
  extend ActiveSupport::Concern
  included do
    def self.basic_auth(account, password)
    end
  end

  def has_password?
  end
end

为model分离一些业务相关模块

这样做是不太符合concern本身的意图的,但借用concern来表达我们的业务逻辑,也没有什么问题。但这样做最大的问题是,这部分的concern不应该和传统的concern混在一起,这样不同model的业务逻辑容易混乱(至少不好查找)。

所以,我们将这部分代码放在了app/models/concerns/user_concern/中,代码类似如下

#app/models/concerns/user_concern/post.rb
# -*- encoding : utf-8 -*-
module UserConcern
  module Post
    extend ActiveSupport::Concern

    included do
      has_many :posts, :dependent => :destroy
      has_many :other_posts, :dependent => :destroy
    end
  end
end

将特殊的功能模块抽象成class

这种办法最受7 Patterns to Refactor Fat ActiveRecord Models推崇,所以,可以借鉴一些这边文章提到的一些方法来抽象我们的代码。这部分代码类似于

#app/models/lib/edit_limit.rb
# -*- encoding : utf-8 -*-
class EditLimit
  NO_MODIFY_LIMIT_USERS_KEY = "no_modify_limit_users"

  MODIFY_LIMIT = 5
  MODIFY_LIMIT_KEY = "editable_times_%d"

  def initialize(user)
    @user = user
  end

  def self.no_modify_limit_users
    Rails.cache.fetch NO_MODIFY_LIMIT_USERS_KEY do
      Set.new
    end
  end

  def self.add_no_modify_limit_user(user)
    no_limit_user_ids = no_modify_limit_users

    no_limit_user_ids << user.id

    Rails.cache.write NO_MODIFY_LIMIT_USERS_KEY, no_limit_user_ids
    no_limit_user_ids
  end
end

最终,我们在user.rb的引用如下所示

#app/models/user.rb
class User < ActiveRecord::Base
  include Authenticatable
  include UserConcern::Post
end

总结

因为Rails本身的设计,对于一些不太复杂的逻辑与应用,实在无需煞费苦心去寻求瘦身与简化。因为符合Rails本身的设计思想在很大程度上就已经很好的分离了model,view,controller(MVC)三者的职责。而一些复杂的应用,在不考虑更好的可读性的前提下(比如将许多逻辑放置在关联表中),也不会形成太过肥胖的model。所以,只有一些人像我这样对可读性有些洁癖,又想让model保持简单的人,才需要类似的解决办法。

参考资料