<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>zamia (imedal)</title>
    <link>https://ruby-china.org/zamia</link>
    <description></description>
    <language>en-us</language>
    <item>
      <title>用 500 行 Golang 代码实现高性能的消息回调中间件</title>
      <description>&lt;h2 id="用500行 Golang 代码实现高性能的消息回调中间件"&gt;用 500 行 Golang 代码实现高性能的消息回调中间件&lt;/h2&gt;
&lt;p&gt;本文描述了如何实现一个消息回调中间件，得益于 golang 管道和协程的编程思想，通过巧妙的设计，只需要约 500 行代码就可以实现高性能、优雅关闭、自动重连等特性，全部代码也已经开源在 &lt;a href="https://github.com/fishtrip/watchman" rel="nofollow" target="_blank" title=""&gt;github/fishtrip/watchman&lt;/a&gt;。&lt;/p&gt;
&lt;h2 id="问题"&gt;问题&lt;/h2&gt;
&lt;p&gt;随着业务复杂度的增加，服务拆分后服务数量不断增加，异步消息队列的引入是必不可少的。当服务较少的时候，比如业务早期，很多时候就是一个比较大的单体应用或者少量几个服务，消息队列（之后写做 MQ，Message Queue）的使用方法如下：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;发送端，直接连接 MQ，根据业务需求发送消息；&lt;/li&gt;
&lt;li&gt;消费端，通过一个后台进程，通过长连接连接至 MQ，然后实时消费消息，然后进行相应的处理；&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;相对来说，发送端比较简单，消费端比较复杂，需要处理的逻辑比较多。比如目前我们公司使用的 sneakers 需要处理如下的逻辑：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;消费端需要长连接，需要独立的进程实时消费消息（某些语言可能是一个独立的线程）；&lt;/li&gt;
&lt;li&gt;消费消息之后，需要加载业务框架（比如 Sneakers 需要加入 Rails 环境执行业务代码）调用相关代码来消费消息；&lt;/li&gt;
&lt;li&gt;MQ 无法连接时，需要自动重连，同时应用也需要能够优雅重启，不至于丢消息。&lt;/li&gt;
&lt;li&gt;消费消息很可能处理失败，这个时候需要比较安全可靠的机制保证不能丢失消息，同时也要求能够过一段时间对消息进行重试，重试多次之后也需要能够对消息进一步做处理；&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;这个时候的系统架构一般如下： 
&lt;img src="https://l.ruby-china.com/photo/2017/554c8fa2-742e-4809-9c2b-f58728bb942b.jpg!large" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;而随着服务增多，如果每个需要消费消息的服务都部署一个这样的后台进程显然不够环保：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;每个服务增加一个进程，增加了部署运维的成本；&lt;/li&gt;
&lt;li&gt;对于队列的管理（创建、销毁、binding）以及消息重试机制，每个服务来自己负责的话，很容易造成标准不统一；&lt;/li&gt;
&lt;li&gt;如果不同的服务是不同的语言、不同的框架，每个语言又都要实现一遍，会浪费不少开发资源；&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;那有没有更好的办法呢？&lt;/p&gt;

&lt;p&gt;其中一般办法是打造一个全局的、高性能的消息回调中间件，中间件来负责队列的管理、消息的收发、重试以及出错处理，这样就不再需要每个服务去考虑诸如消息丢失、消息重试等问题了，基本解决了上面的缺点。具体这个消息回调中心应该具备哪些功能呢？&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;统一管理所有 MQ 队列的创建和消息监听；&lt;/li&gt;
&lt;li&gt;当有消息接收到时，中间件调用相关服务的回调地址，因为回调中心负责所有的服务，该中间件必须是高性能、高并发的；&lt;/li&gt;
&lt;li&gt;中间件应当具备消息重试的功能，同时重试消息的时候不应该丢失消息；&lt;/li&gt;
&lt;li&gt;中间件应当具备「重连」和「优雅关闭」等基础功能，这样才能保证不丢消息；&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;这时候架构如下：
&lt;img src="https://l.ruby-china.com/photo/2017/e389c390-18b5-4011-a92a-4045af5469f2.jpg!large" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;这样的话，每个服务的工作就变得轻量了很多。本文的目的就是来实现一版生产环境可用的消息回调中间件。当然，我们第一版的回调中心也不需要太多功能，有如下的限制：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;整个重试流程需要 RabbitMQ 内置功能支持，所以暂时只支持 RabbitMQ；&lt;/li&gt;
&lt;li&gt;目前只支持 HTTP 回调方式；&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;基本的需求有了，如何实现一个这样的消息回调中间件呢？&lt;/p&gt;
&lt;h2 id="解决思路"&gt;解决思路&lt;/h2&gt;&lt;h3 id="开发语言选择"&gt;开发语言选择&lt;/h3&gt;
&lt;p&gt;Golang 作为「系统级开发语言」，非常适合开发这类中间件。内建的 goroutine/channel 机制非常容易实现高并发。而作为 Golang 新手，这个项目也不复杂，很适合练手和进一步学习。&lt;/p&gt;
&lt;h3 id="消息可靠性"&gt;消息可靠性&lt;/h3&gt;
&lt;p&gt;关于重试和出错处理呢？我们从 Sneakers 的实现中借鉴了它的方法，通过利用 RabbitMQ 内置的机制，也就是通过 x-dead-letter 机制来保证消息在重试时可以做到高可靠性，具体可以参考前段时间我写的&lt;a href="https://ruby-china.org/topics/34022" title=""&gt;这篇文章&lt;/a&gt;。简单总结一下文中的思路：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;消息正常被处理时，直接 ack 消息就好；&lt;/li&gt;
&lt;li&gt;当消息处理出错，需要重试时，reject 消息，此时消息会进入到单独的 retry 队列；&lt;/li&gt;
&lt;li&gt;retry 队列配置好了 ttl 超时时间，等到超时时，消息会进入到 requeue Exchange（RabbitMQ 的概念，用来做消息的路由）；&lt;/li&gt;
&lt;li&gt;消息会再次进入工作队列，等待被下次重试；&lt;/li&gt;
&lt;li&gt;如果消息的重试次数超过了一定的值，那么消息会进入到错误队列等待进一步处理；&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;这里面有两个地方利用了 RabbitMQ 的 Dead-Letter 机制：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;当消息被 reject 之后，消息进入到该队列的 dead-letter Exchange，也就是重试队列；&lt;/li&gt;
&lt;li&gt;当重试队列的消息，在超时时（队列设置了 ttl-expires 时长），消息进入该队列的 dead-letter Exchange，也就是重新进入了工作队列。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;通过这种机制，可以保证在进行消息处理的时候，不管是正常、还是出错时，消息都不会丢失。关于这里进一步的细节可以参考上面的文章。&lt;/p&gt;
&lt;h3 id="实现高并发"&gt;实现高并发&lt;/h3&gt;
&lt;p&gt;对于中间件，性能的要求比较高，性能也包含两个方面：低延迟和高并发。低延迟在这个场景中我们无法解决，因为一个消息回调之后的延迟是其他业务服务决定的。所以我们更多的是追求高并发。&lt;/p&gt;

&lt;p&gt;如何获得高并发？首先是开发语言的选择，这类底层的中间件很适合用 Golang 来实现，为什么呢？因为回调中心的主逻辑就是不断回调各个服务，而各个服务的延迟时间中间件无法控制，所以如果想获得高并发，最好是使用异步事件这种机制。而借助于 Golang 内置的 Channel，既可以获得接近于异步事件的性能，又可以让整个开发变得简单高效，是一个比较合适的选择。&lt;/p&gt;

&lt;p&gt;具体实现呢？其实对于一个回调中心来说，大概分成这么几个步骤：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;获取消息：连接消息队列（目前我们只需要支持 RabbitMQ 即可），消费消息；&lt;/li&gt;
&lt;li&gt;回调业务接口：消费消息之后，根据配置信息，不同的队列可能需要调用不同的回调地址，开始调用业务接口（目前我们只需要支持 HTTP 协议即可）；&lt;/li&gt;
&lt;li&gt;根据回调结果处理消息：如果调用业务接口如果成功，则直接 ack 消息即可；如果调用失败，则 reject 此消息；如果超过最大重试次数，则进入出错处理逻辑；&lt;/li&gt;
&lt;li&gt;出错处理逻辑：把原有消息 ack，同时转发此消息进入 error 队列，等待进一步处理（可能是报警，然后人工处理）；&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;通过消息这么一个「实体」可以把所有上面的流程串联起来，是不是很像 pipeline？而 &lt;a href="https://blog.golang.org/pipelines" rel="nofollow" target="_blank" title=""&gt;pipeline&lt;/a&gt; 的设计模式是 Golang 非常推荐的实现高并发的方式。上面的每一个步骤可以看做一组协程（goroutine），他们之间通过管道通信，因此不存在资源竞争的情况，大大降低了开发成本。&lt;/p&gt;

&lt;p&gt;而上面每个步骤可以通过设计不同的 Goroutine 模型来实现高并发：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;获取消息：需要长连接 RabbitMQ，较好的实现方式是每个队列有独立的一组协程，这样队列之间的消息接受互相不会干扰，如果出现了繁忙队列和较闲的队列时，也不会出现消息处理不及时的情况；&lt;/li&gt;
&lt;li&gt;回调业务接口：每个消息都会调用业务接口，但是业务接口的处理时长对于中间件来说是透明的。因此，这里最好的模型是每个消息一个协程。如果出现了较慢的接口，那么通过 goroutine 的内部调度机制，并不会影响系统的吞吐，同时 goroutine 可以支持上百万的并发，因此用这种模式最合适。&lt;/li&gt;
&lt;li&gt;根据回调结果处理消息：这个步骤主要是要连接 RabbitMQ，发送 ack/reject 消息。默认我们认为 RabbitMQ 是可靠的，这里统一用同一组协程来处理即可。&lt;/li&gt;
&lt;li&gt;出错处理逻辑：这里的消息量应该大大降低，因为多次失败（超过重试次数）的消息才会进入到这里。我们也采用同一组协程处理即可。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;上面四个步骤，我们用了三种协程的设计模型，细化一下上面的图就是这个样子的。
&lt;img src="https://l.ruby-china.com/photo/2017/632cb80f-8f22-4ab3-8465-04c7fd0e2dc1.jpg!large" title="" alt=""&gt;&lt;/p&gt;
&lt;h2 id="实现"&gt;实现&lt;/h2&gt;
&lt;p&gt;有了上面的设计过程，代码并不复杂，大概分为几部分：配置管理、主流程、消息对象、重试逻辑以及优雅关闭等的实现。详细的代码放在 github：&lt;a href="https://github.com/fishtrip/watchman" rel="nofollow" target="_blank" title=""&gt;fishtrip/watchman&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="配置管理"&gt;配置管理&lt;/h3&gt;
&lt;p&gt;配置管理这部分，这个版本我们实现的比较简单，就是读取 yml 配置文件。配置文件主要包含的主要是三部分信息：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;消息队列定义。要根据消息队列的配置调用 RabbitMQ 接口生成相关的队列（重试队列、错误队列等）；&lt;/li&gt;
&lt;li&gt;回调地址配置。不同的消息队列需要不同的回调地址；&lt;/li&gt;
&lt;li&gt;其他配置。如重试次数、超时等。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config/queues.example.yml&lt;/span&gt;
&lt;span class="na"&gt;projects&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
    &lt;span class="na"&gt;queues_default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;notify_base&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:8080"&lt;/span&gt;
      &lt;span class="na"&gt;notify_timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
      &lt;span class="na"&gt;retry_times&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;40&lt;/span&gt;
      &lt;span class="na"&gt;retry_duration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;300&lt;/span&gt;
      &lt;span class="na"&gt;binding_exchange&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;fishtrip&lt;/span&gt;
    &lt;span class="na"&gt;queues&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;queue_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;order_processor"&lt;/span&gt;
        &lt;span class="na"&gt;notify_path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/orders/notify"&lt;/span&gt; 
        &lt;span class="na"&gt;routing_key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;order.state.created"&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;house.state.#"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们使用 yaml.v2 包可以很方便的解析 yaml 配置文件到 struct 之中，比如对于 queue 的定义，struct 实现如下：&lt;/p&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// config.go 28-38&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;QueueConfig&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;QueueName&lt;/span&gt;       &lt;span class="kt"&gt;string&lt;/span&gt;   &lt;span class="s"&gt;`yaml:"queue_name"`&lt;/span&gt;
    &lt;span class="n"&gt;RoutingKey&lt;/span&gt;      &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`yaml:"routing_key"`&lt;/span&gt;
    &lt;span class="n"&gt;NotifyPath&lt;/span&gt;      &lt;span class="kt"&gt;string&lt;/span&gt;   &lt;span class="s"&gt;`yaml:"notify_path"`&lt;/span&gt;
    &lt;span class="n"&gt;NotifyTimeout&lt;/span&gt;   &lt;span class="kt"&gt;int&lt;/span&gt;      &lt;span class="s"&gt;`yaml:"notify_timeout"`&lt;/span&gt;
    &lt;span class="n"&gt;RetryTimes&lt;/span&gt;      &lt;span class="kt"&gt;int&lt;/span&gt;      &lt;span class="s"&gt;`yaml:"retry_times"`&lt;/span&gt;
    &lt;span class="n"&gt;RetryDuration&lt;/span&gt;   &lt;span class="kt"&gt;int&lt;/span&gt;      &lt;span class="s"&gt;`yaml:"retry_duration"`&lt;/span&gt;
    &lt;span class="n"&gt;BindingExchange&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;   &lt;span class="s"&gt;`yaml:"binding_exchange"`&lt;/span&gt;

    &lt;span class="n"&gt;project&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;ProjectConfig&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面之所以需要一个 ProjectConfig 的指针，主要是为了方便读取 project 的配置，因此加载的时候需要把队列指向 project。&lt;/p&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// config.go&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;loadQueuesConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;configFileName&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;allQueues&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;QueueConfig&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;QueueConfig&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// ......&lt;/span&gt;
    &lt;span class="n"&gt;projects&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;projectsConfig&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Projects&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;project&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;projects&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"find project: %s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;project&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


        &lt;span class="c"&gt;// 这里不能写作  queue := project.Queues&lt;/span&gt;
        &lt;span class="n"&gt;queues&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;projects&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Queues&lt;/span&gt;

        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;queue&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;queues&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"find queue: %v"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="c"&gt;// 这里不能写作  queues[j].project = &amp;amp;queue &lt;/span&gt;
            &lt;span class="n"&gt;queues&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;project&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;projects&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="n"&gt;allQueues&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;allQueues&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;queues&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&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="c"&gt;// .......&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面代码中有个地方容易出错，就是在 for 循环内部设置指针的时候不能直接使用 queue 变量，因为此时获取的 queue 变量是一份复制出来的数据，并不是原始数据。&lt;/p&gt;

&lt;p&gt;另外，config.go 中大部分逻辑是按照面向对象的思考方式来书写的，比如：&lt;/p&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// config.go&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;qc&lt;/span&gt; &lt;span class="n"&gt;QueueConfig&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;ErrorQueueName&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;string&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;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"%s-error"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;qc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;QueueName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;qc&lt;/span&gt; &lt;span class="n"&gt;QueueConfig&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;WorkerExchangeName&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;string&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;qc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BindingExchange&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&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;qc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;project&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;QueuesDefaultConfig&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BindingExchange&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;qc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BindingExchange&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过这种方式，可以写出更清晰可维护的代码。&lt;/p&gt;
&lt;h3 id="消息对象封装"&gt;消息对象封装&lt;/h3&gt;
&lt;p&gt;整个程序中，在 channel 中传递的数据都是 Message 对象，通过这种对象封装，可以非常方便的在各种类型的 Goroutine 之间传递数据。&lt;/p&gt;

&lt;p&gt;Message 类的定义如下：&lt;/p&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Message&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;queueConfig&lt;/span&gt;    &lt;span class="n"&gt;QueueConfig&lt;/span&gt; &lt;span class="c"&gt;// 消息来自于哪个队列&lt;/span&gt;
    &lt;span class="n"&gt;amqpDelivery&lt;/span&gt;   &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;amqp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Delivery&lt;/span&gt; &lt;span class="c"&gt;// RabbitMQ 的消息封装&lt;/span&gt;
    &lt;span class="n"&gt;notifyResponse&lt;/span&gt; &lt;span class="n"&gt;NotifyResponse&lt;/span&gt; &lt;span class="c"&gt;// 消息回调结果&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们把 RabbitMQ 中的原生消息以及队列信息、回调结果封装在一起，通过这种方式把 Message 对象在管道之间传递。同时 Message 封装了众多的方法来供其他协程方便的调用。&lt;/p&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Message 相关方法&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;CurrentMessageRetries&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Notify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Message&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;IsMaxRetry&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;IsNotifySuccess&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Ack&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Reject&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Republish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="k"&gt;chan&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;CloneAndPublish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;channel&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;amqp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Channel&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意上面方法的接收对象，带指针的接收对象表示会修改对象的值。&lt;/p&gt;
&lt;h3 id="主流程"&gt;主流程&lt;/h3&gt;
&lt;p&gt;主流程就是我们上面说的，通过 pipeline 的模式、把消息的整条流程串联起来。最核心的代码在这里：&lt;/p&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// main.go&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;resendMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ackMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;workMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;receiveMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;allQueues&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;done&lt;/span&gt;&lt;span class="p"&gt;))))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面每个函数都接收相同的管道定义，因此可以串联使用。其实每个函数的实现区别不大，不同的协程模型可能需要不同的写法。&lt;/p&gt;

&lt;p&gt;下面是 receiveMessage 的写法，并进行了详细的注释。revceiveMessage 对每个消息队列都生成了 N 个协程，然后把读取的消息全部写入管道。&lt;/p&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// main.go&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;receiveMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;queues&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;QueueConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;done&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="k"&gt;chan&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="k"&gt;chan&lt;/span&gt; &lt;span class="n"&gt;Message&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="c"&gt;// 创建一个管道，这个管道会作为函数的返回值&lt;/span&gt;
    &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;chan&lt;/span&gt; &lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ChannelBufferLength&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c"&gt;// WaitGroup 用于同步，这里来控制协程是否结束&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;wg&lt;/span&gt; &lt;span class="n"&gt;sync&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WaitGroup&lt;/span&gt;

    &lt;span class="c"&gt;// 入参是队列配置，这个见下文传入的值&lt;/span&gt;
    &lt;span class="n"&gt;receiver&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;qc&lt;/span&gt; &lt;span class="n"&gt;QueueConfig&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;wg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Done&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c"&gt;// RECONNECT 标记用于跳出循环来重新连接 RabbitMQ&lt;/span&gt;
    &lt;span class="n"&gt;RECONNECT&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;setupChannel&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;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;PanicOnError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="c"&gt;// 消费消息&lt;/span&gt;
            &lt;span class="n"&gt;msgs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Consume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;qc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WorkerQueueName&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="c"&gt;// queue&lt;/span&gt;
                &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                   &lt;span class="c"&gt;// consumer&lt;/span&gt;
                &lt;span class="no"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                &lt;span class="c"&gt;// auto-ack&lt;/span&gt;
                &lt;span class="no"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                &lt;span class="c"&gt;// exclusive&lt;/span&gt;
                &lt;span class="no"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                &lt;span class="c"&gt;// no-local&lt;/span&gt;
                &lt;span class="no"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                &lt;span class="c"&gt;// no-wait&lt;/span&gt;
                &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                  &lt;span class="c"&gt;// args&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;PanicOnError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;msgs&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
                    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"receiver: channel is closed, maybe lost connection"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                        &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                        &lt;span class="k"&gt;continue&lt;/span&gt; &lt;span class="n"&gt;RECONNECT&lt;/span&gt;
                    &lt;span class="p"&gt;}&lt;/span&gt;

                    &lt;span class="c"&gt;// 这里生成消息的 UUID，用来跟踪整个消息流，稍后会解释&lt;/span&gt;
                    &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MessageId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"%s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewV4&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
                    &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;qc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

                    &lt;span class="c"&gt;// 这里把消息写到出管道&lt;/span&gt;
                    &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;

                    &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"receiver: received msg"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;done&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;

                    &lt;span class="c"&gt;// 当主协程收到 done 信号的时候，自己也退出&lt;/span&gt;
                    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"receiver: received a done signal"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="k"&gt;return&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="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;queue&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;queues&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;wg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ReceiverNum&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;ReceiverNum&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

            &lt;span class="c"&gt;// 每个队列生成 N 个协程共同消费&lt;/span&gt;
            &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="n"&gt;receiver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;queue&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="c"&gt;// 控制协程，当所有的消费协程退出时，出口管道也需要关闭，通知下游协程&lt;/span&gt;
    &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;wg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Wait&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"all receiver is done, closing channel"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nb"&gt;close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;)&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;out&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;里面有几个关键点需要注意。&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;每个函数都是类似的结构，一组工作协程和协作协程，当全部工作协程退出时，关闭出口管道，通知下游协程。注意 golang 中，对于管道的使用，需要从写入端关闭，否则很容易出现崩溃。&lt;/li&gt;
&lt;li&gt;我们在每个消息中，记录了一个唯一的 uuid，这个 uuid 用来打日志，来跟踪一整条信息流。&lt;/li&gt;
&lt;li&gt;因为可能出现的网络状况，我们要进行判断，如果出现了连接失败的情况，直接 sleep 一段时间，然后重连。&lt;/li&gt;
&lt;li&gt;done 这个管道是在主协程进行控制的，主要用作优雅关闭。优雅关闭的作用是在升级配置、升级主程序的时候可以保证不丢消息（等待消息真的完成之后才会结束协程，整个程序才会退出）。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="总结"&gt;总结&lt;/h2&gt;
&lt;p&gt;得益于 Golang 的高效的表达能力，通过大约 500 行代码实现了一个稳定的消息回调中间件，同时具备下面的特性：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;高性能。在 macbook pro 15 上简单测试，每个队列的处理能力可以轻松达到 3000 message/second 以上，多个队列也可以做到线性的增加性能，整体应用达到几万每秒很轻松。同时，得益于 golang 的协程设计，如果下游出现了慢调用，那么也不会影响并发。&lt;/li&gt;
&lt;li&gt;优雅关闭。通过对信号的监听，整个程序可以在不丢消息的情况下优雅关闭，利于配置更改和程序重启。这个在生产环境非常重要。&lt;/li&gt;
&lt;li&gt;自动重连。当 RabbitMQ 服务无法连接的时候，应用可以自动重连。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;当然，&lt;a href="http://www.fishtrip.cn" rel="nofollow" target="_blank" title=""&gt;我们团队&lt;/a&gt;目前还都是 golang 新手，也没有做太多的单元测试和性能测试，下一步可能会继续优化，完善测试工作，并且优化配置的管理，欢迎各位去 &lt;a href="https://github.com/fishtrip/watchman" rel="nofollow" target="_blank" title=""&gt;github&lt;/a&gt; 围观源码。&lt;/p&gt;</description>
      <author>zamia</author>
      <pubDate>Sun, 24 Sep 2017 17:20:18 +0800</pubDate>
      <link>https://ruby-china.org/topics/34240</link>
      <guid>https://ruby-china.org/topics/34240</guid>
    </item>
    <item>
      <title>RabbitMQ / Sneakers 消息重试机制及源码简析</title>
      <description>&lt;h2 id="Sneakers 重试机制及源码简析"&gt;Sneakers 重试机制及源码简析&lt;/h2&gt;
&lt;p&gt;&lt;a href="http://sneakers.io" rel="nofollow" target="_blank" title=""&gt;Sneakers&lt;/a&gt; 是基于 &lt;a href="https://www.rabbitmq.com/" rel="nofollow" target="_blank" title=""&gt;RabbitMQ&lt;/a&gt; 的高性能后台任务处理系统，可以方便的对接 RabbitMQ 来解决各种异步消息通信的问题。而异步消息的处理中，为了做到系统的稳定性，任务的重试机制就非常重要，本文简单介绍一下 Sneakers 如何基于 RabbitMQ 内部的机制来实现任务的重试机制的。&lt;/p&gt;
&lt;h2 id="为什么使用 Sneakers"&gt;为什么使用 Sneakers&lt;/h2&gt;
&lt;p&gt;当系统变的越来越大的时候，一般的处理方式是按照产品线、或者按照层次进行服务化的拆分。随着公司的服务越来越多，服务之间的通信也会越来越多。除了各种同步（http、rpc 等）的通信之外，还有很多异步通信的场景，比如以订单举例：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;订单创建成功之后，通知消息系统，需要发送邮件/短信给用户；&lt;/li&gt;
&lt;li&gt;订单支付成功之后，通知工单系统，创建工单用于跟进用户；&lt;/li&gt;
&lt;li&gt;订单完成之后，通知财务系统，需要给供应商转账了；&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这种系统之间的通信，同时又不需要同步进行的操作，使用消息队列就比较合适，目前大鱼使用的是 RabbitMQ。&lt;/p&gt;

&lt;p&gt;同时，使用消息队列之后，也需要一个靠谱的任务处理机制来实时监听消息，保持和消息队列的长连接，一旦有消息到达，就可以开始执行某个任务，本文中 Sneakers 就是这样的一个 Gem。&lt;/p&gt;

&lt;p&gt;同时，消费消息的时候难免需要重试，比如上面的例子中，如果收到消息时，邮件服务器无法连接、工单系统创建工单失败、财务系统可能挂掉，那么这时应该怎么处理呢？一般来说，处理方式是这样的：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;遇到错误时，等待一定的时长，然后重试；&lt;/li&gt;
&lt;li&gt;下次重试时，如果仍然失败，继续等待下次重试；&lt;/li&gt;
&lt;li&gt;重试 N 次之后，如果仍然失败，那么直接进入垃圾消息队列，等待人工处理（比如发送报警给工程师）；&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;背景介绍的差不多了，本文主要要讲的是 Sneakers 如何利用 RabbitMQ 内部提供的 x-dead-letter（死信机制）的机制聪明的进行任务的重试机制的。&lt;/p&gt;
&lt;h2 id="Sneakers 的重试机制简析"&gt;Sneakers 的重试机制简析&lt;/h2&gt;
&lt;p&gt;如果不深入思考，可能觉得一个消息处理系统的重试机制应该很简单，比如读取一条消息，然后处理，如果有异常，则把消息重新发送一遍即可。但是这样有几个问题：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;读取消息之后，如果 worker 崩溃了，那是不是这条消息就丢了呢？&lt;/li&gt;
&lt;li&gt;处理消息时，如果有异常，worker 怎么知道这是第一次异常还是 N 次异常之后呢？如何记录这个重试次数？因为业务上一般重试 N 次之后仍旧失败，就没必要再次重试了，直接报警人工处理即可。&lt;/li&gt;
&lt;li&gt;如果把消息重新发送一遍，这时如果消息发送失败，那么是不是这条消息也就永远丢失了呢？&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;那么 Sneakers 是怎么处理这个问题的呢？它很聪明的利用 x-dead-letter 来处理重试问题。&lt;/p&gt;

