<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>cisolarix (cisolarix)</title>
    <link>https://ruby-china.org/cisolarix</link>
    <description></description>
    <language>en-us</language>
    <item>
      <title>请教 ?z 中的问号运算符的用法</title>
      <description>&lt;p&gt;今天看 &lt;a href="http://api.rubyonrails.org/classes/ActiveModel/Validations.html" rel="nofollow" target="_blank" title=""&gt;ActiveModel::Validations&lt;/a&gt; 部分的文档，发现了这样的用法：&lt;/p&gt;

&lt;p&gt;&lt;img src="http://blog-img-bed.qiniudn.com/active_model_validations_%3Fz.png" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;由于第一眼没见过问号这么个用法，于是在 irb 里测试了一番，并搜索一番，得出结论：&lt;/p&gt;

&lt;p&gt;&lt;code&gt;?z&lt;/code&gt; 就相当于 &lt;code&gt;"z"&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;于是，问题来了，如果 &lt;code&gt;?z&lt;/code&gt; 与 &lt;code&gt;"z"&lt;/code&gt; 是一样的作用，用前者有什么其他优势吗？&lt;/p&gt;</description>
      <author>cisolarix</author>
      <pubDate>Tue, 04 Nov 2014 14:16:02 +0800</pubDate>
      <link>https://ruby-china.org/topics/22461</link>
      <guid>https://ruby-china.org/topics/22461</guid>
    </item>
    <item>
      <title>Rails 中用 RabbitMQ 做消息队列 [译]</title>
      <description>&lt;p&gt;一年前，我曾写过我们的一个&lt;a href="http://codetunes.com/2013/robust-dashboard-application-with-faye/" rel="nofollow" target="_blank" title=""&gt;仪表板程序&lt;/a&gt;以及如何使用 Faye 延迟消息（现已打包成 &lt;a href="https://github.com/monterail/faye-redis-delayed" rel="nofollow" target="_blank" title=""&gt;gem&lt;/a&gt;）来解决性能问题。&lt;/p&gt;



&lt;p&gt;从最开始，我们就选择了&lt;a href="http://en.wikipedia.org/wiki/Service-oriented_architecture" rel="nofollow" target="_blank" title=""&gt;面向服务的架构&lt;/a&gt;，事实证明这也是我们做过的最棒的决定。套件（到写这篇文章时止已有 8 个应用）中的每一个程序都只负责业务的一部分。仪表板程序用来显示其他应用的信息给用户。&lt;/p&gt;

&lt;p&gt;不过，这并不是全部。这个项目最重要的特性之一就是可以在应用之间导入、导出数据。而且，这要求在各应用之间互相提供读写接口，我们需要处理&lt;a href="http://en.wikipedia.org/wiki/Mesh_networking" rel="nofollow" target="_blank" title=""&gt;网状结构&lt;/a&gt;，而不仅是&lt;a href="http://en.wikipedia.org/wiki/Star_network" rel="nofollow" target="_blank" title=""&gt;星型结构&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;特性与用户的同步增长导致了许多性能问题。应用间的连接都通过 HTTP API 以“我要某某某”这种方式进行，套件开始变得缓慢。&lt;/p&gt;

&lt;p&gt;我们考虑过重新设计架构，减少依赖，但是由于每个应用的核心功能都是独立且精心设计过的，所以实际上能提升的空间很有限。并且，以后我们还可能将一些功能抽出来，变成新的应用。&lt;/p&gt;

&lt;p&gt;一个武断的解决方法是：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;我们只要采用 HTTP 缓存就可以了
其实不然。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;HTTP cache 的最大问题就是&lt;code&gt;最大生命周期&lt;/code&gt;。
系统中有两种场景下使用缓存：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;用户执行了某种操作，跳到其它的应用中且希望能看到更新之后的数据&lt;/li&gt;
&lt;li&gt;用户来到仪表板，希望看到数据的当前状态（这些状态可能已经数天未变了）&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;几乎没法用定长时间来确定资源需要被缓存多久。如果时间太短，请求就会太多，尽管数据可能压根没变过。如果时间太长，我们可能会收到成堆的用户邮件，抱怨系统没有运行，其实只是因为他们看到的是未及时更新的数据。&lt;/p&gt;

&lt;p&gt;缓存的出发点是为了提升性能，不是为了破坏用户体验。&lt;/p&gt;

&lt;p&gt;那么我们&lt;code&gt;该&lt;/code&gt;怎么办？&lt;/p&gt;

&lt;p&gt;答案是：&lt;code&gt;回到架构&lt;/code&gt;。&lt;/p&gt;
&lt;h2 id="消息传递是解决之道"&gt;消息传递是解决之道&lt;/h2&gt;
&lt;p&gt;面向对象的编程原则推崇 &lt;a href="http://robots.thoughtbot.com/tell-dont-ask" rel="nofollow" target="_blank" title=""&gt;告诉，而不是问&lt;/a&gt; 的模式。如果我们将之引申到架构的高度，解决思路就自然出现了。&lt;/p&gt;

&lt;p&gt;无需让消费者每次&lt;code&gt;问&lt;/code&gt;生产者要资源，我们让生产者告诉消费者发生了什么变化。&lt;/p&gt;

&lt;p&gt;废话少说，我们上手实战用 RabbitMQ 消息系统来代替老旧的 HTTP API 吧。&lt;/p&gt;
&lt;h2 id="RabbitMQ 是什么"&gt;RabbitMQ 是什么&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://www.rabbitmq.com/" rel="nofollow" target="_blank" title=""&gt;RabbitMQ&lt;/a&gt; 是实现了 &lt;a href="http://en.wikipedia.org/wiki/Advanced_Message_Queuing_Protocol" rel="nofollow" target="_blank" title=""&gt;AMQP&lt;/a&gt; 协议的开源消息中介、队列系统，由 Erlang 写就。信息标签包括：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;管用的消息系统&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;这一点我发现是确实没错。&lt;/p&gt;

&lt;p&gt;要了解 RabbitMQ 能做什么，我们得看看&lt;a href="https://www.rabbitmq.com/getstarted.html" rel="nofollow" target="_blank" title=""&gt;示例拓扑图&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;我们将采用&lt;a href="https://www.rabbitmq.com/tutorials/tutorial-three-ruby.html" rel="nofollow" target="_blank" title=""&gt;发布/订阅拓扑&lt;/a&gt;，并采用多个 fanout 交换与队列。&lt;/p&gt;
&lt;h3 id="安装 RabbitMQ"&gt;安装 RabbitMQ&lt;/h3&gt;
&lt;p&gt;这个部分因操作系统而异。RabbitMQ 的官网有许多&lt;a href="https://www.rabbitmq.com/download.html" rel="nofollow" target="_blank" title=""&gt;指南&lt;/a&gt;供参考。
如果恰巧你的系统是 Mac OS X，且安装了 &lt;a href="http://brew.sh/" rel="nofollow" target="_blank" title=""&gt;Homebrew&lt;/a&gt;，你只需：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;brew install rabbitmq
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用以下命令启动：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/usr/local/opt/rabbitmq/sbin/rabbitmq-server
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;启动 &lt;code&gt;rabbitmq-server&lt;/code&gt; 之后，就可以通过&lt;a href="http://localhost:15672/" rel="nofollow" target="_blank" title=""&gt;http://localhost:15672&lt;/a&gt; 来访问管理界面。默认用户是 &lt;code&gt;guest&lt;/code&gt;，密码也是 &lt;code&gt;guest&lt;/code&gt;。管理界面很有用。后面的小节我们会展开涉及一些。&lt;/p&gt;
&lt;h2 id="简单的架构：博客的仪表板"&gt;简单的架构：博客的仪表板&lt;/h2&gt;
&lt;p&gt;想像两个应用：一个带博文的博客应用，一个显示最近 5 条博文的仪表板程序。仪表板应用无需向博客应用的 HTTP API &lt;code&gt;索要&lt;/code&gt;最近的博文，我们让博客应用主动&lt;code&gt;说出&lt;/code&gt;每一个最新发布的博文。&lt;/p&gt;

&lt;p&gt;&lt;img src="http://blog-img-bed.qiniudn.com/event_sourcing_diagram.png" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;上图中：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Blog&lt;/code&gt; - 用 SQL 数据库的典型 Rails 应用&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;P&lt;/code&gt; - RabbitMQ 生产者&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;X&lt;/code&gt; - RabbitMQ 交换&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Queue&lt;/code&gt; - RabbitMQ 队列&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;C&lt;/code&gt; - RabbitMQ 消费者&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Dashboard&lt;/code&gt; - 用 Redis 的 Rails 应用&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;博客应用中的博文在创建后，会让生产者向交换发出一条消息。交换将消息放入队列。然后，连接到此队列的消费者会抓取这条消息，并更新仪表板的 Redis 缓存。
尽管听起来有点复杂，但借助强大的 Ruby 库，我们其实只需很少的工作。&lt;/p&gt;
&lt;h3 id="博文发布者"&gt;博文发布者&lt;/h3&gt;
&lt;p&gt;首先，创建基本的博文脚手架&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rails new blog
cd blog
bundle
rails generate scaffold post title:string body:text
rake db:migrate
rails server
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;管理博文的地址在： &lt;a href="http://localhost:3000/posts" rel="nofollow" target="_blank" title=""&gt;http://localhost:3000/posts&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;现在，我们需创建一个 RabbitMQ 生产者。我们称之为发布者。我们将使用 &lt;a href="http://rubybunny.info/" rel="nofollow" target="_blank" title=""&gt;bunny&lt;/a&gt; 这个超易用的 RabbitMQ Ruby 客户端。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Gemfile&lt;/code&gt; 中加入以下代码：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# blog/Gemfile
gem "bunny"
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;并运行 &lt;code&gt;bundle install&lt;/code&gt;.
下面实现我们的发布者：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# blog/app/services/publisher.rb
class Publisher
  # In order to publish message we need a exchange name.
  # Note that RabbitMQ does not care about the payload -
  # we will be using JSON-encoded strings
  def self.publish(exchange, message = {})
    # grab the fanout exchange
    x = channel.fanout("blog.#{exchange}")
    # and simply publish message
    x.publish(message.to_json)
  end

  def self.channel
    @channel ||= connection.create_channel
  end

  # We are using default settings here
  # The `Bunny.new(...)` is a place to
  # put any specific RabbitMQ settings
  # like host or port
  def self.connection
    @connection ||= Bunny.new.tap do |c|
      c.start
    end
  end
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在，每条博文创建后，我们都要调用 &lt;code&gt;Publisher.publish&lt;/code&gt;。&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# blog/app/controllers/posts_controller.rb
class PostsController &amp;lt; ApplicationController
  # ...

  def create
    @post = Post.new(post_params)

    if @post.save
      # Publish post data
      Publisher.publish("posts", @post.attributes)

      redirect_to @post, notice: 'Post was successfully created.'
    else
      render :new
    end
  end

  # ...
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;就是这样。
进行下面的操作前，别忘了重启 Rails 服务器。
现在可以创建一条新的博文，来到 &lt;a href="http://localhost:15672/" rel="nofollow" target="_blank" title=""&gt;RabbitMQ 管理界面&lt;/a&gt;，选择“Exchanges”, 选择 &lt;code&gt;blog.posts&lt;/code&gt;，可以看到下图：&lt;/p&gt;

&lt;p&gt;&lt;img src="http://blog-img-bed.qiniudn.com/event_sourcing_bindings.png" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;往下滚动页面，我们可以发现此交换没有绑定。&lt;/p&gt;

&lt;p&gt;&lt;img src="http://blog-img-bed.qiniudn.com/event_sourcing_no_bindings.png" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;也就是发送到此交换的消息没有去往任何地方。&lt;/p&gt;

&lt;p&gt;现在我们需在交换与仪表板应用间建立一个队列，以便消费者更新其本地缓存。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;需要留心持久性：
&lt;a href="https://www.rabbitmq.com/tutorials/tutorial-three-ruby.html" rel="nofollow" target="_blank" title=""&gt;RabbitMQ 发布/订阅示例&lt;/a&gt;中使用了按需创建的随机队列。这种方案在某些情况下很好，但是在我们这里并不很适用。假使出于未知原因，我们的仪表板应用挂掉了，这个临时队列就会被删除，其中发自博客应用的消息就再也不会到达仪表板应用了。这就是为什么我们需要一个静态的可持续的队列来保存消息，以便仪表板应用断线并重新连接后也能收到之前的所有消息。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="仪表板消费者"&gt;仪表板消费者&lt;/h3&gt;
&lt;p&gt;如果你比较熟悉 &lt;a href="http://sidekiq.org/" rel="nofollow" target="_blank" title=""&gt;Sidekiq&lt;/a&gt; 或 &lt;a href="https://github.com/resque/resque" rel="nofollow" target="_blank" title=""&gt;Resque&lt;/a&gt;，下面的内容就很容易。
还有一个特别棒的 Ruby 库用来处理 RabbitMQ 消息队列里的消息。这个工具由 &lt;a href="https://twitter.com/jondot" rel="nofollow" target="_blank" title=""&gt;&lt;/a&gt;&lt;a href="/jondot" class="user-mention" title="@jondot"&gt;&lt;i&gt;@&lt;/i&gt;jondot&lt;/a&gt; 开发，名叫 &lt;a href="http://jondot.github.io/sneakers/" rel="nofollow" target="_blank" title=""&gt;sneakers&lt;/a&gt;（&lt;a href="http://blog.paracode.com/" rel="nofollow" target="_blank" title=""&gt;作者博客&lt;/a&gt;）。&lt;/p&gt;

&lt;p&gt;我们来创建仪表板应用：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rails new dashboard
cd dashboard
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;添加一些 gems：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# dashboard/Gemfile
gem 'redis-rails'
gem 'redis-namespace'
gem 'sneakers'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行 &lt;code&gt;bundle install&lt;/code&gt;.
Redis 和 sneakers 都需要&lt;a href="https://github.com/jondot/sneakers/wiki/How-To:-Rails-Background-Jobs" rel="nofollow" target="_blank" title=""&gt;设置&lt;/a&gt;一番：&lt;/p&gt;
&lt;h4 id="设置 Redis"&gt;设置 Redis&lt;/h4&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# dashboard/config/initializers/redis.rb
$redis = Redis::Namespace.new("dashboard:#{Rails.env}", redis: Redis.new)
&lt;/code&gt;&lt;/pre&gt;&lt;h4 id="设置 Sneakers"&gt;设置 Sneakers&lt;/h4&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# dashboard/Rakefile

# load sneakers tasks
require 'sneakers/tasks'