&lt;p&gt;X-Dead-Letter 机制指的是：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;队列中的消息可以被『Dead-Lettered』，也就是说，当下面的情况发生的时候消息会被重新投递到另外一个队列：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;消息被 reject，同时 requeue = false；&lt;/li&gt;
&lt;li&gt;消息的 ttl 超时时；&lt;/li&gt;
&lt;li&gt;队列中的消息数超出队列的最大长度限制；&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;

&lt;p&gt;Sneakers 内部借助这个机制实现了消息的重试，同时保证了消息不会丢失以及高性能，它是如何实现的呢？&lt;/p&gt;
&lt;h3 id="概览图"&gt;概览图&lt;/h3&gt;
&lt;p&gt;直接上我画的这个 RabbitMQ + Sneakers 内部处理机制的消息流转图：&lt;/p&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/2017/c289d6fb-5eb2-4bfc-8f55-4bfe5b292201.jpg!large" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;上面这个图中：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;exchange - 业务 Ex，业务消息投递到这个 Ex；&lt;/li&gt;
&lt;li&gt;work-queue - 业务队列，任务都在这个队列处理；&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="初始化"&gt;初始化&lt;/h3&gt;
&lt;p&gt;在初始化阶段，Sneakers 会直接在工作队列之外自动创建一系列的 Exchange（用 X 标记）和队列（用 Q 标记）：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;worker_queue-retry(X) - 工作队列的死信目的地；&lt;/li&gt;
&lt;li&gt;worker_queue-retry(Q) - 绑定了上面这个 Exchange 的队列；同时，它的死信会投递到 worker_queue-retry-requeue；&lt;/li&gt;
&lt;li&gt;worker_queue-error(X) - 如果达到最大重试次数，会把消息投递到这个 Ex；&lt;/li&gt;
&lt;li&gt;worker_queue-error(Q) - 绑定了上面这个 Ex；&lt;/li&gt;
&lt;li&gt;worker_queue-retry-requeue(X) - 工作队列会绑定到这个 Ex，也就是说这个 Ex 的所有消息会投递到工作队列。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="消息处理阶段"&gt;消息处理阶段&lt;/h3&gt;
&lt;p&gt;在消息处理阶段，用文字来描述一下上图中的流程就是这样子的：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;正常消息进入 exchange 这个交换器（Ex)；&lt;/li&gt;
&lt;li&gt;worker-queue 会接收到这个消息，然后 Sneakers 的 worker 会来消费消息；&lt;/li&gt;
&lt;li&gt;worker 一切正常的时候，直接 ack 消息即可（这是正常流程，图中没有体现）；&lt;/li&gt;
&lt;li&gt;如果 worker 执行出错，首先判断是否到最大重试次数；&lt;/li&gt;
&lt;li&gt;到达最大重试次数，直接把消息投递到 worker_queue-error 这里即可；&lt;/li&gt;
&lt;li&gt;如果没有到达最大重试次数（也就是需要消息重试），那么直接 reject 消息即可。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;其实到这里，Sneakers 内部的代码逻辑就结束了，是不是还挺简单的。而事实上 reject 消息之后，RabbitMQ 内部会做下面的逻辑：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;消息被投递到 worker_queue-retry(X)，然后 worker_queue-retry(Q) 会接受到这个消息；&lt;/li&gt;
&lt;li&gt;worker_queue-retry(Q) 的消息并没有谁会去主动消费，直到消息的 TTL 到期，然后消息被投递到死信 Ex；&lt;/li&gt;
&lt;li&gt;消息被投递到 worker_queue-requeue(X)，然后这个 Ex 的消息直接投递到工作队列；&lt;/li&gt;
&lt;li&gt;工作队列的消息然后就可以等待被消费，也就是一次重试的完成。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="这样实现的好处"&gt;这样实现的好处&lt;/h3&gt;
&lt;p&gt;这样通过 RabbitMQ 和 Sneakers 的配合，就完成了整个消息的重试机制。这么实现的好处至少有下面几点：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;在整个重试过程中，消息不会丢失。不管是 reject 也好，还是 RabbitMQ 内部的 ttl 超时也好，都是可靠的消息投递过程。&lt;/li&gt;
&lt;li&gt;Sneakers 的代码实现逻辑非常简单，基本上只需要在出错时判断是否到最大重试次数，然后 reject 或者投递到 error 队列即可。&lt;/li&gt;
&lt;li&gt;很好的利用了 ttl 的机制，可以配置消息的重试时间。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;是不是也挺简单的？看看代码中如何实现的吧。&lt;/p&gt;
&lt;h2 id="Sneakers 源码简析"&gt;Sneakers 源码简析&lt;/h2&gt;
&lt;p&gt;其实经过上面的分析就能看出来，代码实现其实是非常简单的。我们分两个阶段看，一个是初始化阶段创建各种 Exchange 和队列，一个是消息处理阶段，调用 perform 函数通过捕获异常来做对应的处理。&lt;/p&gt;

&lt;p&gt;（下面的代码来自于 sneakers 2.5.0 版本）&lt;/p&gt;
&lt;h3 id="初始化"&gt;初始化&lt;/h3&gt;
&lt;p&gt;初始化过程就是上面说的建各种队列：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# lib/sneakers/handlers/maxretry.rb&lt;/span&gt;
&lt;span class="c1"&gt;# class Maxretry#initialize&lt;/span&gt;

&lt;span class="n"&gt;retry_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="vi"&gt;@opts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:retry_exchange&lt;/span&gt;&lt;span class="p"&gt;]&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="vi"&gt;@worker_queue_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-retry"&lt;/span&gt;
&lt;span class="n"&gt;error_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="vi"&gt;@opts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:retry_error_exchange&lt;/span&gt;&lt;span class="p"&gt;]&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="vi"&gt;@worker_queue_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-error"&lt;/span&gt;
&lt;span class="n"&gt;requeue_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="vi"&gt;@opts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:retry_requeue_exchange&lt;/span&gt;&lt;span class="p"&gt;]&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="vi"&gt;@worker_queue_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-retry-requeue"&lt;/span&gt;
&lt;span class="n"&gt;retry_routing_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="vi"&gt;@opts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:retry_routing_key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s2"&gt;"#"&lt;/span&gt;

&lt;span class="c1"&gt;# 这里是创建各种 Exchange&lt;/span&gt;
&lt;span class="vi"&gt;@retry_exchange&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="vi"&gt;@error_exchange&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="vi"&gt;@requeue_exchange&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;retry_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;error_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requeue_name&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="nb"&gt;name&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="vi"&gt;@channel.exchange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:type&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'topic'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:durable&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;exchange_durable?&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# 绑定重试队列，注意 x-dead-letter-exchange 和 x-message-ttl 参数&lt;/span&gt;
&lt;span class="vi"&gt;@retry_queue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="vi"&gt;@channel.queue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retry_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:durable&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;queue_durable?&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;:arguments&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="ss"&gt;:'x-dead-letter-exchange'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;requeue_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;:'x-message-ttl'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="vi"&gt;@opts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:retry_timeout&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;60000&lt;/span&gt;
   &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="vi"&gt;@retry_queue.bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@retry_exchange&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:routing_key&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'#'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# 绑定 error 队列 和 error Ex&lt;/span&gt;
&lt;span class="vi"&gt;@error_queue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="vi"&gt;@channel.queue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:durable&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;queue_durable?&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="vi"&gt;@error_queue.bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@error_exchange&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:routing_key&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'#'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# 注意这里，直接把 worker-queue 和 requeue-exchange 绑定&lt;/span&gt;
&lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@requeue_exchange&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:routing_key&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;retry_routing_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="消息处理阶段"&gt;消息处理阶段&lt;/h3&gt;
&lt;p&gt;消息处理阶段的逻辑基本都在 handler_retry 函数中，直接贴代码，删了一些日志语句，也加了一点注释。&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;handle_retry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hdr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;num_attempts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;failure_count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:headers&lt;/span&gt;&lt;span class="p"&gt;])&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;num_attempts&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="vi"&gt;@max_retries&lt;/span&gt;
      &lt;span class="c1"&gt;# 还不到最大重试次数时，reject 消息，消息会进入到队列的 x-dead-letter-exchange，也就是 worker_queue-retry(X)&lt;/span&gt;
      &lt;span class="vi"&gt;@channel.reject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hdr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delivery_tag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
      &lt;span class="c1"&gt;# 到达了最大重试次数，就直接把消息发送到 error Exchange 即可&lt;/span&gt;
      &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&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="nf"&gt;to_json&lt;/span&gt; &lt;span class="c1"&gt;# 这里省略了一些代码&lt;/span&gt;
      &lt;span class="vi"&gt;@error_exchange.publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:routing_key&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;hdr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;routing_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

      &lt;span class="c1"&gt;# 然后正常 ack 消息，消息会从 work-queue 消失。&lt;/span&gt;
      &lt;span class="vi"&gt;@channel.acknowledge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hdr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delivery_tag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kp"&gt;false&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;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面这个 failture_count 怎么实现的呢？其实是利用了 RabbitMQ 的特性。RabbitMQ 中，每当消息被「dead-lettered」，在消息的头 x-death 中会详细记录消息死亡的计数信息，大概长这个样子：&lt;/p&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/2017/2f625e90-7cc6-4c65-9ca6-ca0e12932db2.jpg!large" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;我们看看 Sneakers 中的实现：&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;failure_count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;headers&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;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;nil?&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'x-death'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;nil?&lt;/span&gt;
      &lt;span class="c1"&gt;# 看看有没有相关信息&lt;/span&gt;
      &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
      &lt;span class="c1"&gt;# 把 x-death 中的相关信息找到，这里要比对 queue 的值&lt;/span&gt;
      &lt;span class="n"&gt;x_death_array&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'x-death'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;select&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;x_death&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
        &lt;span class="n"&gt;x_death&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'queue'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="vi"&gt;@worker_queue_name&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;

      &lt;span class="c1"&gt;# 不同 RabbitMQ 版本做了兼容，直接读 count 或者读数组的条目数&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;x_death_array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;x_death_array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'count'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;x_death_array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x_death&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;sum&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;x_death&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'count'&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;x_death_array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count&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;h2 id="总结"&gt;总结&lt;/h2&gt;
&lt;p&gt;Sneakers 的重试机制代码虽然非常简单，但是通过利用 RabbitMQ 本身的机制，很完美的解决了 RabbitMQ 消息重试的问题。但是也有下面的一些问题：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;对 RabbitMQ 有一定的侵入性，无端增加了非常多的 Exchange 和 Queue，看着有点乱；&lt;/li&gt;
&lt;li&gt;通过 ttl 的方式，无法实现阶梯式的重试时间递增，如期望重试间隔是 10s/100s/1h/2h/10h 这样子；&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;总体来说，Sneakers 这种解决重试的机制仍然是不错的机制，利用中间件本身的特性、加上少量的代码即可实现可靠的消息重试。&lt;/p&gt;

&lt;p&gt;欢迎讨论~&lt;/p&gt;</description>
      <author>zamia</author>
      <pubDate>Mon, 04 Sep 2017 22:06:23 +0800</pubDate>
      <link>https://ruby-china.org/topics/34022</link>
      <guid>https://ruby-china.org/topics/34022</guid>
    </item>
    <item>
      <title>Rails 最佳实践 - 定时任务</title>
      <description>&lt;p&gt;开发 Rails 项目中难免遇到一些需要做定时任务的情况，比如每天晚上去跑一些简单的统计，定时更新一下缓存等等情况，虽然是一个简单的事情，可是随着时间的推动，一个项目中的定时任务可能会很多，比如目前我们有一个项目，定时任务有上百条，已经非常的难以管理和容易出问题了。因此，本文简单总结了一些关注点帮助大家理解和规范定时任务的写法。&lt;/p&gt;

&lt;p&gt;先简单介绍一下定时任务，定时任务是指那些周期性执行或者某些时刻固定执行的任务，一般来讲大家都熟悉 Linux 系统的 crontab 配置文件，这里面就是常见的定时任务了。Rails 环境下，最简单的定时任务直接利用 rake 或者 rails runner 执行一个命令，然后把命令放到 crontab 配置文件中即可。&lt;/p&gt;

&lt;p&gt;对于那些一次性执行的任务，大家都知道应该使用 Job 系统来完成，但是有时候有那种延期执行的任务，这个我个人觉得也不是定时任务应该负责的范畴，一般的 Job 系统，比如 delayed job、sidekiq 等也都提供延时执行的功能，直接使用这些就可以了。所以本文主要指那种周期性执行的任务。&lt;/p&gt;
&lt;h2 id="定时任务的职责"&gt;定时任务的职责&lt;/h2&gt;
&lt;p&gt;就跟一个 Class、一个 method 一样，先确定职责是比较重要的一件事。那么定时任务系统的职责是什么呢？&lt;/p&gt;

&lt;p&gt;最初级的做法是直接在 rake task 里面写一通逻辑，直接把任务的主逻辑放在这里。这样的话有一个最明显的问题是难以测试，大家可以 google 一下相关的方案，可以解决，但是较繁琐。&lt;/p&gt;

&lt;p&gt;稍好一点的做法是把业务逻辑封装在一个 Service 里面，然后在 task 里面去调用这个 Service。至少这样的话可以解决掉测试的问题。但是这样做还是有一个问题，就是错误处理的问题。一般 task 中的业务逻辑还相对比较复杂，当这个 task 出错了怎么办呢？比如使用 crontab 来执行某个任务，任务出错时一般可能就是记录日志，再根据错误日志做个报警而已。可是很多时候任务的错误处理需要更及时、同时也属于业务逻辑的一部分，由报警系统处理再反馈给业务系统显然很不合理。&lt;/p&gt;

&lt;p&gt;所以，目前社区内比较推荐的做法是把定时任务作为一个调度器（scheduler）而存在，定时任务只是一个调度器，真正的业务逻辑都是封装在 Job 中。比如下面这种方式：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# lib/tasks/some_task.rake&lt;/span&gt;
&lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="ss"&gt;some_task: :environment&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="no"&gt;SomeJob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perform_async&lt;/span&gt; &lt;span class="c1"&gt;# 调用 Sidekiq 的 Job 来异步执行&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样的话整个定时任务的代码非常简单，只是充当一个调度器（scheduler）的功能，同时通过使用异步任务的形式还获得了下面的好处：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;任务有重试机制。一般的 Job 都可以很简单的配置重试机制，保证了最终 Job 一定会被成功执行。即使有 bug，修复上线之后无需维护，下次重试的时候就可以正常完成任务，非常方便。&lt;/li&gt;
&lt;li&gt;任务有更实时的错误处理机制。比如大鱼内部有一种 Job，在多次重试失败之后需要更改某个 ActiveRecord 的状态为 fail。这种情况通过 Job 的形式就非常方便了。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;比如 Job 大概长这样（拿 Sidekiq 举例）：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/jobs/some_job.rb&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SomeJob&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;Sidekiq&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Workder&lt;/span&gt;

  &lt;span class="n"&gt;sidekiq_retries_exhausted&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;job&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="n"&gt;process_failure_job&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&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;def&lt;/span&gt; &lt;span class="nf"&gt;perform&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;process_failure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;error&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;/code&gt;&lt;/pre&gt;
&lt;p&gt;总之，定时任务书写的时候应该是轻量级的，最好是与 Job 系统联合使用，定时任务只是作为一个调度器，Job 系统来真正执行业务逻辑。&lt;/p&gt;
&lt;h2 id="定时任务的部署"&gt;定时任务的部署&lt;/h2&gt;
&lt;p&gt;rails 社区中大家使用最多的部署方案就是 whenever + linux crontab 方案了，这种方案简单来说也没太问题，简单易用。但是随着项目的复杂度增加，我不太推荐这种部署方案，主要基于以下理由：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;crontab 方案每一条任务都是独立的，都需要完整加载整个 Rails 运行环境，而 Rails 运行环境相对是比较耗资源的。想象一下每次几十个、上百个定时任务不停的启动停止，系统的负载可想而知。&lt;/li&gt;
&lt;li&gt;crontab 本身是系统的组件，这种方案需要依赖于系统的组件，在很多 production 环境下可能没有 crontab 组件，比如运行在 heroku 上，比如运行在 docker 上。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;一个 web 应用也期望都以&lt;a href="https://12factor.net/zh_cn/processes" rel="nofollow" target="_blank" title=""&gt;一个或多个无状态的进程来运行&lt;/a&gt;的形式来运行，从这个角度上来讲，也应该以一个独立进程来运行 cron 任务，而不是依赖于系统的 cron 组件。&lt;/p&gt;

&lt;p&gt;那么有没有好的方案呢，其实方案有挺多的，都可以解决问题，比如：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/jmettraux/rufus-scheduler" rel="nofollow" target="_blank" title=""&gt;rufus-scheduler&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/plashchynski/crono" rel="nofollow" target="_blank" title=""&gt;crono&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;还有一类是 Job 系统的插件形式，但是也可以以独立进程来运行，我没有实践过，应该也差不多：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/moove-it/sidekiq-scheduler" rel="nofollow" target="_blank" title=""&gt;sidekiq-scheduler&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/resque/resque-scheduler" rel="nofollow" target="_blank" title=""&gt;resque-scheduler&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;下面以 crono 来例简单示例一下定时任务的写法，也非常简单。&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Gemfile&lt;/span&gt;
&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'crono'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'~&amp;gt; 1.1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&amp;gt;= 1.1.2'&lt;/span&gt;

&lt;span class="c1"&gt;# config/cronotab.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;HourlyPerformSomething&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;perform&lt;/span&gt;
        &lt;span class="no"&gt;SomeModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="no"&gt;SomeJob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perform_async&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&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;span class="k"&gt;end&lt;/span&gt;

&lt;span class="no"&gt;Crono&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;HourlyPerformSomething&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;every&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hour&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行时：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bundle &lt;span class="nb"&gt;exec &lt;/span&gt;crono &lt;span class="nv"&gt;RAILS_ENV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;production
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="总结"&gt;总结&lt;/h2&gt;
&lt;p&gt;简单总结下就是下面的 2 条吧：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;优先把定时任务当做调度器使用，主要业务逻辑通过 Job 系统来完成。特别是不要在 rake 任务中写执行大量的业务逻辑，会造成难以测试、无法重试和错误处理的问题。&lt;/li&gt;
&lt;li&gt;优先使用独立的进程来管理定时任务，而不是依赖于 Linux 系统的 crontab。这样的好处是更具有移植性、更易运维。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;欢迎讨论~&lt;/p&gt;</description>
      <author>zamia</author>
      <pubDate>Wed, 18 Jan 2017 15:41:12 +0800</pubDate>
      <link>https://ruby-china.org/topics/32162</link>
      <guid>https://ruby-china.org/topics/32162</guid>
    </item>
    <item>
      <title>Rails 最佳实践之配置管理</title>
      <description>&lt;p&gt;大家日常开发 Rails 项目的过程中一定会遇到些配置项（Configuration)，随着配置越来越多，总归需要管理起来，那么如何管理这些配置呢？本文期望梳理一下，找到一个较好的解决方案。&lt;/p&gt;

&lt;p&gt;（其实是我们自己项目中的配置文件非常多且乱，所以力图找到一个好的方法来管理，从最终效果来看，这种方法应该是比较合理的，拿来跟大家分享。）&lt;/p&gt;

&lt;p&gt;先放结论：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;应用配置统一使用 settings.yml 来管理；&lt;/li&gt;
&lt;li&gt;每一个资源配置（数据库、redis、第三方 API）使用独立的配置文件管理；&lt;/li&gt;
&lt;li&gt;资源配置中的敏感信息要使用环境变量管理；&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;如果不太理解的话可以往下看，如果项目中已经这么做了，不用浪费时间~&lt;/p&gt;
&lt;h2 id="配置的简单分类"&gt;配置的简单分类&lt;/h2&gt;
&lt;p&gt;先把如何管理放在一边，看看配置都包括哪几个类别：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Rails 基础配置 (Framework Configuration)
比如 Rails 需要配置 session store 的地址、asset host、action mailer 等等，这些配置都集中在 /config/application, 不同的环境放在 config/environments下面。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;应用配置（Application Configuration)
比如分页时每页的条数，开发环境数据不多，每页 10 条，线上环境需要 50 条。那这个分页条目就是简单的一项配置了。还有一些比如开关类的配置，临时加一个开关，到了固定的时候开启某些功能，都属于典型的应用配置。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;资源配置（Resource Configuration）
Rails 项目本身应该是无状态的（Stateless），所有的资源都应该独立通过配置的形式体现。比如数据库就是一种资源（Resource），像 Redis、ElasticSearch 都可以认为是一种资源。另外，我们把第三方服务也称为资源，比如项目中需要访问第三方 API，那么这个 API 我们也认为是一种资源。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rails 的基础配置无需多说，大家应该都很熟悉了，基本上 Rails 都已经做好了样例，按照样例配置就可以了。比如 config/application.rb 可能会有这些东西：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;config.autoload_paths += Dir["#{config.root}/app/utilities/"]
config.i18n.default_locale = "zh-CN"
config.active_job.queue_adapter = :sidekiq
config.time_zone = 'Beijing'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以下面直接说说应用配置和资源配置。&lt;/p&gt;
&lt;h2 id="应用配置"&gt;应用配置&lt;/h2&gt;&lt;h3 id="简单版本"&gt;简单版本&lt;/h3&gt;
&lt;p&gt;最常用的方法其实是在 config 目录下面写一个 settings.yml 的文件，比如：&lt;/p&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config/settings.yml&lt;/span&gt;
&lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nl"&gt;&amp;amp;default&lt;/span&gt;
    &lt;span class="na"&gt;page_size&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
    &lt;span class="na"&gt;enable_some_feature&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

&lt;span class="na"&gt;development&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;*default&lt;/span&gt;

&lt;span class="na"&gt;production&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;*default&lt;/span&gt;
    &lt;span class="na"&gt;page_size&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;
    &lt;span class="na"&gt;enable_some_feature&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后添加一个 initializer 来加载它：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config/initializer/load_settings.rb&lt;/span&gt;
&lt;span class="vg"&gt;$settings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;YAML&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load_file&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="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="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/config/settings.yml"&lt;/span&gt;&lt;span class="p"&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;env&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;symbolize_keys&lt;/span&gt;

&lt;span class="c1"&gt;# use config in somewhere&lt;/span&gt;
&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;debug&lt;/span&gt; &lt;span class="vg"&gt;$settings&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:page_size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="config_for 版本"&gt;config_for 版本&lt;/h3&gt;
&lt;p&gt;Rails 4.2 以后也提供了简单的方法来加载配置：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config/application.rb&lt;/span&gt;
&lt;span class="vg"&gt;$settings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;config_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:settings&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用 config_for 可以自动加载对应 RAILS_ENV 的配置，还可以加载 ERB 内容，所以尽量使用 config_for 来加载配置。&lt;/p&gt;

&lt;p&gt;但是上面两种方法本质差不多，也可以这样用，不过稍微也有一些不方便的地方：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;使用不太方便，都是字符串做 key，如果使用 symblolized_keys 的话又不支持级联；&lt;/li&gt;
&lt;li&gt;手动加载时不能使用 ERB，比如加载环境变量等。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="使用gem - railsconfig/config"&gt;使用 gem - railsconfig/config&lt;/h3&gt;
&lt;p&gt;这个 gem 稍微升级了一下，做了方便使用的改动，比如自动支持多环境、支持 ERB、支持『.』调用，还可以多级调用。使用也比较简单，一看文档即知。&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Gemfile&lt;/span&gt;
&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'config'&lt;/span&gt;

&lt;span class="c1"&gt;# 执行下面的命令，会生成一些模板&lt;/span&gt;
&lt;span class="c1"&gt;# rails g config:install&lt;/span&gt;

&lt;span class="c1"&gt;# 调用时&lt;/span&gt;
&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;debug&lt;/span&gt; &lt;span class="no"&gt;Settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some_config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;other_config&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个 Gem 使用起来很方便，所以我们推荐应用配置均使用 setting.yml，不同的环境的文件放到 settings 目录，这些内容并不包含敏感信息，因此可以放心的 checkin 到 git 库中。&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;config/settings.yml
config/settings/production.yml
config/settings/development.yml
config/settings/test.yml
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="资源配置"&gt;资源配置&lt;/h2&gt;
&lt;p&gt;资源配置会稍微麻烦一些，先说下什么是资源。以下内容来源于 &lt;a href="https://12factor.net/zh_cn/backing-services" rel="nofollow" target="_blank" title=""&gt;12factor&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;把后端服务 (backing services) 当作附加资源。&lt;/li&gt;
&lt;li&gt;后端服务是指程序运行所需要的通过网络调用的各种服务，如数据库，消息/队列系统，SMTP 邮件发送服务，以及缓存系统。&lt;/li&gt;
&lt;li&gt;类似数据库的后端服务，通常由部署应用程序的系统管理员一起管理。除了本地服务之外，应用程序有可能使用了第三方发布和管理的服务。&lt;/li&gt;
&lt;li&gt;12-Factor 应用不会区别对待本地或第三方服务。对应用程序而言，两种都是附加资源，通过一个 url 或是其他存储在 配置 中的服务定位/服务证书来获取数据。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;因为后端服务通常由系统管理员（运维同学）来统一管理，运维架构对于开发工程师来讲可能是透明的，比如一个数据库地址，可能是一组集群，管理员只会提供一个入口的 vip 而已。&lt;/p&gt;

&lt;p&gt;明白了什么是资源之后，我们就把资源相关的配置也分为两类：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;非敏感信息，一般也是开发工程师可以感知的信息。比如一个 API 的地址；&lt;/li&gt;
&lt;li&gt;敏感信息，一般不对开发工程师开放。比如一个 API 的认证信息；&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="resource.yml.example 的方式管理"&gt;resource.yml.example 的方式管理&lt;/h3&gt;
&lt;p&gt;对于资源类的配置，Rails 默认的做法是采用 resource.yml，但是在 git 库中使用 resource.yml.example 的形式。&lt;/p&gt;