Rails.application.load_tasks
# dashboard/config/initializers/sneakers.rb
Sneakers.configure({})
Sneakers.logger.level = Logger::INFO # the default DEBUG is too noisy
&lt;/code&gt;&lt;/pre&gt;&lt;h4 id="最近博文服务"&gt;最近博文服务&lt;/h4&gt;
&lt;p&gt;既然我们不用 ActiveRecord，我们需要地方来保存涉及最近博文的功能。我们创建一个叫 &lt;code&gt;RecentPosts&lt;/code&gt; 的服务。&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# app/services/recent_posts.rb
class RecentPosts
  KEY = "recent_posts" # redis key
  STORE_LIMIT = 5      # how many posts should be kept

  # Get list of recent posts from redis
  # Since redis stores data in binary text format
  # we need to parse each list item as JSON
  def self.list(limit = STORE_LIMIT)
    $redis.lrange(KEY, 0, limit-1).map do |raw_post|
      JSON.parse(raw_post).with_indifferent_access
    end
  end

  # Push new post to list and trim it's size
  # to limit required storage space
  # `raw_post` is already a JSON string
  # so there is no need to encode it as JSON
  def self.push(raw_post)
    $redis.lpush(KEY, raw_post)
    $redis.ltrim(KEY, 0, STORE_LIMIT-1)
  end
end
&lt;/code&gt;&lt;/pre&gt;&lt;h4 id="仪表板视图"&gt;仪表板视图&lt;/h4&gt;
&lt;p&gt;仪表板需要视图来查看是否工作正常。&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# dashboard/app/controllers/home_controller.rb
class HomeController &amp;lt; ApplicationController
  def index
    @posts = RecentPosts.list
  end
end
&lt;/code&gt;&lt;/pre&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# dashboard/app/views/home/index.html.erb
&amp;lt;h2&amp;gt;Recently updated posts&amp;lt;/h2&amp;gt;

&amp;lt;table&amp;gt;
  &amp;lt;thead&amp;gt;
    &amp;lt;tr&amp;gt;
      &amp;lt;th&amp;gt;Title&amp;lt;/th&amp;gt;
    &amp;lt;/tr&amp;gt;
  &amp;lt;/thead&amp;gt;

  &amp;lt;tbody&amp;gt;
    &amp;lt;% @posts.each do |post| %&amp;gt;
      &amp;lt;tr&amp;gt;
        &amp;lt;td&amp;gt;&amp;lt;%= post[:title] %&amp;gt;&amp;lt;/td&amp;gt;
      &amp;lt;/tr&amp;gt;
    &amp;lt;% end %&amp;gt;
  &amp;lt;/tbody&amp;gt;
&amp;lt;/table&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# dashboard/config/routes.rb
Rails.application.routes.draw do
  root to: "home#index"
end
&lt;/code&gt;&lt;/pre&gt;&lt;h4 id="工人"&gt;工人&lt;/h4&gt;
&lt;p&gt;最后，我们来建立 sneakers 工人。你可能已经注意到了，这与 sidekiq 的工人很像。&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# dashboard/app/workers/posts_worker.rb
class PostsWorker
  include Sneakers::Worker
  # This worker will connect to "dashboard.posts" queue
  # env is set to nil since by default the actuall queue name would be
  # "dashboard.posts_development"
  from_queue "dashboard.posts", env: nil

  # work method receives message payload in raw format
  # in our case it is JSON encoded string
  # which we can pass to RecentPosts service without
  # changes
  def work(raw_post)
    RecentPosts.push(raw_post)
    ack! # we need to let queue know that message was received
  end
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;就是这样，仪表板应用已经就绪。
要启用工人，运行：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WORKERS=PostsWorker rake sneakers:run
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 &lt;a href="http://localhost:15672/" rel="nofollow" target="_blank" title=""&gt;RabbitMQ 管理页面&lt;/a&gt;，我们可以看到 &lt;code&gt;dashboard.posts&lt;/code&gt; 队列已经创建&lt;/p&gt;

&lt;p&gt;&lt;img src="http://blog-img-bed.qiniudn.com/event_sourcing_queue.png" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;现在，如果你新发布一条博文的话，在 &lt;code&gt;blog.posts&lt;/code&gt; 交换中就会出现一条消息，但 &lt;code&gt;dashboard.posts&lt;/code&gt; 队列依然是空的。
为什么呢？因为我们需要在交换与队列间建立一个绑定。&lt;/p&gt;
&lt;h3 id="全部联系到一起"&gt;全部联系到一起&lt;/h3&gt;
&lt;p&gt;我们需责成 &lt;code&gt;blog.posts&lt;/code&gt; 交换将收到的消息发送给 &lt;code&gt;dashboard.posts&lt;/code&gt; 队列。尽管这个操作可以在 RabbitMQ 管理页面完成，但是我们最好将此操作做成可被自动执行（比如，部署时自动执行）的配置文件的形式。
我们还是使用 bunny 库：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# config/Rakefile
namespace :rabbitmq do
  desc "Setup routing"
  task :setup do
    require "bunny"

    conn = Bunny.new
    conn.start

    ch = conn.create_channel

    # get or create exchange
    x = ch.fanout("blog.posts")

    # get or create queue (note the durable setting)
    queue = ch.queue("dashboard.posts", durable: true)

    # bind queue to exchange
    queue.bind("blog.posts")

    conn.close
  end
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rake rabbitmq:setup
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 &lt;a href="http://localhost:15672/" rel="nofollow" target="_blank" title=""&gt;RabbitMQ 管理页面&lt;/a&gt;查看 &lt;code&gt;blog.posts&lt;/code&gt; 交换的绑定。&lt;/p&gt;

&lt;p&gt;&lt;img src="http://blog-img-bed.qiniudn.com/event_sourcing_bind.png" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;这下，每个新建的&lt;code&gt;博文&lt;/code&gt;，都会被作为消息发送到 &lt;code&gt;blog.posts&lt;/code&gt; 交换中去，然后路由到 &lt;code&gt;dashboard.posts&lt;/code&gt; 队列，被 &lt;code&gt;PostsWorker&lt;/code&gt; 读取到，再由 &lt;code&gt;RecentPosts&lt;/code&gt; 服务放进 redis。&lt;/p&gt;
&lt;h2 id="复杂架构"&gt;复杂架构&lt;/h2&gt;
&lt;p&gt;上面的示例演示了如何连接两个应用。现实世界里，应用间的连接可就复杂多了。请看下图：&lt;/p&gt;