&lt;p&gt;比如 rails 项目产生开始就会产生一个 database.yml.example，一般我们通过修改这个文件，然后 copy/move 一份 database.yml 出来使用。&lt;/p&gt;

&lt;p&gt;这种方式有几个问题，&lt;a href="https://12factor.net/zh_cn/config" rel="nofollow" target="_blank" title=""&gt;12factor&lt;/a&gt; 也有详细描述；&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;随着资源越来越多，这类的 yml 越来越多，管理起来比较麻烦；&lt;/li&gt;
&lt;li&gt;每次添加一个资源都添加 yml 的方式很容易漏掉，导致敏感文件加入了 git 库；&lt;/li&gt;
&lt;li&gt;跟语言绑定，无法跨语言使用等；&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="使用 yml + 环境变量来管理资源类配置"&gt;使用 yml + 环境变量来管理资源类配置&lt;/h3&gt;
&lt;p&gt;虽然 12factor 中推荐应用配置存储在环境变量中，但是我们更近一步，只把资源配置中的敏感信息存储在环境变量中，而非敏感信息仍旧存储在 yml 中。&lt;/p&gt;

&lt;p&gt;这样的话配置的目录大概是这样：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 配置相关目录结构&lt;/span&gt;
&lt;span class="n"&gt;rails&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;
  &lt;span class="n"&gt;config&lt;/span&gt;
    &lt;span class="n"&gt;mongo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;yml&lt;/span&gt;
    &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;yml&lt;/span&gt;
    &lt;span class="n"&gt;rabbitmq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;yml&lt;/span&gt;
    &lt;span class="n"&gt;mail_api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;yml&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;env&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一个资源的配置文件大概是这样：&lt;/p&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# mongo.yml&lt;/span&gt;
&lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nl"&gt;&amp;amp;default&lt;/span&gt;
  &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ENV["MONGO_HOST"]&lt;/span&gt;
  &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ENV["MONGO_PORT"]&lt;/span&gt;
&lt;span class="na"&gt;development&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;*default&lt;/span&gt;
  &lt;span class="na"&gt;database&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mongo_development&lt;/span&gt;
&lt;span class="na"&gt;production&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;*default&lt;/span&gt;
  &lt;span class="na"&gt;database&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mongo_production&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;.env 文件大概是这样子的：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# .env 文件&lt;/span&gt;
&lt;span class="nv"&gt;MONGO_HOST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;127.0.0.1
&lt;span class="nv"&gt;MONGO_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;27017
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过结合 yaml 文件和 env 文件，资源的配置被分割为敏感信息和非敏感信息，所有的 yml 都可以安全的 checkin 到 git 库中，而 .env 文件是在部署的时候由运维工程师通过使用一些自动化工具来部署到线上环境。&lt;/p&gt;

&lt;p&gt;因为 .env 文件在 rails 环境中无法做到自动加载，因为我们还需要一个 gem 来辅助：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Gemfile&lt;/span&gt;
&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'dotenv-rails'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;require: &lt;/span&gt;&lt;span class="s1"&gt;'dotenv/rails-now'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过使用 dotenv 这个 Gem，可以做到很方便的自动加载 .env 文件，它甚至可以自动根据 Rails Env 来加载 .env.production 这样的配置，不过我们不需要这个功能，只需要一个 .env 就可以了。&lt;/p&gt;

&lt;p&gt;然后就可以很方面的加载资源类的配置了：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config/application.rb 中添加&lt;/span&gt;
&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redis&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;config_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:redis&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# 在 config/initializer/load_redis.rb 中就可以使用了:&lt;/span&gt;
&lt;span class="vg"&gt;$redis&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&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;configuration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;)&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="c1"&gt;# config/application.rb&lt;/span&gt;

&lt;span class="c1"&gt;# 所有的资源配置放在 config/resources 下面&lt;/span&gt;
&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redis&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;config_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"resources/redis"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样就基本完成了，这样使用 yml + env 的方式的好处包括：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;所有的 yml 都可以安全的 checkin 到 git 库中了，里面不再包含敏感信息；&lt;/li&gt;
&lt;li&gt;一些非敏感的字段在 yml 中给开发工程师来集中管理，这样 env 变量就比较少了；&lt;/li&gt;
&lt;li&gt;部署的时候线上只需要一个 .env 就可以搞定所有的资源类的配置了；比如 capistrano 只需要 link 这一个文件即可。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="总结"&gt;总结&lt;/h2&gt;
&lt;p&gt;虽然配置管理是一个小事情，但是随着项目越来越复杂，调用的第三方服务越来越多，如果不好好进行规划，配置比较混乱，同时也容易出现安全事故（安全无小事啊！）&lt;/p&gt;

&lt;p&gt;总结一下：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;应用配置，我们通过 config 这个 gem，统一存储在 settings.yml 中，通过 Settings 对象来调用。&lt;/li&gt;
&lt;li&gt;资源配置，基本的资源配置统一存储在 config/resource.yml 中，通过 config_for 来加载；&lt;/li&gt;
&lt;li&gt;资源配置，敏感类信息通过存储在 .env 文件中，在部署时由运维进行管理，dotenv 来加载到 ENV 变量中；&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;以上~欢迎讨论和吐槽&lt;/p&gt;</description>
      <author>zamia</author>
      <pubDate>Sun, 15 Jan 2017 00:28:16 +0800</pubDate>
      <link>https://ruby-china.org/topics/32126</link>
      <guid>https://ruby-china.org/topics/32126</guid>
    </item>
    <item>
      <title>Draper 使用帮助 (即 draper README 翻译)</title>
      <description>&lt;p&gt;本来想写一篇小文章来解释 decorator 的使用，发现 draper 这个 gem 的 readme 写的很赞，不只是说明了 gem 的使用，还清晰的解释了为什么要使用 decorator，什么情况下使用 decorator，所以花了几个小时翻译了一下，方便喜欢看中文的同学~&lt;/p&gt;

&lt;p&gt;rails 本身的 helper 的主要问题是全局命名空间以及调用不方便，draper 使用了 decorator 设计模式，通过面向对象的方法来对原有的 model 进行封装。这个 gem 也非常强大，如果是一个生命期较长的 rails 项目的话，强烈推荐使用 draper 来封装 model 的 view 逻辑来代替 helper。&lt;/p&gt;

&lt;p&gt;发现翻译文章比写一篇还累啊~~&lt;/p&gt;

&lt;p&gt;============== 我是分割线 ============&lt;/p&gt;
&lt;h2 id="Draper: View Models for Rails"&gt;Draper: View Models for Rails&lt;/h2&gt;
&lt;p&gt;Draper 使用『面向对象』的方式，给 Rails 添加了独立的视图层。&lt;/p&gt;

&lt;p&gt;在不使用 Draper 的情况下，类似功能只能通过添加一个 helper 或者给 model 添加一堆逻辑来实现，而使用 Draper 之后，通过对视图逻辑进行封装，使代码组织更清晰，也更易于测试。&lt;/p&gt;
&lt;h2 id="为什么使用装饰器？"&gt;为什么使用装饰器？&lt;/h2&gt;
&lt;p&gt;比如你的应用中有一个 &lt;code&gt;Article&lt;/code&gt; 模型，使用 Draper 之后，可以创建一个对应的 &lt;code&gt;ArticleDecorator&lt;/code&gt;，这个 decorator 封装了 model 对象，并且只封装了视图相关的逻辑。在 controller 中，在传递给 view 层之前，我们可以先装饰一下 article 模型：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/controllers/articles_controller.rb&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;show&lt;/span&gt;
  &lt;span class="vi"&gt;@article&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&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;:id&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;decorate&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样在 view 层使用装饰过的 model 和直接使用 model 基本上没有任何区别。但是，任何时候，如果你开始在 view 中写一堆逻辑、或者你想写一个 helper 方法的时候，就可以通过在 decorator 中实现一个方法来代替。&lt;/p&gt;

&lt;p&gt;下面的例子演示了如何把一个 helper 方法转成 decorator 方法。假如目前有一个 helper 方法长这样：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/helpers/articles_helper.rb&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;publication_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;article&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;article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;published?&lt;/span&gt;
    &lt;span class="s2"&gt;"Published at &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;published_at&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strftime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'%A, %B %e'&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="k"&gt;else&lt;/span&gt;
    &lt;span class="s2"&gt;"Unpublished"&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;这样写完就会觉得很别扭，publication_status 的命名空间是全局的，它在所有的 controllers 和 views 中均可以调用。然后，过了一段时间，当你想实现一个 &lt;code&gt;Book&lt;/code&gt; 对象的 publication status 的时候，并且要求 book 的日期格式跟 article 不一样。怎么整？&lt;/p&gt;

&lt;p&gt;两个办法。要么通过传入参数的类型来判断对象的类型（Ruby 并不是静态类型的语言，所以需要在函数体来判断），然后实现不同的逻辑；要么把这个方法拆成两个方法，&lt;code&gt;book_publication_status&lt;/code&gt; 和 &lt;code&gt;article_publication_status&lt;/code&gt;。随着项目不断变大，需要持续添加方法到全局的命名空间，调用的时候也必须记住所有的函数名。额，ugly……&lt;/p&gt;

&lt;p&gt;这时候，需要使用面向对象的思维。假如你不知道 rails 有个东西叫 helper，你可能想着能这样调用就好了：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sx"&gt;%= @article.publication_status %&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;假如没有 decorator，那么就得在 &lt;code&gt;Article&lt;/code&gt; 模型中实现这个 &lt;code&gt;publication_status&lt;/code&gt; 这个方法，但是这个方法呢，本身又属于视图逻辑，并不属于模型层的逻辑。&lt;/p&gt;

&lt;p&gt;所以，更好的方法呢，是实现一个 decorator：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/decorators/article_decorator.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ArticleDecorator&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Draper&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Decorator&lt;/span&gt;
  &lt;span class="n"&gt;delegate_all&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;publication_status&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;published?&lt;/span&gt;
      &lt;span class="s2"&gt;"Published at &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;published_at&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
      &lt;span class="s2"&gt;"Unpublished"&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;def&lt;/span&gt; &lt;span class="nf"&gt;published_at&lt;/span&gt;
    &lt;span class="n"&gt;object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;published_at&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strftime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"%A, %B %e"&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;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 &lt;code&gt;publication_status&lt;/code&gt; 方法内，我们使用了 &lt;code&gt;published?&lt;/code&gt; 方法，这个方法从哪来的？其实它是在 &lt;code&gt;Article&lt;/code&gt; 中定义的。得益于 &lt;code&gt;delegate_all&lt;/code&gt; 调用，Article 中的方法可以无缝的在这个 decorator 中使用。&lt;/p&gt;

&lt;p&gt;decorator 有一些别名，比如 "presenter"，"exhibit"，"view model"，也有直接叫 "view" （这样的命名约定下，Rails 中的 views 应该叫 templates）的。不管叫什么，使用面向对象编程来代替 helper 这种面向过程编程，都是非常棒的方法！&lt;/p&gt;

&lt;p&gt;综合来说，遇到下面这些情况时，Decorators 尤为合适：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;格式化复杂的数据显示给用户；&lt;/li&gt;
&lt;li&gt;实现模型对象中常用的展示逻辑时，比如由 first_name 和 last_name 合并生成 name 这种情况；&lt;/li&gt;
&lt;li&gt;封装一些对象的属性，比如把 url 属性变成一个超链接。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="安装 （Installation）"&gt;安装（Installation）&lt;/h2&gt;
&lt;p&gt;添加 Draper 到 Gemfile:&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'draper'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'~&amp;gt; 1.3'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在应用的根目录下执行 &lt;code&gt;bundle install&lt;/code&gt; 即可。&lt;/p&gt;

&lt;p&gt;如果是从 0.x 的版本中升级而来，主要的变更在 &lt;a href="https://github.com/drapergem/draper/wiki/Upgrading-to-1.0" rel="nofollow" target="_blank" title=""&gt;wiki&lt;/a&gt; 中有详细说明。&lt;/p&gt;
&lt;h2 id="实现装饰器（Writing Decorators）"&gt;实现装饰器（Writing Decorators）&lt;/h2&gt;
&lt;p&gt;Decorators 继承自 &lt;code&gt;Draper::Decorator&lt;/code&gt;，一般放入 &lt;code&gt;app/decorators&lt;/code&gt; 目录，并且命名保持和相应的模型一致：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/decorators/article_decorator.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ArticleDecorator&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Draper&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Decorator&lt;/span&gt;
&lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="生成器"&gt;生成器&lt;/h3&gt;
&lt;p&gt;引入 draper gem 之后，当使用 rails 生成一个 controller 时：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rails generate resource Article
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也会自动生成一个 decorator，非常方便！！&lt;/p&gt;

&lt;p&gt;如果 Article 模型已经存在的话，也可以直接执行：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rails generate decorator Article
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;来创建一个 &lt;code&gt;ArticleDecorator&lt;/code&gt;。&lt;/p&gt;
&lt;h3 id="调用 Helper 方法"&gt;调用 Helper 方法&lt;/h3&gt;
&lt;p&gt;一般的 rails helper 方法还是不可避免的要使用，不管是 rails 内置的 helper，还是 app 内自定义的 helper，通过 &lt;code&gt;h&lt;/code&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;ArticleDecorator&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Draper&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Decorator&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;emphatic&lt;/span&gt;
    &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;content_tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:strong&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Awesome"&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;/code&gt;&lt;/pre&gt;
&lt;p&gt;嗯，如果觉得总是写一个 &lt;code&gt;h.&lt;/code&gt; 很麻烦，可以在 Decorator 类中添加：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;Draper&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;LazyHelpers&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么会自动 mixin 很多 helper 方法，也就不再需要写 &lt;code&gt;h.&lt;/code&gt; 了。&lt;/p&gt;

&lt;p&gt;（注意：&lt;code&gt;capture&lt;/code&gt; 方法必须通过 &lt;code&gt;h&lt;/code&gt; 或者 &lt;code&gt;helpers&lt;/code&gt; 来调用）&lt;/p&gt;
&lt;h3 id="调用 model 中的方法"&gt;调用 model 中的方法&lt;/h3&gt;
&lt;p&gt;当 decorator 中需要调用 model 中的方法时，除了 &lt;a href="#delegating-methods" title=""&gt;下面&lt;/a&gt; 会提到的 delegation 的方式，任何时候都可以通过 &lt;code&gt;object&lt;/code&gt; 对象（或者 &lt;code&gt;model&lt;/code&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;ArticleDecorator&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Draper&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Decorator&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;published_at&lt;/span&gt;
    &lt;span class="n"&gt;object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;published_at&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strftime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"%A, %B %e"&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;/code&gt;&lt;/pre&gt;&lt;h2 id="使用装饰器（Decorating Objects）"&gt;使用装饰器（Decorating Objects）&lt;/h2&gt;&lt;h3 id="单个对象的装饰"&gt;单个对象的装饰&lt;/h3&gt;
&lt;p&gt;好，现在写完一个装饰器了，如何使用呢？最简单的办法是调用 model 的&lt;code&gt;decorate&lt;/code&gt; 方法：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="vi"&gt;@article&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decorate&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种方式通过命名约定来自动推断应该使用哪个装饰器类。假如需要更灵活的使用，比如 使用 &lt;code&gt;ProductDecorator&lt;/code&gt; 装饰了一个模型 &lt;code&gt;Widget&lt;/code&gt;，可以直接调动装饰器类：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="vi"&gt;@widget&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;ProductDecorator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Widget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# or, equivalently&lt;/span&gt;
&lt;span class="vi"&gt;@widget&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;ProductDecorator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decorate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Widget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="集合的装饰"&gt;集合的装饰&lt;/h3&gt;&lt;h4 id="装饰集合中的所有对象"&gt;装饰集合中的所有对象&lt;/h4&gt;
&lt;p&gt;如果要装饰一个对象的集合，可以一次性装饰所有的对象：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="vi"&gt;@articles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;ArticleDecorator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decorate_collection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;假如集合是一个 ActiveRecord 查询，也可以直接这样使用：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="vi"&gt;@articles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;popular&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decorate&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;注意：&lt;/em&gt; 在 Rails 3 中，&lt;code&gt;.all&lt;/code&gt;方法返回的是一个数据，所以 &lt;u&gt;不能&lt;/u&gt; 使用 &lt;code&gt;Article.all.decorate&lt;/code&gt; 这种方法。但是，Rails 4 中，&lt;code&gt;.all&lt;/code&gt; 方法返回的是一个 query 对象，所以可以用这种方法。&lt;/p&gt;
&lt;h4 id="装饰集合本身"&gt;装饰集合本身&lt;/h4&gt;
&lt;p&gt;如果想给一个集合本身添加一些方法（比如，用于分页），那么可以继承 &lt;code&gt;Draper::CollectionDecorator&lt;/code&gt; ：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/decorators/articles_decorator.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ArticlesDecorator&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Draper&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;CollectionDecorator&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;page_number&lt;/span&gt;
    &lt;span class="mi"&gt;42&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# elsewhere...&lt;/span&gt;
&lt;span class="vi"&gt;@articles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;ArticlesDecorator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# or, equivalently&lt;/span&gt;
&lt;span class="vi"&gt;@articles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;ArticlesDecorator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decorate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Draper 使用 &lt;code&gt;decorate&lt;/code&gt; 方法来装饰每个对象，你也可以通过覆盖集合装饰器的 &lt;code&gt;decorator_class&lt;/code&gt; 方法来改名，或者传递一个 &lt;code&gt;:with&lt;/code&gt; 参数给构造器。&lt;/p&gt;
&lt;h4 id="使用分页"&gt;使用分页&lt;/h4&gt;
&lt;p&gt;有些分页的 gem 会添加一些方法到 &lt;code&gt;ActiveRecord::Relation&lt;/code&gt;，比如 &lt;a href="https://github.com/amatsuda/kaminari" rel="nofollow" target="_blank" title=""&gt;Kaminari&lt;/a&gt; 的 &lt;code&gt;paginate&lt;/code&gt; 方法需要集合实现 &lt;code&gt;current_page&lt;/code&gt;, &lt;code&gt;total_pages&lt;/code&gt;, and
&lt;code&gt;limit_value&lt;/code&gt; 这些方法。为了导出这些方法给一个集合类的装饰器，可以把这些方法 delegate 到 &lt;code&gt;object&lt;/code&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;PaginatingDecorator&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Draper&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;CollectionDecorator&lt;/span&gt;
  &lt;span class="n"&gt;delegate&lt;/span&gt; &lt;span class="ss"&gt;:current_page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:total_pages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:limit_value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:entry_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:total_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:offset_value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:last_page?&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的 &lt;code&gt;delegate&lt;/code&gt; 是 &lt;a href="http://api.rubyonrails.org/classes/Module.html#method-i-delegate" rel="nofollow" target="_blank" title=""&gt;Active
Support&lt;/a&gt; 中的 delegate 方法是相同的，除了 参数 &lt;code&gt;:to&lt;/code&gt; 是可选的，不传递时默认 delegate 到 &lt;code&gt;:object&lt;/code&gt; 对象。&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/mislav/will_paginate" rel="nofollow" target="_blank" title=""&gt;will_paginate&lt;/a&gt; 需要下面这些方法被 delegate :&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;delegate&lt;/span&gt; &lt;span class="ss"&gt;:current_page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:per_page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:offset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:total_entries&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:total_pages&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="装饰相关联的对象"&gt;装饰相关联的对象&lt;/h3&gt;
&lt;p&gt;当主模型被装饰时，可以自动装饰相关联的对象。比如，&lt;code&gt;Article&lt;/code&gt; 模型有一个相关的对象 &lt;code&gt;Author&lt;/code&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;ArticleDecorator&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Draper&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Decorator&lt;/span&gt;
  &lt;span class="n"&gt;decorates_association&lt;/span&gt; &lt;span class="ss"&gt;:author&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当 &lt;code&gt;ArticleDecorator&lt;/code&gt; 装饰一个 &lt;code&gt;Article&lt;/code&gt; 时，draper 会自动使用 &lt;code&gt;AuthorDecorator&lt;/code&gt; 来装饰 &lt;code&gt;Author&lt;/code&gt; 对象。&lt;/p&gt;
&lt;h3 id="装饰 Finders"&gt;装饰 Finders&lt;/h3&gt;
&lt;p&gt;还可以在 decorator 中调用 &lt;code&gt;decorate_finders&lt;/code&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;ArticleDecorator&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Draper&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Decorator&lt;/span&gt;
  &lt;span class="n"&gt;decorates_finders&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样，当在 decorator 调用 finder 类方法时，可以直接返回一个装饰过的对象：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="vi"&gt;@article&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;ArticleDecorator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&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;:id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="什么时候使用装饰器？"&gt;什么时候使用装饰器？&lt;/h3&gt;
&lt;p&gt;理论上，装饰器是和它所装饰的对象行为上是很接近的，所以，看起来在 controller 的 action 方法一开始就装饰这个对象，然后一直使用这个装饰器对象就行了。&lt;/p&gt;

&lt;p&gt;&lt;em&gt;别这么干！&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;因为，装饰器本质上就是为了给 view 层使用的，所以只应该在 view 层使用装饰器。先准备好 model，然后在最后一刻开始装饰它们，然后紧接着在 view 中使用它们。这样的话，就避免了很多尝试修改装饰器对象而导致的诸多隐患。&lt;/p&gt;

&lt;p&gt;为了让装饰器对象只读，draper 也提供了 &lt;code&gt;decorates_assigned&lt;/code&gt; 方法给 controller。它添加了一个 helper 方法，会自动返回一个装饰过后的对象：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/controllers/articles_controller.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ArticlesController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;
  &lt;span class="n"&gt;decorates_assigned&lt;/span&gt; &lt;span class="ss"&gt;:article&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;show&lt;/span&gt;
    &lt;span class="vi"&gt;@article&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&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;:id&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;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;decorates_assigned :article&lt;/code&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;article&lt;/span&gt;
  &lt;span class="vi"&gt;@decorated_article&lt;/span&gt; &lt;span class="o"&gt;||=&lt;/span&gt; &lt;span class="vi"&gt;@article.decorate&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="n"&gt;helper_method&lt;/span&gt; &lt;span class="ss"&gt;:article&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也就是说，只需要在 views 中调用 &lt;code&gt;article&lt;/code&gt; 方法（取代常用的 &lt;a href="/article" class="user-mention" title="@article"&gt;&lt;i&gt;@&lt;/i&gt;article&lt;/a&gt; 对象），就可以直接得到一个装饰后的 &lt;a href="/article" class="user-mention" title="@article"&gt;&lt;i&gt;@&lt;/i&gt;article&lt;/a&gt; 对象。而在 controller 中，可以继续使用 &lt;code&gt;@article&lt;/code&gt; 变量来进行各种逻辑操作，比如 &lt;code&gt;@article.comments.build&lt;/code&gt; 可以创建一个 comment。&lt;/p&gt;
&lt;h2 id="测试 (Testing)"&gt;测试 (Testing)&lt;/h2&gt;
&lt;p&gt;Draper 支持 Rspec，MiniTest::Rails 以及 Test::Unit，并且生成 decorator 的时候会自动创建一些测试用例。&lt;/p&gt;
&lt;h3 id="Rspec"&gt;Rspec&lt;/h3&gt;
&lt;p&gt;spec 测试文件一般放在 &lt;code&gt;spec/decorators&lt;/code&gt; 中。如果放在另外一个目录，那么需要使用 &lt;code&gt;type: :decorator&lt;/code&gt;来标记它们。&lt;/p&gt;

&lt;p&gt;在 controller 测试中，可能会想判断一个实例变量是否被正确的装饰了。可以使用下面这些 matchers 来判断：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;assigns&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:article&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;should&lt;/span&gt; &lt;span class="n"&gt;be_decorated&lt;/span&gt;

&lt;span class="c1"&gt;# 或者，下面这样可以显式的指定装饰器类&lt;/span&gt;
&lt;span class="n"&gt;assigns&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:article&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;should&lt;/span&gt; &lt;span class="n"&gt;be_decorated_with&lt;/span&gt; &lt;span class="no"&gt;ArticleDecorator&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;另外，&lt;code&gt;model.decorate == model&lt;/code&gt;，所以，添加 decorator 之后，已经写好的 specs 应该仍然可以通过测试。&lt;/p&gt;
&lt;h4 id="Spork 用户"&gt;Spork 用户&lt;/h4&gt;
&lt;p&gt;在 &lt;code&gt;spec_helper.rb&lt;/code&gt; 文件中的 &lt;code&gt;Spork.prefork&lt;/code&gt; 段，需要添加下面的代码：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'draper/test/rspec_integration'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="独立测试装饰器"&gt;独立测试装饰器&lt;/h3&gt;
&lt;p&gt;在测试中，Draper 会创建一个 view 的上下文环境来存取 helper 方法。默认情况下，Draper 会创建一个 &lt;code&gt;ApplicationController&lt;/code&gt;，然后在 view 的上下文中使用它。假如你想通过测试每一个组件来加入测试，那么可以通过添加下面的代码到 spec_helper 或者类似文件中来删除这种依赖：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Draper&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ViewContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test_strategy&lt;/span&gt; &lt;span class="ss"&gt;:fast&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样的话，装饰器就不再能够存在应用的 helper 方法，如果需要有选择的引入一些 herlper 方法，可以传递一个 block 给这个方法：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Draper&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ViewContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test_strategy&lt;/span&gt; &lt;span class="ss"&gt;:fast&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;ApplicationHelper&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h4 id="Stub 路由相关的 helper"&gt;Stub 路由相关的 helper&lt;/h4&gt;
&lt;p&gt;假如需要写一些依赖于 routes helper 的装饰器方法，那么可以 stub 这些路由，而无需引入 Rails。&lt;/p&gt;