&lt;p&gt;&lt;img src="http://blog-img-bed.qiniudn.com/68747470733a2f2f646c2e64726f70626f7875736572636f6e74656e742e636f6d2f732f3676716d30773037736b68393036732f323031342d30352d3138253230617425323030332e32372e706e67.png" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;与上一示例比，有如下不同：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Blog&lt;/code&gt; 现在发布多条信息到多个交换&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Dashboard&lt;/code&gt; 从多个队列获取消息&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Admin&lt;/code&gt; 另外一个应用，同时兼任生产者与消费者&lt;/li&gt;
&lt;li&gt;Exchange ↔ Queue 绑定复杂多了 - 可能是一对多（&lt;code&gt;blog.posts&lt;/code&gt;）或多对一（&lt;code&gt;*.page_views&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Admin&lt;/code&gt; 用 SQL 取代了 redis 的消费者应用&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;我们过一遍其中几个点&lt;/p&gt;
&lt;h3 id="缓存存储"&gt;缓存存储&lt;/h3&gt;
&lt;p&gt;为缓存选择合适的存储是个很宽泛的话题。我只举几个例子。&lt;/p&gt;
&lt;h4 id="最近博文"&gt;最近博文&lt;/h4&gt;
&lt;p&gt;在仪表板里，我们只关心最新的五条博文。这很适合用 redis 列表。我们用 &lt;code&gt;LPUSH&lt;/code&gt; 将新条目原子性地放到列表的前端，用 &lt;code&gt;LRANGE&lt;/code&gt; 来获取最先的几个条目，用 &lt;code&gt;LTRIM&lt;/code&gt; 限制存储。无需做任何排序或过滤。Redis 够用了。&lt;/p&gt;
&lt;h4 id="页面访问统计"&gt;页面访问统计&lt;/h4&gt;
&lt;p&gt;另一个使用 redis 的地方是获取页面访问统计。用 &lt;code&gt;INCR&lt;/code&gt;, &lt;code&gt;INCRBY&lt;/code&gt; 或 &lt;code&gt;HINCR&lt;/code&gt;，很容易原子性地增长计数器，而无需顾虑竞争条件。猛击&lt;a href="https://github.com/monterail/rails-event-sourcing-example/blob/master/dashboard/app/services/page_views.rb" rel="nofollow" target="_blank" title=""&gt;这儿&lt;/a&gt;来看示例。&lt;/p&gt;
&lt;h4 id="带过滤与排序的结构化缓存"&gt;带过滤与排序的结构化缓存&lt;/h4&gt;
&lt;p&gt;当然，redis 也不是银弹。有时我们也需要对缓存进行过滤或排序。这种情况下，我们可以创建一个普通的 SQL 数据库模型，并在其中存储必要的信息。见 Admin 应用中存储博文的示例，用到了 &lt;a href="https://github.com/monterail/rails-event-sourcing-example/blob/master/admin/db/schema.rb#L16" rel="nofollow" target="_blank" title=""&gt;Blog::Post&lt;/a&gt; 模型与 &lt;a href="https://github.com/monterail/rails-event-sourcing-example/blob/master/admin/app/workers/blog/posts_worker.rb" rel="nofollow" target="_blank" title=""&gt;Blog::PostsWorker&lt;/a&gt;。&lt;/p&gt;
&lt;h3 id="Exchange ↔ Queue 绑定"&gt;Exchange ↔ Queue 绑定&lt;/h3&gt;
&lt;p&gt;复杂的架构需要复杂的绑定。幸运的是，我们可以用上面示例中的方法来解决。
我们只需修改下启动任务：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# config/Rakefile
namespace :rabbitmq do
  desc "Setup routing"
  task :setup do
    require "bunny"

    conn = Bunny.new
    conn.start

    ch = conn.create_channel

    # connect one exchange to multiple queues
    x = ch.fanout("blog.posts")
    ch.queue("dashboard.posts", durable: true).bind("blog.posts")
    ch.queue("admin.posts", durable: true).bind("blog.posts")

    # connect mutliple exchanges to the same queue
    x = ch.fanout("admin.page_views")
    ch.queue("dashboard.page_views", durable: true).bind("admin.page_views")

    x = ch.fanout("blog.page_views")
    ch.queue("dashboard.page_views", durable: true).bind("blog.page_views")

    conn.close
  end
end
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="结语"&gt;结语&lt;/h2&gt;
&lt;p&gt;RabbitMQ 处理复杂的面向服务的系统有很大的潜能。用可持续的队列，我们可以在一部分应用挂掉的时候，依然可以保持数据的一致性。对于仪表板式的应用，用户依然可以访问缓存过的数据，即使下层应用挂掉的时候。&lt;/p&gt;

&lt;p&gt;与不断地访问 API 相比，消息传递性能提升明显。速度，持续性与可用性都有提高。理论上，我们放弃一些临时的一致性和实时数据更新。然而大多数情况下，我们发现用户在应用间访问时，缓存已经得到更新，最新的数据也是可用的。&lt;/p&gt;

&lt;p&gt;可用的示例程序托管在&lt;a href="https://github.com/monterail/rails-event-sourcing-example" rel="nofollow" target="_blank" title=""&gt;此&lt;/a&gt;。&lt;/p&gt;</description>
      <author>cisolarix</author>
      <pubDate>Wed, 29 Oct 2014 10:44:02 +0800</pubDate>
      <link>https://ruby-china.org/topics/22332</link>
      <guid>https://ruby-china.org/topics/22332</guid>
    </item>
    <item>
      <title>求助，有没有方法快速递格式化以下代码?</title>
      <description>&lt;p&gt;这是格式化之前的代码：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CreateStudents&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Migration&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;change&lt;/span&gt;
    &lt;span class="n"&gt;create_table&lt;/span&gt; &lt;span class="ss"&gt;:students&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:comment&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"学生信息表"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt; &lt;span class="ss"&gt;:number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:comment&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"学号"&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt; &lt;span class="ss"&gt;:name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:comment&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"姓名"&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;date&lt;/span&gt; &lt;span class="ss"&gt;:birthday&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:comment&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"生日"&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt; &lt;span class="ss"&gt;:gender&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:comment&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"性别"&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt; &lt;span class="ss"&gt;:id_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:comment&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"证件类型"&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt; &lt;span class="ss"&gt;:id_number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:comment&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"证件号码"&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt; &lt;span class="ss"&gt;:country&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:comment&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"国别"&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt; &lt;span class="ss"&gt;:nation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:comment&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"民族"&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt; &lt;span class="ss"&gt;:birth_place&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:comment&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"籍贯"&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt; &lt;span class="ss"&gt;:political_status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:comment&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"政治面貌"&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt; &lt;span class="ss"&gt;:education_level&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:comment&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"培养层次"&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt; &lt;span class="ss"&gt;:enroll_method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:comment&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"入学方式"&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;date&lt;/span&gt; &lt;span class="ss"&gt;:enrolled_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:comment&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"入学时间"&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt; &lt;span class="ss"&gt;:charge_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:comment&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"收费类别"&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt; &lt;span class="ss"&gt;:enrolled_year&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:comment&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"学籍年度"&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt; &lt;span class="ss"&gt;:enrolled_place&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:comment&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"生源所在地"&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt; &lt;span class="ss"&gt;:bank_card_number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:comment&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"银行卡号"&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt; &lt;span class="ss"&gt;:bank_account_number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="ss"&gt;:comment&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"银行账号"&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;boolean&lt;/span&gt; &lt;span class="ss"&gt;:is_active_duty&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:default&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:comment&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"是否现役军人"&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;boolean&lt;/span&gt; &lt;span class="ss"&gt;:is_equivalent_degree&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:default&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:comment&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"是否同等学历"&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;boolean&lt;/span&gt; &lt;span class="ss"&gt;:is_on_record&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:default&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:comment&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"是否在籍"&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;boolean&lt;/span&gt; &lt;span class="ss"&gt;:is_at_school&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:default&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:comment&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"是否在校"&lt;/span&gt;

      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timestamps&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是格式化之后的代码：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CreateStudents&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Migration&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;change&lt;/span&gt;
    &lt;span class="n"&gt;create_table&lt;/span&gt; &lt;span class="ss"&gt;:students&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:comment&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"学生信息表"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;  &lt;span class="ss"&gt;:number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;               &lt;span class="ss"&gt;:comment&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"学号"&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;  &lt;span class="ss"&gt;:name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                 &lt;span class="ss"&gt;:comment&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"姓名"&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;date&lt;/span&gt;    &lt;span class="ss"&gt;:birthday&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;             &lt;span class="ss"&gt;:comment&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"生日"&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;  &lt;span class="ss"&gt;:gender&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;               &lt;span class="ss"&gt;:comment&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"性别"&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;  &lt;span class="ss"&gt;:id_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;              &lt;span class="ss"&gt;:comment&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"证件类型"&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;  &lt;span class="ss"&gt;:id_number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;            &lt;span class="ss"&gt;:comment&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"证件号码"&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;  &lt;span class="ss"&gt;:country&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;              &lt;span class="ss"&gt;:comment&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"国别"&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;  &lt;span class="ss"&gt;:nation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;               &lt;span class="ss"&gt;:comment&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"民族"&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;  &lt;span class="ss"&gt;:birth_place&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="ss"&gt;:comment&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"籍贯"&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;  &lt;span class="ss"&gt;:political_status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="ss"&gt;:comment&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"政治面貌"&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;  &lt;span class="ss"&gt;:education_level&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="ss"&gt;:comment&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"培养层次"&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;  &lt;span class="ss"&gt;:enroll_method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="ss"&gt;:comment&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"入学方式"&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;date&lt;/span&gt;    &lt;span class="ss"&gt;:enrolled_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="ss"&gt;:comment&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"入学时间"&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;  &lt;span class="ss"&gt;:charge_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="ss"&gt;:comment&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"收费类别"&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;  &lt;span class="ss"&gt;:enrolled_year&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="ss"&gt;:comment&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"学籍年度"&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;  &lt;span class="ss"&gt;:enrolled_place&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="ss"&gt;:comment&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"生源所在地"&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;  &lt;span class="ss"&gt;:bank_card_number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="ss"&gt;:comment&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"银行卡号"&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;  &lt;span class="ss"&gt;:bank_account_number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="ss"&gt;:comment&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"银行账号"&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;boolean&lt;/span&gt; &lt;span class="ss"&gt;:is_active_duty&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="ss"&gt;:default&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="ss"&gt;:comment&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"是否现役军人"&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;boolean&lt;/span&gt; &lt;span class="ss"&gt;:is_equivalent_degree&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:default&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="ss"&gt;:comment&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"是否同等学历"&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;boolean&lt;/span&gt; &lt;span class="ss"&gt;:is_on_record&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;         &lt;span class="ss"&gt;:default&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="ss"&gt;:comment&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"是否在籍"&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;boolean&lt;/span&gt; &lt;span class="ss"&gt;:is_at_school&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;         &lt;span class="ss"&gt;:default&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="ss"&gt;:comment&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"是否在校"&lt;/span&gt;

      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timestamps&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;</description>
      <author>cisolarix</author>
      <pubDate>Sat, 27 Sep 2014 10:23:15 +0800</pubDate>
      <link>https://ruby-china.org/topics/21744</link>
      <guid>https://ruby-china.org/topics/21744</guid>
    </item>
    <item>
      <title>Action mailer 的 reply-to 是不是有问题？</title>
      <description>&lt;p&gt;RT.&lt;/p&gt;

&lt;p&gt;用 action mailer 以 smtp 的方式通过 mailgun 发送。但是收到邮件之后回复的时候，回复地址不能正确识别。还是看图吧。&lt;/p&gt;

&lt;p&gt;&lt;img src="//l.ruby-china.com/photo/951f043615dfaa524d517912ff0c9957.png" title="" alt=""&gt;
这是代码。&lt;/p&gt;

&lt;p&gt;&lt;img src="//l.ruby-china.com/photo/f52d262b9f7f810b6d0a203b0417373d.png" title="" alt=""&gt;
这是 gmail 中查看源文件的结果（在 qq 邮箱里也是同样的结果）&lt;/p&gt;

&lt;p&gt;&lt;img src="//l.ruby-china.com/photo/d4c7e1b7a69d42d1bd49bccf2eb55c86.png" title="" alt=""&gt;
看，回复地址用的是 from 的那个地址，并不是 reply-to 的那个地址。&lt;/p&gt;

&lt;p&gt;我看人家正常的回复地址那个地方是 &lt;code&gt;reply-to&lt;/code&gt;，而我的是 &lt;code&gt;Reply-To[]&lt;/code&gt; 。&lt;/p&gt;

&lt;p&gt;PS：我的 Rails 版本是 4&lt;/p&gt;</description>
      <author>cisolarix</author>
      <pubDate>Fri, 20 Sep 2013 22:12:19 +0800</pubDate>
      <link>https://ruby-china.org/topics/14250</link>
      <guid>https://ruby-china.org/topics/14250</guid>
    </item>
    <item>
      <title>订阅了 users 页面几乎所有的 RSS feed</title>
      <description>&lt;p&gt;Google Reader 关闭之后，先后试用了几家提供订阅服务的网站，感觉都不是很喜欢。正好试用 NetNewsWire, 就将 0&lt;a href="http://ruby-china.org/users" rel="nofollow" target="_blank"&gt;http://ruby-china.org/users&lt;/a&gt; 页面中所有人都订阅了。当然，是指提供了 feed 的诸位。有空慢慢拜读各位大作。&lt;/p&gt;</description>
      <author>cisolarix</author>
      <pubDate>Sun, 28 Jul 2013 19:55:44 +0800</pubDate>
      <link>https://ruby-china.org/topics/12852</link>
      <guid>https://ruby-china.org/topics/12852</guid>
    </item>
    <item>
      <title>Nitrous 在维护中？</title>
      <description>&lt;p&gt;&lt;img src="//l.ruby-china.com/photo/7f608e5690664e03cbaf33fdbfc6e654.jpg" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;如图，我选的是东亚的节点，不知道是所有的节点都维护还是怎么的。&lt;/p&gt;</description>
      <author>cisolarix</author>
      <pubDate>Fri, 12 Jul 2013 10:42:04 +0800</pubDate>
      <link>https://ruby-china.org/topics/12450</link>
      <guid>https://ruby-china.org/topics/12450</guid>
    </item>
    <item>
      <title>thoughtbot 的按月付费好贵啊</title>
      <description>&lt;p&gt;比 codeschool, teamtreehouse, tutsplus, frontendmasters 等等 要贵一大截。99 刀一个月。有没有人尝试过？&lt;/p&gt;

&lt;p&gt;PS: 忘了附上链接了。&lt;a href="https://learn.thoughtbot.com/" rel="nofollow" target="_blank" title=""&gt;thoughtbot learn&lt;/a&gt;&lt;/p&gt;</description>
      <author>cisolarix</author>
      <pubDate>Fri, 10 May 2013 11:56:05 +0800</pubDate>
      <link>https://ruby-china.org/topics/10874</link>
      <guid>https://ruby-china.org/topics/10874</guid>
    </item>
    <item>
      <title>这么好的重构文章不转怎么行？ 记 tower.im 的一次重构</title>
      <description>&lt;p&gt;2013.05.01 by &lt;a href="/zchar" class="user-mention" title="@zchar"&gt;&lt;i&gt;@&lt;/i&gt;zchar&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;原链接 &lt;a href="http://blog.mycolorway.com/2013/05/01/tower-refactor/" rel="nofollow" target="_blank"&gt;http://blog.mycolorway.com/2013/05/01/tower-refactor/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="http://tower.im/" rel="nofollow" target="_blank" title=""&gt;Tower.im&lt;/a&gt;上线已经快半年了，这半年来我们团队小步快跑，为 tower 增加了许多新的功能，使它渐渐完善起来，在这个过程中，tower 的代码量也逐渐增加，随着时间的流逝，系统中积淀的糟糕的代码渐渐增多，于是趁着节假日的到来，花了些时间对代码做了部分重构，在这里记录下来，和大家分享。&lt;/p&gt;

&lt;p&gt;我们知道，重构代码，目的是为了对内让代码变得更精简，提高阅读性和可维护性，而对外不改变旧有的功能，所以首先应该分拆重构任务，一步步执行，不要想一次到位，其次，是通过测试用例保护重构过程，确保重构以后的代码仍然能正常工作。&lt;/p&gt;

&lt;p&gt;明确了目标和方法后，我们就可以进入具体的重构阶段了。Tower.im 使用 Ruby on Rails 这个 MVC 的 Web 框架开发，在代码的组织形式上，Rails 社区比较推崇‘Skinny Controller, Fat Model’，因此，这次的代码重构主要在 Controller 层和 Model 层进行。&lt;/p&gt;