&lt;p&gt;假如你在使用 Rspec，minitest-rails 或者 minitest 中 Test::Unit 语法，那么可以直接使用 &lt;code&gt;helpers&lt;/code&gt; 对象在你的测试中，因为这些测试是继承自 &lt;code&gt;Draper::TestCase&lt;/code&gt;。假如你在使用 minitest 的 spec 语法，并且没有使用 minitest-rails，可以显式的引入 Draper 的 &lt;code&gt;helpers&lt;/code&gt;：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;describe&lt;/span&gt; &lt;span class="no"&gt;YourDecorator&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;Draper&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ViewHelpers&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后，就可以使用你熟悉的方式来 stub 一些路由的 helper 方法了（下面的例子使用了 Rspec 的 &lt;code&gt;stub&lt;/code&gt; 方法）：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;helpers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;users_path: &lt;/span&gt;&lt;span class="s1"&gt;'/users'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="高级使用技巧 （Advanced Usage）"&gt;高级使用技巧（Advanced Usage）&lt;/h2&gt;&lt;h3 id="共享装饰器方法"&gt;共享装饰器方法&lt;/h3&gt;
&lt;p&gt;可能会遇到多个装饰器都有类似方法的情况，因为装饰器就是一个 Ruby 对象，所以完全可以使用常用的 Ruby 的技巧来共享相关功能。&lt;/p&gt;

&lt;p&gt;比如，Rails 控制器中，一般的 Controller 都是继承自 &lt;code&gt;ApplicationController&lt;/code&gt;，可以在装饰器的实现中也使用类似技巧：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/decorators/application_decorator.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ApplicationDecorator&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Draper&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Decorator&lt;/span&gt;
&lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后，让所有的装饰器都继承自这个 &lt;code&gt;ApplicationDecorator&lt;/code&gt;，不再直接继承自&lt;code&gt;Draper::Decorator&lt;/code&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;ArticleDecorator&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationDecorator&lt;/span&gt;
  &lt;span class="c1"&gt;# decorator methods&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="只 delegate 指定的方法"&gt;只 delegate 指定的方法&lt;/h3&gt;
&lt;p&gt;当装饰器调用 &lt;code&gt;delegate_all&lt;/code&gt; 的时候，所有被调用的方法如果没有在装饰器中定义，那么都会委托至原有的 model 对象。这样有点过度了~&lt;/p&gt;

&lt;p&gt;所以如果想严格控制哪些方法可以在 view 中被调用，那么可以只 delegate 部分方法到 model 中：&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;ArticleDecorator&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Draper&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Decorator&lt;/span&gt;
  &lt;span class="n"&gt;delegate&lt;/span&gt; &lt;span class="ss"&gt;:title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:body&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们省略了参数 &lt;code&gt;:to&lt;/code&gt; ，这样的话，默认委托至 &lt;code&gt;object&lt;/code&gt; 对象。&lt;/p&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;ArticleDecorator&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Draper&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Decorator&lt;/span&gt;
  &lt;span class="n"&gt;delegate&lt;/span&gt; &lt;span class="ss"&gt;:title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:body&lt;/span&gt;
  &lt;span class="n"&gt;delegate&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;:title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;to: :author&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;prefix: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样，在 view 的模板中，假如 &lt;a href="/article" class="user-mention" title="@article"&gt;&lt;i&gt;@&lt;/i&gt;article&lt;/a&gt; 已经被装饰过，那么可以像下面这样使用：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="vi"&gt;@article.title&lt;/span&gt; &lt;span class="c1"&gt;# 返回 the article's `.title`&lt;/span&gt;
&lt;span class="vi"&gt;@article.body&lt;/span&gt;  &lt;span class="c1"&gt;# 返回 the article's `.body`&lt;/span&gt;
&lt;span class="vi"&gt;@article.author_name&lt;/span&gt;  &lt;span class="c1"&gt;# 返回 the article's `author.name`&lt;/span&gt;
&lt;span class="vi"&gt;@article.author_title&lt;/span&gt; &lt;span class="c1"&gt;# 返回 the article's `author.title`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="传递上下文"&gt;传递上下文&lt;/h3&gt;
&lt;p&gt;假如需要传递额外的数据给装饰器，可以在创建装饰器的时候，使用一个 &lt;code&gt;context&lt;/code&gt; 参数来传递数据。比如：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decorate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;context: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;role: :admin&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;传递给 &lt;code&gt;:context&lt;/code&gt; 参数的数据，在装饰器中可以通过 context 方法来获取。&lt;/p&gt;