&lt;p&gt;我们来看一个具体的例子。在 Tower 中，像任务、讨论、文档、文件这样的基础资源，都对应着自己的 Controller，比如任务对应着 todos_controller，讨论对应 messages_controller，这些 controllers 里面定义着一些接口方法，比如对这些基础资源进行增删改等等。在旧有代码中，最复杂的一个方法是为这些基础资源添加评论。这个方法的名称叫 comment，让我们看看老的代码长什么样：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;comment&lt;/span&gt;
  &lt;span class="vi"&gt;@message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="vi"&gt;@project.messages.active&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_by_guid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:muid&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;render_404&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@message.blank&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;

  &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="n"&gt;comment_content&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;paras&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'_wysihtml5_mode'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;to_i&lt;/span&gt;
    &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gsub!&lt;/span&gt; &lt;span class="s1"&gt;'

'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'

'&lt;/span&gt;
    &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Sanitize&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;Sanitize&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Config&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;BASIC&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Sanitize&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;gsub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/[\n\r]{1,2}/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt; 

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="mi"&gt;20000&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;lt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bytesize&lt;/span&gt;
    &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;json: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;errors: &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="ss"&gt;msg: &lt;/span&gt;&lt;span class="s1"&gt;'内容最多支持 5000 字，太长了保存不了哦'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;target: :comment_content&lt;/span&gt;&lt;span class="p"&gt;}]},&lt;/span&gt; &lt;span class="ss"&gt;status: &lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt; 

  &lt;span class="n"&gt;attach_guids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:attach_guids&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;present?&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;\&lt;/span&gt;
    &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:attach_guids&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;','&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; 

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;blank?&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;attach_guids&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;empty?&lt;/span&gt;
      &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;json: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;erros: &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="ss"&gt;msg: &lt;/span&gt;&lt;span class="s1"&gt;'没有输入内容哦'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;target: :comment_content&lt;/span&gt;&lt;span class="p"&gt;}]},&lt;/span&gt; &lt;span class="ss"&gt;status: &lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
      &lt;span class="n"&gt;pic_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
      &lt;span class="n"&gt;filenames&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;attach_guids&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;guid&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
        &lt;span class="n"&gt;ta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;current_member&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tmp_attachments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_by_guid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;guid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;pic_count&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;content_type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;starts_with?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'image/'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;ta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;

      &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;lt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;pic_count&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;"[&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;pic_count&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;P]"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;\&lt;/span&gt;
        &lt;span class="n"&gt;filenames&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;', '&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;truncate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt; 

  &lt;span class="vi"&gt;@message.cc_members&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:cc_guids&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;respond_to?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:split&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?\&lt;/span&gt;
    &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:cc_guids&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;','&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
  &lt;span class="vi"&gt;@message.last_commentator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;current_member&lt;/span&gt;
  &lt;span class="vi"&gt;@message.last_comment_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;
  &lt;span class="vi"&gt;@message.last_conn_guid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;live_conn_guid&lt;/span&gt;
  &lt;span class="vi"&gt;@message.save&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; 

  &lt;span class="n"&gt;comment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="vi"&gt;@message.comments.create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;content: &lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;creator: &lt;/span&gt;&lt;span class="n"&gt;current_member&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;last_conn_guid: &lt;/span&gt;&lt;span class="n"&gt;live_conn_guid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;attach_guids&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;any?&lt;/span&gt;
    &lt;span class="n"&gt;tmp_file_dir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'files'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'tmp'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;attach_guids&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each_with_index&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;guid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="n"&gt;ta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;current_member&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tmp_attachments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_by_guid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;guid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;tmp_file_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tmp_file_dir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;ta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;guid&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;tmp_file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tmp_file_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'r'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 

      &lt;span class="n"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attachments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;team: &lt;/span&gt;&lt;span class="n"&gt;current_team&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;attachtarget: &lt;/span&gt;&lt;span class="vi"&gt;@project&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;creator: &lt;/span&gt;&lt;span class="n"&gt;current_member&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;byte_size: &lt;/span&gt;&lt;span class="n"&gt;ta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;byte_size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;content_type: &lt;/span&gt;&lt;span class="n"&gt;ta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;content_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="n"&gt;ta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;file: &lt;/span&gt;&lt;span class="n"&gt;tmp_file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;position: &lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt; 

  &lt;span class="n"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reload&lt;/span&gt;
  &lt;span class="n"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_event_add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current_member&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_new_comment_emails&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current_member&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 

  &lt;span class="vi"&gt;@message.reload&lt;/span&gt; 

  &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;json: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="ss"&gt;guid: &lt;/span&gt;&lt;span class="n"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;guid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;html: &lt;/span&gt;&lt;span class="n"&gt;render_to_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="ss"&gt;partial: &lt;/span&gt;&lt;span class="s1"&gt;'comments/comment'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;locals: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;comment: &lt;/span&gt;&lt;span class="n"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;是不是快崩溃了？这么长的代码，既难以阅读，也难以维护，必须重构。先让我们看看重构以后的结果：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;comment&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;render_404&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="vi"&gt;@message.is_active&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;

  &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cc_members&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attach_guids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;comment_params&lt;/span&gt;
  &lt;span class="n"&gt;comment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="vi"&gt;@message.create_comment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;current_member&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;live_conn_guid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attach_guids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cc_members&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;json: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="ss"&gt;guid: &lt;/span&gt;&lt;span class="n"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;guid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;html: &lt;/span&gt;&lt;span class="n"&gt;render_to_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="ss"&gt;partial: &lt;/span&gt;&lt;span class="s1"&gt;'comments/comment'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;locals: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;comment: &lt;/span&gt;&lt;span class="n"&gt;comment&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;是不是好多了？我理解的重构的核心口诀是“拆”，把复杂代码中的逻辑进一步细分、拆散，放到它应该属于的地方。在拆的过程中，应该考虑到复用，那些拆出来的逻辑应该最大程度的和其它代码共享，达到 &lt;a href="http://en.wikipedia.org/wiki/Don%27t_Repeat_Yourself" rel="nofollow" target="_blank" title=""&gt;DRY&lt;/a&gt; 的目的。具体对于 comment 这个接口来说，我们做了如下的几个重构的步骤：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;在 controller 中使用 before_filter 注入全局对象&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;在 comment 方法的第一行，可以看到我们通过 find_by_guid 方法获取了 &lt;a href="/message" class="user-mention" title="@message"&gt;&lt;i&gt;@&lt;/i&gt;message&lt;/a&gt; 对象，虽然只是一行代码，但是考虑到整个 messages_controller 里面，绝大多数方法都会复用它，所以可以在 controller 里面定义一个 before_filter 方法，来为需要的接口注入 &lt;a href="/message" class="user-mention" title="@message"&gt;&lt;i&gt;@&lt;/i&gt;message&lt;/a&gt; 对象：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;before_filter&lt;/span&gt; &lt;span class="ss"&gt;:load_message_instance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;only: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:comment&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;only 后面可以指定这个 controller 里面有哪些方法是通过 load_message_instance 来注入 &lt;a href="/message" class="user-mention" title="@message"&gt;&lt;i&gt;@&lt;/i&gt;message&lt;/a&gt; 变量，接下来，只需要在 messages_controller 里面定义一个 load_message_instance 的 private method 就可以了：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="kp"&gt;private&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;load_message_instance&lt;/span&gt;
    &lt;span class="vi"&gt;@message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="vi"&gt;@project.messages.find_by_guid&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt; &lt;span class="ss"&gt;:muid&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样首先通过 before_filter，DRY 掉了 controller 里面绝大多数方法重复的一行代码，虽然看上去微不足道，但是随着 controller 的扩展，积少成多，能让你的代码保持精简。&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;使用 params helpers 封装评论所需参数&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;我们知道，controller 中的方法很多时候只是做一个“中转”：接收 http 请求，获取 GET/POST 参数，对参数进行处理后，交由 model 层对数据进行操作，最后将数据经过某种方式的 render 后，返还给浏览器。而在这个中转过程中，我们有大量的操作是在对请求参数做处理，比如在旧版本的 comment 方法中，我们对评论内容进行了过滤，对抄送的成员和附件进行了参数组装，这些操作其实可以放在一个 helper 方法里面来完成，而且考虑到除了 messages_controller 外，其它资源对象的 comment 方法其实在最开始都会对参数进行同样的处理，所以我们可以直接把对 comment 方法的 params 处理封装成一个 application_helper 方法：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;comment_params&lt;/span&gt;
  &lt;span class="n"&gt;cc_members&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:cc_members&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;respond_to?&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:split&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;\&lt;/span&gt;
    &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:cc_guids&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;','&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
  &lt;span class="n"&gt;attach_guids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:attach_guids&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;present?&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;\&lt;/span&gt;
    &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:attach_guids&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;','&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:[]&lt;/span&gt;
  &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;get_safe_content&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:comment_content&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                             &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'_wysihtml5_mode'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;to_i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                             &lt;span class="n"&gt;attach_guids&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cc_members&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attach_guids&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到，这个在 application_helper 中的方法，实际上只是对三个 POST 参数进行了相应的处理，最后以数组的形式返回，而这个 comment_params 方法中，又有一次小的重构，即通过 get_safe_content 方法对复杂的 content 进行处理，实际上这里仍然可以继续优化下去，不过就目前为止，已经足够了。这样，在 messages_controller 里面，我们通过数组的连续赋值特性，就可以一行代码获取数据层实际上需要的数据了：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cc_members&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attach_guids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;comment_params&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;哇，这样又精简掉了一大堆丑陋的代码。&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;将模型验证放进模型中&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;细心的童鞋应该会发现，重构后的代码里面没有任何的错误处理，是我们去掉了这部分代码么？实际上并没有，在 Rails 里面，对数据的合法性验证往往是放在 model 层去处理的，controller 只需要“中转”，没必要去判断。Rails 的 model 类往往会使用一系列的 validators 来对数据进行合法性验证，比如旧有代码里面，我们需要对 content 的长度和是否为空进行判断，其实只需要在 comment 的 model 里，用一个 validates 语句就能搞定：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;validates&lt;/span&gt; &lt;span class="ss"&gt;:content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="ss"&gt;presence: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;message: &lt;/span&gt;&lt;span class="s2"&gt;"没有输入内容哦"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="ss"&gt;length: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;maximum: &lt;/span&gt;&lt;span class="mi"&gt;20000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="ss"&gt;too_long: &lt;/span&gt;&lt;span class="s2"&gt;"内容最多支持 5000 字，太长了保存不了哦"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样，当 content 本身为空，或者超过长度限制的时候，model 会直接 raise 错误信息，当然，更进一步的，你还可以把错误信息本身放到 I18n 里面，全站复用。&lt;/p&gt;

&lt;p&gt;这个改动涉及到 Rails 框架本身的习惯，你当然也可以在 controller 层去做数据验证，但是使用 model 层自己的 validates，才是在正确的地点，做正确的事情。&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;使用 Concerns 抽象模型结构&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;最后，是这次优化的重头戏。Rails 社区虽然提倡‘Skinny Controller, Fat Model’，但在实际开发中，太 fat 的 model 层也是很让人头疼的，所以我们引入了 Concerns 机制来对模型层进一步做抽象。关于 Concerns，大家可以参考 DHH 的这篇文章：&lt;a href="http://37signals.com/svn/posts/3372-put-chubby-models-on-a-diet-with-concerns" rel="nofollow" target="_blank" title=""&gt;Put chubby models on a diet with concerns&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;对于 tower 来说，基础资源都可以被评论，这个意思是指，每个基础资源的模型都 has_many comments，每个基础资源的模型都有一个 cc_members 字段保存着当前的抄送成员的 id 数组，在 model 中也有一系列的方法来实现对 cc_members 的操作，这些代码其实大部分都可以重用，而且它们都从侧面反映出一个实时，就是基础资源模型本身，应该是一个 commentable 的抽象。因此，我们可以使用 concerns 实现这个抽象：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Extensions&lt;/span&gt;
  &lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Commentable&lt;/span&gt;
    &lt;span class="kp"&gt;extend&lt;/span&gt; &lt;span class="no"&gt;ActiveSupport&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Concern&lt;/span&gt;

    &lt;span class="n"&gt;included&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="ss"&gt;has_many: &lt;/span&gt;&lt;span class="n"&gt;comments&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;as: :commentable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;dependent: :delete_all&lt;/span&gt;
      &lt;span class="n"&gt;belongs_to&lt;/span&gt; &lt;span class="ss"&gt;:last_commentator&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;class_name: :Member&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_comment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;creator&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;live_conn_guid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attach_guids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cc_members&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="c1"&gt;# 具体代码省略&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;cc_members&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;guids&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="c1"&gt;# 具体代码省略&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;cc_members&lt;/span&gt;
      &lt;span class="c1"&gt;# 具体代码省略&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;cc_member_add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;member&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="c1"&gt;# 具体代码省略&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到，在这个 Commentable 里面，我们抽出了所有 Commentable 对象所应该具有的关系，并且创建了一系列的方法供基础资源使用，这些方法不是接口方法，它们不仅定义了方法名称和参数，还实际提供相应的功能，在完成了这个 concern 以后，我们就可以通过 Module Mixin 的方法，把这个 commentable concern 混入所有基础资源模型里面了：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Message&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;lt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;Extensions&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Commentable&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样，我们只需要在 &lt;a href="/message" class="user-mention" title="@message"&gt;&lt;i&gt;@&lt;/i&gt;message&lt;/a&gt; 这个资源对象上调用 create_comment 方法，传入合适的参数，就能完成创建评论的操作了，核心还是，让 model 层去做关于数据的处理，controller 只需要做中间的中转即可。&lt;/p&gt;

&lt;p&gt;Concerns 非常强大，在 Tower 中，像 回收站 (trashable)、星标 (starable) 等很多功能，都能通过 Concerns 来做抽象，让代码保持简洁。在 Rails4 中，Concerns 也被默认加载，算是众望所归吧。&lt;/p&gt;

&lt;p&gt;总结&lt;/p&gt;

&lt;p&gt;以上就是对 Tower.im 的一次简单的代码优化，当然，可以做的还有很多，不过目前这个 comment 方法已经可以让人轻松理解，我们的重构目的达到。&lt;/p&gt;

&lt;p&gt;再总结一下优化过程：在 Tower 里面拆分任务，小步改进（把复杂的代码先从方法中剥离出来，放到它应该放在的地方），跑通测试，然后循环往复。&lt;/p&gt;

&lt;p&gt;最后推荐大家一个网站：&lt;a href="https://codeclimate.com/?v=b" rel="nofollow" target="_blank" title=""&gt;Code Climate&lt;/a&gt;，如果你的项目也使用 Rails 框架，可以把代码放到上面跑一跑，看看最后是个什么级别的呢？&lt;/p&gt;

&lt;p&gt;参考文档&lt;/p&gt;