&lt;p&gt;假如使用了 &lt;code&gt;decorates_association&lt;/code&gt;，那么主模型的上下文数据会传递给相关的对象。也可以覆盖这个 &lt;code&gt;:context&lt;/code&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;ArticleDecorator&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Draper&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Decorator&lt;/span&gt;
  &lt;span class="n"&gt;decorates_association&lt;/span&gt; &lt;span class="ss"&gt;:author&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;context: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;foo: &lt;/span&gt;&lt;span class="s2"&gt;"bar"&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;或者，如果希望修改主模型的上线文数据，可以使用 lambda 表达式：&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;ArticleDecorator&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Draper&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Decorator&lt;/span&gt;
  &lt;span class="n"&gt;decorates_association&lt;/span&gt; &lt;span class="ss"&gt;:author&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;context: &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parent_context&lt;/span&gt;&lt;span class="p"&gt;){&lt;/span&gt; &lt;span class="n"&gt;parent_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;foo: &lt;/span&gt;&lt;span class="s2"&gt;"bar"&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;h3 id="指定装饰器类"&gt;指定装饰器类&lt;/h3&gt;
&lt;p&gt;当使用 &lt;code&gt;decorates_association&lt;/code&gt;时，Draper 使用 &lt;code&gt;decorate&lt;/code&gt; 方法来装饰每一个关联对象，假如想使用一个不同的类来装饰，可以使用 &lt;code&gt;with&lt;/code&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;ArticleDecorator&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Draper&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Decorator&lt;/span&gt;
  &lt;span class="n"&gt;decorates_association&lt;/span&gt; &lt;span class="ss"&gt;:author&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;with: &lt;/span&gt;&lt;span class="no"&gt;FancyPersonDecorator&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果是一个集合的关联（比如 has_many），可以传递一个 &lt;code&gt;CollectionDecorator&lt;/code&gt; 的子类，这样的话会装饰整个集合；或者传递一个 &lt;code&gt;Decorator&lt;/code&gt; 的子类，这样的话会装饰每一个集合内的对象。&lt;/p&gt;
&lt;h3 id="限定 association 的范围"&gt;限定 association 的范围&lt;/h3&gt;
&lt;p&gt;如果期望被装饰的关联对象被排序、限定个数或者其他限定，可以传递 &lt;code&gt;:scope&lt;/code&gt; 参数给 &lt;code&gt;decorates_association&lt;/code&gt;，这样的话，这个方法会在对象装饰 &lt;em&gt;之前&lt;/em&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;ArticleDecorator&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Draper&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Decorator&lt;/span&gt;
  &lt;span class="n"&gt;decorates_association&lt;/span&gt; &lt;span class="ss"&gt;:comments&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;scope: :recent&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="代理类方法"&gt;代理类方法&lt;/h3&gt;
&lt;p&gt;如果想代理类方法给模型类，包括使用 &lt;code&gt;decorates_finders&lt;/code&gt; 的时候，Draper 必须要知道具体的模型类是什么。默认情况下，Draper 默认你的装饰器被命名为 &lt;code&gt;SomeModelDecorator&lt;/code&gt;，然后会代理所有的未知方法给 &lt;code&gt;SomeModel&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;如果，命名不符合约定，Draper 无法推导出相应的模型类，则需要显式的调用 &lt;code&gt;decorates&lt;/code&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;MySpecialArticleDecorator&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Draper&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Decorator&lt;/span&gt;
  &lt;span class="n"&gt;decorates&lt;/span&gt; &lt;span class="ss"&gt;:article&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;h3 id="使模型可以被装饰"&gt;使模型可以被装饰&lt;/h3&gt;
&lt;p&gt;模型对象通过 mixin &lt;code&gt;Draper::Decoratable&lt;/code&gt; 模块来获得 &lt;code&gt;decorate&lt;/code&gt; 方法，这个行为默认在 &lt;code&gt;ActiveRecord::Base&lt;/code&gt; 和 &lt;code&gt;Mongoid::Document&lt;/code&gt; 中被引入。&lt;/p&gt;

&lt;p&gt;所以如果你 &lt;a href="https://github.com/drapergem/draper/wiki/Using-other-ORMs" rel="nofollow" target="_blank" title=""&gt;使用了其他 ORM&lt;/a&gt;（包括 3.0 版本之前的 Mongoid），或者想装饰普通的 Ruby 对象，那么需要显式的 include 这个模块。&lt;/p&gt;
&lt;h2 id="贡献者"&gt;贡献者&lt;/h2&gt;
&lt;p&gt;Draper was conceived by Jeff Casimir and heavily refined by Steve Klabnik and a
great community of open source
&lt;a href="https://github.com/drapergem/draper/contributors" rel="nofollow" target="_blank" title=""&gt;contributors&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="核心开发者"&gt;核心开发者&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Jeff Casimir (jeff@jumpstartlab.com)&lt;/li&gt;
&lt;li&gt;Steve Klabnik (steve@jumpstartlab.com)&lt;/li&gt;
&lt;li&gt;Vasiliy Ermolovich&lt;/li&gt;
&lt;li&gt;Andrew Haines &lt;/li&gt;
&lt;/ul&gt;</description>
      <author>zamia</author>
      <pubDate>Sun, 28 Feb 2016 18:27:43 +0800</pubDate>
      <link>https://ruby-china.org/topics/29142</link>
      <guid>https://ruby-china.org/topics/29142</guid>
    </item>
    <item>
      <title>Ruby 异常处理最佳实践</title>
      <description>&lt;p&gt;『异常处理』是个比较容易忽视的话题，特别是 ruby/rails 圈，很多写了很久代码的同学恐怕都没有怎么写过异常处理的代码。实际上，异常处理还是挺重要的，想写出『健壮』的代码，必须得了解清楚异常的机制以及异常处理的最佳实践。&lt;/p&gt;

&lt;p&gt;关于异常处理，问题无非是下面几个，并且各个高级语言的异常处理大同小异，使用上没有太大区别：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;异常是为了解决什么问题而出现的？&lt;/li&gt;
&lt;li&gt;异常都有哪些类型？分别应该怎么应对？&lt;/li&gt;
&lt;li&gt;什么时候需要使用异常？什么时候需要捕获异常？&lt;/li&gt;
&lt;li&gt;使用异常的最佳实践有哪些？&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;这篇文章并不会说明 ruby 异常的语法啊使用啊什么的，只是尝试回答上面的问题，期望让大家对异常有一个更深入的认识，不过我自己对异常的认识也没有那么深刻，所以有不对的地方，欢迎指出。&lt;/p&gt;

&lt;p&gt;另外，文后的参考文章中都讲的很不错，推荐阅读。&lt;/p&gt;
&lt;h2 id="为什么要使用异常（Exception）？"&gt;为什么要使用异常（Exception）？&lt;/h2&gt;
&lt;p&gt;有些语言本身不提供异常机制，比如 C 语言，那么方法/函数之间的通信一般是用错误码（error code）来实现（可以自行 google 一下 error code vs exception），后来慢慢的高级语言中的 Exception 才成为标配。一般新的特性的出现总是为了解决某些问题的，Exception 解决了什么问题，或者说它带来了哪些好处？&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Exception 可以附带更多的信息&lt;/p&gt;

&lt;p&gt;错误码一般是全局预定义好的一批，然后进行一些归类，系统中所有的函数统一使用这些错误码，函数出现异常的时候使用错误码来告知调用方有异常出现。每一个错误码对应一个唯一的信息。&lt;/p&gt;

&lt;p&gt;而异常这种方式本质上是一个类/对象，那么异常本身就可以被继承、被封装，也可以添加自己的方法，自己的属性，所以它的信息量就会更大、更灵活；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Exception 处理是跳跃式的&lt;/p&gt;

&lt;p&gt;这个应该是最重要的特性，支持异常处理的语言中，异常被抛出之后，程序的运行逻辑直接跳到相应的 catch 这个异常的地方继续执行。这中间可能经过了多次的函数调用，但是并不影响异常的执行。&lt;/p&gt;

&lt;p&gt;这条规则的好处在于，不用所有的函数都要考虑去捕获异常，只要在某个清楚知道如何处理改异常的地方去处理就好了。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Exception 的机制很清晰的独立了异常处理的代码和正常的业务逻辑&lt;/p&gt;

&lt;p&gt;因为 throw/catch 的机制，所以可以很清晰的独立开异常处理的逻辑和正常的业务逻辑，这个比较容易理解。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="什么时候应该使用异常？"&gt;什么时候应该使用异常？&lt;/h2&gt;
&lt;p&gt;新手一般很难搞清楚异常到底是怎么定义的，什么情况下应该算是『异常』？是不是每遇到一个什么错误都应该抛一个异常？&lt;/p&gt;

&lt;p&gt;首先，异常一般指下面三种情况：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;程序错误&lt;/p&gt;

&lt;p&gt;也就是程序写的有问题，比如 java 中最常见的 NullPointerException, ruby 中的 ZeroDivisionError、NoMethodError 等等。&lt;/p&gt;

&lt;p&gt;这类错误一般不用去捕获，一旦发生时让程序挂起即可。通过一些测试和 debug，找到相应的问题修正代码逻辑即可。比如 NullPointerException，那么通过计算或者处理下标，把下标控制在 size 的范围内即可。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;调用方式不正确导致的错误&lt;/p&gt;

&lt;p&gt;每一个函数/类/URL，某种程度上，都可以理解为是一种 API，是调用方和实现方的接口。如果调用方传入了不符合约定的参数，就会导致错误发生。比如典型情况，参数需要一段 XML，但是传入的参数并不是一个有效的 xml，就会导致一个异常发生。&lt;/p&gt;

&lt;p&gt;这种情况下，最好的处理方式是告知调用方哪里出错了，这样 client 就可以去重试。可以通过异常机制处理，也可以通过简单的 if/else 去处理。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;运行时资源出现异常&lt;/p&gt;

&lt;p&gt;这种是最多的情况，一般需要处理的异常大多是这种情况。比如网络调用时网络不通了、申请内存时系统没有内存了，都属于『运行时资源异常』的情况。&lt;/p&gt;

&lt;p&gt;这时一般是通过抛异常的形式，由底层抛出异常，在业务层重新封装并提供合适的信息，最终由上层去处理。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="使用异常的最佳实践"&gt;使用异常的最佳实践&lt;/h2&gt;
&lt;p&gt;使用异常的最佳实践不太好总结，其实只有是两个问题最重要：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;我要不要抛一个异常？&lt;/li&gt;
&lt;li&gt;要不要捕获某个异常？&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;所以，在编码的时候，每当需要 raise 或者 catch 一个异常，就可以想想这两个问题，然后用下面的原则来判断，相信会得到更好的答案。&lt;/p&gt;
&lt;h3 id="什么时候抛一个异常？"&gt;什么时候抛一个异常？&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;参数校验可以提前做，避免不必要的异常&lt;/p&gt;

&lt;p&gt;比如在用户注册时，可能手机号已经被注册过了，昵称被占用了，这种情况很有可能发生，一般情况下直接使用参数校验，然后返回给客户端即可，无需使用异常。&lt;/p&gt;

&lt;p&gt;这种情况，属于上面的参数错误，这种情况一般通过参数的校验，提前返回效果更好。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;有时候应该属于正常的业务逻辑，不应该使用异常&lt;/p&gt;

&lt;p&gt;比如电商系统中，预订某件商品的时候一定会遇到库存不足的情况，这种情况应该先判断库存是否足够，库存不足的时候直接返回即可。这种情况一般是不需要使用异常来进行处理的。&lt;/p&gt;

&lt;p&gt;这种处理方式也更接近于 ruby 的哲学，比如 java 中数组越界会提示 NullPointerException，但是 ruby 中直接返回 nil，表示没有这个值。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;编程错误，一般需要 raise 异常&lt;/p&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;Parent&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;foo&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="no"&gt;NotImplementedError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"need implementation in child class"&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;class&lt;/span&gt; &lt;span class="nc"&gt;Child&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;foo&lt;/span&gt;
        &lt;span class="s2"&gt;"bar"&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;这种时候，如果写程序的时候调用了父类的 foo 函数，直接抛出异常。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;框架内的某些特殊约定，使用异常才能完成某项功能&lt;/p&gt;

&lt;p&gt;比如：&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;* rails 事务中，需要抛出异常，事务才会回滚；
    * sneaker 中，需要抛出异常，消息才不会被消费，然后可以重试；&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;发生资源异常时，一般业务层需要『转译』异常&lt;/p&gt;

&lt;p&gt;日常工作中，我们很少需要写很底层的库，而资源类的异常一般是比较底层的库抛出的，比如网络异常、文件系统异常、内存异常、锁异常等。对于日常写的更多的业务层的代码，一般会把这类异常进行转译（把原来的异常封装一下，更换为一个新的类）。这样的好处在于可以提供更多的信息，也可以更好的对类做分层（上层的代码只会依赖于紧邻的下一层）。&lt;/p&gt;

&lt;p&gt;一个简单的例子来自于 rails/active_record 的源码，我们知道 ar 封装了很多读取数据库的 API，比如下面的代码在数据库连接不可用的时候会抛出异常：&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;Order&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;Base&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# 此时，数据库挂掉的话，调用 find 会发生什么？&lt;/span&gt;
&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12345&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;异常打出的提示是这样的：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;ActiveRecord::StatementInvalid: Mysql2::Error: Lost connection to MySQL server during query: SELECT  &lt;code&gt;orders&lt;/code&gt;.* FROM &lt;code&gt;orders&lt;/code&gt;  WHERE &lt;code&gt;orders&lt;/code&gt;.&lt;code&gt;id&lt;/code&gt; = 5 LIMIT 1&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;可以很明显的看到，Mysql2 gem 抛了一个异常，ActiveRecord 对它进行了『转译』。&lt;/p&gt;

&lt;p&gt;不过 ruby 不支持异常的直接封装，所以转译的时候只能把 message 给封装进来。下面是 rails 的实现：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# active_record/connection_adapters/abstract_adapter.rb，省略了部分代码&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"SQL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;binds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
  &lt;span class="vi"&gt;@instrumenter.instrument&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="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="no"&gt;Exception&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
  &lt;span class="n"&gt;message&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="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="si"&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;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;message&lt;/span&gt;&lt;span class="si"&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;sql&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="n"&gt;exception&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;translate_exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# 转译异常&lt;/span&gt;
  &lt;span class="n"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_backtrace&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;backtrace&lt;/span&gt; &lt;span class="c1"&gt;# 存储 backtrace&lt;/span&gt;
  &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="n"&gt;exception&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;translate_exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="c1"&gt;# override in derived class&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;StatementInvalid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&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;/li&gt;
&lt;/ol&gt;

&lt;p&gt;不过什么时候抛出异常，争议也比较多，最基本的前提是在一个系统内保持一致的行为。如果约定某些情况下抛出异常，那么整个系统都应该抛出异常。&lt;/p&gt;
&lt;h3 id="什么时候需要捕获异常？"&gt;什么时候需要捕获异常？&lt;/h3&gt;
&lt;p&gt;这个原则上就比较简单了，只有在『你有能力处理的时候才去捕获异常』，否则就不要去捕获。&lt;/p&gt;

&lt;p&gt;如果再细节一点的话，可能下面的一些原则可以帮助写出更健壮的程序：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;不要直接吞掉异常&lt;/p&gt;

&lt;p&gt;就是 rescue 之后什么也不错，事实上，这种情况还经常出现。如果真觉得应该在这一层捕获异常，那么至少打点日志吧。（有了日志，运维同学才可以监控、可以报警啊喂）&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;begin&lt;/span&gt;
    &lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;something&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&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;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"something is wrong: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;message&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="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;debug&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;backtrace&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;'\n'&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;/li&gt;
&lt;li&gt;
&lt;p&gt;尽量捕获更细节的异常，不要直接捕获 Exception 这种顶级异常&lt;/p&gt;

&lt;p&gt;也就是上面的原则，能处理什么异常就捕获什么异常。异常机制的最终目的是进行某种程度的 recovery，所以只对自己能处理的异常进行捕获即可。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;异常也是普通的类，所以也应纳入到分层体系中去&lt;/p&gt;

&lt;p&gt;也就是上面提到的『转译』异常，比如在业务层，可能需要对异常进行封装，然后由上层来捕获。而不是直接由上层的代码直接捕获底层的异常。&lt;/p&gt;

&lt;p&gt;一般来说，资源类的错误是在实现层去触发，在中间层进行转译，然后在上层进行捕获和处理。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;异常的使用本身就有一些争议，但是日常工作中肯定避免不了使用异常，所以还是要对异常有一定的理解的，异常也能帮助大家写出更健壮的程序~&lt;/p&gt;

&lt;p&gt;以上。&lt;/p&gt;
&lt;h2 id="参考文章"&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href="http://www.onjava.com/pub/a/onjava/2003/11/19/exceptions.html" rel="nofollow" target="_blank"&gt;http://www.onjava.com/pub/a/onjava/2003/11/19/exceptions.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://howtodoinjava.com/best-practices/java-exception-handling-best-practices/" rel="nofollow" target="_blank"&gt;http://howtodoinjava.com/best-practices/java-exception-handling-best-practices/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://msdn.microsoft.com/en-us/library/hh279678.aspx" rel="nofollow" target="_blank"&gt;https://msdn.microsoft.com/en-us/library/hh279678.aspx&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://www.monkeyandcrow.com/blog/reading_rails_handling_exceptions/" rel="nofollow" target="_blank"&gt;http://www.monkeyandcrow.com/blog/reading_rails_handling_exceptions/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://code.tutsplus.com/articles/writing-robust-web-applications-the-lost-art-of-exception-handling--net-36395" rel="nofollow" target="_blank"&gt;http://code.tutsplus.com/articles/writing-robust-web-applications-the-lost-art-of-exception-handling--net-36395&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</description>
      <author>zamia</author>
      <pubDate>Thu, 25 Feb 2016 19:29:00 +0800</pubDate>
      <link>https://ruby-china.org/topics/29104</link>
      <guid>https://ruby-china.org/topics/29104</guid>
    </item>
    <item>
      <title>Rails 中乐观锁与悲观锁的使用</title>
      <description>&lt;h2 id="简介"&gt;简介&lt;/h2&gt;
&lt;p&gt;只有有资源的争用就少不了使用各种锁，包括关系数据库中使用的悲观锁和乐观锁，分布式系统中的分布式锁（比如使用 zoo keeper 或者 redis 等实现），MRI ruby 中也存在 GIL(global intepreter lock)，mongodb 中也存在全局锁、database-level 锁和 collection-level 锁等等。&lt;/p&gt;

&lt;p&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;Rails 提供了很好用的 API 来帮助开发者分别去使用这两种锁，写起来很简单（写完之后我都怀疑写这篇文章是否有必要了，大家随意看看），不过对于一些新手同学可能有帮助。&lt;/p&gt;
&lt;h2 id="悲观锁"&gt;悲观锁&lt;/h2&gt;&lt;h3 id="常用场景"&gt;常用场景&lt;/h3&gt;
&lt;p&gt;一般对于资源的争用都可以使用悲观锁，比如电商系统中涉及到订单的部分，比如用户支付完成后可能会同时有多条支付成功的通知（做过支付的都知道一般有同步通知和异步通知），比如订单改价的同时可能用户正在支付等等，对于这种会对订单状态发生改变的操作，我们内部一般对这种操作都做加锁处理。&lt;/p&gt;
&lt;h3 id="使用"&gt;使用&lt;/h3&gt;
&lt;p&gt;rails 的 &lt;a href="http://api.rubyonrails.org/classes/ActiveRecord/Locking/Pessimistic.html" rel="nofollow" target="_blank" title=""&gt;API 文档&lt;/a&gt;中有详细的说明：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# select * from accounts where id=1 for update&lt;/span&gt;
&lt;span class="no"&gt;Account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# 注意，这种最终会导致一个行锁&lt;/span&gt;

&lt;span class="c1"&gt;# select * from accounts where name = 'shugo' limit 1 for update&lt;/span&gt;
&lt;span class="no"&gt;Account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"name = 'shugo'"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;lock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;
&lt;span class="c1"&gt;# 注意，这里可不是行锁，这里会是一个表锁&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意上面的区别，mysql innodb 里面，对于 "select * from where xxx for update" 的情况，是会锁住整张表的，所以最好不要这样来用。Rails 也提供了一个很方便的方法 with_lock 来锁住单个记录，并且内嵌在事务之中。下面代码中的两段是等价的：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;
&lt;span class="n"&gt;account&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="no"&gt;Account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transaction&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lock!&lt;/span&gt;
    &lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;balance&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;
    &lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save!&lt;/span&gt; 
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# 和下面是等价的&lt;/span&gt;

&lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_lock&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;balance&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;
    &lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save!&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="乐观锁"&gt;乐观锁&lt;/h2&gt;&lt;h3 id="常用场景"&gt;常用场景&lt;/h3&gt;
&lt;p&gt;悲观锁出错概率小，因为一旦获得锁，其他进程会堵塞，但是也导致速度会受影响，系统开销比较大，不利于并发。乐观锁适用于资源竞争不是那么多的地方，这样系统的开销较小，速度也比较快。&lt;/p&gt;

&lt;p&gt;乐观锁本质上算是一个利用多版本管理来控制并发的技术，如果事务提交之后，数据库发现写入进程传入的版本号与目前数据库中的版本号不一致，说明有其他人已经修改过数据，不再允许本事务的提交。所以，使用乐观锁之前需要给数据库增加一列 :lock_version，Rails 会自动识别这一列，像数据库提交数据的时候自动带上。另外，乐观锁是默认打开的，如果要关闭，需要配置一下。&lt;/p&gt;

&lt;p&gt;在大鱼系统中，库存管理是使用乐观锁的，我们的流量没那么大，不太可能多个用户同时预订同一个住宿的同一个间夜，概率比较小，所以目前是使用乐观锁来实现的。如果抛异常，那么还可以进行重试。&lt;/p&gt;
&lt;h3 id="使用"&gt;使用&lt;/h3&gt;
&lt;p&gt;记得使用前添加 lock_version 的字段给相应的表，其他的就是自动的了，如果事务提交失败，那么 Rails 会抛一个 ActiveRecord::StaleObjectError 的异常。&lt;/p&gt;

&lt;p&gt;比如，下面这段代码会进行重试：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;retry_times&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;

&lt;span class="k"&gt;begin&lt;/span&gt;
    &lt;span class="vi"&gt;@order.with_lock&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
        &lt;span class="vi"&gt;@order.set_paid&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;rescue&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;StaleObjectError&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
    &lt;span class="n"&gt;retry_times&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;retry_times&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
        &lt;span class="k"&gt;retry&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="需要注意的地方"&gt;需要注意的地方&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;一般，使用锁的时候和事务同时使用，所以 with_lock 是用的比较多的，而且尽量使用行锁而不是表锁。&lt;/li&gt;
&lt;li&gt;另外，也注意异常的处理，需要使用那些会抛异常的方法；&lt;/li&gt;
&lt;li&gt;对于乐观锁，还需要注意如果是前端操作频繁，那么还需要把 lock_version 写入到 form 表单中，否则起不到锁的作用，&lt;a href="https://blog.engineyard.com/2011/a-guide-to-optimistic-locking" rel="nofollow" target="_blank" title=""&gt;这里讲的很详细了&lt;/a&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;以上~ （发现这篇没什么内容，不过我记得最初大家写的时候也经常犯错，算是一个总结吧）&lt;/p&gt;
&lt;h2 id="参考"&gt;参考&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href="http://railscasts.com/episodes/59-optimistic-locking-revised" rel="nofollow" target="_blank"&gt;http://railscasts.com/episodes/59-optimistic-locking-revised&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.engineyard.com/2011/a-guide-to-optimistic-locking" rel="nofollow" target="_blank"&gt;https://blog.engineyard.com/2011/a-guide-to-optimistic-locking&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://api.rubyonrails.org/classes/ActiveRecord/Locking/Optimistic.html" rel="nofollow" target="_blank"&gt;http://api.rubyonrails.org/classes/ActiveRecord/Locking/Optimistic.html&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</description>
      <author>zamia</author>
      <pubDate>Sat, 06 Feb 2016 17:25:00 +0800</pubDate>
      <link>https://ruby-china.org/topics/28963</link>
      <guid>https://ruby-china.org/topics/28963</guid>
    </item>
    <item>
      <title>Nginx + Rails 下如何进行文件的安全下载？</title>
      <description>&lt;h2 id="问题"&gt;问题&lt;/h2&gt;
&lt;p&gt;一般常见的文件下载有两类需求：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;公开的文件下载，比如 rails 的 assets、或者用户上传的一些可以公开的文件，比如自己的头像；&lt;/li&gt;
&lt;li&gt;较隐私的文件下载，使用场景也不少，比如某些企业后台上传的用户认证资料；&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;前一类需求 rails 中比较容易实现，一般直接把用户上传的文件存储目录直接放在 /public/some/dir 中，然后按照日期或者 ID 之类的做个目录结构也就够用了。&lt;/p&gt;

&lt;p&gt;对于后一类场景，具体的需求有两点：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;必须经过用户认证和授权，检查认证和权限之后才能访问文件；这个就是典型的业务层需要处理的，也就是 app server(rails 层) 需要实现的；&lt;/li&gt;
&lt;li&gt;保证速度，不能给 app server 过多负担。这个是 web server 擅长做的，比如 nginx 可以使用系统的 sendfile 功能可以直接从文件系统发送至网络层，可以少 2 次的内存 copy；web server 也可以做一些缓存等等；&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;根据上面的需求，文件的安全下载实现起来稍微麻烦一点，不过根据『这么通用的需求一定有现成的解决方案』的原则，其实配置和使用起来还是比较简单的，这篇小文就总结下使用 nginx 和 rails 配合、利用 X-Accel（一般也称作 X-Sendfile）来实现隐私文件的安全下载。&lt;/p&gt;
&lt;h2 id="底层原理和实现"&gt;底层原理和实现&lt;/h2&gt;
&lt;p&gt;这是典型的 web server 和 application server 互相配合的过程，内部的访问过程 &lt;a href="http://thedataasylum.com/articles/how-rails-nginx-x-accel-redirect-work-together.html" rel="nofollow" target="_blank" title=""&gt;这篇文章&lt;/a&gt; 也讲的比较清楚，下面的总结更详细一些，补充了一些源码和日志，能帮助大家彻底搞清楚整个过程。&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;浏览器访问一个地址，比如 /download/files/123，这个地址对应一个需要验证权限的文件；&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET /download/files/123 HTTP/1.1
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;nginx 收到这个请求之后根据路由配置，把请求转发到 rails。&lt;/p&gt;

&lt;p&gt;nginx 转发的时候根据配置，添加上两个参数，转发给 rails。后端 rails 服务器会根据这两个参数来对 response body 进行修改，后面会提到。&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET /download/files/123 HTTP/1.0
X-Forwarded-For: 127.0.0.1
X-Sendfile-Type: X-Accel-Redirect
X-Accel-Mapping: /var/www/fishtrip/private=/private
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的参数 X-Sendfile-Type 告知后端 nginx 支持什么样的参数（像 apache、lighttpd 支持的参数名称不同）；参数 X-Accel-Mapping 告知后端应该怎么样做文件名称的 mapping。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;rails 收到这个请求之后，正常流进某个 controller#action，经过业务代码的判断之后，找到这个 url 对应的真正的文件名，然后使用 sendfile 发送文件。&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;show&lt;/span&gt;
    &lt;span class="n"&gt;pic&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;find&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;:id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;send_file&lt;/span&gt; &lt;span class="n"&gt;pic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;type: &lt;/span&gt;&lt;span class="s2"&gt;"image/jpeg"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;disposition: &lt;/span&gt;&lt;span class="s1"&gt;'inline'&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其实在整个过程中，rails 的背后是 &lt;a href="http://www.rubydoc.info/github/rack/rack/Rack/Sendfile" rel="nofollow" target="_blank" title=""&gt;Rack::Sendfile&lt;/a&gt; 这个 middleware 在工作。看看它的源码中的 call 函数的实现：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# File rack/lib/rack/sendfile.rb&lt;/span&gt;
&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;variation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s1"&gt;'X-Accel-Redirect'&lt;/span&gt;
 &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expand_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_path&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;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;map_accel_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'Content-Length'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'0'&lt;/span&gt;
   &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;
   &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&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;env&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'rack.errors'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"X-Accel-Mapping header missing"&lt;/span&gt;
 &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="c1"&gt;# some code here&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到这个 middleware 吧 content-length 置为 0，把 body 置空，返回给前端一个计算过 mapping 的 url。&lt;/p&gt;

&lt;p&gt;其中的函数 map_accel_path 是 private 函数，长这样：&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;map_accel_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;file&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;mapping&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'HTTP_X_ACCEL_MAPPING'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;internal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;external&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mapping&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="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nb"&gt;p&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;internal&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sr"&gt;/i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;external&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;/code&gt;&lt;/pre&gt;
&lt;p&gt;其实就是根据 nginx 传入的 X-Accel-Mapping 参数把实际的地址替换成一个 mapping 地址。&lt;/p&gt;

&lt;p&gt;所以，这样也就不难猜测我们自己写的 action 里面的 sendfile 的实现了，sendfile 只需要实现一个支持 to_path 调用的对象即可。去 Rails 中看看它的实现：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# File actionpack/lib/action_controller/metal/data_streaming.rb&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;send_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="c1"&gt;#:doc:&lt;/span&gt;
 &lt;span class="c1"&gt;# 省略一些代码&lt;/span&gt;
 &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:status&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;
 &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;content_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:content_type&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;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;key?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:content_type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;response_body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;FileBody&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&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;里面的 FileBody 类就支持 to_path 调用；&lt;/p&gt;

&lt;p&gt;所以，经过 Rails 和 Rack::Sendfile 的配合，rails 返回给 nginx 的就是一个没有 body，只有 headers 的 response，长下面这个样子（来源于 nginx 的 debug 日志，略去了部分内容）：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http proxy header: "Content-Disposition: inline; filename="abc.jpg""
http proxy header: "Content-Transfer-Encoding: binary"
http proxy header: "Content-Type: image/jpeg"
http proxy header: "Content-Length: 0"
http proxy header: "X-Accel-Redirect: /private/files/abc.jpg"
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;nginx 收到 rails 返回的数据之后，会检查 X-Accel-Redirect 参数的值，然后内部再根据 location 的配置进行内部跳转，找到真正的文件地址（需要配置，见下文），并且调用操作系统的 sendfile 接口，直接返回给用户。&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;这个就是整个文件安全下载的过程了，这个过程涉及到 nginx - rails - Rack::Sendfile - nginx 的这么一个过程，看起来有点复杂。实际上我们使用的时候配置起来还是比较简单的。&lt;/p&gt;
&lt;h2 id="实现和配置"&gt;实现和配置&lt;/h2&gt;
&lt;p&gt;配置主要是两部分，nginx 和 rails 的部分，如果使用 capistrano 部署线上服务，因为涉及到软链的问题，所以配置略有不同。&lt;/p&gt;
&lt;h3 id="nginx"&gt;nginx&lt;/h3&gt;
&lt;p&gt;根据上面的原理部分的描述，nginx 的配置也分为两个部分：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;跳转后端时参数配置&lt;/p&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="err"&gt;set&lt;/span&gt; &lt;span class="err"&gt;$app_root&lt;/span&gt; &lt;span class="err"&gt;/var/some/dir&lt;/span&gt;&lt;span class="c"&gt;;
&lt;/span&gt;
&lt;span class="err"&gt;location&lt;/span&gt; &lt;span class="err"&gt;/download&lt;/span&gt; &lt;span class="err"&gt;{&lt;/span&gt;
        &lt;span class="err"&gt;proxy_set_header&lt;/span&gt; &lt;span class="err"&gt;X-Forwarded-For&lt;/span&gt; &lt;span class="err"&gt;$proxy_add_x_forwarded_for&lt;/span&gt;&lt;span class="c"&gt;;
&lt;/span&gt;        &lt;span class="err"&gt;proxy_set_header&lt;/span&gt; &lt;span class="err"&gt;X-Sendfile-Type&lt;/span&gt; &lt;span class="err"&gt;X-Accel-Redirect&lt;/span&gt;&lt;span class="c"&gt;;
&lt;/span&gt;        &lt;span class="err"&gt;proxy_set_header&lt;/span&gt; &lt;span class="err"&gt;X-Accel-Mapping&lt;/span&gt; &lt;span class="err"&gt;"$app_root/&lt;/span&gt;&lt;span class="py"&gt;private&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/private";&lt;/span&gt;

        &lt;span class="err"&gt;proxy_set_header&lt;/span&gt; &lt;span class="err"&gt;Host&lt;/span&gt; &lt;span class="err"&gt;$http_host&lt;/span&gt;&lt;span class="c"&gt;;
&lt;/span&gt;        &lt;span class="err"&gt;proxy_redirect&lt;/span&gt; &lt;span class="err"&gt;off&lt;/span&gt;&lt;span class="c"&gt;;
&lt;/span&gt;        &lt;span class="err"&gt;expires&lt;/span&gt; &lt;span class="err"&gt;off&lt;/span&gt;&lt;span class="c"&gt;;
&lt;/span&gt;
        &lt;span class="err"&gt;proxy_pass&lt;/span&gt; &lt;span class="err"&gt;http://backend&lt;/span&gt;&lt;span class="c"&gt;;
&lt;/span&gt;&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个配置的作用是 nginx 在把请求转发给 rails 后端的时候添加 X-Senfile-Type 和 X-Accel-Mapping 参数；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;收到后端回复后内部地址的配置 &lt;/p&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="err"&gt;location&lt;/span&gt; &lt;span class="err"&gt;/private&lt;/span&gt; &lt;span class="err"&gt;{&lt;/span&gt;
        &lt;span class="err"&gt;internal&lt;/span&gt;&lt;span class="c"&gt;;
&lt;/span&gt;        &lt;span class="err"&gt;alias&lt;/span&gt; &lt;span class="err"&gt;$app_root/private&lt;/span&gt;&lt;span class="c"&gt;;
&lt;/span&gt;&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个配置的作用是 nginx 收到 rails 后端返回的值时可以正确找到文件的实际地址；&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="rails"&gt;rails&lt;/h3&gt;
&lt;p&gt;rails 的配置就更简单了，添加一句话即可：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;action_dispatch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;x_sendfile_header&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'X-Accel-Redirect'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="capistrano"&gt;capistrano&lt;/h3&gt;
&lt;p&gt;capistrano 的部署使用了软链的方法，所以上面 nginx 配置的地方，需要添加一个正则即可。这样后端 rails 就可以正常的去 mapping 了（也就是 Rack::Sendfile 里面的 map_accel_path 是支持正则的）：&lt;/p&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="err"&gt;proxy_set_header&lt;/span&gt; &lt;span class="err"&gt;X-Accel-Mapping&lt;/span&gt; &lt;span class="err"&gt;"$app_root/releases/\d{14}/&lt;/span&gt;&lt;span class="py"&gt;private&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/private";&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果实际部署情况跟这个不一致，只要走类似的方法就行了，你懂的~&lt;/p&gt;
&lt;h2 id="总结"&gt;总结&lt;/h2&gt;
&lt;p&gt;虽然文件的安全下载是一个小功能，而且现在文件的云存储很多（大鱼也迁移到了云存储上...），但是通过这里例子也可以看看 web server 和 application server 是如何配合工作的，反向代理的很多功能也都是类似机制完成的。&lt;/p&gt;

&lt;p&gt;通过这个例子也可以了解一下 rack 的工作机制，可以看到如果通过简单的代码来实现一个相对复杂的功能。&lt;/p&gt;

&lt;p&gt;以上~&lt;/p&gt;
&lt;h2 id="参考文章"&gt;参考文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href="http://thedataasylum.com/articles/how-rails-nginx-x-accel-redirect-work-together.html" rel="nofollow" target="_blank"&gt;http://thedataasylum.com/articles/how-rails-nginx-x-accel-redirect-work-together.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://gist.github.com/Djo/11374407" rel="nofollow" target="_blank"&gt;https://gist.github.com/Djo/11374407&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.nginx.com/resources/wiki/start/topics/examples/x-accel/" rel="nofollow" target="_blank"&gt;https://www.nginx.com/resources/wiki/start/topics/examples/x-accel/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://airbladesoftware.com/notes/rails-nginx-x-accel-mapping/" rel="nofollow" target="_blank"&gt;http://airbladesoftware.com/notes/rails-nginx-x-accel-mapping/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://www.rubydoc.info/github/rack/rack/Rack/Sendfile" rel="nofollow" target="_blank"&gt;http://www.rubydoc.info/github/rack/rack/Rack/Sendfile&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</description>
      <author>zamia</author>
      <pubDate>Thu, 04 Feb 2016 15:22:47 +0800</pubDate>
      <link>https://ruby-china.org/topics/28951</link>
      <guid>https://ruby-china.org/topics/28951</guid>
    </item>
    <item>
      <title>开源一个小 gem，帮助更好的集成 fis3 和 rails</title>
      <description>&lt;h2 id="开源一个小gem，帮助更好的集成fis3和rails"&gt;开源一个小 gem，帮助更好的集成 fis3 和 rails&lt;/h2&gt;
&lt;p&gt;之前公司的前端 js、css 都挺乱的，最近随着前端架构师的加入，开始梳理前端工程。由于 asset pipeline 本身的一些限制（主要是无法做到 js 的模块化加载），比来比去，我们选择了 fis 的前端编译方案 &lt;a href="http://fis.baidu.com" rel="nofollow" target="_blank"&gt;http://fis.baidu.com&lt;/a&gt;，好处主要有两个：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;前端 js 模块化开发，js 模块可以动态 require、按需加载；&lt;/li&gt;
&lt;li&gt;就近原则封装组件，可以把同一个组件的 js、css、模板文件放在同一个子目录。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;引入 FIS 之后，如何跟 rails 集成呢，我们也试了好几种方案：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;由 fis 来编译 js、css 以及 .html.erb 文件，然后把编译好的文件再被 layout 引用。&lt;/p&gt;

&lt;p&gt;简单描述起来还不太好说明白，因为要给每个资源打上动态的 hash 串，所以这种方案中，把 html 也给编译了；然后让 rails 再去 render 这个 html。整个流程比较复杂，难以理解和维护。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;由 fis 来编译 js、css 文件。然后生成 map.json 文件，rails 读取 map.json 文件解析之后，由 helper 来解析地址。&lt;/p&gt;

&lt;p&gt;这个方案的好处是理解容易，前后端不需要互相影响开发。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;这个方案其实也是 asset pipeline 的基本原理，也是 webpack-rails 的基本原理。现在社区很少有把 fis 和 rails 结合起来用的团队，所以我们开发了一个小 gem 来完成这个功能：&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/fishtrip/fis3-rails" rel="nofollow" target="_blank"&gt;https://github.com/fishtrip/fis3-rails&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;基本上这个 gem 就做两件事：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;解析 map.json 文件，存储资源文件路径和编译后的地址；&lt;/li&gt;
&lt;li&gt;由 helper 来根据资源文件名来 render 出正确的资源；&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;但是，主要注意的是，因为 FIS 中 javascript 文件是可以引用一个 css 文件的，所以我们又写了一个 helper 解决这样的问题（这一点类似 webpack）。&lt;/p&gt;

&lt;p&gt;所以 fis3-rails 一共提供了三个 helper：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;调用某个 fis js 资源时调用 fis3_javascript_tag，如：&lt;/p&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;fis3_javascript_tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'js/mobile/base'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;调用某个 fis css 资源时调用 fis3_stylesheet_tag，如：&lt;/p&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;fis3_stylesheet_tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'css/mobile/base'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;因为 fis 中的 js 文件也可能导致 css 产生，所以使用 js 时也需要同时调用 fis3_stylesheet_tag_by_js，如：&lt;/p&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;fis3_stylesheet_tag_by_js&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'js/mobile/base'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;时间仓促，也是简单实现，但是目前大鱼的线上已经开始在使用了，目前来看，还比较稳定。&lt;/p&gt;

&lt;p&gt;如果有 rails 团队想引入 fis3，欢迎各位交流和指教~&lt;/p&gt;</description>
      <author>zamia</author>
      <pubDate>Wed, 20 Jan 2016 19:51:48 +0800</pubDate>
      <link>https://ruby-china.org/topics/28796</link>
      <guid>https://ruby-china.org/topics/28796</guid>
    </item>
    <item>
      <title>Rails 路由 - 解决多子域名问题</title>
      <description>&lt;h2 id="rails多子域名问题"&gt;rails 多子域名问题&lt;/h2&gt;
&lt;p&gt;Rails 项目发展到较大规模的时候、或者为了其他各种原因，一定会遇到多子域名的问题。目前网上的很多资料只是简单的介绍了利用 constraints 进行操作的方法，并没有系统的解决多子域名实操的时候会遇到的各种问题。比如：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;多个子域名下，路由和控制器的设计？&lt;/li&gt;
&lt;li&gt;多个 routes 文件如何拆分？&lt;/li&gt;
&lt;li&gt;url helper 使用的时候的注意事项？&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;这篇文章算是对上述问题进行的一个较深入的总结和实操，请阅读之前需要 Rails 路由先有个大概的了解，期望大家读完之后对 Rails 路由理解的更加深入。&lt;/p&gt;
&lt;h3 id="多子域名问题解决"&gt;多子域名问题解决&lt;/h3&gt;&lt;h3 id="constraint是基础"&gt;constraint 是基础&lt;/h3&gt;
&lt;p&gt;Rails 提供了 constraints 方法来对一组路由进行限制，比如官方文档（Rails 3.2）中提供的例子：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;constraints&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:id&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="sr"&gt;/\d+\.\d+/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;resources&lt;/span&gt; &lt;span class="ss"&gt;:orders&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的例子中只有 /posts/123.456 这样的 id 是允许的，而 "/posts/123" 就是无效的。这也是 constraint（限制）的意思。&lt;/p&gt;

&lt;p&gt;那么，根据这个思路，我们可以限制一组路由只在某个子域名下生效，也就达到了多子域名的目的：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;constraints&lt;/span&gt; &lt;span class="ss"&gt;:subdomain&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"m"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;resources&lt;/span&gt; &lt;span class="ss"&gt;:orders&lt;/span&gt; &lt;span class="c1"&gt;# mobile下&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="n"&gt;constraints&lt;/span&gt; &lt;span class="ss"&gt;:subdomain&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"www"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;resources&lt;/span&gt; &lt;span class="ss"&gt;:orders&lt;/span&gt; &lt;span class="c1"&gt;# pc下&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;另外，不要在 routes 文件里面 hard code 一些配置，因为一般不同的环境的子域名可能不一样，比如你的测试环境可能需要 alpha.m.example.com 这样的域名，而正式环境才是 m.example.com 这样的域名。我们稍微修改的好一点（示例代码也要写好啊，否则新手容易跟着画瓢）：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;### config/environments/development.rb&lt;/span&gt;
&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mobile_subdomain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"m"&lt;/span&gt;
&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;main_subdomain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"www"&lt;/span&gt;

&lt;span class="c1"&gt;### config/routes.rb&lt;/span&gt;
&lt;span class="n"&gt;constraints&lt;/span&gt; &lt;span class="ss"&gt;:subdomain&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;configuration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mobile_subdomain&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;resources&lt;/span&gt; &lt;span class="ss"&gt;:orders&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="n"&gt;constraints&lt;/span&gt; &lt;span class="ss"&gt;:subdomain&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;configuration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;main_subdomain&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;resources&lt;/span&gt; &lt;span class="ss"&gt;:orders&lt;/span&gt;
  &lt;span class="c1"&gt;# 下面是其他路由，比如&lt;/span&gt;
  &lt;span class="n"&gt;resources&lt;/span&gt; &lt;span class="ss"&gt;:users&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是这样显然是不 work 的（手册跟实际工作是有差距的），因为 constraints 只是做了『限制』，路由指向的 controller 并不会改变，也就是说上面的 mobile 子域名中的 orders 也是同样指向了 ::OrdersController 这个控制器，实际工作中，我们一般是期望 m.example.com/orders 路由指向 ::Mobile::USersController 这个控制器的。&lt;/p&gt;

&lt;p&gt;那应该如何解决呢？这个时候就需要使用 scope 了。&lt;/p&gt;
&lt;h3 id="先提下namespace"&gt;先提下 namespace&lt;/h3&gt;
&lt;p&gt;scope 日常不太常使用，因为大家一般都是使用 namespace 就够了，比如最常见的写法：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;namespace :admin do
  resources :orders
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样会把 /admin/orders 路由指向 ::Admin::PostsController，很方便吧？&lt;/p&gt;
&lt;h3 id="scope来解决"&gt;scope 来解决&lt;/h3&gt;
&lt;p&gt;但是在子域名环境下，这样是达不到目的的。因为我们期望 "m.example.com/orders" 能指向 ::Mobile::OrdersController，那么该如何搞呢，这个时候就需要 scope 了。scope 提供了比较 namepace 更细粒度的控制参数，完全可以满足我们的需求。&lt;/p&gt;

&lt;p&gt;下面的代码来自于官方文档，稍微翻译了下：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;### 把 url "/posts" (不包含/admin前缀) 指向Admin::PostsController&lt;/span&gt;
&lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="ss"&gt;:module&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"admin"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;resources&lt;/span&gt; &lt;span class="ss"&gt;:posts&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt; 

&lt;span class="c1"&gt;### 把 posts相关路由添加 "/admin/" 前缀&lt;/span&gt;
&lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="ss"&gt;:path&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"/admin"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;resources&lt;/span&gt; &lt;span class="ss"&gt;:posts&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;### 修改 url helper，用 +sekret_posts_path+ 替代 +posts_path+&lt;/span&gt;
&lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="ss"&gt;:as&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"sekret"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;resources&lt;/span&gt; &lt;span class="ss"&gt;:posts&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="n"&gt;constraints&lt;/span&gt; &lt;span class="ss"&gt;:subdomain&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;configuration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mobile_subdomain&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="ss"&gt;module: &lt;/span&gt;&lt;span class="s1"&gt;'mobile'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; 
    &lt;span class="n"&gt;resources&lt;/span&gt; &lt;span class="ss"&gt;:orders&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;constraints&lt;/span&gt; &lt;span class="ss"&gt;:subdomain&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;configuration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;main_subdomain&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;resources&lt;/span&gt; &lt;span class="ss"&gt;:orders&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样的话，mobile 子域名下的/orders 会路由到 "::Mobile::OrdersController"，目标达成！&lt;/p&gt;
&lt;h3 id="优化一下"&gt;优化一下&lt;/h3&gt;
&lt;p&gt;这样好像还不够好，这样写有一个小小的问题，就是你在 mobile 下面引用一个 url helper 的时候，比如：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;### app/mobile/orders/show.html.erb&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sx"&gt;%= link_to order_path(@order), order.id %&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;读代码的人比较难以直观的知道这个 order_path 是指的哪个域名下的 url，而且如果多个子域名下有 url 路径重复的话，一旦写错，rails 不会提示错误，只有访问的时候才会报错。所以，最好给 scope 加一个 as 参数，把 mobile 下的 url helper 独立出来：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;### 添加as参数会修改url helper&lt;/span&gt;
&lt;span class="n"&gt;constraints&lt;/span&gt; &lt;span class="ss"&gt;:subdomain&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;configuration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mobile_subdomain&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="ss"&gt;module: &lt;/span&gt;&lt;span class="s1"&gt;'mobile'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;as: &lt;/span&gt;&lt;span class="s1"&gt;'mobile'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; 
    &lt;span class="n"&gt;resources&lt;/span&gt; &lt;span class="ss"&gt;:orders&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;### 调用的时候&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sx"&gt;%= link_to mobile_order_path(@order), order.id %&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="namespace 和 scope 其实是一个东西"&gt;namespace 和 scope 其实是一个东西&lt;/h3&gt;
&lt;p&gt;其实事实上，看 rails 源码可以看到，namespace 只不过是包装了一层，底层完全是用 scope 来实现的：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;### File actionpack/lib/action_dispatch/routing/mapper.rb, line 679&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;namespace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;
  &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;
  &lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;:path&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:as&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:module&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="ss"&gt;:shallow_path&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:shallow_prefix&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;merge!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;yield&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;h2 id="rails routes文件拆分"&gt;rails routes 文件拆分&lt;/h2&gt;
&lt;p&gt;一旦你有了多个子域名，可能你的 routes 文件开始变大很难维护了，这个时候把 routes 文件拆分是一个好主意。&lt;/p&gt;

&lt;p&gt;routes 文件拆分有两个办法，一种是 rails 内建的，但是 Rails4 已经移除了，一种是不受 rails 版本影响的方法。&lt;/p&gt;

&lt;p&gt;&lt;a href="http://blog.arkency.com/2015/02/how-to-split-routes-dot-rb-into-smaller-parts/" rel="nofollow" target="_blank" title=""&gt;这篇英文的文章&lt;/a&gt;写得挺清楚了，下面简单说一下。&lt;/p&gt;
&lt;h3 id="修改rails路径配置参数"&gt;修改 rails 路径配置参数&lt;/h3&gt;
&lt;p&gt;通过修改 "config/routes" 配置来解决：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;paths&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"config/routes"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="no"&gt;Dir&lt;/span&gt;&lt;span class="p"&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;'config'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'routes'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'*.rb'&lt;/span&gt;&lt;span class="p"&gt;)]&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="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;paths&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"config/routes"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sx"&gt;%w(
      config/routes/admin.rb
      config/routes/api.rb
      config/routes.rb
    )&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nb"&gt;p&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="nb"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="利用instance_eval"&gt;利用 instance_eval&lt;/h3&gt;
&lt;p&gt;利用 instance_eval 来加载其他路由文件即可：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Example&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;draw&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;draw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;routes_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;instance_eval&lt;/span&gt;&lt;span class="p"&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;read&lt;/span&gt;&lt;span class="p"&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="s2"&gt;"config"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"routes"&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;routes_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.rb"&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="c1"&gt;# subdomain&lt;/span&gt;
  &lt;span class="n"&gt;draw&lt;/span&gt; &lt;span class="ss"&gt;:mobile&lt;/span&gt;
  &lt;span class="n"&gt;draw&lt;/span&gt; &lt;span class="ss"&gt;:api&lt;/span&gt;

  &lt;span class="c1"&gt;### 下面正常写其他路由就可以了&lt;/span&gt;
  &lt;span class="n"&gt;resources&lt;/span&gt; &lt;span class="ss"&gt;:users&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样把其他 routes 文件放在 config/routes/ 目录下即可。&lt;/p&gt;
&lt;h2 id="多个子域名下的_path和_url的使用"&gt;多个子域名下的_path 和_url 的使用&lt;/h2&gt;
&lt;p&gt;主要提一个 *_path 和 *_url 的区别，虽然一个是相对地址、一个是绝对地址，但是单个域名下，其实区别不大，所以很多人都是随手用。&lt;/p&gt;

&lt;p&gt;但是一旦有了多个子域名，如果还是随手混用就会导致很多问题。所以，多个子域名下应该注意下面的几个约定：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;除非必要，只用 *_path&lt;/p&gt;

&lt;p&gt;多个子域名下，大部分的内链还是在子域名内部的，所以尽量使用*_path 来引用 url。如果不是的话，请考虑产品设计的是否合理、是否本子域名下也需要一个独立的 url 来满足需求。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果使用 *_url，那么一定要加上子域名的参数&lt;/p&gt;

&lt;p&gt;如果在跨域名访问的情况下（或者是 mailer 中），使用 *_url 的时候一定要加上 subdomain 的参数：&lt;/p&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="n"&gt;mobile_users_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;subdomain: &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;configuration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mobile_subdomain&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;nickname&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用 url helper，而不是字符串来代表地址。&lt;/p&gt;

&lt;p&gt;这一点，初级的 rails 工程师很容易犯，觉得写一个字符串非常简单，干嘛还要搞一个 url helper？可是一旦使用字符串表达 url，一旦需要重构代码、升级产品的时候基本上代码是不可维护的，这时候只能默默流泪了。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;javascript 代码中引用 url&lt;/p&gt;

&lt;p&gt;这种情况也不少，可以使用 data-url 的形式，如：&lt;/p&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;### 这样
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;data-url=&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;mobile_users_path&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
### 或者这样
&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;content_tag&lt;/span&gt; &lt;span class="ss"&gt;:div&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:'data-url'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;mobile_users_path&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  some content
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="差不多就是这样"&gt;差不多就是这样&lt;/h2&gt;
&lt;p&gt;好，差不多就是这样，希望在多子域名的问题上对大家有帮助，欢迎大家提意见~&lt;/p&gt;

&lt;p&gt;广告时间：
&lt;a href="http://www.fishtrip.cn" rel="nofollow" target="_blank" title=""&gt;大鱼自助游&lt;/a&gt;还在招 ror 工程师哦，初高级均可，欢迎&lt;a title=""&gt;直接抛简历&lt;/a&gt;给我。&lt;/p&gt;</description>
      <author>zamia</author>
      <pubDate>Sun, 15 Nov 2015 14:56:21 +0800</pubDate>
      <link>https://ruby-china.org/topics/28065</link>
      <guid>https://ruby-china.org/topics/28065</guid>
    </item>
    <item>
      <title>[北京] 大鱼自助游 坐标三元桥 招聘 Ruby 开发工程师&amp;数据分析工程师</title>
      <description>&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/2015/7e70ba94d6f3b161a9310c41fab91974.jpg" title="" alt=""&gt;&lt;/p&gt;
&lt;h2 id="大鱼招聘爱旅行的攻城狮!!!!!! :smile:"&gt;大鱼招聘爱旅行的攻城狮!!!!!! &lt;img title=":smile:" alt="😄" src="https://twemoji.ruby-china.com/2/svg/1f604.svg" class="twemoji"&gt;
&lt;/h2&gt;
&lt;p&gt;Hi! 这里大鱼自助游。 &lt;/p&gt;

&lt;p&gt;大鱼自助游 (&lt;a href="http://www.fishtrip.cn" rel="nofollow" target="_blank" title=""&gt;www.fishtrip.cn&lt;/a&gt;)   自上线一直致力于为中国人提供最优质的出境自由行服务。 
才不要做第二个 TripAdvisor 或者 Booking.com&lt;/p&gt;

&lt;p&gt;大鱼要打造前所未有的个性化海外自由行产品交易平台，提供最具当地特色的住宿。 &lt;/p&gt;
&lt;h2 id="过去一年大鱼做了这些事情："&gt;过去一年大鱼做了这些事情：&lt;/h2&gt;
&lt;p&gt;• 发布了全国第一款办证类 App「入台证神器」 
• 成功开拓了台湾、日本站 
• 通过强大的后台与上千家中小供应商实现对接 
• 推出大鱼 • 旅行猎人计划并取得巨大反响  &lt;/p&gt;
&lt;h2 id="未来我们希望做的事情："&gt;未来我们希望做的事情：&lt;/h2&gt;
&lt;p&gt;• 将大鱼做成一个世界性的出境游交易平台，为各国旅游者精选值得信赖有品质保证的特色住宿
• 我们的口号是：只推荐特色住宿 (=ﾟωﾟ) ノ
• 和我们一起从住宿开始体验世界吧：）&lt;/p&gt;

&lt;p&gt;#大鱼能给你什么&lt;/p&gt;

&lt;p&gt;• 开放的互联网创业环境
• 释放潜力、爆发小宇宙的平台
• 富有竞争力的薪资和贴心的住房补贴
• 厨艺高超的阿姨准备的午餐和下午茶
• 只是每天抢饭的场面略凶残，请准备好
• 成为大鱼 • 旅行猎人的机会
• 边旅行边赚钱
• 为大鱼在世界各地开疆拓土&lt;/p&gt;

&lt;p&gt;大鱼搬家了，从南三环的温馨小宅搬到了东三环的“工业朋克风”办公室，欢迎大家来办公区前厅的大鱼咖啡聊一聊&lt;/p&gt;

&lt;p&gt;必须有图有真相↓&lt;/p&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/2015/0393f4a4f6afd00927a6e4230c7de2e2.jpg" title="" alt=""&gt;
&lt;img src="https://l.ruby-china.com/photo/2015/1d93347ed69f39a4281fb9e8c38d0488.jpg" title="" alt=""&gt;
&lt;img src="https://l.ruby-china.com/photo/2015/444880643b0c4bb62c7dc86e98211e12.jpg" title="" alt=""&gt;
&lt;img src="https://l.ruby-china.com/photo/2015/e32f5cb8c328fce6a9d393df340b85bf.jpg" title="" alt=""&gt;
&lt;img src="https://l.ruby-china.com/photo/2015/330141a9730da9e72384126c713cba40.jpg" title="" alt=""&gt;&lt;/p&gt;
&lt;h2 id="关于我们"&gt;关于我们&lt;/h2&gt;
&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/2015/09b1a9480a2a09d15a85e78a44837098.jpg" title="" alt=""&gt;
如果你好奇这是怎样的一群人，一两句话确实挺难概括的，你最好亲自来看看。 &lt;/p&gt;

&lt;p&gt;这里有 Google、百度、微软出来的大牛，也有毕业不久的新鲜人。水瓶、双子、天蝎居多，不羁放纵爱自由。&lt;/p&gt;

&lt;p&gt;我们时常仰望星空，却不忘脚踏实地。相信成大事者，心中必有爱。爱创业、爱互联网，更爱旅行。 
&lt;img src="https://l.ruby-china.com/photo/2015/dc0a3ac79258643ae0171101121de6cd.jpg" title="" alt=""&gt;
认识我们：&lt;a href="http://www.fishtrip.cn/about_us" rel="nofollow" target="_blank"&gt;http://www.fishtrip.cn/about_us&lt;/a&gt; 
所以你想加入我们一起来做一点有趣的事情吗？ &lt;/p&gt;
&lt;h5 id="Ruby"&gt;Ruby&lt;/h5&gt;
&lt;p&gt;10k-20k 北京 经验 1-3 年 本科及以上 全职&lt;/p&gt;

&lt;p&gt;职位描述&lt;/p&gt;

&lt;p&gt;◇  岗位职责&lt;/p&gt;

&lt;p&gt;（1）参与大鱼网站的架构设计，以及各个后端模块的开发；&lt;/p&gt;

&lt;p&gt;（2）承担大鱼网站的后端开发和维护工作。&lt;/p&gt;

&lt;p&gt;◇  任职要求&lt;/p&gt;

&lt;p&gt;（1）大学本科及以上学历，1 年以上相关工作经验；&lt;/p&gt;

&lt;p&gt;（2）熟悉互联网产品和服务的开发过程，有至少一门动态语言或脚本语言的开发经验，对互联网后端技术架构有基本的理解；&lt;/p&gt;

&lt;p&gt;（3）认可创业团队氛围，有上进心与自我驱动力，能够承受一定的工作压力。&lt;/p&gt;

&lt;p&gt;◇      加分项&lt;/p&gt;

&lt;p&gt;（1）对前端技术如 css/html/javascript 等有一定了解；&lt;/p&gt;

&lt;p&gt;（2）有电商行业开发经历；&lt;/p&gt;

&lt;p&gt;（3）对开源、技术分享、敏捷开发感兴趣；&lt;/p&gt;

&lt;p&gt;（4）全栈工程师。&lt;/p&gt;
&lt;h5 id="数据分析工程师"&gt;数据分析工程师&lt;/h5&gt;
&lt;p&gt;10-30k 北京 应届生或者有工作经验皆可，本科及以上 全职&lt;/p&gt;

&lt;p&gt;◇ 岗位职责&lt;/p&gt;

&lt;p&gt;（1）负责大鱼数据爬取、数据采集以及数据整理等工作。&lt;/p&gt;

&lt;p&gt;◇ 任职要求&lt;/p&gt;

&lt;p&gt;（1）大学本科及以上学历，1 年以上相关工作经验（优秀应届生亦可）；
（2）能够熟练使用 Python 或 Ruby 语言，并有过项目经验；
（3）能够熟练使用 html、css、javascript 前端语言；
（4）拥有独立思考能力，并对于钻研问题充满兴趣；
（5）具有较好的理解沟通能力，工作积极主动，有团队合作精神，责任心强；
（6）认可创业团队氛围，有上进心与自我驱动力，能够承受一定的工作压力。&lt;/p&gt;

&lt;p&gt;◇ 加分项&lt;/p&gt;

&lt;p&gt;（1）熟悉 rails 架构者优先；
（2）有电商行业开发经历者优先；
（3）全栈工程师优先&lt;/p&gt;

&lt;p&gt;简历发送至：- &lt;a href="hr@fishtrip.cn" title=""&gt;hr@fishtrip.cn&lt;/a&gt;  &lt;img title=":laughing:" alt="😆" src="https://twemoji.ruby-china.com/2/svg/1f606.svg" class="twemoji"&gt;
标题：姓名 + 职位+Ruby China&lt;/p&gt;</description>
      <author>zamia</author>
      <pubDate>Thu, 29 Oct 2015 14:52:41 +0800</pubDate>
      <link>https://ruby-china.org/topics/27886</link>
      <guid>https://ruby-china.org/topics/27886</guid>
    </item>
    <item>
      <title>[北京] 大鱼自助游诚招 ruby 工程师&amp;数据分析工程师</title>
      <description>&lt;h2 id="Hi! 这里大鱼自助游。"&gt;Hi! 这里大鱼自助游。&lt;/h2&gt;
&lt;p&gt;大鱼自助游 (&lt;a href="http://www.fishtrip.cn)%E8%87%AA%E4%B8%8A%E7%BA%BF%E4%B8%80%E7%9B%B4%E8%87%B4%E5%8A%9B%E4%BA%8E%E4%B8%BA%E4%B8%AD%E5%9B%BD%E4%BA%BA%E6%8F%90%E4%BE%9B%E6%9C%80%E4%BC%98%E8%B4%A8%E7%9A%84%E5%87%BA%E5%A2%83%E8%87%AA%E7%94%B1%E8%A1%8C%E6%9C%8D%E5%8A%A1%E3%80%82" title=""&gt;www.fishtrip.cn) 自上线一直致力于为中国人提供最优质的出境自由行服务。&lt;/a&gt; 
才不要做第二个 TripAdvisor 或者 Booking.com, 大鱼要打造前所未有的个性化海外自由行产品交易平台，提供最具当地特色的住宿。 &lt;/p&gt;
&lt;h2 id="【过去一年大鱼做了这些事情】"&gt;【过去一年大鱼做了这些事情】&lt;/h2&gt;
&lt;p&gt;• 发布了全国第一款办证类 App「入台证神器」 
• 成功开拓了台湾、日本站 
• 通过强大的后台与上千家中小供应商实现对接 
• 推出大鱼 • 旅行猎人计划并取得巨大反响 &lt;/p&gt;
&lt;h2 id="【未来我们希望做的事情】"&gt;【未来我们希望做的事情】&lt;/h2&gt;
&lt;p&gt;将大鱼做成一个世界性的出境游交易平台，为各国旅游者精选值得信赖有品质保证的特色住宿。
我们的口号是：只推荐特色住宿 (=ﾟωﾟ) ノ&lt;/p&gt;

&lt;p&gt;和我们一起从住宿开始体验世界吧：）&lt;/p&gt;
&lt;h2 id="【大鱼能给你什么】"&gt;【大鱼能给你什么】&lt;/h2&gt;
&lt;p&gt;• 开放的互联网创业环境
• 释放潜力、爆发小宇宙的平台
• 富有竞争力的薪资和贴心的住房补贴
• 厨艺高超的阿姨准备的午餐和下午茶
• 只是每天抢饭的场面略凶残，请准备好
• 成为大鱼 • 旅行猎人的机会
• 边旅行边赚钱
• 为大鱼在世界各地开疆拓土&lt;/p&gt;
&lt;h2 id="【关于我们】"&gt;【关于我们】&lt;/h2&gt;
&lt;p&gt;如果你好奇这是怎样的一群人，一两句话确实挺难概括的，你最好亲自来看看。 &lt;/p&gt;

&lt;p&gt;这里有 Google、百度、微软出来的大牛，也有毕业不久的新鲜人。水瓶、双子、天蝎居多，不羁放纵爱自由。&lt;/p&gt;

&lt;p&gt;我们时常仰望星空，却不忘脚踏实地。相信成大事者，心中必有爱。爱创业、爱互联网，更爱旅行。 &lt;/p&gt;

&lt;p&gt;认识我们：&lt;a href="http://www.fishtrip.cn/about_us" rel="nofollow" target="_blank"&gt;http://www.fishtrip.cn/about_us&lt;/a&gt; &lt;/p&gt;
&lt;h2 id="所以你想加入我们一起来做一点有趣的事情吗？"&gt;所以你想加入我们一起来做一点有趣的事情吗？&lt;/h2&gt;&lt;h2 id="大鱼特意来这里寻找这样的小伙伴"&gt;大鱼特意来这里寻找这样的小伙伴&lt;/h2&gt;&lt;h3 id="初级/高级Ruby工程师"&gt;初级/高级 Ruby 工程师&lt;/h3&gt;
&lt;p&gt;10k-30k 北京 应届生或有工作经验均可 本科及以上 全职&lt;/p&gt;

&lt;p&gt;职位描述&lt;/p&gt;

&lt;p&gt;◇  岗位职责&lt;/p&gt;

&lt;p&gt;（1）参与大鱼网站的架构设计，以及各个后端模块的开发；&lt;/p&gt;

&lt;p&gt;（2）承担大鱼网站的后端开发和维护工作。&lt;/p&gt;

&lt;p&gt;◇  任职要求&lt;/p&gt;

&lt;p&gt;（1）大学本科及以上学历，1 年以上相关工作经验，应届生也行；&lt;/p&gt;

&lt;p&gt;（2）熟悉互联网产品和服务的开发过程，有至少一门动态语言或脚本语言的开发经验，对互联网后端技术架构有基本的理解；&lt;/p&gt;

&lt;p&gt;（3）认可创业团队氛围，有上进心与自我驱动力，能够承受一定的工作压力。&lt;/p&gt;

&lt;p&gt;◇      加分项&lt;/p&gt;

&lt;p&gt;（1）对前端技术如 css/html/javascript 等有一定了解；&lt;/p&gt;

&lt;p&gt;（2）有电商行业开发经历；&lt;/p&gt;

&lt;p&gt;（3）对开源、技术分享、敏捷开发感兴趣；&lt;/p&gt;

&lt;p&gt;（4）全栈工程师。&lt;/p&gt;
&lt;h3 id="数据分析工程师"&gt;数据分析工程师&lt;/h3&gt;
&lt;p&gt;10-30k 北京 应届生或者有工作经验皆可，本科及以上 全职&lt;/p&gt;

&lt;p&gt;◇  岗位职责&lt;/p&gt;

&lt;p&gt;（1）负责大鱼数据爬取、数据采集以及数据整理等工作。&lt;/p&gt;

&lt;p&gt;◇ 任职要求
（1）大学本科及以上学历，1 年以上相关工作经验（优秀应届生亦可）；&lt;/p&gt;

&lt;p&gt;（2）能够熟练使用 Python 或 Ruby 语言，并有过项目经验；&lt;/p&gt;

&lt;p&gt;（3）能够熟练使用 html、css、javascript 前端语言；&lt;/p&gt;

&lt;p&gt;（4）拥有独立思考能力，并对于钻研问题充满兴趣；&lt;/p&gt;

&lt;p&gt;（5）具有较好的理解沟通能力，工作积极主动，有团队合作精神，责任心强；&lt;/p&gt;

&lt;p&gt;◇ 加分项
（6）熟悉 rails 架构者优先；&lt;/p&gt;

&lt;p&gt;（7）有电商行业开发经历者优先；&lt;/p&gt;

&lt;p&gt;（8）全栈工程师优先&lt;/p&gt;

&lt;p&gt;大鱼搬家了，从南三环的温馨小宅搬到了东三环的“工业朋克风”办公室，欢迎大家来办公区前厅的大鱼咖啡聊一聊&lt;/p&gt;
&lt;h2 id="简历发送方法"&gt;简历发送方法&lt;/h2&gt;
&lt;p&gt;简历请发送至 hongdi@fishtrip.cn   格式：姓名 + 应聘职位 + 渠道&lt;/p&gt;

&lt;p&gt;感谢各位：）&lt;/p&gt;

&lt;p&gt;我们等你！&lt;/p&gt;</description>
      <author>zamia</author>
      <pubDate>Tue, 30 Jun 2015 14:36:53 +0800</pubDate>
      <link>https://ruby-china.org/topics/26250</link>
      <guid>https://ruby-china.org/topics/26250</guid>
    </item>
    <item>
      <title>[北京] 大鱼自助游诚聘 Ruby 工程师</title>
      <description>&lt;p&gt;Hi! 这里大鱼自助游。 &lt;/p&gt;

&lt;p&gt;大鱼自助游 (&lt;a href="http://www.fishtrip.cn" rel="nofollow" target="_blank" title=""&gt;www.fishtrip.cn&lt;/a&gt;) 自上线一直致力于为中国人提供最优质的出境自由行服务。 
才不要做第二个 TripAdvisor 或者 Booking.com, 大鱼要打造前所未有的个性化海外自由行产品交易平台，提供最具当地特色的住宿。 &lt;/p&gt;

&lt;p&gt;过去一年大鱼做了这些事情： &lt;/p&gt;

&lt;p&gt;• 发布了全国第一款办证类 App「入台证神器」 
• 成功开拓了台湾、日本站 
• 通过强大的后台与上千家中小供应商实现对接 
• 推出大鱼 • 旅行猎人计划并取得巨大反响
•发布了旅行猎人专属 App「大鱼旅行猎人」&lt;/p&gt;

&lt;p&gt;未来我们希望做的事情： 
将大鱼做成一个世界性的出境游交易平台，为各国旅游者精选值得信赖有品质保证的特色住宿。
我们的口号是：只推荐特色住宿 (=ﾟωﾟ) ノ&lt;/p&gt;

&lt;p&gt;和我们一起从住宿开始体验世界吧：）&lt;/p&gt;

&lt;p&gt;【关于我们】 &lt;/p&gt;

&lt;p&gt;如果你好奇这是怎样的一群人，一两句话确实挺难概括的，你最好亲自来看看。 &lt;/p&gt;

&lt;p&gt;这里有 Google、百度、微软出来的大牛，也有毕业不久的新鲜人。水瓶、双子、天蝎居多，不羁放纵爱自由。&lt;/p&gt;

&lt;p&gt;我们时常仰望星空，却不忘脚踏实地。相信成大事者，心中必有爱。爱创业、爱互联网，更爱旅行。 &lt;/p&gt;

&lt;p&gt;认识我们：&lt;a href="http://www.fishtrip.cn/about_us" rel="nofollow" target="_blank"&gt;http://www.fishtrip.cn/about_us&lt;/a&gt; 
所以你想加入我们一起来做一点有趣的事情吗？ &lt;/p&gt;

&lt;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;大鱼特意来 Ruby China 找这样的人&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/blockquote&gt;

&lt;p&gt;【Ruby on Rails 工程师】10K~20K
• 参与大鱼网站的架构设计，以及各个后端模块的开发； 
• 承担大鱼网站的后端开发和维护工作。 &lt;/p&gt;

&lt;p&gt;• 1 年以上相关工作经验； 
• 熟悉互联网产品和服务的开发过程，有至少一门动态语言或脚本语言的开发经验，对互联网后端技术架构有基本的理解； 
• 认可创业团队氛围，有上进心与自我驱动力，能够承受一定的工作压力。 
• 对前端技术如 css/html/javascript 等有一定了解； 
• 有电商行业开发经历； 
• 对开源、技术分享、敏捷开发感兴趣； 
• 全栈工程师优先。&lt;/p&gt;

&lt;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;大鱼也想从 Ruby China 试一试找这样的人&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/blockquote&gt;

&lt;p&gt;【前端工程师】10K~20K
• 负责大鱼网站的前端开发，与 UI 设计师、产品经理、交互设计师一起，实现易用、好用的互联网旅游产品； 
• 负责开发和维护公司移动端（H5）的开发； 
• 负责前端框架的选型和开发，制定前端开发规范； 
• 开发易维护、可复用的各种前端组件。 &lt;/p&gt;

&lt;p&gt;• 2 年以上相关工作经验； 
• 深入理解 Javascript 和 CSS 等前端技术，对 HTML5 有一定了解； 
• 具有良好的团队协作能力、沟通能力与学习能力，热爱技术，喜欢钻研； 
• 移动互联网/电子商务行业经验优先； 
• 经验丰富，带过前端工程团队优先； 
• 对产品和交互有兴趣者优先。&lt;/p&gt;

&lt;p&gt;【Android/iOS 工程师】10K~20K
• 负责开发和维护大鱼的移动客户端，包括 Android、iOS 以及平板应用。 &lt;/p&gt;

&lt;p&gt;• 1 年以上相关工作经验； 
• 1～3 个移动设备客户端产品的开发及上线； 
• 学习能力较强，对移动端的技术或产品有浓厚兴趣； 
• 计算机基础知识扎实，对移动端应用的整体架构有一定把握能力； 
• 具备较好的团队协助能力，乐于分享和交流； 
• 有 HTML5 或者桌面软件开发经验者优先； 
• 有 Linux/C/Java 开发经验者优先。 &lt;/p&gt;

&lt;p&gt;【PHP 工程师】10K~20K
• 负责开发大鱼项目中各类产品需求，对已知的线上问题快速定位与修复； 
• 参与大鱼核心业务模块系统分析、设计、开发； 
• 负责根据大鱼不同应用需求，优化系统设计和前台表现，不断提升系统效能和安全性。 &lt;/p&gt;

&lt;p&gt;• 2 年以上相关工作经验； 
• 精通 PHP，能够深入理解 PHP 面向对象编程设计理念，并且熟悉 MVC 开发框架，有 PHP 实践经验； 
• 精通 LAMP 架构，熟练应用 Shell/JavaScript/Mysql 等技术； 
• 具有较好的理解沟通能力，工作积极主动，有团队合作精神，责任心强； 
• 认可创业团队氛围，有上进心与自我驱动力，能够承受一定的工作压力。 
• 具有旅游行业相关系统开发经验者优先； 
• 有团队管理经验者优先。&lt;/p&gt;

&lt;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;大鱼能给你什么&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/blockquote&gt;

&lt;p&gt;• 开放的互联网创业环境
• 舒适的人体工程学座椅，一把好椅子真的很重要
• 除霾装置已到位，让办公室变得空气清新
• 释放潜力、爆发小宇宙的平台
• 富有竞争力的薪资和贴心的住房补贴
• 厨艺高超的阿姨准备的午餐和下午茶，只是每天抢饭的场面略凶残，请准备好
• 大鱼咖啡员工专属优惠，花更少的钱喝更纯正的咖啡
• 员工生日特别福利，HR 为你挑选特别的生日礼物
• 年假带薪病假不用讲，修生养息假也请好好利用
• 成为大鱼 • 旅行猎人的机会，边旅行边赚钱
• 为大鱼在世界各地开疆拓土&lt;/p&gt;

&lt;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;推荐有奖&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/blockquote&gt;

&lt;p&gt;成功推荐你身边的朋友加入大鱼，将获得往返台北机票及三晚住宿！
这么好的事情，走过路过不要过错啊喂！&lt;/p&gt;

&lt;p&gt;大鱼搬家了，从南三环的温馨小宅搬到了东三环的“工业朋克风”办公室，欢迎大家来办公区前厅的大鱼咖啡聊一聊&lt;/p&gt;

&lt;p&gt;必须有图有真相↓&lt;/p&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/2015/cc26bb07db522bdcf96b57451a2859f8.jpg" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/2015/45c7430bfae87ab7c8e0a9cb805b2f40.jpg" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/2015/9121315267c5a2cea6407bbb6f0c539d.jpg" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/2015/4f8e9591573a7891466562a88c9118d5.jpg" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/2015/4f6cc4c22e09328a7b001c5658c038fa.jpg" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;--&lt;/p&gt;

&lt;p&gt;欢迎发送简历至 hr@fishtrip.cn 或 medal@fishtrip.cn&lt;/p&gt;

&lt;p&gt;感谢各位：）&lt;/p&gt;

&lt;p&gt;我们等你！&lt;/p&gt;</description>
      <author>zamia</author>
      <pubDate>Fri, 15 May 2015 20:44:50 +0800</pubDate>
      <link>https://ruby-china.org/topics/25596</link>
      <guid>https://ruby-china.org/topics/25596</guid>
    </item>
    <item>
      <title>Rails 中的事务处理</title>
      <description>&lt;p&gt;发现大鱼团队中不少同学对 Rails 中事务的使用不当，发现这篇文章不错，花一点时间翻译了一下，希望对各位有用~&lt;/p&gt;
&lt;h2 id="Rails中的事务"&gt;Rails 中的事务&lt;/h2&gt;
&lt;p&gt;翻译自：&lt;a href="http://markdaggett.com/blog/2011/12/01/transactions-in-rails/" rel="nofollow" target="_blank"&gt;http://markdaggett.com/blog/2011/12/01/transactions-in-rails/&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="使用事务的原因"&gt;使用事务的原因&lt;/h2&gt;
&lt;p&gt;事务用来确保多条 SQL 语句要么全部执行成功、要么不执行。事务可以帮助开发者保证应用中的数据一致性。常见的使用事务的场景是银行转账，钱从一个账户转移到另外一个账户。如果中间的某一步出错，那么整个过程应该重置。这个例子的伪码如下：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transaction&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;david&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;withdrawal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;mary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deposit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&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;Rails 中，通过 ActiveRecord 对象的类方法或者实例方法即可实现事务：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transaction&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="vi"&gt;@client.users.create&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;
  &lt;span class="vi"&gt;@user.clients&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;destroy!&lt;/span&gt;
  &lt;span class="no"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;destroy!&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="vi"&gt;@client.transaction&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="vi"&gt;@client.users.create&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;
  &lt;span class="vi"&gt;@user.clients&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;destroy!&lt;/span&gt;
  &lt;span class="no"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;destroy!&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到上面的例子中，每个事务中均含有多个不同的 model。在同一个事务中调用多个 model 对象是常见的行为，因为事务是和一个数据库连接绑定在一起的，而不是某个 model 对象；而同时，也只有在对多个纪录进行操作，并且希望这些操作作为一个整体的时候，事务才是必要的。&lt;/p&gt;

&lt;p&gt;另外，Rails 已经把类似 #save 和 #destroy 的方法包含在一个事务中了，因此，对于单条数据库记录来说，不需要再使用显式的调用了。&lt;/p&gt;
&lt;h2 id="触发事务回滚"&gt;触发事务回滚&lt;/h2&gt;
&lt;p&gt;事务通过 rollback 过程把记录的状态进行重置。在 Rails 中，rollback 只会被一个 exception 触发。这是非常关键的一点，很多事务块中的代码不会触发异常，因此即使出错，事务也不会回滚。比如下面的写法：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transaction&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;david&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;david&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;mary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&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;因为 Rails 中，#update_attribute 方法在调用失败的时候也不会触发 exception，它只是简单的返回 false，因此必须确保 transaction 块中的函数在失败时会抛异常。正确的写法是这样的：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transaction&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;david&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update_attributes!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:amount&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;mary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update_attributes!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:amount&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;100&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;注意，Rails 中约定，带有叹号的函数一般会在失败时抛异常。&lt;/p&gt;

&lt;p&gt;同时，我也看到一些代码中，在事务块中使用了 #find_by 方法，实际上，find_by 等魔术方法当找不到记录的时候会返回 nil，而 #find_ 方法在找不到记录的时候会抛出一个 ActiveRecord::RecordNotFound 异常。比如下面的例子：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transaction&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;david&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_by_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"david"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;david&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;john&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;john&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update_attributes!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:amount&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;mary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update_attributes!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:amount&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;100&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;/code&gt;&lt;/pre&gt;
&lt;p&gt;发现上面的逻辑错误了吗？nil 对象也有一个 id 方法，导致记录没有被找到的错误被隐藏了。同时，由于 find_by 也不会抛出异常，因此下面的代码被错误的执行了。这就意味着，有的时候在某些场景下，我们需要人工抛异常。因此这段代码因此改成下面的形式：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transaction&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;david&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_by_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"david"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;raise&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;RecordNotFound&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;david&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;nil?&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;david&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;john&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;john&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update_attributes!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:amount&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;mary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update_attributes!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:amount&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;100&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;/code&gt;&lt;/pre&gt;
&lt;p&gt;当错误出现时，事务本身会回滚，同时异常也会在外层抛出。因此，你的调用方必须考虑 catch 这个异常，并进行相应的处理。&lt;/p&gt;

&lt;p&gt;有一个特殊的异常，ActiveRecord::Rollback，当它被抛出时，事务本身会回滚，但是它并不会被重新抛出，因此你也不需要在外部进行 catch 和处理。&lt;/p&gt;
&lt;h2 id="何时使用嵌套事务？"&gt;何时使用嵌套事务？&lt;/h2&gt;
&lt;p&gt;错误使用或者过多使用嵌套异常是比较常见的错误。当你把一个 transaction 嵌套在另外一个事务之中时，就会存在父事务和子事务，这种写法有时候会导致奇怪的结果。比如下面来自于 Rails API 文档的例子：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transaction&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="no"&gt;User&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;:username&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Kotori'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transaction&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="no"&gt;User&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;:username&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Nemu'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;raise&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;Rollback&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;上面提到，ActiveRecord::Rollback 不会传播到上层的方法中去，因此这个例子中，父事务并不会收到子事务抛出的异常。因为子事务块中的内容也被合并到了父事务中去，因此这个例子中，两条 User 记录都会被创建！&lt;/p&gt;

&lt;p&gt;可以把嵌套事务这样理解，子事务中的内容被归并到了父事务中，这样子事务变空。&lt;/p&gt;

&lt;p&gt;为了保证一个子事务的 rollback 被父事务知晓，必须手动在子事务中添加 :require_new =&amp;gt; true 选项。比如下面的写法：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transaction&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="no"&gt;User&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;:username&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Kotori'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:requires_new&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="k"&gt;do&lt;/span&gt;
    &lt;span class="no"&gt;User&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;:username&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Nemu'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;raise&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;Rollback&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="no"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transaction&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="no"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transaction&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;buy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@quantity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update_attributes!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:sales_count&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="vi"&gt;@sales_count&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&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;/code&gt;&lt;/pre&gt;&lt;h2 id="事务相关的回调"&gt;事务相关的回调&lt;/h2&gt;
&lt;p&gt;上面提到 #save 和 #destroy 方法被自动包裹在一个事务中，因此相关的回调，比如 #after_save 仍然属于事务的一部分，因此回调代码也有可能被回滚。&lt;/p&gt;

&lt;p&gt;因此，如果你希望代码在事务外部执行的话，那么可以使用 #after_commit 或者 # after_rollback 这样的回调函数。&lt;/p&gt;
&lt;h2 id="事务陷阱"&gt;事务陷阱&lt;/h2&gt;
&lt;p&gt;不要在事务内部去捕捉 ActiveRecord::RecordInvalid 异常。因为某些数据库下，这个异常会导致事务失效，比如 Postgres。一旦事务失效，要想让代码正确工作，就必须从头重新执行事务。&lt;/p&gt;

&lt;p&gt;另外，测试回滚或者事务回滚相关的回调时，最好关掉 transactional_fixtures 选项，一般的测试框架中，这个选项是打开的。&lt;/p&gt;
&lt;h2 id="常见的事务反模式"&gt;常见的事务反模式&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;单条记录操作时使用事务&lt;/li&gt;
&lt;li&gt;不必要的使用嵌套式事务&lt;/li&gt;
&lt;li&gt;事务中的代码不会导致回滚&lt;/li&gt;
&lt;li&gt;在 controller 中使用事务&lt;/li&gt;
&lt;/ol&gt;</description>
      <author>zamia</author>
      <pubDate>Mon, 04 May 2015 22:30:05 +0800</pubDate>
      <link>https://ruby-china.org/topics/25427</link>
      <guid>https://ruby-china.org/topics/25427</guid>
    </item>
    <item>
      <title>[北京 | 互联网 | 出境游] 大鱼诚招 Ruby 攻城狮</title>
      <description>&lt;p&gt;Hi! 这里大鱼自助游。 &lt;/p&gt;

&lt;p&gt;大鱼自助游 (&lt;a href="http://www.fishtrip.cn" rel="nofollow" target="_blank" title=""&gt;www.fishtrip.cn&lt;/a&gt;) 自上线一直致力于为中国人提供最优质的出境自由行服务。 
才不要做第二个 TripAdvisor 或者 Booking.com, 大鱼要打造前所未有的个性化海外自由行产品交易平台，提供最具当地特色的住宿。 &lt;/p&gt;

&lt;p&gt;过去一年大鱼做了这些事情： &lt;/p&gt;

&lt;p&gt;• 发布了全国第一款办证类 App「入台证神器」 
• 成功开拓了台湾、日本站 
• 通过强大的后台与上千家中小供应商实现对接 
• 推出大鱼 • 旅行猎人计划并取得巨大反响 &lt;/p&gt;

&lt;p&gt;未来我们希望做的事情： 
将大鱼做成一个世界性的出境游交易平台，为各国旅游者精选值得信赖有品质保证的特色住宿。
我们的口号是：只推荐特色住宿 (=ﾟωﾟ) ノ&lt;/p&gt;

&lt;p&gt;和我们一起从住宿开始体验世界吧：）&lt;/p&gt;

&lt;p&gt;【关于我们】 &lt;/p&gt;

&lt;p&gt;如果你好奇这是怎样的一群人，一两句话确实挺难概括的，你最好亲自来看看。 &lt;/p&gt;

&lt;p&gt;这里有 Google、百度、微软出来的大牛，也有毕业不久的新鲜人。水瓶、双子、天蝎居多，不羁放纵爱自由。&lt;/p&gt;

&lt;p&gt;我们时常仰望星空，却不忘脚踏实地。相信成大事者，心中必有爱。爱创业、爱互联网，更爱旅行。 &lt;/p&gt;

&lt;p&gt;认识我们：&lt;a href="http://www.fishtrip.cn/about_us" rel="nofollow" target="_blank"&gt;http://www.fishtrip.cn/about_us&lt;/a&gt; 
所以你想加入我们一起来做一点有趣的事情吗？ &lt;/p&gt;

&lt;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;大鱼特意来 Ruby China 找这样的人&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/blockquote&gt;

&lt;p&gt;【Ruby on Rails 工程师】10K~20K
• 参与大鱼网站的架构设计，以及各个后端模块的开发； 
• 承担大鱼网站的后端开发和维护工作。 &lt;/p&gt;

&lt;p&gt;• 1 年以上相关工作经验； 
• 熟悉互联网产品和服务的开发过程，有至少一门动态语言或脚本语言的开发经验，对互联网后端技术架构有基本的理解； 
• 认可创业团队氛围，有上进心与自我驱动力，能够承受一定的工作压力。 
• 对前端技术如 css/html/javascript 等有一定了解； 
• 有电商行业开发经历； 
• 对开源、技术分享、敏捷开发感兴趣； 
• 全栈工程师优先。&lt;/p&gt;

&lt;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;大鱼也想从 Ruby China 试一试找这样的人&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/blockquote&gt;

&lt;p&gt;【前端工程师】10K~20K
• 负责大鱼网站的前端开发，与 UI 设计师、产品经理、交互设计师一起，实现易用、好用的互联网旅游产品； 
• 负责开发和维护公司移动端（H5）的开发； 
• 负责前端框架的选型和开发，制定前端开发规范； 
• 开发易维护、可复用的各种前端组件。 &lt;/p&gt;

&lt;p&gt;• 2 年以上相关工作经验； 
• 深入理解 Javascript 和 CSS 等前端技术，对 HTML5 有一定了解； 
• 具有良好的团队协作能力、沟通能力与学习能力，热爱技术，喜欢钻研； 
• 移动互联网/电子商务行业经验优先； 
• 经验丰富，带过前端工程团队优先； 
• 对产品和交互有兴趣者优先。&lt;/p&gt;

&lt;p&gt;【Android/iOS 工程师】10K~20K
• 负责开发和维护大鱼的移动客户端，包括 Android、iOS 以及平板应用。 &lt;/p&gt;

&lt;p&gt;• 1 年以上相关工作经验； 
• 1～3 个移动设备客户端产品的开发及上线； 
• 学习能力较强，对移动端的技术或产品有浓厚兴趣； 
• 计算机基础知识扎实，对移动端应用的整体架构有一定把握能力； 
• 具备较好的团队协助能力，乐于分享和交流； 
• 有 HTML5 或者桌面软件开发经验者优先； 
• 有 Linux/C/Java 开发经验者优先。 &lt;/p&gt;

&lt;p&gt;【PHP 工程师】10K~20K
• 负责开发大鱼项目中各类产品需求，对已知的线上问题快速定位与修复； 
• 参与大鱼核心业务模块系统分析、设计、开发； 
• 负责根据大鱼不同应用需求，优化系统设计和前台表现，不断提升系统效能和安全性。 &lt;/p&gt;

&lt;p&gt;• 2 年以上相关工作经验； 
• 精通 PHP，能够深入理解 PHP 面向对象编程设计理念，并且熟悉 MVC 开发框架，有 PHP 实践经验； 
• 精通 LAMP 架构，熟练应用 Shell/JavaScript/Mysql 等技术； 
• 具有较好的理解沟通能力，工作积极主动，有团队合作精神，责任心强； 
• 认可创业团队氛围，有上进心与自我驱动力，能够承受一定的工作压力。 
• 具有旅游行业相关系统开发经验者优先； 
• 有团队管理经验者优先。&lt;/p&gt;

&lt;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;大鱼能给你什么&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/blockquote&gt;

&lt;p&gt;• 开放的互联网创业环境
• 释放潜力、爆发小宇宙的平台
• 富有竞争力的薪资和贴心的住房补贴
• 厨艺高超的阿姨准备的午餐和下午茶
• 只是每天抢饭的场面略凶残，请准备好
• 成为大鱼 • 旅行猎人的机会
• 边旅行边赚钱
• 为大鱼在世界各地开疆拓土&lt;/p&gt;

&lt;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;推荐有奖&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/blockquote&gt;

&lt;p&gt;成功推荐你身边的朋友加入大鱼，将获得往返台北机票及三晚住宿！
这么好的事情，走过路过不要过错啊喂！&lt;/p&gt;

&lt;p&gt;大鱼搬家了，从南三环的温馨小宅搬到了东三环的“工业朋克风”办公室，欢迎大家来办公区前厅的大鱼咖啡聊一聊&lt;/p&gt;

&lt;p&gt;必须有图有真相↓&lt;/p&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/2015/cc26bb07db522bdcf96b57451a2859f8.jpg" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/2015/45c7430bfae87ab7c8e0a9cb805b2f40.jpg" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/2015/9121315267c5a2cea6407bbb6f0c539d.jpg" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/2015/4f8e9591573a7891466562a88c9118d5.jpg" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/2015/4f6cc4c22e09328a7b001c5658c038fa.jpg" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;--&lt;/p&gt;

&lt;p&gt;欢迎发送简历至 hr@fishtrip.cn 或 medal@fishtrip.cn&lt;/p&gt;

&lt;p&gt;感谢各位：）&lt;/p&gt;

&lt;p&gt;我们等你！&lt;/p&gt;</description>
      <author>zamia</author>
      <pubDate>Thu, 12 Mar 2015 16:03:48 +0800</pubDate>
      <link>https://ruby-china.org/topics/24617</link>
      <guid>https://ruby-china.org/topics/24617</guid>
    </item>
    <item>
      <title>[北京诚聘] [互联网 | 旅游 | 创业] 一条大鱼在等你，工程师们是时候在春天活动活动啦</title>
      <description>&lt;p&gt;Hi! 这里大鱼自助游。 &lt;/p&gt;

&lt;p&gt;大鱼自助游 (&lt;a href="http://www.fishtrip.cn)%E8%87%AA%E4%B8%8A%E7%BA%BF%E4%B8%80%E7%9B%B4%E8%87%B4%E5%8A%9B%E4%BA%8E%E4%B8%BA%E4%B8%AD%E5%9B%BD%E4%BA%BA%E6%8F%90%E4%BE%9B%E6%9C%80%E4%BC%98%E8%B4%A8%E7%9A%84%E5%87%BA%E5%A2%83%E8%87%AA%E7%94%B1%E8%A1%8C%E6%9C%8D%E5%8A%A1%E3%80%82" title=""&gt;www.fishtrip.cn) 自上线一直致力于为中国人提供最优质的出境自由行服务。&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;才不要做第二个 TripAdvisor 或者 Booking.com, 大鱼要打造前所未有的个性化海外自由行产品交易平台，提供最具当地特色的住宿。 &lt;/p&gt;

&lt;p&gt;过去一年大鱼做了这些事情： &lt;/p&gt;

&lt;p&gt;• 发布了全国第一款办证类 App「入台证神器」 
• 成功开拓了台湾、日本、帕劳、巴西站 
• 通过强大的后台与上千家中小供应商实现对接 
• 推出大鱼 • 旅行猎人计划并取得巨大反响 &lt;/p&gt;

&lt;p&gt;未来我们希望做的事情： &lt;/p&gt;

&lt;p&gt;将大鱼做成一个世界性的出境游交易平台，为各国旅游者精选值得信赖有品质保证的特色住宿。&lt;/p&gt;

&lt;p&gt;我们的口号是：只推荐特色住宿 (=ﾟωﾟ) ノ&lt;/p&gt;

&lt;p&gt;【关于我们】 &lt;/p&gt;

&lt;p&gt;如果你好奇这是怎样的一群人，一两句话确实挺难概括的，你最好亲自来看看。 &lt;/p&gt;

&lt;p&gt;这里有 Google、百度、微软出来的大牛，也有毕业不久的新鲜人。水瓶、双子、天蝎居多，不羁放纵爱自由。&lt;/p&gt;

&lt;p&gt;我们时常仰望星空，却不忘脚踏实地。相信成大事者，心中必有爱。爱创业、爱互联网，更爱旅行。 &lt;/p&gt;

&lt;p&gt;认识我们：&lt;a href="http://www.fishtrip.cn/about_us" rel="nofollow" target="_blank"&gt;http://www.fishtrip.cn/about_us&lt;/a&gt; 
所以你想加入我们一起来做一点有趣的事情吗？ &lt;/p&gt;

&lt;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;大鱼在找这样的人&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/blockquote&gt;

&lt;p&gt;【Ruby on Rails 工程师】10K~20K
• 参与大鱼网站的架构设计，以及各个后端模块的开发； 
• 承担大鱼网站的后端开发和维护工作。 &lt;/p&gt;

&lt;p&gt;• 1 年以上相关工作经验； 
• 熟悉互联网产品和服务的开发过程，有至少一门动态语言或脚本语言的开发经验，对互联网后端技术架构有基本的理解； 
• 认可创业团队氛围，有上进心与自我驱动力，能够承受一定的工作压力。 
• 对前端技术如 css/html/javascript 等有一定了解； 
• 有电商行业开发经历； 
• 对开源、技术分享、敏捷开发感兴趣； 
• 全栈工程师优先。&lt;/p&gt;

&lt;p&gt;【前端工程师】10K~20K
• 负责大鱼网站的前端开发，与 UI 设计师、产品经理、交互设计师一起，实现易用、好用的互联网旅游产品； 
• 负责开发和维护公司移动端（H5）的开发； 
• 负责前端框架的选型和开发，制定前端开发规范； 
• 开发易维护、可复用的各种前端组件。 &lt;/p&gt;

&lt;p&gt;• 2 年以上相关工作经验； 
• 深入理解 Javascript 和 CSS 等前端技术，对 HTML5 有一定了解； 
• 具有良好的团队协作能力、沟通能力与学习能力，热爱技术，喜欢钻研； 
• 移动互联网/电子商务行业经验优先； 
• 经验丰富，带过前端工程团队优先； 
• 对产品和交互有兴趣者优先。&lt;/p&gt;

&lt;p&gt;【Android/iOS 工程师】10K~20K
• 负责开发和维护大鱼的移动客户端，包括 Android、iOS 以及平板应用。 &lt;/p&gt;

&lt;p&gt;• 1 年以上相关工作经验； 
• 1～3 个移动设备客户端产品的开发及上线； 
• 学习能力较强，对移动端的技术或产品有浓厚兴趣； 
• 计算机基础知识扎实，对移动端应用的整体架构有一定把握能力； 
• 具备较好的团队协助能力，乐于分享和交流； 
• 有 HTML5 或者桌面软件开发经验者优先； 
• 有 Linux/C/Java 开发经验者优先。 &lt;/p&gt;

&lt;p&gt;【PHP 工程师】10K~20K
• 负责开发大鱼项目中各类产品需求，对已知的线上问题快速定位与修复； 
• 参与大鱼核心业务模块系统分析、设计、开发； 
• 负责根据大鱼不同应用需求，优化系统设计和前台表现，不断提升系统效能和安全性。 &lt;/p&gt;

&lt;p&gt;• 2 年以上相关工作经验； 
• 精通 PHP，能够深入理解 PHP 面向对象编程设计理念，并且熟悉 MVC 开发框架，有 PHP 实践经验； 
• 精通 LAMP 架构，熟练应用 Shell/JavaScript/Mysql 等技术； 
• 具有较好的理解沟通能力，工作积极主动，有团队合作精神，责任心强； 
• 认可创业团队氛围，有上进心与自我驱动力，能够承受一定的工作压力。 
• 具有旅游行业相关系统开发经验者优先； 
• 有团队管理经验者优先。&lt;/p&gt;

&lt;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;大鱼能给你什么&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/blockquote&gt;

&lt;p&gt;• 开放的互联网创业环境
• 释放潜力、爆发小宇宙的平台
• 富有竞争力的薪资和贴心的住房补贴
• 厨艺高超的阿姨准备的午餐和下午茶，
• 只是每天抢饭的场面略凶残，请准备好
• 成为大鱼 • 旅行猎人的机会，
• 边旅行边赚钱，
• 为大鱼在世界各地开疆拓土&lt;/p&gt;

&lt;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;推荐有奖&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/blockquote&gt;

&lt;p&gt;成功推荐你身边的朋友加入大鱼，将获得往返台北机票及三晚住宿！
这么好的事情，走过路过不要过错啊喂！&lt;/p&gt;

&lt;p&gt;欢迎你到北京南三环和我们聊聊，&lt;/p&gt;
&lt;h2 id="也可以发送简历至 hr@fishtrip.cn "&gt;也可以发送简历至 hr@fishtrip.cn &lt;/h2&gt;
&lt;p&gt;感谢各位：）&lt;/p&gt;

&lt;p&gt;我们等你！&lt;/p&gt;</description>
      <author>zamia</author>
      <pubDate>Wed, 28 Jan 2015 14:17:18 +0800</pubDate>
      <link>https://ruby-china.org/topics/23988</link>
      <guid>https://ruby-china.org/topics/23988</guid>
    </item>
    <item>
      <title>[北京] 大鱼自助游诚聘前端 /Ruby/iOS/Android 工程师</title>
      <description>&lt;p&gt;坐在世界 500 强格子间里的你，是不是也想做做真正喜欢的事情？
过着朝九晚五安逸生活的你，改变世界的梦想还在吗？
是时候走出你的舒适圈了吧？&lt;/p&gt;

&lt;p&gt;讨厌低效率！
讨厌阿谀奉承！
讨厌复杂的人际关系！&lt;/p&gt;

&lt;p&gt;如果你也
不安现状，追求不一样的生活；&lt;/p&gt;

&lt;p&gt;如果你也
心中有爱，脚踏实地，也时而仰望星空；&lt;/p&gt;

&lt;p&gt;如果你也
极度热爱互联网，热爱创业，热爱旅行；&lt;/p&gt;

&lt;p&gt;你，就是大鱼在找的人。&lt;/p&gt;

&lt;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;关于大鱼&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/blockquote&gt;

&lt;p&gt;大鱼致力于为中国人提供最优质的出境自由行服务，大鱼不要做第二个 tripadvisor 或者 booking.com, 大鱼要打造前所未有的海外旅游产品在线交易平台。&lt;/p&gt;

&lt;p&gt;是的，大鱼正在用互联网思维颠覆传统行业。&lt;/p&gt;

&lt;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;关于你未来的小伙伴&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/blockquote&gt;

&lt;p&gt;• 开放平等，高压有 (dou) 爱 (bi) 的氛围
• 有 Google、百度、微软出来的大牛，
• 也有毕业不久的新鲜人
• 80 后为主，不拒绝 70 后，不歧视 90 后
• 水瓶、双子、天蝎居多，
• 不羁放纵爱自由
• 他们的真面目是……&lt;a href="http://www.fishtrip.cn/about_us" rel="nofollow" target="_blank"&gt;http://www.fishtrip.cn/about_us&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;大鱼在找这样的人&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/blockquote&gt;

&lt;p&gt;Ruby on Rails 工程师 2~3 名
薪资范围在 8K ~ 20K，根据能力可再面谈，从初学者到高手大牛我们都需要；
基础要求是 1 年以上工作经验，编程基础扎实，乐于学习，开放的心态；
全栈工程师优先，如果你懂底层、乐于学习业务、也能写前端那就更好了！&lt;/p&gt;

&lt;p&gt;前端工程师 1~2 名
薪资范围在 8K ~ 20K，期望经验比较丰富，能给大鱼的前端带来更多经验，以后能够带领前端团队；
基础要求是 2 年以上前端经验，对 javascript、html5、css 等有较深的理解；
如果你对产品和交互感兴趣，或者带过前端团队那就更匹配了；&lt;/p&gt;

&lt;p&gt;Android/iOS 工程师 各 1 ~ 2 名
薪资范围 10K ~ 20K，根据您的资历面谈，工程师或者经验丰富的工程师均可；
基础要求是 1 年以上移动端工作经验，完整经历过至少一个 App 的开发设计和上线；
如果您懂产品、对移动端有强烈的兴趣更好；&lt;/p&gt;

&lt;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;大鱼能给你什么&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/blockquote&gt;

&lt;p&gt;• 开放的互联网创业环境
• 释放潜力、爆发小宇宙的平台
• 富有竞争力的薪资和贴心的住房补贴
• 厨艺高超的阿姨准备的午餐和下午茶，
• 只是每天抢饭的场面略凶残，请准备好
• 成为大鱼 • 旅行猎人的机会，
• 边旅行边赚钱，
• 为大鱼在世界各地开疆拓土&lt;/p&gt;

&lt;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;推荐有奖&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/blockquote&gt;

&lt;p&gt;成功推荐你身边的朋友加入大鱼，将获得往返台北机票及三晚住宿！
这么好的事情，走过路过不要过错啊喂！&lt;/p&gt;

&lt;p&gt;欢迎你到北京南三环和我们聊聊，&lt;/p&gt;
&lt;h2 id="也可以发送简历至 hr@fishtrip.cn "&gt;也可以发送简历至 hr@fishtrip.cn &lt;/h2&gt;
&lt;p&gt;感谢各位：）&lt;/p&gt;

&lt;p&gt;我们等你！&lt;/p&gt;</description>
      <author>zamia</author>
      <pubDate>Tue, 11 Nov 2014 17:47:31 +0800</pubDate>
      <link>https://ruby-china.org/topics/22613</link>
      <guid>https://ruby-china.org/topics/22613</guid>
    </item>
    <item>
      <title>Rails 重构: 利用 Service 优化 Fat Model</title>
      <description>&lt;h2 id="Rails重构: 利用Service优化Fat Model"&gt;Rails 重构：利用 Service 优化 Fat Model&lt;/h2&gt;
&lt;p&gt;给公司内部写的文章，分享出来，希望对大家有帮助。&lt;/p&gt;

&lt;p&gt;本文主要说明什么时候需要重构 fat model，以及通过一个简单的案例来讲解如何一步步重构复杂的 model，把它变成扩展性良好的 service。&lt;/p&gt;
&lt;h2 id="源起"&gt;源起&lt;/h2&gt;
&lt;p&gt;在这篇文章 &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; 中，其中提到了一点，利用 Service 重构“Fat”Model。&lt;/p&gt;

&lt;p&gt;原文中提到了几点需要重构成 service 的场景：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;功能逻辑比较复杂&lt;/li&gt;
&lt;li&gt;功能涉及到了多个 model 的时候&lt;/li&gt;
&lt;li&gt;此功能会和外部系统交互&lt;/li&gt;
&lt;li&gt;此功能并非底层 model 的核心责任，关联不大&lt;/li&gt;
&lt;li&gt;此功能可能会有多种实现方式&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;上面的总结很好，但是也很模糊，比如到底什么样的功能算是复杂？涉及到多个 model 就应该优化成 service 吗？怎么样才叫做关联不大？&lt;/p&gt;

&lt;p&gt;每个人的判断可能不太一样，对于一个 team 来讲，需要一个相对比较明确的约定来定义什么时候，你的业务逻辑需要重构层一个 service 了。目前大鱼的开发团队是这么简单约定的：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;当 model class 中出现跨 model 的‘写’行为的时候&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;为什么是这样的约定？&lt;/p&gt;

&lt;p&gt;因为一般出现了跨 model 的写的时候，说明你的业务逻辑比较复杂了，可以考虑封装成一个 service 来完成这件相对“独立”的功能；特别是如果 model 的回调中，出现了跨 model 的写，这种情况更应该避免，因为将来逻辑复杂时，很有可能回调的条件已不再满足了。&lt;/p&gt;

&lt;p&gt;所以 service 的封装的粒度应该是比较明确的，那就是对于复杂的功能逻辑，如果同时又比较独立，将来有一定的可能性会扩展成一个 engine、甚至独立的组件（如 http api），那么显然是应该封装层 service 的，那么目前的一个“较好”的标准，就是如果 model 内部出现了跨 model 的“写”，应当考虑把这部分功能封装层 service，以便将来的扩展。&lt;/p&gt;
&lt;h2 id="问题场景"&gt;问题场景&lt;/h2&gt;
&lt;p&gt;案例实现的是电商中常见的“常用联系人”的功能，下面是一个简化的需求说明：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;用户提交订单的时候，需要记录本订单使用的联系人姓名、电话、邮箱；每个订单需要存储一份；&lt;/li&gt;
&lt;li&gt;订单提交后，系统需要把联系人信息记录在‘常用联系人’表中，供下次用户快捷填写；每个用户有多个’常用联系人‘&lt;/li&gt;
&lt;li&gt;每个用户都有唯一的一份联系信息，每次订单提交后需要更新此信息；此信息会用在其他系统作为用户的标志；&lt;/li&gt;
&lt;li&gt;常用联系人、订单联系人之间利用真实姓名进行弱关联，同一个名字认为是同一个用户&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="实现以及重构过程"&gt;实现以及重构过程&lt;/h2&gt;&lt;h3 id="基本的表结构"&gt;基本的表结构&lt;/h3&gt;
&lt;p&gt;OrderContact 订单联系人表：跟订单是 1:1 的&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="ss"&gt;order_id: &lt;/span&gt;&lt;span class="n"&gt;integer&lt;/span&gt;
&lt;span class="ss"&gt;real_name: &lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;
&lt;span class="ss"&gt;cellphone: &lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;
&lt;span class="ss"&gt;email: &lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;
&lt;span class="o"&gt;***&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;其他字段忽略&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;UserContact 常用旅客表：跟 User 是 N:1 的&lt;/p&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;user_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;integer&lt;/span&gt;
&lt;span class="na"&gt;real_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
&lt;span class="na"&gt;cellphone&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
&lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
&lt;span class="na"&gt;***&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;其他字段忽略&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;UserProfile 用户基本信息表，跟 User 是 1:1 的&lt;/p&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;user_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;integer&lt;/span&gt;
&lt;span class="na"&gt;real_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
&lt;span class="na"&gt;cellphone&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
&lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
&lt;span class="na"&gt;****&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;其他字段忽略&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果是你来实现这个需求，你会怎么写？hoho，请继续看下去吧！&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 最基本的几个关联关系&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;
  &lt;span class="n"&gt;has_many&lt;/span&gt; &lt;span class="ss"&gt;:user_contacts&lt;/span&gt;
  &lt;span class="n"&gt;has_one&lt;/span&gt; &lt;span class="ss"&gt;:user_profile&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt;
  &lt;span class="n"&gt;has_one&lt;/span&gt; &lt;span class="ss"&gt;:order_contact&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserContact&lt;/span&gt;
  &lt;span class="n"&gt;belongs_to&lt;/span&gt; &lt;span class="ss"&gt;:user&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderContact&lt;/span&gt;
  &lt;span class="n"&gt;belongs_to&lt;/span&gt; &lt;span class="ss"&gt;:order&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserProfile&lt;/span&gt;
  &lt;span class="n"&gt;belongs_to&lt;/span&gt; &lt;span class="ss"&gt;:user&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="第一次，常见Rails的写法"&gt;第一次，常见 Rails 的写法&lt;/h3&gt;
&lt;p&gt;常见的 rails 的写法是，在 order_contact model 层添加 after_save 回调，分别去更新对应的 user_contact 和 user_profile 即可，写起来也会很快捷；&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;OrderContact&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;Base&lt;/span&gt;
  &lt;span class="n"&gt;belongs_to&lt;/span&gt; &lt;span class="ss"&gt;:order&lt;/span&gt;

  &lt;span class="n"&gt;after_save&lt;/span&gt; &lt;span class="ss"&gt;:update_user_contact&lt;/span&gt;
  &lt;span class="n"&gt;after_save&lt;/span&gt; &lt;span class="ss"&gt;:update_user_profile&lt;/span&gt;

  &lt;span class="kp"&gt;private&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;update_user_contact&lt;/span&gt;
    &lt;span class="n"&gt;user_contact&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;user_contacts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;by_real_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;real_name&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;first_or_initialize&lt;/span&gt;

    &lt;span class="n"&gt;user_contact&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;email&lt;/span&gt;
    &lt;span class="n"&gt;user_contact&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cellphone&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cellphone&lt;/span&gt;
    &lt;span class="n"&gt;user_contact&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;real_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;real_name&lt;/span&gt;
    &lt;span class="n"&gt;user_contact&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&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;update_user_profile&lt;/span&gt;
    &lt;span class="n"&gt;user_profile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;user_profile&lt;/span&gt;

    &lt;span class="n"&gt;user_profile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;email&lt;/span&gt;
    &lt;span class="n"&gt;user_profile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cellphone&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cellphone&lt;/span&gt;
    &lt;span class="n"&gt;user_profile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;real_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;real_name&lt;/span&gt;
    &lt;span class="n"&gt;user_profile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&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;class&lt;/span&gt; &lt;span class="nc"&gt;OrderController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;
    &lt;span class="c1"&gt;# 创建订单的时候保存联系人信息&lt;/span&gt;
    &lt;span class="vi"&gt;@order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Order&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="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:order&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="vi"&gt;@order_contact&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="vi"&gt;@order.create_order_contact&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;:order_contact&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;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样的写法有两个问题：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;从当前的逻辑来看，所有的 order_contact 更新的时候都必须更新另外两个 model，可是可能马上需求就要变化。这种利用 callback 的写法，当需求变化的时候再改动就会比较困难，这个时候负责新功能的工程师需要理清楚原有的思路，并且必须陷入到 ActiveRecord 类中去；&lt;/li&gt;
&lt;li&gt;例子中的 UserContact 类和 UserProfile 类，可能很快也会变化，这个时候直接在 order_contact 类 中调用它们的 attribute=() 方法就显得很不合适了；至少这些类需要提供一个写接口，这样才能应对变化；&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;好，接下来就把上面两个缺点给重构掉：&lt;/p&gt;
&lt;h3 id="重构一下，去掉回调"&gt;重构一下，去掉回调&lt;/h3&gt;
&lt;p&gt;基本的策略是把 after_save 的回调，方法是在 controller 里面调用相关的方法了；然后我们要去掉直接在 model 里面去写另外一个 model 的逻辑，方法是让它提供相应的封装好的写接口；&lt;/p&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;OrderContact&lt;/span&gt;
  &lt;span class="c1"&gt;# 删除原有的 after_save 以及相关的方法&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserContact&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update_by_order_contact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order_contact&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;user_contact&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;user_contacts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;by_real_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;real_name&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;first_or_initialize&lt;/span&gt;

    &lt;span class="n"&gt;user_contact&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;real_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;order_contact&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;real_name&lt;/span&gt;
    &lt;span class="n"&gt;user_contact&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cellphone&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;order_contact&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cellphone&lt;/span&gt; 
    &lt;span class="n"&gt;user_contact&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;order_contact&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;email&lt;/span&gt;

    &lt;span class="n"&gt;user_contact&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&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;class&lt;/span&gt; &lt;span class="nc"&gt;UserProfile&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update_by_order_contact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order_contact&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;user_profile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;profile&lt;/span&gt;

    &lt;span class="n"&gt;user_profile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;real_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;order_contact&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;real_name&lt;/span&gt;
    &lt;span class="n"&gt;user_profile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cellphone&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;order_contact&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cellphone&lt;/span&gt; 
    &lt;span class="n"&gt;user_profile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;order_contact&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;email&lt;/span&gt;

    &lt;span class="n"&gt;user_profile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&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;然后在 controller 里面直接调用&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;OrderController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;
    &lt;span class="c1"&gt;# 创建订单的时候保存联系人信息&lt;/span&gt;
    &lt;span class="vi"&gt;@order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Order&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="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:order&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="vi"&gt;@order_contact&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="vi"&gt;@order.create_order_contact&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;:order_contact&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

    &lt;span class="c1"&gt;# 调用写接口，更新user相关的信息&lt;/span&gt;
    &lt;span class="no"&gt;UserProfile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update_by_order_contact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@order.user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="vi"&gt;@order_contact&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="no"&gt;UserContact&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update_by_order_contact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@order.user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="vi"&gt;@order_contact&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;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的代码利用类函数的方法把“写”代码移到了正确的类中，代码看起来清晰了一些，但是好像复杂性并没有降低，反而有点冗余了：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;在 controller 中原来是自动调用，现在需要写独立的代码，以后将来又有了新的类，不只是 UserProfile 和 UserContact 呢？就得在很多地方多添加一行，比如新的类是 UserInfo，那在每个 controller 里面都必须都写一行；&lt;/li&gt;
&lt;li&gt;静态函数里面其实隐含了一个需求，那就是更新常用联系人是根据 真实姓名 来更新的，在这一行里面提现：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;user_contact&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;user_contacts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;by_real_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;real_name&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;first_or_initialize&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其实这个就是典型的业务逻辑了，显然也不应该放藏的这么深，将来也很难去维护。&lt;/p&gt;

&lt;p&gt;那么，有没有更好的办法呢？&lt;/p&gt;
&lt;h3 id="Service出场，封装成service"&gt;Service 出场，封装成 service&lt;/h3&gt;
&lt;p&gt;利用 service 的方法，我们把所有的业务逻辑抽离出来，把数据逻辑继续留在 model 层中；并且是把“更新用户信息”当做一个独立的小模块来实现，而现在这个 service 只提供一个接口，那就是根据 order_contact 来更新用户信息。&lt;/p&gt;

&lt;p&gt;从这个角度看问题，我们创建 UserInfoService, 并且它的职责以及范围就很清楚了，继续往下改进：&lt;/p&gt;

&lt;p&gt;model 层改成这个样子：&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;UserContact&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;Base&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;update_by_order_contact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_contact&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;real_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;order_contact&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;real_name&lt;/span&gt;
    &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cellphone&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;order_contact&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cellphone&lt;/span&gt; 
    &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;order_contact&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;email&lt;/span&gt;
    &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&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;class&lt;/span&gt; &lt;span class="nc"&gt;UserProfile&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;Base&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;update_by_order_contact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_contact&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;real_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;order_contact&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;real_name&lt;/span&gt;
    &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cellphone&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;order_contact&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cellphone&lt;/span&gt; 
    &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;order_contact&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;email&lt;/span&gt;
    &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&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;新的 UserInfoService 只是一个简单的 ruby 类：&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;UserInfoService&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user&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;refresh_from_order_contact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_contact&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# 更新常用联系人&lt;/span&gt;
    &lt;span class="n"&gt;user_contact&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;find_user_contact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_contact&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;real_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;user_contact&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update_by_order_contact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_contact&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# 更新用户个人信息&lt;/span&gt;
    &lt;span class="vi"&gt;@user.profile.update_by_order_contact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_contact&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="kp"&gt;private&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;find_user_contact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;real_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@user.user_contacts.by_real_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;real_name&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;first_or_initialize&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;OrderController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;
    &lt;span class="c1"&gt;# 创建订单的时候保存联系人信息&lt;/span&gt;
    &lt;span class="vi"&gt;@order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Order&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="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:order&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="vi"&gt;@order_contact&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="vi"&gt;@order.create_order_contact&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;:order_contact&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

    &lt;span class="c1"&gt;# 调用写接口，更新user相关的信息&lt;/span&gt;
    &lt;span class="no"&gt;UserInfoService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@order.user&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;refresh_by_order_contact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@order_contact&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;/code&gt;&lt;/pre&gt;
&lt;p&gt;经过上面的改动，有没有感觉代码更清晰呢？这些写有下面几个好处：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;把更新用户信息这个逻辑抽离，service 本身是可复用的，可以用在任何地方，包括 task；console 的调试等；&lt;/li&gt;
&lt;li&gt;service 的接口很明确，就是根据 order_contact 更新用户的信息；如果以后有了新的需求，我们可以添加新的接口；如果原有的需求发生了变化，也可以修改目前的方法；都是很简单的。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="总结"&gt;总结&lt;/h2&gt;
&lt;p&gt;总结一下什么时候应该抽取 service:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;当发生跨 model 的写的时候。这不是必然，但是可以认为是一个信号，表示你的业务逻辑开始变的复杂了。同时，当跨 model 的“写”都遵守了这个规则时，rails 的 model 层就会变成一个真正的 DAL（Data Access Layer），不再是混合了数据逻辑和业务逻辑的“Fat Model”；&lt;/li&gt;
&lt;li&gt;一般来讲，callback 是要慎用的，特别是 callback 里面涉及到了调用其他 model、修改其他 model 的情况，这个时候就可以考虑把相关的逻辑抽成 service。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;其他像文章最初提到的一些规则都比较模糊，需要经验丰富的工程师才能比较明确的判断，比如业务逻辑比较复杂、相对独立、将来可能会被升级成独立的模块的时候，这些需要一定的经验积累才比较容易判断。&lt;/p&gt;

&lt;p&gt;service 的好处，基本上是抽象层独立的类之后的好处：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;复用性比较好。因为是 ruby plain object，所以复用性上很简单，可以用在各种地方；&lt;/li&gt;
&lt;li&gt;比较独立，可扩展性比较好。可以扩展 service，给它添加新的方法、修改原有的行为均可；&lt;/li&gt;
&lt;li&gt;可测试性也会较好。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;抽取 service 的本质是要把数据逻辑层和业务逻辑区别对待，让 model 层稍微轻一些；Rails 里面有 view logic、data logic、domain logic，把它们区别对待是最基本的，这样才能写出更清晰、可维护的大型应用来。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;当然，上面的代码还有可以优化的空间，比如把 email、cellphone、real_name 作为一个结构体在各个接口之间传递，不过不是本篇关注的重点，就暂时不写了。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;大概就是这些了，以上，欢迎指正。&lt;/p&gt;</description>
      <author>zamia</author>
      <pubDate>Wed, 22 Oct 2014 01:01:15 +0800</pubDate>
      <link>https://ruby-china.org/topics/22179</link>
      <guid>https://ruby-china.org/topics/22179</guid>
    </item>
    <item>
      <title> [北京] 大鱼自助游诚聘 Ruby/ 前端 /Android/iOS 工程师</title>
      <description>&lt;p&gt;坐在世界 500 强格子间里的你，是不是也想做做真正喜欢的事情？
过着朝九晚五安逸生活的你，改变世界的梦想还在吗？
是时候走出你的舒适圈了吧？&lt;/p&gt;

&lt;p&gt;讨厌低效率！
讨厌阿谀奉承！
讨厌复杂的人际关系！&lt;/p&gt;

&lt;p&gt;如果你也
不安现状，追求不一样的生活；&lt;/p&gt;

&lt;p&gt;如果你也
心中有爱，脚踏实地，也时而仰望星空；&lt;/p&gt;

&lt;p&gt;如果你也
极度热爱互联网，热爱创业，热爱旅行；&lt;/p&gt;

&lt;p&gt;你，就是大鱼在找的人。&lt;/p&gt;

&lt;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;关于大鱼&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/blockquote&gt;

&lt;p&gt;大鱼致力于为中国人提供最优质的出境自由行服务，大鱼不要做第二个 tripadvisor 或者 booking.com, 大鱼要打造前所未有的海外旅游产品在线交易平台。
是的，大鱼正在用互联网思维颠覆传统行业。&lt;/p&gt;

&lt;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;关于你未来的小伙伴&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/blockquote&gt;

&lt;p&gt;· 开放平等，高压有 (dou) 爱 (bi) 的氛围
· 有 Google、百度、微软出来的大牛，
· 也有毕业不久的新鲜人
· 80 后为主，不拒绝 70 后，不歧视 90 后
· 水瓶、双子、天蝎居多，
· 不羁放纵爱自由
· 他们的真面目是……&lt;a href="http://www.fishtrip.cn/about_us" rel="nofollow" target="_blank"&gt;http://www.fishtrip.cn/about_us&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;大鱼在找这样的人&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/blockquote&gt;

&lt;p&gt;Ruby on Rails 工程师 2~3 名
薪资范围在 8K ~ 20K，根据能力可再面谈，从初学者到高手大牛我们都需要；
基础要求是 1 年以上工作经验，编程基础扎实，乐于学习，开放的心态；
全栈工程师优先，如果你懂底层、乐于学习业务、也能写前端那就更好了！&lt;/p&gt;

&lt;p&gt;前端工程师 1~2 名
薪资范围在 8K ~ 20K，期望经验比较丰富，能给大鱼的前端带来更多经验，以后能够带领前端团队；
基础要求是 2 年以上前端经验，对 javascript、html5、css 等有较深的理解；
如果你对产品和交互感兴趣，或者带过前端团队那就更匹配了；&lt;/p&gt;

&lt;p&gt;Android/iOS 工程师 各 1 ~ 2 名
薪资范围 10K ~ 20K，根据您的资历面谈，工程师或者经验丰富的工程师均可；
基础要求是 1 年以上移动端工作经验，完整经历过至少一个 App 的开发设计和上线；
如果您懂产品、对移动端有强烈的兴趣更好；&lt;/p&gt;

&lt;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;大鱼能给你什么&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/blockquote&gt;

&lt;p&gt;· 开放的互联网创业环境
· 释放潜力、爆发小宇宙的平台
· 富有竞争力的薪资和贴心的住房补贴
· 厨艺高超的阿姨准备的午餐和下午茶，
· 只是每天抢饭的场面略凶残，请准备好
· 成为大鱼 · 旅行猎人的机会，
· 边旅行边赚钱，
· 为大鱼在世界各地开疆拓土&lt;/p&gt;

&lt;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;推荐有奖&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/blockquote&gt;

&lt;p&gt;成功推荐你身边的朋友加入大鱼，将获得往返台北机票及三晚住宿！
这么好的事情，走过路过不要过错啊喂！&lt;/p&gt;

&lt;p&gt;欢迎你到北京南三环和我们聊聊，&lt;/p&gt;
&lt;h2 id="也可以发送简历至hr@fishtrip.cn或houmengnan@fishtrip.cn"&gt;也可以发送简历至 hr@fishtrip.cn 或 houmengnan@fishtrip.cn&lt;/h2&gt;
&lt;p&gt;感谢各位，我们等着你：）&lt;/p&gt;</description>
      <author>zamia</author>
      <pubDate>Mon, 20 Oct 2014 22:48:42 +0800</pubDate>
      <link>https://ruby-china.org/topics/22146</link>
      <guid>https://ruby-china.org/topics/22146</guid>
    </item>
    <item>
      <title>[北京] 大鱼自助游招聘工程师 Ruby/ 前端 /iOS/Android</title>
      <description>&lt;h2 id="大鱼简介"&gt;大鱼简介&lt;/h2&gt;
&lt;p&gt;大鱼是一家位于北京的互联网团队，官网是 &lt;a href="http://www.fishtrip.cn" rel="nofollow" target="_blank" title=""&gt;www.fishtrip.cn&lt;/a&gt; 。我们致力于解决国内用户出境自由行中遇到的问题，通过提供最优质的境外自由行的服务，让用户直接找到当地的优质资源提供者，如民宿、体验活动、包车等，并且在大鱼上自由组装成自己的行程，提供一站式的自由行体验。
大鱼目前开放了台湾、帕劳和巴西三个地区，接下来也马上会开放日本，敬请期待吧！&lt;/p&gt;
&lt;h2 id="此次开放职位"&gt;此次开放职位&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Ruby  on Rails 工程师&lt;/strong&gt; 2~3 名
薪资范围在 8K ~ 20K，根据能力可再面谈，从初学者到高手大牛我们都需要；
基础要求是 1 年以上工作经验，编程基础扎实，乐于学习，开放的心态；
全栈工程师优先，如果你懂底层、乐于学习业务、也能写前端那就更好了！&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;前端工程师&lt;/strong&gt; 1~2 名
薪资范围在 8K ~ 20K，期望经验比较丰富，能给大鱼的前端带来更多经验，以后能够带领前端团队；
基础要求是 2 年以上前端经验，对 javascript、html5、css 等有较深的理解；
如果你对产品和交互感兴趣，或者带过前端团队那就更匹配了；&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Android/iOS 工程师&lt;/strong&gt; 各 1 ~ 2 名
薪资范围 10K ~ 20K，根据您的资历面谈，工程师或者经验丰富的工程师均可；
基础要求是 1 年以上移动端工作经验，完整经历过至少一个 App 的开发设计和上线；
如果您懂产品、对移动端有强烈的兴趣更好；&lt;/p&gt;
&lt;h2 id="推荐有奖"&gt;推荐有奖&lt;/h2&gt;
&lt;p&gt;每一次成功推荐（试用期通过）都可以获得大鱼免费提供的台北往返机票，爱旅游的同学快快推荐朋友们加入吧！
而且是任何职位都是此奖励啊，我们的 jobs 页面还有很多其他类型的职位都可以推荐。&lt;/p&gt;
&lt;h2 id="投递简历"&gt;投递简历&lt;/h2&gt;
&lt;p&gt;mailto: &lt;strong&gt;hr@fishtrip.cn&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="更多职位"&gt;更多职位&lt;/h2&gt;
&lt;p&gt;&lt;a href="http://www.fishtrip.cn/jobs" rel="nofollow" target="_blank"&gt;http://www.fishtrip.cn/jobs&lt;/a&gt;&lt;/p&gt;</description>
      <author>zamia</author>
      <pubDate>Wed, 23 Jul 2014 16:59:23 +0800</pubDate>
      <link>https://ruby-china.org/topics/20641</link>
      <guid>https://ruby-china.org/topics/20641</guid>
    </item>
  </channel>
</rss>