&lt;p&gt;1、&lt;a href="http://yedingding.com/2013/03/04/steps-to-refactor-controller-and-models-in-rails-projects.html" rel="nofollow" target="_blank" title=""&gt;重构 Rails 项目之最佳实践&lt;/a&gt;
2、&lt;a href="http://terrytai.com/articles/f8796e81-refactor-session-for-re-education-in-thought-works" rel="nofollow" target="_blank" title=""&gt;A Refactor Session for Re-education in ThoughtWorks&lt;/a&gt;
3、&lt;a href="http://37signals.com/svn/posts/3372-put-chubby-models-on-a-diet-with-concerns" rel="nofollow" target="_blank" title=""&gt;Put chubby models on a diet with concerns&lt;/a&gt;
4、&lt;a href="http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/" rel="nofollow" target="_blank" title=""&gt;7 Patterns to Refactor Fat ActiveRecord Models&lt;/a&gt;&lt;/p&gt;</description>
      <author>cisolarix</author>
      <pubDate>Fri, 03 May 2013 12:10:07 +0800</pubDate>
      <link>https://ruby-china.org/topics/10681</link>
      <guid>https://ruby-china.org/topics/10681</guid>
    </item>
    <item>
      <title>Padrino 最新的版本是不是变化很大啊？</title>
      <description>&lt;p&gt;Padrino 最新发布的 0.11.0 的版本改动感觉好多啊。PageDown 往下翻了 2.5 屏之多。&lt;/p&gt;

&lt;p&gt;详见：
&lt;a href="http://www.padrinorb.com/changes" rel="nofollow" target="_blank"&gt;http://www.padrinorb.com/changes&lt;/a&gt;&lt;/p&gt;</description>
      <author>cisolarix</author>
      <pubDate>Fri, 05 Apr 2013 16:36:46 +0800</pubDate>
      <link>https://ruby-china.org/topics/9973</link>
      <guid>https://ruby-china.org/topics/9973</guid>
    </item>
    <item>
      <title>看到彩程设计的 Tower 正式上线了。</title>
      <description>&lt;p&gt;赶紧注册了。还在熟悉当中。。。&lt;/p&gt;

&lt;p&gt;PS：邮件通知服务好像用的是 mailgun 的。&lt;/p&gt;</description>
      <author>cisolarix</author>
      <pubDate>Mon, 26 Nov 2012 13:34:21 +0800</pubDate>
      <link>https://ruby-china.org/topics/7097</link>
      <guid>https://ruby-china.org/topics/7097</guid>
    </item>
    <item>
      <title>jQuery v1.8.2 官方说 minified 的版本是 32kB，为啥我下载下来之后发现时 92kB 呢？</title>
      <description>&lt;p&gt;RT。暂时未截图，如果需要，可以补上。&lt;/p&gt;</description>
      <author>cisolarix</author>
      <pubDate>Mon, 15 Oct 2012 18:23:52 +0800</pubDate>
      <link>https://ruby-china.org/topics/6078</link>
      <guid>https://ruby-china.org/topics/6078</guid>
    </item>
    <item>
      <title>请教一个 jQuery 从 twitter 抓取最近 5 条推文的诡异问题</title>
      <description>&lt;p&gt;昨天学习 &lt;a href="http://book.douban.com/subject/10569608/" rel="nofollow" target="_blank"&gt;http://book.douban.com/subject/10569608/&lt;/a&gt; 这本书的第六章，章后最后一题要求用 jQuery 从 twitter 上抓取最近的 5 条推文，并显示在 div#dictionary 区域。
我一开始用的是 getJSON 方法。代码如下：
$(function(){
      var url = "&lt;a href="https://api.twitter.com/1/statuses/user_timeline.json" rel="nofollow" target="_blank"&gt;https://api.twitter.com/1/statuses/user_timeline.json&lt;/a&gt;?      screen_name=cisolarix&amp;amp;count=5";
     $.getJSON(url + "?callback=?", function(data){
      var html = '';
      $.each(data, function(idx, value){
      html += '&lt;/p&gt;' + value.text +'';
      html += '' + value.created_at + '';
    });
    $('#dictionary').html(html);
  }).error(function(){
    console.log('error');
  });
});

&lt;p&gt;但是始终在控制台打印 error 信息。在 chrome 的 developer tool --&amp;gt; network 中能看到请求的回复 status 是 200. 直接将请求 url 复制到 chrome 中也是可以正常得到 json 数据的，只是推文条数为 3 条（初步怀疑是 twitter 做了限制）。&lt;/p&gt;

&lt;p&gt;上面的方法测试未果后，用 $.ajax() 方法测试，代码如下:
    $(function(){
      $.ajax('&lt;a href="https://api.twitter.com/1/statuses/user_timeline.json" rel="nofollow" target="_blank"&gt;https://api.twitter.com/1/statuses/user_timeline.json&lt;/a&gt;', {
          crossDomain: true,
          data: {
            screen_name: 'cisolarix',
            count: 5
          },
          dataType: 'jsonp'
        }).done(function (tweets) {
          var html = '';
          $.each(tweets, function(idx, value){
            html += '&lt;/p&gt;' + value.text +'';
            html += '' + value.created_at + '';
          });
          $('#dictionary').html(html);
        });
    });

&lt;p&gt;这次就可以正常显示推文了，尽管还是三条。
我想请教的是，为啥 getJSON 方法在这种情境下不能得到 json 数据？
提前谢谢各位。&lt;/p&gt;

&lt;p&gt;PS：我使用的 jQuery 版本为：v1.6.2
PS again: 上面的代码段不知道如何跟正文区别显示。&lt;/p&gt;</description>
      <author>cisolarix</author>
      <pubDate>Mon, 15 Oct 2012 12:15:43 +0800</pubDate>
      <link>https://ruby-china.org/topics/6068</link>
      <guid>https://ruby-china.org/topics/6068</guid>
    </item>
    <item>
      <title>Progmatic 的 Ruby 教程团购集结号</title>
      <description>&lt;p&gt;团购&lt;a href="http://pragmaticstudio.com/ruby" rel="nofollow" target="_blank" title="Progmatic 的 Ruby 教程"&gt;Progmatic 的 Ruby 教程&lt;/a&gt;的想法来自与&lt;a href="http://ruby-china.org/topics/5026" title="网上有哪些付费的学习资源，各位大神推荐一下"&gt;这个帖子&lt;/a&gt;的讨论。&lt;/p&gt;

&lt;p&gt;但是鉴于 199 刀的价格对于我们天朝小民有点小贵。所以希望大家一起团购，共同学习，共同进步。&lt;/p&gt;

&lt;p&gt;&lt;a href="/ted" class="user-mention" title="@ted"&gt;&lt;i&gt;@&lt;/i&gt;ted&lt;/a&gt; 已经表示愿意参加团购&lt;/p&gt;</description>
      <author>cisolarix</author>
      <pubDate>Thu, 13 Sep 2012 09:38:07 +0800</pubDate>
      <link>https://ruby-china.org/topics/5515</link>
      <guid>https://ruby-china.org/topics/5515</guid>
    </item>
    <item>
      <title>昨天买了 codeschool 一个月的课程</title>
      <description>&lt;p&gt;RT，第一个月是 20 刀，据说以后每个月是 25 刀。主要有练习，这个挺吸引我的。&lt;/p&gt;</description>
      <author>cisolarix</author>
      <pubDate>Fri, 07 Sep 2012 19:53:36 +0800</pubDate>
      <link>https://ruby-china.org/topics/5416</link>
      <guid>https://ruby-china.org/topics/5416</guid>
    </item>
  </channel>
</rss>
