<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>freefishz (jacky.zhang)</title>
    <link>https://ruby-china.org/freefishz</link>
    <description></description>
    <language>en-us</language>
    <item>
      <title>[上海] 上海优数科技有限公司聘中 / 高级研发工程师 (12~25K) - 955</title>
      <description>&lt;h2 id="公司介绍"&gt;公司介绍&lt;/h2&gt;
&lt;p&gt;上海优数科技有限公司，是一家专注于专病领域的临床大数据及人工智能的创业公司。公司致力于与中国领先的医疗机构共同建立“医疗大数据”平台，利用机器学习和人工智能技术，辅助开展新型临床、科研、医院管理等服务，助力医疗机构和医生，为患者提供更好的医疗服务。&lt;/p&gt;

&lt;p&gt;2018 年公司已成功发布了一款专病临床数据库，在全国范围内积累了 50 多家知名三甲医院用户，并与多家世界排名前十的药企开展了科研合作项目。&lt;/p&gt;

&lt;p&gt;2019 年公司还将推出两款专病临床数据库产品，并打造聚焦专病领域的医患交流平台，预计医院用户数将超过 200 家。&lt;/p&gt;
&lt;h3 id="职位"&gt;职位&lt;/h3&gt;
&lt;p&gt;中/高级研发工程师&lt;/p&gt;
&lt;h3 id="岗位职责"&gt;岗位职责&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;负责完成公司产品研发，根据工作能力，可以侧重于前端或后端，或能够全栈开发；&lt;/li&gt;
&lt;li&gt;参与产品设计，与产品经理、交互设计、UI 设计共同讨论制定产品的需求方案；&lt;/li&gt;
&lt;li&gt;对各种前/后端新技术进行探索和尝试，负责关键服务的设计和实现；&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="岗位要求"&gt;岗位要求&lt;/h3&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;至少精通一门后端开发语言，如 Ruby、Python、Go、Java、Node.js 等；&lt;/li&gt;
&lt;li&gt;熟悉一种前端框架，或具备良好的 JavaScript 编程基础；&lt;/li&gt;
&lt;li&gt;熟悉 HTTP 协议并理解 RESTFUL 规范；&lt;/li&gt;
&lt;li&gt;熟悉 Linux 系统和终端化操作；&lt;/li&gt;
&lt;li&gt;熟悉 Git 的基本原理和操作；&lt;/li&gt;
&lt;li&gt;具有良好的沟通能力和团队合作精神；&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="工作时间"&gt;工作时间&lt;/h3&gt;
&lt;p&gt;955&lt;/p&gt;
&lt;h3 id="加分项"&gt;加分项&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;精通 Ruby on Rails；&lt;/li&gt;
&lt;li&gt;有云平台/docker 的开发部署经验；&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="联系方式"&gt;联系方式&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;简历请发送：urodata@ebianque.cn，请注明来自 ruby-china&lt;/li&gt;
&lt;li&gt;办公地点：虹口区，地铁 3 号线大柏树站旁&lt;/li&gt;
&lt;/ul&gt;</description>
      <author>freefishz</author>
      <pubDate>Wed, 27 Mar 2019 15:14:09 +0800</pubDate>
      <link>https://ruby-china.org/topics/38297</link>
      <guid>https://ruby-china.org/topics/38297</guid>
    </item>
    <item>
      <title>Docker 环境下自动更新 Let’ s Encrypt SSL 证书</title>
      <description>&lt;blockquote&gt;
&lt;p&gt;说明：以下脚本在 Ubuntu 18.04 运行通过，大部分脚本执行需要管理员权限。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="1. 准备docker环境"&gt;1. 准备 docker 环境&lt;/h2&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# 安装必备工具包
apt-get -y install apt-transport-https ca-certificates curl software-properties-common

# 添加docker阿里云源，相对官方源速度更快
curl -fsSL http://mirrors.aliyun.com/docker-ce/linux/ubuntu/gpg | sudo apt-key add -
add-apt-repository "deb [arch=amd64] http://mirrors.aliyun.com/docker-ce/linux/ubuntu $(lsb_release -cs) stable"

# 安装最新版社区版docker
apt update -y 
apt install -y docker-ce

# 如果想要安装指定版本的docker-ce，如下：
# 查看有哪些版本
apt-cache madison docker-ce
# 安装指定版本
apt install -y docker-ce=[版本]

#  设置阿里云docker源
mkdir -p /etc/docker
tee /etc/docker/daemon.json &amp;lt;&amp;lt;-'EOF'
{
  "registry-mirrors": ["https://uon07it7.mirror.aliyuncs.com"]
}
EOF

systemctl daemon-reload
systemctl restart docker

# 下载安装docker-compose
curl -L https://github.com/docker/compose/releases/download/1.21.2/docker-compose-$(uname -s)-$(uname -m) -o /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="2. Let's Encrypt免费证书签发过程简介"&gt;2. Let's Encrypt 免费证书签发过程简介&lt;/h2&gt;
&lt;p&gt;Let's Encrypt 免费证书签发过程包含以下三个阶段：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;在本地服务器上安装&lt;code&gt;Certbot&lt;/code&gt;，&lt;code&gt;Certbot&lt;/code&gt;是签发/更新证书的客户端程序；&lt;/li&gt;
&lt;li&gt;运行&lt;code&gt;Certbot&lt;/code&gt;获取&lt;code&gt;SSL/TLS&lt;/code&gt;证书，证书有效期为&lt;code&gt;3个月&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;设置定时脚本每周运行一次&lt;code&gt;Certbot&lt;/code&gt;更新证书。如果证书有效期小于 30 天，&lt;code&gt;Certbot&lt;/code&gt;会更新证书；&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="3. Certbot工作原理简介"&gt;3. Certbot 工作原理简介&lt;/h2&gt;
&lt;p&gt;不论是第一次申请证书，还是更新证书，&lt;code&gt;Certbot&lt;/code&gt;都会发起一次&lt;a href="https://github.com/ietf-wg-acme/acme" rel="nofollow" target="_blank" title=""&gt;ACME&lt;/a&gt;请求，来验证你是否拥有该域名。如果验证通过，&lt;code&gt;Certbot&lt;/code&gt;就会将新证书安装到本地服务，其实就是将证书保存在一个目录中。证书一般包含两个文件（包含公钥、私钥以及证书等信息），web 服务器需要配置使用这两个证书文件，来实现 HTTPS 访问。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ACME&lt;/code&gt;验证过程如下：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;假设你有一个域名：example.com，和一个公网 IP：xxx.xxx.xxx.xxx，并设置好了 DNS 解析；&lt;/li&gt;
&lt;li&gt;配置好一台 web 服务器，在 80 端口和 443 端口接受 examle.com 的请求；&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Certbot&lt;/code&gt;向&lt;code&gt;Let's Encrypt&lt;/code&gt;发起证书申请；&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Let's Encrypt&lt;/code&gt;返回&lt;code&gt;Certbot&lt;/code&gt;一个唯一的&lt;code&gt;token&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Certbot&lt;/code&gt;配置 web 服务器，使&lt;code&gt;token&lt;/code&gt;可以通过 url：&lt;code&gt;http://example.com/.well-known/acme-challenge/{token}&lt;/code&gt;访问；&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Let's Encrypt CA&lt;/code&gt;访问上述 url，如果获取到的&lt;code&gt;token&lt;/code&gt;和它发送给&lt;code&gt;Certbot&lt;/code&gt;的&lt;code&gt;token&lt;/code&gt;一致，就可以证明你拥有该域名；&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;注意：&lt;code&gt;Certbot&lt;/code&gt;需要配置 web 服务器的相应权限。以 nginx 为例，&lt;code&gt;Certbot&lt;/code&gt;需要权限将&lt;code&gt;token&lt;/code&gt;写入&lt;code&gt;.well-known/acme-challenge&lt;/code&gt;目录。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="4. 通过docker来运行Certbot"&gt;4. 通过 docker 来运行 Certbot&lt;/h2&gt;
&lt;p&gt;为了方便维护、升级，推荐使用 docker 来运行&lt;code&gt;Certbot&lt;/code&gt;。整个过程可以分为两部分：首次申请证书和更新证书。&lt;/p&gt;
&lt;h3 id="4.1 首次申请证书"&gt;4.1 首次申请证书&lt;/h3&gt;
&lt;p&gt;创建 web 服务目录：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mkdir -p /letsencrypt/site #这里以静态网页为例，也可以设为反向代理。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建 docker-compose 文件：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;nano /letsencrypt/docker-compose.yml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;docker-compose.yml 内容如下：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;version: '3.1'

services:
  demo-site:
    container_name: 'demo-site'
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf
      - ./site:/usr/share/nginx/html
    networks:
      - docker-network

networks:
  docker-network:
    driver: bridge
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建 nginx 配置文件：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;nano /letsencrypt/nginx.conf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;nginx.conf 内容如下：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;server {
    listen 80;
    server_name example.com www.example.com;

    location ~ /.well-known/acme-challenge {
        allow all;
        root /usr/share/nginx/html;
    }

    root /usr/share/nginx/html;
    index index.html;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;启动 web 服务器：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cd /letsencrypt &amp;amp;&amp;amp; docker-compose up -d
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行&lt;code&gt;Certbot&lt;/code&gt;申请证书：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker run --rm -it \
-v /letsencrypt/certbot/etc/letsencrypt:/etc/letsencrypt \             # 证书申请工作目录
-v /letsencrypt/certbot/var/log/letsencrypt:/var/log/letsencrypt \     # 日志记录
-v /letsencrypt/site:/data/letsencrypt \                               # ACME验证token目录，与nginx服务器共享
certbot/certbot \
certonly --webroot \                                                   # 指定ACME验证方式：token文件验证
--email youremail@domain.com --agree-tos --no-eff-email \              # 申请者邮件
--webroot-path=/data/letsencrypt \                                     # ACME验证token文件放置目录
-d example.com -d www.example.com                                      # 指定要申请证书的域名列表
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;如果脚本正常运行，可以在&lt;code&gt;/letsencrypt/certbot/etc/letsencrypt/live&lt;/code&gt;下找到&lt;code&gt;example.com&lt;/code&gt;文件夹，其中包含申请成功的证书文件：&lt;code&gt;fullchain.pem&lt;/code&gt;和&lt;code&gt;privkey.pem&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;停止 web 服务器：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cd /letsencrypt &amp;amp;&amp;amp; docker-compose down
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;更新 docker-compose 配置：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;version: '3.1'

services:
  demo-site:
    container_name: 'demo-site'
    image: nginx:alpine
    ports:
      - "80:80"     # 保留80端口，用于证书更新
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf
      - ./site:/usr/share/nginx/html
      - ./certbot/etc/letsencrypt/live:/letsencrypt/live        # 当前证书目录
      - ./certbot/etc/letsencrypt/archive:/letsencrypt/archive  # 历史证书目录
      - ./dhparam-2048.pem:/letsencrypt/dhparam-2048.pem        # 使用2048位DH（Diffie-Hellman）参数
    networks:
      - docker-network

networks:
  docker-network:
    driver: bridge
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;生成 2048 位的 DH 参数文件命令如下：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;openssl dhparam -out /letsencrypt/dhparam-2048.pem 2048
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;live 目录的证书会 soft link 到 archive 目录，而 docker 对 soft link 支持不好，因此需要同时映射 live 和 archive 目录。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;更新 nginx 配置，启用 HTTPS：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;server {
    listen      80;
    server_name example.com www.example.com;

    # 重定向到https
    location / {
        rewrite ^ https://$host$request_uri? permanent;
    }

    # 高优先级，仅用于更新证书
    location ~ /.well-known/acme-challenge {
        allow all;
        root /data/letsencrypt;
    }
}

server {
    listen 443 ssl http2;
    server_name example.com www.example.com;

    server_tokens off;

    ssl on;

    ssl_certificate /letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /letsencrypt/live/example.com/privkey.pem;

    ssl_buffer_size 8k;

    ssl_dhparam /letsencrypt/dhparam-2048.pem; # 使用2048位DH参数，加强安全

    ssl_protocols TLSv1.2 TLSv1.1 TLSv1;
    ssl_prefer_server_ciphers on;
    ssl_ciphers ECDH+AESGCM:ECDH+AES256:ECDH+AES128:DH+3DES:!ADH:!AECDH:!MD5;

    ssl_ecdh_curve secp384r1;
    ssl_session_tickets off;

    # OCSP stapling
    ssl_stapling on;
    ssl_stapling_verify on;
    resolver 8.8.8.8;

    root /usr/share/nginx/html;
    index index.html;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;DH 以及 OCSP 内容请参考：&lt;a href="https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html" rel="nofollow" target="_blank" title=""&gt;Strong SSL Security On nginx&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;重新启动 web 服务器：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cd /letsencrypt &amp;amp;&amp;amp; docker-compose up -d
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="4.2 更新证书"&gt;4.2 更新证书&lt;/h3&gt;
&lt;p&gt;设置更新脚本：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;touch /letsencrypt/renew.sh 
chmod +x /letsencrypt/renew.sh
nano /letsencrypt/renew.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;renew.sh 脚本内容如下：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;

docker run &lt;span class="nt"&gt;-it&lt;/span&gt; &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;-v&lt;/span&gt; /letsencrypt/certbot/etc/letsencrypt:/etc/letsencrypt &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;-v&lt;/span&gt; /letsencrypt/certbot/var/lib/letsencrypt:/var/lib/letsencrypt &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;-v&lt;/span&gt; /letsencrypt/certbot/var/log/letsencrypt:/var/log/letsencrypt &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;-v&lt;/span&gt; /letsencrypt/site:/data/letsencrypt &lt;span class="se"&gt;\&lt;/span&gt;
certbot/certbot &lt;span class="se"&gt;\&lt;/span&gt;
renew &lt;span class="nt"&gt;--webroot&lt;/span&gt; &lt;span class="nt"&gt;-w&lt;/span&gt; /data/letsencrypt &lt;span class="nt"&gt;--quiet&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; docker &lt;span class="nb"&gt;kill&lt;/span&gt; &lt;span class="nt"&gt;--signal&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;HUP demo-site
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;最后一行脚本说明：在更新完证书后，通知 nginx 重新加载配置。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;通过 crontab 设置定时任务：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;crontab -e
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;添加一行，每周执行一次更新脚本：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0 1 * * 0 /letsencrypt/renew.sh
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="5. 安全加强"&gt;5. 安全加强&lt;/h2&gt;
&lt;p&gt;可以通过&lt;a href="ssllabs.com" title=""&gt;ssllabs.com&lt;/a&gt;验证证书，如果按照上述配置，应该可以获得&lt;code&gt;A+&lt;/code&gt;评价。
当然，还可以进一步通过&lt;a href="securityheaders.io" title=""&gt;securityheaders.io&lt;/a&gt;校验网站安全性，对于 nginx 可以添加以下配置：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;server {
    # ....

    location / {
        #security headers
        add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload";
        add_header X-XSS-Protection "1; mode=block" always;
        add_header X-Content-Type-Options "nosniff" always;
        add_header X-Frame-Options "DENY" always;
        #CSP
        add_header Content-Security-Policy "frame-src 'self'; default-src 'self'; script-src 'self' 'unsafe-inline' https://maxcdn.bootstrapcdn.com https://ajax.googleapis.com; img-src 'self'; style-src 'self' https://maxcdn.bootstrapcdn.com; font-src 'self' data: https://maxcdn.bootstrapcdn.com; form-action 'self'; upgrade-insecure-requests;" always;
        add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    }

    # ....
}
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="6. 小结"&gt;6. 小结&lt;/h2&gt;
&lt;p&gt;本文参考&lt;a href="https://www.humankode.com/ssl/how-to-set-up-free-ssl-certificates-from-lets-encrypt-using-docker-and-nginx" rel="nofollow" target="_blank" title="How to Set Up Free SSL Certificates from Let's Encrypt using Docker and Nginx"&gt;How to Set Up Free SSL Certificates from Let's Encrypt using Docker and Nginx&lt;/a&gt;，对 Docker 环境下如何使用 Let's Encrypt 自动获取/更新 SSL 证书做了一个简明攻略。&lt;/p&gt;

&lt;p&gt;如果你正在使用 K8S，&lt;a href="https://kubernetes.github.io/ingress-nginx" rel="nofollow" target="_blank" title=""&gt;ingress nginx&lt;/a&gt;和&lt;a href="https://traefik.io" rel="nofollow" target="_blank" title=""&gt;traefik&lt;/a&gt;都对&lt;code&gt;let‘s encrypt&lt;/code&gt;提供了很好的支持，配合&lt;a href="https://helm.sh/" rel="nofollow" target="_blank" title=""&gt;helm&lt;/a&gt;来安装部署，也更为简单方便。&lt;/p&gt;</description>
      <author>freefishz</author>
      <pubDate>Tue, 29 Jan 2019 10:03:28 +0800</pubDate>
      <link>https://ruby-china.org/topics/38061</link>
      <guid>https://ruby-china.org/topics/38061</guid>
    </item>
    <item>
      <title>GitLab, Docker, Ruby on Rails CI/CD 实践</title>
      <description>&lt;h2 id="前言"&gt;前言&lt;/h2&gt;
&lt;p&gt;一直以来公司的开发、测试及生产环境都基于实体机，CI/CD 通过&lt;code&gt;Jenkins&lt;/code&gt;完成。&lt;/p&gt;

&lt;p&gt;最近公司的运维工程师离职了，新的还未觅得。另外，公司的业务正朝着多线方向发展，未来计划采用基于&lt;code&gt;SeviceMesh&lt;/code&gt;的微服务方式部署到&lt;code&gt;K8S&lt;/code&gt;平台。先将环境迁移到&lt;code&gt;Docker&lt;/code&gt;，对于零运维经验的人，看上去是一个不错的开始。&lt;/p&gt;

&lt;p&gt;本文假设&lt;code&gt;GitLab&lt;/code&gt;已成功搭建运行，若想了解如何搭建 GitLab，请参考&lt;a href="https://www.jianshu.com/p/116616579edc" rel="nofollow" target="_blank" title=""&gt;这篇文章&lt;/a&gt;。&lt;/p&gt;
&lt;h2 id="1. GitLab CI/CD工作流"&gt;1. GitLab CI/CD工作流&lt;/h2&gt;
&lt;p&gt;先来看一张官网的图：
&lt;img src="https://upload-images.jianshu.io/upload_images/3351976-15073dcccff4a978.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" title="" alt="图1.1 GitLab CI/CD流程图"&gt;&lt;/p&gt;

&lt;p&gt;说明：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt; GitLab CI/CD的PIPELINE是由一系列&lt;code&gt;stage&lt;/code&gt;构成的，如图中 CI PIPELINE 的&lt;code&gt;BUILD&lt;/code&gt;，&lt;code&gt;UNIT TEST&lt;/code&gt;和&lt;code&gt;INTEGRATION TESTS&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;每个&lt;code&gt;stage&lt;/code&gt;又包含一系列任务，如&lt;code&gt;INTEGRATION TESTS&lt;/code&gt;包含了 3 个任务；&lt;/li&gt;
&lt;li&gt;默认上一个&lt;code&gt;stage&lt;/code&gt;的所有任务都成功执行，才会执行下一个&lt;code&gt;stage&lt;/code&gt;中的任务（可自定义执行规则）；&lt;/li&gt;
&lt;li&gt;系统默认设置了 3 个&lt;code&gt;stage&lt;/code&gt;：&lt;code&gt;build&lt;/code&gt;，&lt;code&gt;test&lt;/code&gt;和&lt;code&gt;deploy&lt;/code&gt;（可自定义，见下面配置文件）；&lt;/li&gt;
&lt;li&gt;主要配置都由项目根目录下的&lt;code&gt;.gitlab-ci.yml&lt;/code&gt;设定；&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;再来看一下 GitLab 的执行过程：
&lt;img src="https://upload-images.jianshu.io/upload_images/3351976-8f910f4fda2633f1.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" title="" alt="图1.2 GitLab执行过程图"&gt;&lt;/p&gt;

&lt;p&gt;说明：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;每个项目根目录都有一个&lt;code&gt;.gitlab-ci.yml&lt;/code&gt;配置文件；&lt;/li&gt;
&lt;li&gt;配置文件的主要内容包括：

&lt;ul&gt;
&lt;li&gt;定义一系列任务；&lt;/li&gt;
&lt;li&gt;设置任务在哪个&lt;code&gt;stage&lt;/code&gt;执行；&lt;/li&gt;
&lt;li&gt;设置任务应该由哪个&lt;code&gt;GitLab Runner&lt;/code&gt;负责执行；&lt;/li&gt;
&lt;li&gt;设置&lt;code&gt;GitLab Runner&lt;/code&gt;应该使用什么执行环境执行该任务，如某个 docker 镜像；&lt;/li&gt;
&lt;li&gt;设置任务依赖的&lt;code&gt;git&lt;/code&gt;分支；&lt;/li&gt;
&lt;li&gt;设置任务的触发条件，如代码提交或手工触发；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GitLab Runner&lt;/code&gt;需要在使用前先在 GitLab 注册：

&lt;ul&gt;
&lt;li&gt;一般每个&lt;code&gt;GitLab Runner&lt;/code&gt;都是相互独立的服务器或虚拟机，如本地办公室的开发服务器、云端的测试服务器、专门用于打包构建 app 的黑苹果电脑、专门用于某个项目的服务器等；&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GitLab Runner&lt;/code&gt;根据任务配置，为任务准备执行环境，如&lt;code&gt;shell&lt;/code&gt;，&lt;code&gt;docker&lt;/code&gt;，&lt;code&gt;k8s&lt;/code&gt;等；&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GitLab Runner&lt;/code&gt;注册时可以设置一到多个&lt;code&gt;tag&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GitLab&lt;/code&gt;通过配置文件中任务设置&lt;code&gt;tag&lt;/code&gt;，调度相应的&lt;code&gt;GitLab Runner&lt;/code&gt;运行任务；&lt;/li&gt;
&lt;li&gt;若多个 &lt;code&gt;GitLab Runner&lt;/code&gt;匹配执行条件，系统会随机选择一个；&lt;/li&gt;
&lt;li&gt;若没有相匹配的&lt;code&gt;GitLab Runner&lt;/code&gt;，或所有匹配的&lt;code&gt;GitLab Runner&lt;/code&gt;都在忙，则任务会处于等待状态；&lt;/li&gt;
&lt;li&gt; &lt;code&gt;GitLab Runner&lt;/code&gt;可设置同时执行任务的数量；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="2. 安装、注册GitLab Runner"&gt;2. 安装、注册 GitLab Runner&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;本示例使用&lt;code&gt;Docker&lt;/code&gt;运行&lt;code&gt;GitLab Runner&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;安装完后还需要在 GitLab 里注册，才能使用；&lt;/li&gt;
&lt;li&gt;本示例采用&lt;code&gt;alpine-10.7.2&lt;/code&gt;；&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;示例脚本如下：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker run --detach \
  --name gitlab-runner \
  --restart always \
  --volume /opt/data/gitlab-runner/config:/etc/gitlab-runner \ # 配置文件
  --volume /var/run/docker.sock:/var/run/docker.sock \         # 支持dind(Docker in Docker, 在Docker中构建Docker镜像)
  gitlab/gitlab-runner:alpine-v10.7.2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;GitLab Runner 跑起来之后，运行以下脚本完成注册。详情参考&lt;a href="https://docs.gitlab.com/runner/register/index.html#docker" rel="nofollow" target="_blank" title=""&gt;这里&lt;/a&gt;。&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker exec -it gitlab-runner gitlab-runner register \
  --name shared-runner \                              # 给GitLab Runner起个名
  --url "https://gitlab.com/" \                       # GitLab服务器地址
  --registration-token "PROJECT_REGISTRATION_TOKEN" \ # GitLab注册Token，可在GitLab管理界面获得
  --description "ruby-2.5" \                          # GitLab Runner的一些描述
  --tag-list nodejs,java,ruby \                       # 给GitLab Runner打上标签，配置文件可根据标签指定某个Runner来执行任务
  --run-untagged true \                               # 是否可以运行未指定标签的任务
  --locked false \                                    # 是否锁定到某个项目
  --executor "docker" \                               # 任务执行环境
  --docker-volumes /opt/data/ws:/share:rw \           # 使用docker执行环境时，自动挂载的目录（可选）
  --docker-image ruby:2.5                             # 使用docker执行环境时，设置默认执行镜像
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;说明：&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;任务执行环境：每种环境支持的功能有所区别。详情参考&lt;a href="https://docs.gitlab.com/runner/executors/README.html" rel="nofollow" target="_blank" title=""&gt;这里&lt;/a&gt;。&lt;/li&gt;
&lt;li&gt;自动挂载目录：根据需求自行决定是否需要，一些通用的脚本和工具可放在这里。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;注册完成后可以 GitLab 管理界面看到注册成功的 GitLab Runner，如下图所示：
&lt;img src="https://upload-images.jianshu.io/upload_images/3351976-205862c0d80ce76a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" title="" alt="图2.1 GitLab Runner 列表"&gt;&lt;/p&gt;

&lt;p&gt;同时，在&lt;code&gt;/opt/data/gitlab-runner/config/&lt;/code&gt;目录下，可以找到&lt;code&gt;config.toml&lt;/code&gt;配置文件：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;concurrent = 1           # 任务并发数
check_interval = 0

[[runners]]
  name = "rails builder"
  url = "https://gitlab.com/"
  token = "PROJECT_REGISTRATION_TOKEN"
  executor = "docker"
  clone_url = "https://gitlab.com/"
  [runners.docker]
    tls_verify = false
    image = "ruby:2.5"
    privileged = false
    disable_cache = false
    volumes = ["/var/run/docker.sock:/var/run/docker.sock", "/opt/data/ws:/share:rw"]
    shm_size = 0
  [runners.cache]
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="3. 定义.gitlab-ci.yml"&gt;3. 定义.gitlab-ci.yml&lt;/h2&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# 重新定义stages，可选，也可以使用默认的；
stages:
  - compile
  - build
  - deploy

# 将一些通用设置抽出来；
.general: &amp;amp;general
  only:
    - dev                           # 设置任务依赖的 git 分支
  when: manual                      # 设置手工触发
  tags:
    - ror                           # 设置哪个GitLab Runner来执行任务
  image: gitlab.com/builder:ror-v1  # 设置任务的执行环境，这里为docker镜像
  script:                           # 设置任务具体内容，依次列出shell脚本
    - /share/script/$CI_JOB_NAME.sh

# 编译任务，任务名称可任意设置
# 修订：将.bundle目录加入artifacts，build阶段就不需要再次bundle install了
compile:
  &amp;lt;&amp;lt;: *general        # 引用通用设置
  stage: compile      # 设置任务在哪个stage执行
  artifacts:          # 任务执行完毕后，哪些内容需要打包，供下载或给下一个任务使用
    expire_in: 12h    # 过期时间，过期后自动删除打包内容
    paths:
    - public/assets/  # rails项目编译后的assets
    - public/packs/   # rails项目中用到了react，这是编译后的react内容
    - .bundle/           # bundle install后的配置文件 &amp;lt; 修订：新增&amp;gt;

# 构建docker镜像任务
build:
  &amp;lt;&amp;lt;: *general
  stage: build
  image: docker:latest  # 使用dind（Docker in Docker）的方式来构建镜像 

# 部署任务
deploy:
  &amp;lt;&amp;lt;: *general
  stage: deploy
  dependencies: []      # 依赖任务列表
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置文件提交到&lt;code&gt;GitLab&lt;/code&gt;后，在&lt;code&gt;管理界面 -&amp;gt; CI/CD -&amp;gt; Pipelines&lt;/code&gt;可以看到如下所示：&lt;/p&gt;

&lt;p&gt;&lt;img src="https://upload-images.jianshu.io/upload_images/3351976-44db00690a316462.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" title="" alt="图3.1 GitLab CI/CD Pipeline"&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src="https://upload-images.jianshu.io/upload_images/3351976-ac62a8301ac700fc.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" title="" alt="图3.2 Pipleline详情"&gt;&lt;/p&gt;
&lt;h3 id="3.1 图例说明"&gt;3.1 图例说明&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;每次代码提交都会产生一条新的&lt;code&gt;Pipeline&lt;/code&gt;，每条都有一个编号，如图中 1 标注；&lt;/li&gt;
&lt;li&gt;点击&lt;code&gt;Pipeline&lt;/code&gt;编号可以看到详情，如图 3.2 所示。在图中可以手工触发相应的任务；&lt;/li&gt;
&lt;li&gt;图中第一条已经手工触发运行过了，状态是&lt;code&gt;passed&lt;/code&gt;，第二条状态是&lt;code&gt;skipped&lt;/code&gt;（还未手工触发）;&lt;/li&gt;
&lt;li&gt;配置文件中设置了 3 个&lt;code&gt;stage&lt;/code&gt;，如图中 2 标注；&lt;/li&gt;
&lt;li&gt;由于&lt;code&gt;compile&lt;/code&gt;任务设置了&lt;code&gt;artifacts&lt;/code&gt;，图中 3 标注有可以点击下载的选项；&lt;/li&gt;
&lt;li&gt;图中 3 标注的左边可以手工触发任务执行；&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="3.2 script说明"&gt;3.2 script 说明&lt;/h3&gt;
&lt;p&gt;将 shell 脚本依次列在&lt;code&gt;script&lt;/code&gt;的优缺点：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;优点：可以将脚本变更记录纳入版本控制；&lt;/li&gt;
&lt;li&gt;缺点：不方便调试，每次修改都需要先提交；&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;为了方便调试，示例中将所有脚本都写在单独的 shell 文件中。&lt;/p&gt;

&lt;p&gt;前面提到运行&lt;code&gt;GitLab Runner&lt;/code&gt;时，我们配置了&lt;code&gt;/opt/data/ws:/share:rw&lt;/code&gt;。该配置会自动将主机的&lt;code&gt;/opt/data/ws&lt;/code&gt;目录自动挂载到任务运行环境（Docker）的&lt;code&gt;/share&lt;/code&gt;目录。因此，可以将所有 shell 脚本都放在本地&lt;code&gt;/opt/data/ws&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;GitLab&lt;/code&gt;自带了一些环境变量供配置文件使用。示例中的&lt;code&gt;$CI_JOB_NAME&lt;/code&gt;就是其中的一个，该变量会自动赋值为任务名称。例如，在&lt;code&gt;compile&lt;/code&gt;任务中，该变量为&lt;code&gt;compile&lt;/code&gt;，执行&lt;code&gt;compile.sh&lt;/code&gt;。因此，可以在主机的&lt;code&gt;/opt/data/ws&lt;/code&gt;目录下创建三个 shell 文件&lt;code&gt;compile.sh&lt;/code&gt;，&lt;code&gt;build.sh&lt;/code&gt;和&lt;code&gt;deploy.sh&lt;/code&gt;，分别用于执行相应的任务。&lt;/p&gt;
&lt;h3 id="3.3 artifacts与dependencies说明"&gt;3.3 artifacts 与 dependencies 说明&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;每个任务都可以通过&lt;code&gt;artifacts&lt;/code&gt;声明，任务执行完毕后，哪些内容需要打包暂存，供下载或给下一个任务使用；&lt;/li&gt;
&lt;li&gt;若没有特别声明，每个任务都会默认继承前面任务的所有&lt;code&gt;artifacts&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;可以通过&lt;code&gt;dependencies&lt;/code&gt;声明，依赖哪些任务的的&lt;code&gt;artifacts&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;若不想继承任何&lt;code&gt;artifacts&lt;/code&gt;，可声明&lt;code&gt;dependencies&lt;/code&gt;为空，如&lt;code&gt;deploy&lt;/code&gt;任务所示；&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;运行&lt;code&gt;compile&lt;/code&gt;任务，在任务结束时，可以看到如下关于&lt;code&gt;artifacts&lt;/code&gt;的信息：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;...
Uploading artifacts...
public/assets/: found 631 matching files           
public/packs/: found 15 matching files             
Uploading artifacts to coordinator... ok            id=7282 responseStatus=201 Created token=ExCbBThh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行&lt;code&gt;build&lt;/code&gt;任务，在任务开始前，可以看到如下关于&lt;code&gt;artifacts&lt;/code&gt;的信息：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Downloading artifacts for compile (7282)...
Downloading artifacts from coordinator... ok        id=7282 responseStatus=200 OK token=ExCbBThh
...
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="4. 构建Rails编译环境"&gt;4. 构建 Rails 编译环境&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;将编译环境和运行环境分开，主要是想得到一个小而干净的镜像；&lt;/li&gt;
&lt;li&gt;使用&lt;code&gt;ubuntu 18.04&lt;/code&gt;作为编译环境，默认可安装&lt;code&gt;ruby 2.5&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;安装编译工具包需要配置时区，因此顺道安装设置了时区；&lt;/li&gt;
&lt;li&gt;安装&lt;code&gt;nodejs&lt;/code&gt;和&lt;code&gt;yarn&lt;/code&gt;（开发用到两者了）；&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FROM ubuntu:18.04
MAINTAINER jacky.zhang &amp;lt;chenghaoz@gmail.com&amp;gt;

# 安装并配置ruby、bundler
RUN apt update &amp;amp;&amp;amp; \
    apt install -y ruby &amp;amp;&amp;amp; \
    gem sources --add https://gems.ruby-china.com/ --remove https://rubygems.org/ &amp;amp;&amp;amp; \
    gem install bundler --no-rdoc --no-ri &amp;amp;&amp;amp; \
    bundle config mirror.https://rubygems.org https://gems.ruby-china.com

ENV DEBIAN_FRONTEND=noninteractive # 避免设置时区有交互，打断安装过程

# 安装必备软件包（根据业务要求裁剪），并设置时区
RUN apt-get install -y build-essential libpq-dev libmysqlclient-dev imagemagick ghostscript apt-transport-https curl git ruby-dev tzdata &amp;amp;&amp;amp; \
    ln -fs /usr/share/zoneinfo/Asia/Shanghai /etc/localtime &amp;amp;&amp;amp; \
    dpkg-reconfigure -f noninteractive tzdata

# 安装并配置nodejs、yarn
RUN curl -sL https://deb.nodesource.com/setup_8.x | bash - &amp;amp;&amp;amp; \
    curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - &amp;amp;&amp;amp; \
    echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list &amp;amp;&amp;amp; \
    apt-get update &amp;amp;&amp;amp; \
    apt-get install -y nodejs yarn &amp;amp;&amp;amp; \
    sh -c 'echo https://registry.npm.taobao.org &amp;gt; ~/.npmrc'    
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="5. 构建Rails运行环境"&gt;5. 构建 Rails 运行环境&lt;/h2&gt;
&lt;p&gt;一直以来都使用&lt;code&gt;mina&lt;/code&gt;部署 Rails 服务，服务器环境为：&lt;code&gt;Ubuntu + Nginx + Passenger&lt;/code&gt;。该环境稳定运行了好多年，因此想继续沿用。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;几点说明：&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;没有使用&lt;code&gt;ruby:2.5-alpine&lt;/code&gt;来做基础镜像的原因：

&lt;ul&gt;
&lt;li&gt;构建&lt;code&gt;Passenger&lt;/code&gt;过程相对复杂，需要从源码编译；&lt;/li&gt;
&lt;li&gt;构建完的镜像也没小多少（也许有优化空间？）;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ubuntu&lt;/code&gt;环境相比较熟悉；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;设置系统时区：&lt;code&gt;上海&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;安装&lt;code&gt;msyql&lt;/code&gt;和&lt;code&gt;postgresql&lt;/code&gt;驱动（业务同时需要连接两个数据库）；&lt;/li&gt;
&lt;li&gt;安装&lt;code&gt;imagemagick&lt;/code&gt;支持图像处理；&lt;/li&gt;
&lt;li&gt;安装&lt;code&gt;nodejs&lt;/code&gt;支持（应该可以去掉，未验证）；&lt;/li&gt;
&lt;li&gt;安装&lt;code&gt;cron&lt;/code&gt;定时任务服务（业务需要）；&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;nginx&lt;/code&gt;需要单独安装，否则&lt;code&gt;Pas&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Passenger&lt;/code&gt;官方安装文档中说明，需要先安装&lt;code&gt;ruby&lt;/code&gt;。经验证，最新&lt;code&gt;Passenger&lt;/code&gt;自带&lt;code&gt;ruby 2.5&lt;/code&gt;运行环境。若满足业务需求，可以不用单独安装&lt;code&gt;ruby&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;构建完镜像大小约&lt;code&gt;400M&lt;/code&gt;，若清理一下&lt;code&gt;/var/lib/apt/lists/&lt;/code&gt;，还可以减掉&lt;code&gt;40M&lt;/code&gt;；&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;Dockerfile&lt;/code&gt;如下：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FROM ubuntu:18.04
MAINTAINER jacky.zhang &amp;lt;chenghaoz@gmail.com&amp;gt;

ENV DEBIAN_FRONTEND=noninteractive # 避免设置时区有交互，打断安装过程

# 安装必备软件包（根据业务要求裁剪），并设置时区；
RUN apt-get update &amp;amp;&amp;amp; \
    apt-get install -y nginx cron imagemagick ghostscript libpq-dev libmysqlclient-dev nodejs tzdata &amp;amp;&amp;amp; \
    ln -fs /usr/share/zoneinfo/Asia/Shanghai /etc/localtime &amp;amp;&amp;amp; \
    dpkg-reconfigure -f noninteractive tzdata

# 安装Passenger，自带ruby 2.5；
RUN apt-get install -y dirmngr gnupg &amp;amp;&amp;amp; \
    apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 561F9B9CAC40B2F7 &amp;amp;&amp;amp; \
    apt-get install -y apt-transport-https ca-certificates &amp;amp;&amp;amp; \
    sh -c 'echo deb https://oss-binaries.phusionpassenger.com/apt/passenger bionic main &amp;gt; /etc/apt/sources.list.d/passenger.list' &amp;amp;&amp;amp; \
    apt-get update &amp;amp;&amp;amp; \
    apt-get install -y libnginx-mod-http-passenger &amp;amp;&amp;amp; \
    apt-get remove -y dirmngr gnupg &amp;amp;&amp;amp; \
    apt-get autoremove -y &amp;amp;&amp;amp; \
    apt-get clean

# 安装并设置bundle
RUN gem sources --add https://gems.ruby-china.com/ --remove https://rubygems.org/ &amp;amp;&amp;amp; \
    gem install bundler --no-rdoc --no-ri &amp;amp;&amp;amp; \
    bundle config mirror.https://rubygems.org https://gems.ruby-china.com

EXPOSE 80

# 默认nginx和cron服务不开机启动；
# ubuntu 18设置开机启动相对复杂，简单起见，就写在入口脚本里了；
ENTRYPOINT service nginx start &amp;amp;&amp;amp; service cron start &amp;amp;&amp;amp; tail -f /dev/null
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="6. 部署脚本"&gt;6. 部署脚本&lt;/h2&gt;&lt;h3 id="6.1 compile.sh"&gt;6.1 compile.sh&lt;/h3&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'compiling starts ...'&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'bundle: link and install '&lt;/span&gt;
&lt;span class="c"&gt;# 为了避免每次都安装所有gem，将bundle缓存在公共目录；&lt;/span&gt;
&lt;span class="nb"&gt;ln&lt;/span&gt; &lt;span class="nt"&gt;-fs&lt;/span&gt; /share/env/bundle vendor/bundle 
bundle &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--deployment&lt;/span&gt; &lt;span class="nt"&gt;--clean&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'compile assets'&lt;/span&gt;
&lt;span class="c"&gt;# 为了避免每次都所有安装npm包，将npm包缓存在公共目录；&lt;/span&gt;
&lt;span class="c"&gt;# 注意：&lt;/span&gt;
&lt;span class="c"&gt;# 这里不能使用link，否则nodejs编译会报错，或出现莫名其妙的bug；&lt;/span&gt;
&lt;span class="c"&gt;# 具体原因应该是某些npm包的路径规则引起的；&lt;/span&gt;
&lt;span class="nb"&gt;mv&lt;/span&gt; /share/env/node_modules node_modules 
&lt;span class="nv"&gt;RAILS_ENV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;production bundle &lt;span class="nb"&gt;exec &lt;/span&gt;rails assets:precompile
&lt;span class="nb"&gt;mv &lt;/span&gt;node_modules /share/env/node_modules

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'compiling ends.'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="6.2 build.sh"&gt;6.2 build.sh&lt;/h3&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/sh&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'building docker image starts ...'&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'copy bundle'&lt;/span&gt;
&lt;span class="c"&gt;# 将缓存的bundle拷贝过来&lt;/span&gt;
&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /share/env/bundle vendor/bundle 

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'build start ...'&lt;/span&gt;
docker build &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="nb"&gt;test&lt;/span&gt;:latest &lt;span class="nb"&gt;.&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'remove untaged images'&lt;/span&gt; 
&lt;span class="c"&gt;# 如有必要移除未打标签的镜像&lt;/span&gt;
docker rmi &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;docker images | &lt;span class="nb"&gt;grep &lt;/span&gt;none | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $3}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'building ends.'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;项目根目录的 Dockerfile 如下：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FROM gitlab.com/passenger:latest
MAINTAINER jacky.zhang &amp;lt;chenghaoz@gmail.com&amp;gt;

# passenger 工作目录
ENV APP_ROOT=/var/www/app
RUN mkdir -p $APP_ROOT

 # passenger默认使用www-data用户
COPY --chown=www-data . $APP_ROOT
WORKDIR $APP_ROOT

# 再运行一次bundle安装，会在项目根目录生成一些配置文件（可以在编译时缓存，以后优化） 
# 如果用到whenver，就更新一下吧
# RUN RAILS_ENV=production bundle install --deployment &amp;amp;&amp;amp; \
#     RAILS_ENV=production bundle exec whenever --update-crontab
# 修订：删除RAILS_ENV=production bundle install --deployment
RUN RAILS_ENV=production bundle exec whenever --update-crontab

&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="6.3 deploy.sh"&gt;6.3 deploy.sh&lt;/h3&gt;
&lt;p&gt;部署过程主要通过 ssh 到远程服务器来完成：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;先做备份；&lt;/li&gt;
&lt;li&gt;移除旧的&lt;code&gt;docker&lt;/code&gt;容器；&lt;/li&gt;
&lt;li&gt;用新的镜像重新部署，使用本地的配置文件，如 nginx、项目的环境变量等；&lt;/li&gt;
&lt;li&gt;部署完毕，根据需要运行&lt;code&gt;db:migration&lt;/code&gt;，或重启&lt;code&gt;sidekiq&lt;/code&gt;服务等；&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;上述任务可以写在一个 shell 脚本中完成，过程相对简单这里略过；&lt;/p&gt;
&lt;h2 id="7. 结束"&gt;7. 结束&lt;/h2&gt;
&lt;p&gt;本文记录了从零经验开始学习使用 GitLab 搭建 CI/CD 的一些经验，希望能帮到新入门的运维人员。
后续，正在进行 rancher + k8s + istio 的 ServiceMesh 实践，有时间话再来分享。&lt;/p&gt;</description>
      <author>freefishz</author>
      <pubDate>Wed, 17 Oct 2018 15:57:43 +0800</pubDate>
      <link>https://ruby-china.org/topics/37638</link>
      <guid>https://ruby-china.org/topics/37638</guid>
    </item>
    <item>
      <title>[上海] 上海优数科技有限公司 诚招中 / 高级研发工程师 (12~25K)</title>
      <description>&lt;h2 id="公司介绍"&gt;公司介绍&lt;/h2&gt;
&lt;p&gt;上海优数科技有限公司，是一家专注于专病领域的临床大数据及人工智能的创业公司。公司致力于与中国领先的医疗机构共同建立“医疗大数据”平台，利用机器学习和人工智能技术，辅助开展新型临床、科研、医院管理等服务，助力医疗机构和医生，为患者提供更好的医疗服务。&lt;/p&gt;

&lt;p&gt;2018 年公司已成功发布了一款专病临床数据库，在全国范围内积累了 50 多家知名三甲医院用户，并与多家世界排名前十的药企开展了科研合作项目。&lt;/p&gt;

&lt;p&gt;2019 年公司还将推出两款专病临床数据库产品，并打造聚焦专病领域的医患交流平台，预计医院用户数将超过 200 家。&lt;/p&gt;
&lt;h3 id="职位"&gt;职位&lt;/h3&gt;
&lt;p&gt;中/高级研发工程师&lt;/p&gt;
&lt;h3 id="岗位职责"&gt;岗位职责&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;负责完成公司产品研发，根据工作能力，可以侧重于前端或后端，或能够全栈开发；&lt;/li&gt;
&lt;li&gt;参与产品设计，与产品经理、交互设计、UI 设计共同讨论制定产品的需求方案；&lt;/li&gt;
&lt;li&gt;对各种前/后端新技术进行探索和尝试，负责关键服务的设计和实现；&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="岗位要求"&gt;岗位要求&lt;/h3&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;至少精通一门后端开发语言，如 Ruby、Python、Go、Java、Node.js 等；&lt;/li&gt;
&lt;li&gt;熟悉一种前端框架，或具备良好的 JavaScript 编程基础；&lt;/li&gt;
&lt;li&gt;熟悉 HTTP 协议并理解 RESTFUL 规范；&lt;/li&gt;
&lt;li&gt;熟悉 Linux 系统和终端化操作；&lt;/li&gt;
&lt;li&gt;熟悉 Git 的基本原理和操作；&lt;/li&gt;
&lt;li&gt;具有良好的沟通能力和团队合作精神；&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="加分项"&gt;加分项&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;精通 Ruby on Rails；&lt;/li&gt;
&lt;li&gt;有云平台/docker 的开发部署经验；&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="联系方式"&gt;联系方式&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;简历请发送：urodata@ebianque.cn，请注明来自 ruby-china&lt;/li&gt;
&lt;li&gt;办公地点：虹口区，地铁 3 号线大柏树站旁&lt;/li&gt;
&lt;/ul&gt;</description>
      <author>freefishz</author>
      <pubDate>Tue, 04 Sep 2018 10:48:21 +0800</pubDate>
      <link>https://ruby-china.org/topics/37433</link>
      <guid>https://ruby-china.org/topics/37433</guid>
    </item>
    <item>
      <title>[上海] 上海悠安致医疗科技有限公司 招前 / 后端 / 全栈工程师 ( 15~30K)</title>
      <description>&lt;h2 id="公司介绍"&gt;公司介绍&lt;/h2&gt;
&lt;p&gt;上海悠安致医疗科技有限公司，是一家专注于医疗大数据研究与应用的高科技企业。公司致力于与中国领先的医疗机构共同建立“医疗大数据”平台，利用公司的机器学习和人工智能技术，对医疗数据进行集成、挖掘、利用，辅助开展新型临床、科研、医院管理等服务，助力医疗机构和医生，为患者提供更好的医疗服务。&lt;/p&gt;
&lt;h2 id="前端工程师"&gt;前端工程师&lt;/h2&gt;&lt;h3 id="职位描述"&gt;职位描述&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;负责完成公司医疗互联网产品的前端开发；&lt;/li&gt;
&lt;li&gt;参与产品设计，与产品经理、交互设计、UI 设计共同讨论制定产品的需求方案；&lt;/li&gt;
&lt;li&gt;对各种前端新技术进行探索和尝试，协助公司前端团队技术保持更新；&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="职位要求"&gt;职位要求&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;精通 H5、CSS3、JavaScript、ES6、Bootstrap、jQuery、Webpack、Gulp 等 Web 开发技术； &lt;/li&gt;
&lt;li&gt;至少掌握一种前端开发框架，如 Vue、React、Angular、Ember 等；&lt;/li&gt;
&lt;li&gt;会一门后端开发技术者优先，如 Ruby、Python、PHP、Java、Node.js 等； &lt;/li&gt;
&lt;li&gt;熟悉 Linux 操作，能够独立在 Linux 环境处理基本任务；&lt;/li&gt;
&lt;li&gt;具有良好的沟通能力和团队合作精神，有责任感；&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="Ruby工程师"&gt;Ruby 工程师&lt;/h2&gt;&lt;h3 id="职位描述"&gt;职位描述&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;配合产品团队梳理业务逻辑，为业务开发提供方案支持；&lt;/li&gt;
&lt;li&gt;负责 web 后端架构，负责关键服务的设计和实现。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="职位要求"&gt;职位要求&lt;/h3&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;至少精通一门后端开发语言，如 Ruby、Python、PHP、Java、Node.js 等；&lt;/li&gt;
&lt;li&gt;有云平台/docker 的开发部署经验者优先；&lt;/li&gt;
&lt;li&gt;良好的沟通与表达能力，思路清晰，有强烈的责任心和创新意识，业务逻辑理解与分析能力强；&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="联系方式"&gt;联系方式&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;简历请发送：zhouwl@karsem.com，请注明来自 ruby-china&lt;/li&gt;
&lt;li&gt;办公地点：最近从常熟路搬到浦东展想大厦&lt;/li&gt;
&lt;li&gt;更多职位详见：&lt;a href="https://www.liepin.com/company/9211782/" rel="nofollow" target="_blank"&gt;https://www.liepin.com/company/9211782/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;我们期待认真踏实，有责任心，对互联网医疗有浓厚兴趣的您来加入！&lt;/p&gt;</description>
      <author>freefishz</author>
      <pubDate>Fri, 09 Jun 2017 16:17:47 +0800</pubDate>
      <link>https://ruby-china.org/topics/33194</link>
      <guid>https://ruby-china.org/topics/33194</guid>
    </item>
    <item>
      <title>[译] 重构 Rails MVC 组件的 7 个设计模式</title>
      <description>&lt;blockquote&gt;
&lt;p&gt;原文请阅读 &lt;a href="https://www.sitepoint.com/7-design-patterns-to-refactor-mvc-components-in-rails/" rel="nofollow" target="_blank" title=""&gt;7 Design Patterns to Refactor MVC Components in Rails&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;img src="https://dab1nmslvvntp.cloudfront.net/wp-content/uploads/2016/11/1478719913dphead.png" title="" alt="MVC"&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="#1.%20Service%20Objects%20(and%20Interactor%20Objects)" title=""&gt;Service Objects (and Interactor Objects)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#2.%20Value%20Objects" title=""&gt;Value Objects&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#3.%20Form%20Objects" title=""&gt;Form Objects&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#4.%20Query%20Objects" title=""&gt;Query Objects&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#5.%20View%20Objects%20(Serializer/Presenter)" title=""&gt;View Objects (Serializer/Presenter)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#6.%20Policy%20Objects" title=""&gt;Policy Objects&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#7.%20Decorators" title=""&gt;Decorators&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="1. Service Objects (and Interactor Objects)"&gt;1. Service Objects (and Interactor Objects)&lt;/h2&gt;
&lt;p&gt;当 Controller 中的 action 有以下症状时适用：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;过于复杂（如，计算员工的工资）&lt;/li&gt;
&lt;li&gt;调用外部 api 服务&lt;/li&gt;
&lt;li&gt;明显不属于任何 model（如，删除过期数据）&lt;/li&gt;
&lt;li&gt;使用多个 model（如，从一个文件中导入数据到多个 model）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="示例"&gt;示例&lt;/h2&gt;
&lt;p&gt;以下示例中，主要工作由外部 Stripe 服务完成。该服务基于邮件地址和来源创建 Stripe 客户，并将所有服务费用绑定到该客户的账号上。&lt;/p&gt;
&lt;h2 id="问题分析"&gt;问题分析&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Controller 中包含调用外部服务的代码 &lt;/li&gt;
&lt;li&gt;Controller 负责构建调用外部服务所需的数据&lt;/li&gt;
&lt;li&gt;Controller 难于维护和扩展&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ChargesController&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="err"&gt;&amp;nbsp;&amp;nbsp;&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:amount&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;to_i&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt; &lt;span class="o"&gt;=&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="err"&gt;&amp;nbsp;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="ss"&gt;email: &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;:email&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="ss"&gt;source: &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;:source&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="n"&gt;charge&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Stripe&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Charge&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="err"&gt;&amp;nbsp;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="ss"&gt;customer: &lt;/span&gt;&lt;span class="n"&gt;customer&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="err"&gt;&amp;nbsp;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="ss"&gt;amount: &lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="ss"&gt;description: &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;:description&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="ss"&gt;currency: &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;:currency&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;'USD'&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="n"&gt;redirect_to&lt;/span&gt; &lt;span class="n"&gt;charges_path&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&amp;nbsp;&lt;/span&gt; &lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="no"&gt;Stripe&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;CardError&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;exception&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="n"&gt;flash&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:error&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&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;message&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="n"&gt;redirect_to&lt;/span&gt; &lt;span class="n"&gt;new_charge_path&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&amp;nbsp;&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&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;ChargesController&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="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&amp;nbsp;&lt;/span&gt; &lt;span class="no"&gt;CheckoutService&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;params&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&amp;nbsp;&lt;/span&gt; &lt;span class="n"&gt;redirect_to&lt;/span&gt; &lt;span class="n"&gt;charges_path&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="no"&gt;Stripe&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;CardError&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;exception&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&amp;nbsp;&lt;/span&gt; &lt;span class="n"&gt;flash&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:error&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&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;message&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&amp;nbsp;&lt;/span&gt; &lt;span class="n"&gt;redirect_to&lt;/span&gt; &lt;span class="n"&gt;new_charge_path&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&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;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CheckoutService&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="no"&gt;DEFAULT_CURRENCY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'USD'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;freeze&lt;/span&gt;

&lt;span class="err"&gt;&amp;nbsp;&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;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&amp;nbsp;&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;each_pair&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;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="nb"&gt;instance_variable_set&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;key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&amp;nbsp;&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&amp;nbsp;&lt;/span&gt; &lt;span class="no"&gt;Stripe&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Charge&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;charge_attributes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="kp"&gt;private&lt;/span&gt;

&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="nb"&gt;attr_reader&lt;/span&gt; &lt;span class="ss"&gt;:email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:source&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="ss"&gt;:description&lt;/span&gt;

&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;currency&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&amp;nbsp;&lt;/span&gt; &lt;span class="vi"&gt;@currency&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="no"&gt;DEFAULT_CURRENCY&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;amount&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&amp;nbsp;&lt;/span&gt; &lt;span class="vi"&gt;@amount.to_i&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;customer&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&amp;nbsp;&lt;/span&gt; &lt;span class="vi"&gt;@customer&lt;/span&gt; &lt;span class="o"&gt;||=&lt;/span&gt; &lt;span class="no"&gt;Stripe&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Customer&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;customer_attributes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;customer_attributes&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&amp;nbsp;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="ss"&gt;email: &lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="ss"&gt;source: &lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&amp;nbsp;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;charge_attributes&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&amp;nbsp;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="ss"&gt;customer: &lt;/span&gt;&lt;span class="n"&gt;customer&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="err"&gt;&amp;nbsp;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="ss"&gt;amount: &lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="ss"&gt;description: &lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="ss"&gt;currency: &lt;/span&gt;&lt;span class="n"&gt;currency&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&amp;nbsp;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&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;CheckoutService&lt;/code&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;ChargesController&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="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&amp;nbsp;&lt;/span&gt; &lt;span class="no"&gt;CheckoutService&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;params&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&amp;nbsp;&lt;/span&gt; &lt;span class="n"&gt;redirect_to&lt;/span&gt; &lt;span class="n"&gt;charges_path&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="no"&gt;Stripe&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;CardError&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;exception&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&amp;nbsp;&lt;/span&gt; &lt;span class="n"&gt;flash&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:error&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&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;error&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&amp;nbsp;&lt;/span&gt; &lt;span class="n"&gt;redirect_to&lt;/span&gt; &lt;span class="n"&gt;new_charge_path&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&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;为了解决这个问题，可以在一个 Interactor 对象中调用&lt;code&gt;CheckoutService&lt;/code&gt;，并捕获可能产生的异常。Interactor 模式常用于封装业务逻辑，每个 Interactor 一般只描述一条业务逻辑。&lt;/p&gt;

&lt;p&gt;Interactor 模式通过简单 Ruby 对象（plain old Ruby objects, POROs）可以帮助我们实现单一原则（Single Responsibility Principle, SRP）。Interactor 与 Service Object 类似，只是通常会返回执行状态及相关信息，而且一般会在 Interactor 内部使用 Service Object。下面是该设计模式的使用示例：&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;ChargesController&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="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&amp;nbsp;&lt;/span&gt; &lt;span class="n"&gt;interactor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;CheckoutInteractor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&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="err"&gt;&amp;nbsp;&amp;nbsp;&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;interactor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;success?&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt;    &lt;span class="n"&gt;redirect_to&lt;/span&gt; &lt;span class="n"&gt;charges_path&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&amp;nbsp;&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt;    &lt;span class="n"&gt;flash&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:error&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;interactor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt;    &lt;span class="n"&gt;redirect_to&lt;/span&gt; &lt;span class="n"&gt;new_charge_path&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&amp;nbsp;&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&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;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CheckoutInteractor&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&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;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&amp;nbsp;&lt;/span&gt; &lt;span class="n"&gt;interactor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&amp;nbsp;&lt;/span&gt; &lt;span class="n"&gt;interactor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&amp;nbsp;&lt;/span&gt; &lt;span class="n"&gt;interactor&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="nb"&gt;attr_reader&lt;/span&gt; &lt;span class="ss"&gt;:error&lt;/span&gt;

&lt;span class="err"&gt;&amp;nbsp;&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;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&amp;nbsp;&lt;/span&gt; &lt;span class="vi"&gt;@context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;success?&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&amp;nbsp;&lt;/span&gt; &lt;span class="vi"&gt;@error.nil&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&amp;nbsp;&lt;/span&gt; &lt;span class="no"&gt;CheckoutService&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;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="no"&gt;Stripe&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;CardError&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;exception&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&amp;nbsp;&lt;/span&gt; &lt;span class="nb"&gt;fail&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&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;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="kp"&gt;private&lt;/span&gt;

&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="nb"&gt;attr_reader&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;

&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fail!&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="err"&gt;&amp;nbsp;&amp;nbsp;&lt;/span&gt; &lt;span class="vi"&gt;@error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&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 就达到了瘦身的目的。瘦身以后，Controller 只负责成功支付和失败支付时的页面跳转。&lt;/p&gt;
&lt;h2 id="2. Value Objects"&gt;2. Value Objects&lt;/h2&gt;
&lt;p&gt;Value Object 设计模式推崇简洁的对象（仅包含一些给定的值），并支持根据给定的逻辑，或基于指定的属性进行对象间相互比较（不基于 id）。Value Object 的例子如，以不同币种表示的货币。我们可以用一个币种（如，美元）来比较这些对象。同样，Value Object 也可以用于表示温度，并可用单位开来进行比较。&lt;/p&gt;
&lt;h2 id="示例"&gt;示例&lt;/h2&gt;
&lt;p&gt;假设有一所带电加热的智能房子，加热器可以通过网络接口加以控制。Controller 的一个方法将从温度传感器那里收到指定加热器的参数：温度数值和温度单位（华氏、摄氏或开）。如果是其他温度单位，一律先转换为开。然后，检查温度是否小于 25°C 并大于等于当前温度。&lt;/p&gt;
&lt;h2 id="问题分析"&gt;问题分析&lt;/h2&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;AutomatedThermostaticValvesController&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="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="no"&gt;SCALES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sx"&gt;%w(kelvin celsius fahrenheit)&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="no"&gt;DEFAULT_SCALE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'kelvin'&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="no"&gt;MAX_TEMPERATURE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;273.15&lt;/span&gt;

&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="n"&gt;before_action&lt;/span&gt; &lt;span class="ss"&gt;:set_scale&lt;/span&gt;

&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;heat_up&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="n"&gt;was_heat_up&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;false&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;previous_temperature&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;next_temperature&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;next_temperature&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;MAX_TEMPERATURE&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="n"&gt;valve&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;degrees: &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;:degrees&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="ss"&gt;scale: &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;:scale&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="no"&gt;Heater&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;next_temperature&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="n"&gt;was_heat_up&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;json: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;was_heat_up: &lt;/span&gt;&lt;span class="n"&gt;was_heat_up&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="kp"&gt;private&lt;/span&gt;

&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;previous_temperature&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="n"&gt;kelvin_degrees_by_scale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;valve&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;degrees&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;valve&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;next_temperature&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="n"&gt;kelvin_degrees_by_scale&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;:degrees&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="vi"&gt;@scale&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;set_scale&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="vi"&gt;@scale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;SCALES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;include?&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;:scale&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:scale&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;DEFAULT_SCALE&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;valve&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="vi"&gt;@valve&lt;/span&gt; &lt;span class="o"&gt;||=&lt;/span&gt; &lt;span class="no"&gt;AutomatedThermostaticValve&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="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;kelvin_degrees_by_scale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;degrees&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="n"&gt;degrees&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;degrees&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_f&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s1"&gt;'kelvin'&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="n"&gt;degrees&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s1"&gt;'celsius'&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="n"&gt;degrees&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;273.15&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s1"&gt;'fahrenheit'&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;degrees&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;273.15&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&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;首先，将温度比较逻辑移到 Model 中，Controller 只需要将参数传给`update'方法。但这样一来，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;AutomatedThermostaticValvesController&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;heat_up&lt;/span&gt;
    &lt;span class="n"&gt;valve&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;next_degrees: &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;:degrees&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="ss"&gt;next_scale: &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;:scale&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

    &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;json: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;was_heat_up: &lt;/span&gt;&lt;span class="n"&gt;valve&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;was_heat_up&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;valve&lt;/span&gt;
    &lt;span class="vi"&gt;@valve&lt;/span&gt; &lt;span class="o"&gt;||=&lt;/span&gt; &lt;span class="no"&gt;AutomatedThermostaticValve&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;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AutomatedThermostaticValve&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="no"&gt;SCALES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sx"&gt;%w(kelvin celsius fahrenheit)&lt;/span&gt;
  &lt;span class="no"&gt;DEFAULT_SCALE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'kelvin'&lt;/span&gt;

  &lt;span class="n"&gt;before_validation&lt;/span&gt; &lt;span class="ss"&gt;:check_next_temperature&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;if: :next_temperature&lt;/span&gt;
  &lt;span class="n"&gt;after_save&lt;/span&gt; &lt;span class="ss"&gt;:launch_heater&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;if: :was_heat_up&lt;/span&gt;

  &lt;span class="nb"&gt;attr_accessor&lt;/span&gt; &lt;span class="ss"&gt;:next_degrees&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:next_scale&lt;/span&gt;
  &lt;span class="nb"&gt;attr_reader&lt;/span&gt; &lt;span class="ss"&gt;:was_heat_up&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;temperature&lt;/span&gt;
    &lt;span class="n"&gt;kelvin_degrees_by_scale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;degrees&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scale&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;next_temperature&lt;/span&gt;
    &lt;span class="n"&gt;kelvin_degrees_by_scale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;next_degrees&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;next_scale&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;next_degrees&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;present?&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;max_temperature&lt;/span&gt;
    &lt;span class="n"&gt;kelvin_degrees_by_scale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'celsius'&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;next_scale&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@next_scale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;SCALES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;include?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;scale&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;DEFAULT_SCALE&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;check_next_temperature&lt;/span&gt;
    &lt;span class="vi"&gt;@was_heat_up&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;false&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;temperature&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;next_temperature&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;next_temperature&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;max_temperature&lt;/span&gt;
      &lt;span class="vi"&gt;@was_heat_up&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;
      &lt;span class="n"&gt;assign_attributes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="ss"&gt;degrees: &lt;/span&gt;&lt;span class="n"&gt;next_degrees&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;scale: &lt;/span&gt;&lt;span class="n"&gt;next_scale&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="vi"&gt;@was_heat_up&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;launch_heater&lt;/span&gt;
    &lt;span class="no"&gt;Heater&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;temperature&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;kelvin_degrees_by_scale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;degrees&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;degrees&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;degrees&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_f&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;
    &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s1"&gt;'kelvin'&lt;/span&gt;
      &lt;span class="n"&gt;degrees&lt;/span&gt;
    &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s1"&gt;'celsius'&lt;/span&gt;
      &lt;span class="n"&gt;degrees&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;273.15&lt;/span&gt;
    &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s1"&gt;'fahrenheit'&lt;/span&gt;
      &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;degrees&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;273.15&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为了让 Model 瘦身，我们将创建 Value Objects。Value Objects 接受温度数值和温度单位作为初始化参数。在进行比较时，使用&lt;code&gt;&amp;lt;=&amp;gt;&lt;/code&gt;操作符比较转换为开之后的温度。&lt;/p&gt;

&lt;p&gt;同时，Value Object 也包含一个&lt;code&gt;to_h&lt;/code&gt;方法用于批量赋值。另外，还提供了工厂方法&lt;code&gt;from_kelvin&lt;/code&gt;、&lt;code&gt;from_celsius&lt;/code&gt;和&lt;code&gt;from_fahrenheit&lt;/code&gt;，便于以指定单位创建&lt;code&gt;Temperature&lt;/code&gt;对象，如&lt;code&gt;Temperature.from_celsius(0)&lt;/code&gt;将会创建一个 0°C 或 273°К的温度对象。&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;AutomatedThermostaticValvesController&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;heat_up&lt;/span&gt;
    &lt;span class="n"&gt;valve&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;next_degrees: &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;:degrees&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="ss"&gt;next_scale: &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;:scale&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;json: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;was_heat_up: &lt;/span&gt;&lt;span class="n"&gt;valve&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;was_heat_up&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;valve&lt;/span&gt;
    &lt;span class="vi"&gt;@valve&lt;/span&gt; &lt;span class="o"&gt;||=&lt;/span&gt; &lt;span class="no"&gt;AutomatedThermostaticValve&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;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AutomatedThermostaticValve&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;before_validation&lt;/span&gt; &lt;span class="ss"&gt;:check_next_temperature&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;if: :next_temperature&lt;/span&gt;
  &lt;span class="n"&gt;after_save&lt;/span&gt; &lt;span class="ss"&gt;:launch_heater&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;if: :was_heat_up&lt;/span&gt;

  &lt;span class="nb"&gt;attr_accessor&lt;/span&gt; &lt;span class="ss"&gt;:next_degrees&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:next_scale&lt;/span&gt;
  &lt;span class="nb"&gt;attr_reader&lt;/span&gt; &lt;span class="ss"&gt;:was_heat_up&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;temperature&lt;/span&gt;
    &lt;span class="no"&gt;Temperature&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;degrees&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scale&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;temperature&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;assign_attributes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_h&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;next_temperature&lt;/span&gt;
    &lt;span class="no"&gt;Temperature&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;next_degrees&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;next_scale&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;next_degrees&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;present?&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;check_next_temperature&lt;/span&gt;
    &lt;span class="vi"&gt;@was_heat_up&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;false&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;temperature&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;next_temperature&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;next_temperature&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="no"&gt;Temperature&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;MAX&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;temperature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;next_temperature&lt;/span&gt;
      &lt;span class="vi"&gt;@was_heat_up&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;true&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;launch_heater&lt;/span&gt;
    &lt;span class="no"&gt;Heater&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;kelvin_degrees&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;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Temperature&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;Comparable&lt;/span&gt;
  &lt;span class="no"&gt;SCALES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sx"&gt;%w(kelvin celsius fahrenheit)&lt;/span&gt;
  &lt;span class="no"&gt;DEFAULT_SCALE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'kelvin'&lt;/span&gt;

  &lt;span class="nb"&gt;attr_reader&lt;/span&gt; &lt;span class="ss"&gt;:degrees&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:scale&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:kelvin_degrees&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;degrees&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'kelvin'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@degrees&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;degrees&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_f&lt;/span&gt;
    &lt;span class="vi"&gt;@scale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;scale&lt;/span&gt;
    &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="no"&gt;SCALES&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="n"&gt;scale&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="no"&gt;DEFAULT_SCALE&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="vi"&gt;@kelvin_degrees&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="vi"&gt;@scale&lt;/span&gt;
    &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s1"&gt;'kelvin'&lt;/span&gt;
      &lt;span class="vi"&gt;@degrees&lt;/span&gt;
    &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s1"&gt;'celsius'&lt;/span&gt;
      &lt;span class="vi"&gt;@degrees&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;273.15&lt;/span&gt;
    &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s1"&gt;'fahrenheit'&lt;/span&gt;
      &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@degrees&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;273.15&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="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_celsius&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;degrees_celsius&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;degrees_celsius&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'celsius'&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="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_fahrenheit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;degrees_fahrenheit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;degrees_celsius&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'fahrenheit'&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="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_kelvin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;degrees_kelvin&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;degrees_kelvin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'kelvin'&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;&amp;lt;&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;other&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;kelvin_degrees&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;other&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;kelvin_degrees&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;to_h&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;degrees: &lt;/span&gt;&lt;span class="n"&gt;degrees&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;scale: &lt;/span&gt;&lt;span class="n"&gt;scale&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="no"&gt;MAX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;from_celsius&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;25&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;最终的结果是，Controller 和 Model 同时得到了瘦身。Controller 不包含任何与温度相关的业务逻辑，Model 也不包含任何与温度转换相关的逻辑，仅调用了&lt;code&gt;Temperature&lt;/code&gt;提供的方法。&lt;/p&gt;
&lt;h2 id="3. Form Objects"&gt;3. Form Objects&lt;/h2&gt;
&lt;p&gt;Form Object 模式适用于封装数据校验和持久化。&lt;/p&gt;
&lt;h2 id="示例"&gt;示例&lt;/h2&gt;
&lt;p&gt;假设我们有一个典型 Rails Model 和 Controller 用于创建新用户。&lt;/p&gt;
&lt;h2 id="问题分析"&gt;问题分析&lt;/h2&gt;
&lt;p&gt;Model 中包含了所有校验逻辑，因此不能为其他实体重用，如 Admin。&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;UsersController&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="vi"&gt;@user&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;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@user.save&lt;/span&gt;
      &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;json: &lt;/span&gt;&lt;span class="vi"&gt;@user&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
      &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;json: &lt;/span&gt;&lt;span class="vi"&gt;@user.error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;status: :unprocessable_entity&lt;/span&gt;
    &lt;span class="k"&gt;end&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;user_params&lt;/span&gt;
    &lt;span class="n"&gt;params&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;permit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:full_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:password_confirmation&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;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;User&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="no"&gt;EMAIL_REGEX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/@/&lt;/span&gt; &lt;span class="c1"&gt;# Some fancy email regex&lt;/span&gt;

  &lt;span class="n"&gt;validates&lt;/span&gt; &lt;span class="ss"&gt;:full_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;presence: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;
  &lt;span class="n"&gt;validates&lt;/span&gt; &lt;span class="ss"&gt;:email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;presence: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;format: &lt;/span&gt;&lt;span class="no"&gt;EMAIL_REGEX&lt;/span&gt;
  &lt;span class="n"&gt;validates&lt;/span&gt; &lt;span class="ss"&gt;:password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;presence: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;confirmation: &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;解决方案就是将所有校验逻辑移到一个单独负责校验的类中，可以称之为&lt;code&gt;UserForm&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;UserForm&lt;/span&gt;
  &lt;span class="no"&gt;EMAIL_REGEX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;//&lt;/span&gt; &lt;span class="c1"&gt;# Some fancy email regex&lt;/span&gt;

  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;ActiveModel&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Model&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;Virtus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;model&lt;/span&gt;

  &lt;span class="n"&gt;attribute&lt;/span&gt; &lt;span class="ss"&gt;:id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;Integer&lt;/span&gt;
  &lt;span class="n"&gt;attribute&lt;/span&gt; &lt;span class="ss"&gt;:full_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;String&lt;/span&gt;
  &lt;span class="n"&gt;attribute&lt;/span&gt; &lt;span class="ss"&gt;:email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;String&lt;/span&gt;
  &lt;span class="n"&gt;attribute&lt;/span&gt; &lt;span class="ss"&gt;:password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;String&lt;/span&gt;
  &lt;span class="n"&gt;attribute&lt;/span&gt; &lt;span class="ss"&gt;:password_confirmation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;String&lt;/span&gt;

  &lt;span class="n"&gt;validates&lt;/span&gt; &lt;span class="ss"&gt;:full_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;presence: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;
  &lt;span class="n"&gt;validates&lt;/span&gt; &lt;span class="ss"&gt;:email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;presence: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;format: &lt;/span&gt;&lt;span class="no"&gt;EMAIL_REGEX&lt;/span&gt;
  &lt;span class="n"&gt;validates&lt;/span&gt; &lt;span class="ss"&gt;:password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;presence: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;confirmation: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;

  &lt;span class="nb"&gt;attr_reader&lt;/span&gt; &lt;span class="ss"&gt;:record&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;persist&lt;/span&gt;
    &lt;span class="vi"&gt;@record&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;id&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;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&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;new&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;valid?&lt;/span&gt;
      &lt;span class="vi"&gt;@record.attributes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;except&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:password_confirmation&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="vi"&gt;@record.save&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;
      &lt;span class="kp"&gt;true&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
      &lt;span class="kp"&gt;false&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在，就可以在 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;UsersController&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="vi"&gt;@form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;UserForm&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;user_params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@form.persist&lt;/span&gt;
      &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;json: &lt;/span&gt;&lt;span class="vi"&gt;@form.record&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
      &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;json: &lt;/span&gt;&lt;span class="vi"&gt;@form.errors&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;status: :unpocessably_entity&lt;/span&gt;
    &lt;span class="k"&gt;end&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;user_params&lt;/span&gt;
    &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;permit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:full_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:password_confirmation&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;最终，用户 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;User&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;/code&gt;&lt;/pre&gt;&lt;h2 id="4. Query Objects"&gt;4. Query Objects&lt;/h2&gt;
&lt;p&gt;该模式适用于从 Controller 和 Model 中抽取查询逻辑，并将它封装到可重用的类。&lt;/p&gt;
&lt;h2 id="示例"&gt;示例&lt;/h2&gt;
&lt;p&gt;假设我们请求一个文章列表，查询条件是类型为 video、查看数大于 100 并且当前用户可以访问。&lt;/p&gt;
&lt;h2 id="问题分析"&gt;问题分析&lt;/h2&gt;
&lt;p&gt;所有查询逻辑都在 Controller 中（即所有查询条件都在 Controller 中添加）。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;不可重用&lt;/li&gt;
&lt;li&gt;难于测试&lt;/li&gt;
&lt;li&gt;文章 Scheme 的任何改变都可能影响这段代码&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Article&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="c1"&gt;# t.string :status&lt;/span&gt;
    &lt;span class="c1"&gt;# t.string :type&lt;/span&gt;
    &lt;span class="c1"&gt;# t.integer :view_count&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;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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;index&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;Article&lt;/span&gt;
                  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;accessible_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current_ability&lt;/span&gt;&lt;span class="p"&gt;)&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="ss"&gt;type: :video&lt;/span&gt;&lt;span class="p"&gt;)&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="s1"&gt;'view_count &amp;gt; ?'&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;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重构的第一步就是封装查询条件，提供简洁的 API 接口。在 Rails 中，可以使用 scope 实现：&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;Article&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;scope&lt;/span&gt; &lt;span class="ss"&gt;:with_video_type&lt;/span&gt;&lt;span class="p"&gt;,&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;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;type: :video&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="ss"&gt;:popular&lt;/span&gt;&lt;span class="p"&gt;,&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;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'view_count &amp;gt; ?'&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="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="ss"&gt;:popular_with_video_type&lt;/span&gt;&lt;span class="p"&gt;,&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;popular&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_video_type&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;现在就可以使用这些简洁的 API 接口来查询，而不用关心底层是如何实现的。如果 article 的 scheme 发生了改变，仅需要修改 article 类即可。&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;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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;index&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;Article&lt;/span&gt; 
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;accessible_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current_ability&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;popular_with_video_type&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;看起来不错，不过又有一些新问题出现了。首先，需要为每个想要封装的查询条件创建 scope，最终会导致 Model 中充斥诸多针对不同应用场景的 scope 组合。其次，scope 不能在不同的 model 中重用，比如不用使用 Article 的 scope 来查询 Attachment。最后，将所有查询相关的逻辑都塞到 Article 类中也违反了单一原则。解决方案是使用 Query Object。&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;PopularVideoQuery&lt;/span&gt; 
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;relation&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
    &lt;span class="n"&gt;relation&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="ss"&gt;type: :video&lt;/span&gt;&lt;span class="p"&gt;)&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="s1"&gt;'view_count &amp;gt; ?'&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;span class="k"&gt;end&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;index&lt;/span&gt; 
    &lt;span class="n"&gt;relation&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;accessible_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current_ability&lt;/span&gt;&lt;span class="p"&gt;)&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;PopularVideoQuery&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="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;relation&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;哈，这样就可以做到重用了！现在可以将它用于查询任何具有相似 scheme 的类了：&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;Attachment&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="c1"&gt;# t.string :type &lt;/span&gt;
  &lt;span class="c1"&gt;# t.integer :view_count&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="no"&gt;PopularVideoQuery&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="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Attachment&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="nf"&gt;to_sql&lt;/span&gt;
&lt;span class="c1"&gt;# "SELECT \"attachments\".* FROM \"attachments\" WHERE \"attachments\".\"type\" = 'video' AND (view_count &amp;gt; 100)"&lt;/span&gt;
&lt;span class="no"&gt;PopularVideoQuery&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="nf"&gt;call&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="nf"&gt;to_sql&lt;/span&gt;
&lt;span class="c1"&gt;# "SELECT \"articles\".* FROM \"articles\" WHERE \"articles\".\"type\" = 'video' AND (view_count &amp;gt; 100)"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果想进一步支持链式调用的话，也很简单。只需要让&lt;code&gt;call&lt;/code&gt;方法遵循&lt;code&gt;ActiveRecord::Relation&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;BaseQuery&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;|&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;other&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="no"&gt;ChainedQuery&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&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;relation&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="n"&gt;other&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;relation&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="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ChainedQuery&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;BaseQuery&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="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@block&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;block&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;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;relation&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@block.call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;relation&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="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;WithStatusQuery&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;BaseQuery&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;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;status&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;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;relation&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;relation&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="ss"&gt;status: &lt;/span&gt;&lt;span class="vi"&gt;@status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;WithStatusQuery&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="ss"&gt;:published&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="no"&gt;PopularVideoQuery&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;
&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&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="nf"&gt;to_sql&lt;/span&gt;
&lt;span class="c1"&gt;# "SELECT \"articles\".* FROM \"articles\" WHERE \"articles\".\"status\" = 'published' AND \"articles\".\"type\" = 'video' AND (view_count &amp;gt; 100)"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在，我们得到了一个封装所有查询逻辑，可重用，提供简洁接口并易于测试的类。&lt;/p&gt;
&lt;h2 id="5. View Objects (Serializer/Presenter)"&gt;5. View Objects (Serializer/Presenter)&lt;/h2&gt;
&lt;p&gt;View Object 适用于将 View 中的数据及相关计算从 Controller 和 Model 抽离出来，如一个网站的 HTML 页面或 API 终端请求的 JSON 响应。&lt;/p&gt;
&lt;h2 id="示例"&gt;示例&lt;/h2&gt;
&lt;p&gt;View 中一般通常存在以下计算：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;根据服务器协议和图片路径创建图片 URL&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;p&gt;View 中包含了太多计算逻辑。&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 重构之前&lt;/span&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="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;span class="c1"&gt;#/app/views/articles/show.html.erb&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sx"&gt;% content_for &lt;/span&gt;&lt;span class="ss"&gt;:header&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="sx"&gt;%&amp;gt;
 &amp;lt;title&amp;gt;&lt;/span&gt;
     &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sx"&gt;%= @article.title_for_head_tag || I18n.t('default_title_for_head') %&amp;gt;
 &amp;lt;/title&amp;gt;
 &amp;lt;meta name=&lt;/span&gt;&lt;span class="s1"&gt;'description'&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;%= @article.description_for_head_tag || I18n.t('default_description_for_head') %&amp;gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;meta&lt;/span&gt; &lt;span class="n"&gt;property&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"og:type"&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"article"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;meta&lt;/span&gt; &lt;span class="n"&gt;property&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"og:title"&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;%= @article.title %&amp;gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sx"&gt;% if &lt;/span&gt;&lt;span class="vi"&gt;@article.description_for_head_tag&lt;/span&gt; &lt;span class="o"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;meta&lt;/span&gt; &lt;span class="n"&gt;property&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"og:description"&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;%= @article.description_for_head_tag %&amp;gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sx"&gt;% end &lt;/span&gt;&lt;span class="o"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sx"&gt;% if &lt;/span&gt;&lt;span class="vi"&gt;@article.image&lt;/span&gt; &lt;span class="o"&gt;%&amp;gt;&lt;/span&gt;
     &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;meta&lt;/span&gt; &lt;span class="n"&gt;property&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"og:image"&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;%= "&lt;/span&gt;&lt;span class="c1"&gt;#{request.protocol}#{request.host_with_port}#{@article.main_image}" %&amp;gt;"&amp;gt;&lt;/span&gt;
  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sx"&gt;% end &lt;/span&gt;&lt;span class="o"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sx"&gt;% end &lt;/span&gt;&lt;span class="o"&gt;%&amp;gt;&lt;/span&gt;

&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sx"&gt;% if &lt;/span&gt;&lt;span class="vi"&gt;@article.image&lt;/span&gt; &lt;span class="o"&gt;%&amp;gt;&lt;/span&gt;
 &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sx"&gt;%= image_tag @article.image.url %&amp;gt;
&amp;lt;% else %&amp;gt;
 &amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;image_tag&lt;/span&gt; &lt;span class="s1"&gt;'no-image.png'&lt;/span&gt;&lt;span class="o"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sx"&gt;% end &lt;/span&gt;&lt;span class="o"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;h1&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
 &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sx"&gt;%= @article.title %&amp;gt;
&amp;lt;/h1&amp;gt;

&amp;lt;p&amp;gt;
 &amp;lt;%=&lt;/span&gt; &lt;span class="vi"&gt;@article.text&lt;/span&gt; &lt;span class="o"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/p&amp;gt;

&amp;lt;% if @article.author %&amp;gt;
&amp;lt;p&amp;gt;
 &amp;lt;%= "&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="vi"&gt;@article.author.first_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sr"&gt; &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="vi"&gt;@article.author.last_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sr"&gt;" %&amp;gt;
&amp;lt;/&lt;/span&gt;&lt;span class="nb"&gt;p&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;%&lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="sx"&gt;%&amp;gt;

&amp;lt;p&amp;gt;&lt;/span&gt;
 &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sx"&gt;%= t('date') %&amp;gt;
 &amp;lt;%=&lt;/span&gt; &lt;span class="vi"&gt;@article.created_at.strftime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"%B %e, %Y"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/p&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为了解决这个问题，可以先创建一个 presenter 基类，然后再创建一个&lt;code&gt;ArticlePresenter&lt;/code&gt;类的实例。&lt;code&gt;ArticlePresenter&lt;/code&gt;方法根据适当的计算返回想要的标签。&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="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;span class="c1"&gt;#/app/presenters/base_presenter.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BasePresenter&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;object&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="vi"&gt;@object&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;object&lt;/span&gt;
   &lt;span class="vi"&gt;@template&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;template&lt;/span&gt;
 &lt;span class="k"&gt;end&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;presents&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="n"&gt;define_method&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="k"&gt;do&lt;/span&gt;
     &lt;span class="vi"&gt;@object&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;h&lt;/span&gt;
   &lt;span class="vi"&gt;@template&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;#/app/helpers/application_helper.rb&lt;/span&gt;
&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;ApplicationHelper&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;presenter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;klass&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;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;class&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;Presenter"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;constantize&lt;/span&gt;
    &lt;span class="n"&gt;presenter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;klass&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;model&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="k"&gt;yield&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;presenter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;block_given?&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;#/app/presenters/article_presenters.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ArticlePresenter&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;BasePresenter&lt;/span&gt;
 &lt;span class="n"&gt;presents&lt;/span&gt; &lt;span class="ss"&gt;:article&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;:text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;to: :article&lt;/span&gt;

 &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;meta_title&lt;/span&gt;
   &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="o"&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;title_for_head_tag&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="no"&gt;I18n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'default_title_for_head'&lt;/span&gt;&lt;span class="p"&gt;)&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="ss"&gt;:title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&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;meta_description&lt;/span&gt;
   &lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&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;description_for_head_tag&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="no"&gt;I18n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'default_description_for_head'&lt;/span&gt;&lt;span class="p"&gt;)&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="ss"&gt;:meta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;content: &lt;/span&gt;&lt;span class="n"&gt;description&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;og_type&lt;/span&gt;
   &lt;span class="n"&gt;open_graph_meta&lt;/span&gt; &lt;span class="s2"&gt;"article"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"og:type"&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;og_title&lt;/span&gt;
   &lt;span class="n"&gt;open_graph_meta&lt;/span&gt; &lt;span class="s2"&gt;"og:title"&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="nf"&gt;title&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;og_description&lt;/span&gt;
   &lt;span class="n"&gt;open_graph_meta&lt;/span&gt; &lt;span class="s2"&gt;"og:description"&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="nf"&gt;description_for_head_tag&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;description_for_head_tag&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;og_image&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;image&lt;/span&gt;
     &lt;span class="n"&gt;image&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;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;protocol&lt;/span&gt;&lt;span class="si"&gt;}#{&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;host_with_port&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;main_image&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
     &lt;span class="n"&gt;open_graph_meta&lt;/span&gt; &lt;span class="s2"&gt;"og:image"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;image&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;author_name&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;author&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="ss"&gt;:p&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;article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;author&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;first_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;article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;author&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;last_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
   &lt;span class="k"&gt;end&lt;/span&gt;
 &lt;span class="k"&gt;end&lt;/span&gt;

 &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;image&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;image&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;image_tag&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;image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;url&lt;/span&gt;
  &lt;span class="k"&gt;else&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;image_tag&lt;/span&gt; &lt;span class="s1"&gt;'no-image.png'&lt;/span&gt;
  &lt;span class="k"&gt;end&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;open_graph_meta&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;property&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="ss"&gt;:meta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;content: &lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;property: &lt;/span&gt;&lt;span class="n"&gt;property&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;现在 View 中不包含任何与计算相关的逻辑，所有组件都抽离到了 presenter 中，并可在其他 View 中重用，如下：&lt;/p&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;#/app/views/articles/show.html.erb
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="n"&gt;presenter&lt;/span&gt; &lt;span class="vi"&gt;@article&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;article_presenter&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
 &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="n"&gt;content_for&lt;/span&gt; &lt;span class="ss"&gt;:header&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
   &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;article_presenter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;meta_title&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
   &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;article_presenter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;meta_description&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
   &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;article_presenter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;og_type&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
   &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;article_presenter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;og_title&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
   &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;article_presenter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;og_description&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
   &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;article_presenter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;og_image&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
 &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;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;article_presenter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;image&lt;/span&gt;&lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
 &lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt; &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;article_presenter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;title&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt; &lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
 &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;  &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;article_presenter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt; &lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
 &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;article_presenter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;author_name&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&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;h2 id="6. Policy Objects"&gt;6. Policy Objects&lt;/h2&gt;
&lt;p&gt;Policy Object 模式与 Service Object 模式相似，前者负责读操作，后者负责写操作。Policy Object 模式适用于封装复杂的业务规则，并易于替换。比如，可以使用一个访客 Policy Object 来识别一个访客是否可以访问某些特定资源。当用户是管理员时，可以很方便的将访客 Policy Object 替换为包含管理员规则的管理员 Policy Object。&lt;/p&gt;
&lt;h2 id="示例"&gt;示例&lt;/h2&gt;
&lt;p&gt;在用户创建一个项目之前，Controller 将检查当前用户是否为管理者，是否有权限创建项目，当前用户项目数量是否小于最大值，以及在 Redis 中是否存在阻塞的项目创建。&lt;/p&gt;
&lt;h2 id="问题分析"&gt;问题分析&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;自由 Controller 知道项目创建的规则&lt;/li&gt;
&lt;li&gt;Controller 包含了额外的逻辑代码&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProjectsController&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;can_create_project?&lt;/span&gt;
       &lt;span class="vi"&gt;@project&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Project&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;project_params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;json: &lt;/span&gt;&lt;span class="vi"&gt;@project&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;status: :created&lt;/span&gt;
     &lt;span class="k"&gt;else&lt;/span&gt;
       &lt;span class="n"&gt;head&lt;/span&gt; &lt;span class="ss"&gt;:unauthorized&lt;/span&gt;
     &lt;span class="k"&gt;end&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;can_create_project?&lt;/span&gt;
     &lt;span class="n"&gt;current_user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;manager?&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
       &lt;span class="n"&gt;current_user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;projects&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;lt;&lt;/span&gt; &lt;span class="no"&gt;Project&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max_count&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&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;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'projects_creation_blocked'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s1"&gt;'1'&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;project_params&lt;/span&gt;
     &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:project&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;permit&lt;/span&gt;&lt;span class="p"&gt;(&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;:description&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;redis&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;current&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;User&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;enum&lt;/span&gt; &lt;span class="ss"&gt;role: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:manager&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:employee&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:guest&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;为了让 Controller 瘦身，可以将规则代码移到 Model 中。所有检查逻辑都将移出 Controller。但是这样一来，User 类就知道了 Redis 和 Project 类的逻辑。并且 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;User&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;enum&lt;/span&gt; &lt;span class="ss"&gt;role: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:manager&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:employee&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:guest&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;can_create_project?&lt;/span&gt;
    &lt;span class="n"&gt;manager?&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&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="nf"&gt;count&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Project&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max_count&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&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;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'projects_creation_blocked'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s1"&gt;'1'&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;redis&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;current&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;ProjectsController&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;current_user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;can_create_project?&lt;/span&gt;
       &lt;span class="vi"&gt;@project&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Project&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;project_params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;json: &lt;/span&gt;&lt;span class="vi"&gt;@project&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;status: :created&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
       &lt;span class="n"&gt;head&lt;/span&gt; &lt;span class="ss"&gt;:unauthorized&lt;/span&gt;
    &lt;span class="k"&gt;end&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;project_params&lt;/span&gt;
     &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:project&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;permit&lt;/span&gt;&lt;span class="p"&gt;(&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;:description&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;在这种情况下，可以将这些规则抽取到一个 Policy Object 中，从而使 Controller 和 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;CreateProjectPolicy&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="n"&gt;redis_client&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="vi"&gt;@redis_client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;redis_client&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;allowed?&lt;/span&gt;
    &lt;span class="vi"&gt;@user.manager&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;below_project_limit&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;project_creation_blocked&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;below_project_limit&lt;/span&gt;
    &lt;span class="vi"&gt;@user.projects.count&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Project&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max_count&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;project_creation_blocked&lt;/span&gt;
    &lt;span class="vi"&gt;@redis_client.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'projects_creation_blocked'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;'1'&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;ProjectsController&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;allowed?&lt;/span&gt;
       &lt;span class="vi"&gt;@project&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Project&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;project_params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;json: &lt;/span&gt;&lt;span class="vi"&gt;@project&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="n"&gt;created&lt;/span&gt;
     &lt;span class="k"&gt;else&lt;/span&gt;
       &lt;span class="n"&gt;head&lt;/span&gt; &lt;span class="ss"&gt;:unauthorized&lt;/span&gt;
     &lt;span class="k"&gt;end&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;project_params&lt;/span&gt;
     &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:project&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;permit&lt;/span&gt;&lt;span class="p"&gt;(&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;:description&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;policy&lt;/span&gt;
     &lt;span class="no"&gt;CreateProjectPolicy&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;current_user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;redis&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;redis&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;current&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;User&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;enum&lt;/span&gt; &lt;span class="ss"&gt;role: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:manager&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:employee&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:guest&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;最终的结果是一个干净的 Controller 和 Model。Policy Object 封装了所有权限检查逻辑，并且所有外部依赖都从 Controller 注入到 Policy Object 中。所有的类都各司其职。&lt;/p&gt;
&lt;h2 id="7. Decorators"&gt;7. Decorators&lt;/h2&gt;
&lt;p&gt;Decorator 模式允许我们给某个类的实例添加各种辅助行为，而不影响相同类的其他实例。该设计模式广泛用于在不同类之间划分功能，也可以用来替代继承以遵循单一原则。&lt;/p&gt;
&lt;h2 id="示例"&gt;示例&lt;/h2&gt;
&lt;p&gt;假设 View 中存在许多计算：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;根据&lt;code&gt;title_for_head&lt;/code&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;/li&gt;
&lt;/ul&gt;
&lt;h2 id="问题分析"&gt;问题分析&lt;/h2&gt;
&lt;p&gt;View 中包含了过多的计算逻辑：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;#/app/controllers/cars_controller.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CarsController&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;show&lt;/span&gt;
   &lt;span class="vi"&gt;@car&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Car&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;span class="c1"&gt;#/app/views/cars/show.html.erb&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sx"&gt;% content_for &lt;/span&gt;&lt;span class="ss"&gt;:header&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="sx"&gt;%&amp;gt;
 &amp;lt;title&amp;gt;&lt;/span&gt;
   &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sx"&gt;% if &lt;/span&gt;&lt;span class="vi"&gt;@car.title_for_head&lt;/span&gt; &lt;span class="o"&gt;%&amp;gt;&lt;/span&gt;
     &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sx"&gt;%="&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt; &lt;span class="vi"&gt;@car.title_for_head&lt;/span&gt; &lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sx"&gt; | &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'beautiful_cars'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sx"&gt;" %&amp;gt;
   &amp;lt;% else %&amp;gt;
     &amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'beautiful_cars'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&amp;gt;&lt;/span&gt;
   &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sx"&gt;% end &lt;/span&gt;&lt;span class="o"&gt;%&amp;gt;&lt;/span&gt;
 &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/title&amp;gt;
 &amp;lt;% if @car.description_for_head%&amp;gt;
   &amp;lt;meta name='description' content= "&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sx"&gt;%= @car.description_for_head %&amp;gt;}"&amp;gt;
 &amp;lt;% end %&amp;gt;
&amp;lt;% end %&amp;gt;

&amp;lt;% if @car.image %&amp;gt;
 &amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;image_tag&lt;/span&gt; &lt;span class="vi"&gt;@car.image.url&lt;/span&gt; &lt;span class="o"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sx"&gt;% else &lt;/span&gt;&lt;span class="o"&gt;%&amp;gt;&lt;/span&gt;
 &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sx"&gt;%= image_tag 'no-images.png'%&amp;gt;
&amp;lt;% end %&amp;gt;
&amp;lt;h1&amp;gt;
 &amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'brand'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&amp;gt;&lt;/span&gt;
 &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sx"&gt;% if &lt;/span&gt;&lt;span class="vi"&gt;@car.brand&lt;/span&gt; &lt;span class="o"&gt;%&amp;gt;&lt;/span&gt;
   &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sx"&gt;%= @car.brand %&amp;gt;
 &amp;lt;% else %&amp;gt;
    &amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'undefined'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&amp;gt;&lt;/span&gt;
 &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sx"&gt;% end &lt;/span&gt;&lt;span class="o"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/h1&amp;gt;

&amp;lt;p&amp;gt;
 &amp;lt;%= t('model') %&amp;gt;
 &amp;lt;% if @car.model %&amp;gt;
   &amp;lt;%= @car.model %&amp;gt;
 &amp;lt;% else %&amp;gt;
    &amp;lt;%= t('undefined') %&amp;gt;
 &amp;lt;% end %&amp;gt;
&amp;lt;/&lt;/span&gt;&lt;span class="nb"&gt;p&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;p&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
 &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sx"&gt;%= t('notes') %&amp;gt;
 &amp;lt;% if @car.notes %&amp;gt;
   &amp;lt;%=&lt;/span&gt; &lt;span class="vi"&gt;@car.notes&lt;/span&gt; &lt;span class="o"&gt;%&amp;gt;&lt;/span&gt;
 &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sx"&gt;% else &lt;/span&gt;&lt;span class="o"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sx"&gt;%= t('undefined') %&amp;gt;
 &amp;lt;% end %&amp;gt;
&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;
 &amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'owner'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&amp;gt;&lt;/span&gt;
 &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sx"&gt;% if &lt;/span&gt;&lt;span class="vi"&gt;@car.owner&lt;/span&gt; &lt;span class="o"&gt;%&amp;gt;&lt;/span&gt;
   &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sx"&gt;%= @car.owner %&amp;gt;
 &amp;lt;% else %&amp;gt;
    &amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'undefined'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&amp;gt;&lt;/span&gt;
 &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sx"&gt;% end &lt;/span&gt;&lt;span class="o"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/p&amp;gt;

&amp;lt;p&amp;gt;
 &amp;lt;%= t('city') %&amp;gt;
 &amp;lt;% if @car.city %&amp;gt;
   &amp;lt;%= @car.city %&amp;gt;
 &amp;lt;% else %&amp;gt;
    &amp;lt;%= t('undefined') %&amp;gt;
 &amp;lt;% end %&amp;gt;
&amp;lt;/&lt;/span&gt;&lt;span class="nb"&gt;p&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;p&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
 &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sx"&gt;%= t('owner_phone') %&amp;gt;
 &amp;lt;% if @car.phone %&amp;gt;
   &amp;lt;%=&lt;/span&gt; &lt;span class="vi"&gt;@car.phone&lt;/span&gt; &lt;span class="o"&gt;%&amp;gt;&lt;/span&gt;
 &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sx"&gt;% else &lt;/span&gt;&lt;span class="o"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sx"&gt;%= t('undefined') %&amp;gt;
 &amp;lt;% end %&amp;gt;
&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;
 &amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'state'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&amp;gt;&lt;/span&gt;
 &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sx"&gt;% if &lt;/span&gt;&lt;span class="vi"&gt;@car.used&lt;/span&gt; &lt;span class="o"&gt;%&amp;gt;&lt;/span&gt;
   &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sx"&gt;%= t('used') %&amp;gt;
 &amp;lt;% else %&amp;gt;
   &amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'new'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&amp;gt;&lt;/span&gt;
 &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sx"&gt;% end &lt;/span&gt;&lt;span class="o"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/p&amp;gt;

&amp;lt;p&amp;gt;
 &amp;lt;%= t('date') %&amp;gt;
 &amp;lt;%= @car.created_at.strftime("%B %e, %Y")%&amp;gt;
&amp;lt;/&lt;/span&gt;&lt;span class="nb"&gt;p&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以使用 Draper 这个装饰 gem，将所有逻辑抽取到&lt;code&gt;CarDecorator&lt;/code&gt;中。&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;#/app/controllers/cars_controller.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CarsController&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;show&lt;/span&gt;
   &lt;span class="vi"&gt;@car&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Car&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;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;#/app/decorators/car_decorator.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CarDecorator&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;meta_title&lt;/span&gt;
   &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
     &lt;span class="k"&gt;if&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;title_for_head&lt;/span&gt;
       &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&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;title_for_head&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="no"&gt;I18n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'beautiful_cars'&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="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'beautiful_cars'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
     &lt;span class="k"&gt;end&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="ss"&gt;:title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&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;meta_description&lt;/span&gt;
   &lt;span class="k"&gt;if&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;description_for_head&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="ss"&gt;:meta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="ss"&gt;content: &lt;/span&gt;&lt;span class="n"&gt;object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;description_for_head&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;image&lt;/span&gt;
   &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&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;image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;present?&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;url&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'no-images.png'&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;image_tag&lt;/span&gt; &lt;span class="n"&gt;result&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;brand&lt;/span&gt;
   &lt;span class="n"&gt;get_info&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;brand&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;model&lt;/span&gt;
   &lt;span class="n"&gt;get_info&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;model&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;notes&lt;/span&gt;
   &lt;span class="n"&gt;get_info&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;notes&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;owner&lt;/span&gt;
   &lt;span class="n"&gt;get_info&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;owner&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;city&lt;/span&gt;
   &lt;span class="n"&gt;get_info&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;city&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;owner_phone&lt;/span&gt;
   &lt;span class="n"&gt;get_info&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;phone&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;state&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;used&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="no"&gt;I18n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'used'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;I18n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'new'&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;created_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;created_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;"%B %e, %Y"&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;get_info&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;
   &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;present?&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'undefined'&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;改造后不包含任何计算的整洁 View：&lt;/p&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;#/app/views/cars/show.html.erb
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="n"&gt;content_for&lt;/span&gt; &lt;span class="ss"&gt;:header&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
 &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="vi"&gt;@car.meta_title&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
 &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="vi"&gt;@car.meta_description&lt;/span&gt;&lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&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;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="vi"&gt;@car.image&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt; &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'brand'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt; &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="vi"&gt;@car.brand&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt; &lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt; &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'model'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt; &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="vi"&gt;@car.model&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;  &lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt; &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'notes'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt; &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="vi"&gt;@car.notes&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;  &lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt; &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'owner'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt; &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="vi"&gt;@car.owner&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;  &lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt; &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'city'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt; &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="vi"&gt;@car.city&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;    &lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt; &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'owner_phone'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt; &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="vi"&gt;@car.phone&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt; &lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt; &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'state'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt; &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="vi"&gt;@car.state&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;   &lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt; &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'date'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt; &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="vi"&gt;@car.created_at&lt;/span&gt;&lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt; &lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="总结"&gt;总结&lt;/h2&gt;
&lt;p&gt;相信如上这些概念将有助于你了解在何时以及如何重构代码。这些工具可以帮助你有效的管理代码的复杂度。其实，在开发的最初就应该小心地规划代码逻辑的组织，这样就可以避免之后在重构上花费大量时间。&lt;/p&gt;</description>
      <author>freefishz</author>
      <pubDate>Wed, 30 Nov 2016 10:47:40 +0800</pubDate>
      <link>https://ruby-china.org/topics/31742</link>
      <guid>https://ruby-china.org/topics/31742</guid>
    </item>
    <item>
      <title>另一篇 The Rails Doctrine - 中文翻译</title>
      <description>&lt;blockquote&gt;
&lt;p&gt;最近在 Ruby China 看到 &lt;a href="/huacnlee" class="user-mention" title="@huacnlee"&gt;&lt;i&gt;@&lt;/i&gt;huacnlee&lt;/a&gt; 分享的 &lt;a href="https://ruby-china.org/topics/30703" title=""&gt;The Rails Doctrine - 中文翻译&lt;/a&gt;，拜读之下受益匪浅。
唯一遗憾是原译文不大符合我的阅读习惯，因此就有了这篇译文的诞生。希望与我有相似阅读习惯的小伙伴们会喜欢。&lt;img title=":smile:" alt="😄" src="https://twemoji.ruby-china.com/2/svg/1f604.svg" class="twemoji"&gt;
本翻译参考了 DHH 的原文 &lt;a href="http://rubyonrails.org/doctrine/" rel="nofollow" target="_blank" title=""&gt;The Rails Doctrine&lt;/a&gt; 和 &lt;a href="/huacnlee" class="user-mention" title="@huacnlee"&gt;&lt;i&gt;@&lt;/i&gt;huacnlee&lt;/a&gt; 的译文，在这里对两位表示由衷的感谢！
译文中如有不当之处，欢迎指正。
PS. 终于赶在十一长假结束前结束了翻译&lt;img title=":sleeping:" alt="😴" src="https://twemoji.ruby-china.com/2/svg/1f634.svg" class="twemoji"&gt; 。DHH 的文风果然名不虚传&lt;img title=":confounded:" alt="😖" src="https://twemoji.ruby-china.com/2/svg/1f616.svg" class="twemoji"&gt;  。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="Rails 信条"&gt;Rails 信条&lt;/h2&gt;
&lt;p&gt;Ruby on Rails 能够现象般地崛起并取得如此卓越的成就，很大程度上应归功于其对新技术的运用及切入的时机。但技术优势一般会随着时间推移而逐渐削弱，而好的时机也并不总会长久相伴。Rails 为何能始终保持与时俱进，并不断扩大其影响力和社区呢？这里有必要给大众一个合理的解释。我认为最主要的原因就是其一直坚守的那些饱受争议的信条。&lt;/p&gt;

&lt;p&gt;这些信条在过去的十年中也在不断地演进，但最重要的依旧还是那些最基础的信条。我不会自诩是这些信条的原创者。毕竟，Rails 取得的最大的成就就是：围绕诸多离经叛道的思想（主要是关于程序设计和程序员本质），融合并培养了一个如此强大的社群。&lt;/p&gt;

&lt;p&gt;闲话到此，以下就是 Rails 中最重要的 9 个信条，请用心领悟：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="#%E5%B0%BD%E5%8F%AF%E8%83%BD%E5%9C%B0%E8%AE%A9%E7%A8%8B%E5%BA%8F%E5%91%98%E5%BC%80%E5%BF%83" title=""&gt;尽可能地让程序员开心&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#%E7%BA%A6%E5%AE%9A%E4%BC%98%E4%BA%8E%E9%85%8D%E7%BD%AE" title=""&gt;约定优于配置&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#%E4%B8%BB%E5%8E%A8%E6%8E%A8%E8%8D%90" title=""&gt;主厨推荐&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#%E5%A4%9A%E5%85%83%E5%8C%96" title=""&gt;多元化&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#%E6%8E%A8%E5%B4%87%E4%BC%98%E7%BE%8E%E7%9A%84%E4%BB%A3%E7%A0%81" title=""&gt;推崇优美的代码&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#%E6%8F%90%E4%BE%9B%E5%BC%80%E5%8F%91%E5%88%A9%E5%99%A8" title=""&gt;提供开发利器&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#%E9%9D%A2%E5%90%91%E7%BB%BC%E5%90%88%E5%BA%94%E7%94%A8" title=""&gt;面向综合应用&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#%E6%BC%94%E8%BF%9B%E4%BC%98%E4%BA%8E%E7%A8%B3%E5%AE%9A" title=""&gt;演进优于稳定&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#%E5%85%BC%E6%94%B6%E5%B9%B6%E8%93%84" title=""&gt;兼收并蓄&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="尽可能地让程序员开心"&gt;尽可能地让程序员开心&lt;/h2&gt;
&lt;p&gt;没有 Ruby 就不会有 Rails，因此，第一信条必然是来自于创造 Ruby 的核心理念。&lt;/p&gt;

&lt;p&gt;Ruby 最初的理念确实将“程序员的开心”放在最首要的位置，也就是将它放在许多曾经驱动程序设计语言和生态圈前进的真理之前。&lt;/p&gt;

&lt;p&gt;当 Python 推崇“完成一件事情，有且最好只有一种方式”，Ruby 却沉醉于表达方式的丰富多彩和优雅精妙。当 Java 因其保护程序员自身的特性而备受推崇，Ruby 却在欢迎工具里就附上了自尽的绳子。当 Smalltalk 专注于消息传递的纯粹性，Ruby 却近乎贪婪地增加关键字和构造器。&lt;/p&gt;

&lt;p&gt;Ruby 如此与众不同是因为它非常尊重事物的多样性。而这些多样性中的绝大部分恰是为了满足程序员开心而服务的。这种追求不仅引起了 Ruby 与其他编程语言环境的争论，也开启了主流文化对“究竟什么是程序员，以及他们应该如何工作的”的认知。&lt;/p&gt;

&lt;p&gt;Ruby 不仅了解程序员的编程感受，而且还会尽量满足甚至改善他们的编程感受。无论这些感受是不当的、异想天开的或是令人愉悦的。Matz 跨越了复杂度如此惊人的实现门槛，才让机器最终面带微笑以取悦它的人类伙伴。Ruby 充满了视觉假象，那些表面上看上去如此简洁、清晰和优美的代码，其背后的实现却如杂技般错综复杂。当然这些选择并不是没有代价的（不信的话，可以问问 JRuby 那些尝试对 Ruby 做逆向工程的人），这也恰恰是这些选择如此值得称赞的原因。&lt;/p&gt;

&lt;p&gt;正是这种从另一个角度致敬程序设计和程序员的方式，使我深深的爱上了 Ruby。这不仅仅因为它的简单易用和充满美学的设计，也不是任何一项单一的技术成就，而是一种愿景，一种反文化。Ruby 就是程序设计领域中与现有专业程序设计模式相左，而又符合人类思维习惯的缺失部分。&lt;/p&gt;

&lt;p&gt;我曾经说过，发现 Ruby 就像是找到了完全适合我大脑思维习惯的魔术手套。比我曾经梦想过的任何手套都要来得合用。它甚至成为了我从“写程序只是因为我需要程序”到“写程序是因为我爱上了这种思维的运用和表达方式”的转折点。就像是找到了“流动之泉”（流动指的是著作 &lt;a href="http://www.amazon.com/Flow-Harper-Perennial-Modern-Classics-ebook/dp/B000W94FE6" rel="nofollow" target="_blank" title=""&gt;Flow: The Psychology of Optimal Experince&lt;/a&gt; 中描述的一种意识状态，处在这种状态中的人通常非常愉悦，富有创造力，并且完全沉醉其中），并能随意进入其中。熟悉 Csikszentmihalyi（上述著作的作者）著作的人都应该知道，这种影响力简直是有过之而无不及。&lt;/p&gt;

&lt;p&gt;毫不夸张的说，Ruby 改变了我，并为我设定了人生努力的方向。这种启示是如此深刻，以致于让我对布道这个 Matz 的作品充满了使命感，也就是去传播这个意义深远的作品和它的优点。&lt;/p&gt;

&lt;p&gt;读到这里，我可以想象你们中绝大部份的人都会难以置信地摇摇头。我不怪你们。当我对程序设计的认识还处在“程序设计只不过是个工具”的阶段时，如果有人跟我描述上述经历，我也会摇头的。并且，我还可能嘲笑这种过分夸张近似于宗教语言般的描述。但这确实是我的真实想法，也是我的肺腑之言，即便这也许会让某些人或绝大部分人感到不适。&lt;/p&gt;

&lt;p&gt;无论如何，这对 Rails 来说究竟意味着什么，以及这个理念是如何指引 Rails 持续演进的呢？要回答这个问题，我想先来看看另一条早期经常被用来描述 Ruby 的原则是非常具有启发性的：最小惊奇原则。即 Ruby 应该如你预期般运行。通过以下与 Python 对比的例子可以非常容易地理解这个原则：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="err"&gt;$&lt;/span&gt; &lt;span class="n"&gt;irb&lt;/span&gt;
&lt;span class="n"&gt;irb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&lt;span class="mo"&gt;001&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;exit&lt;/span&gt;
&lt;span class="err"&gt;$&lt;/span&gt; &lt;span class="n"&gt;irb&lt;/span&gt;
&lt;span class="n"&gt;irb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&lt;span class="mo"&gt;001&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;quit&lt;/span&gt;

&lt;span class="err"&gt;$&lt;/span&gt; &lt;span class="n"&gt;python&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;exit&lt;/span&gt;
&lt;span class="no"&gt;Use&lt;/span&gt; &lt;span class="nb"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="no"&gt;Ctrl&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="no"&gt;D&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="nf"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt; &lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="nb"&gt;exit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ruby 可以同时接受 &lt;code&gt;exit&lt;/code&gt; 和 &lt;code&gt;quit&lt;/code&gt; 来退出终端交互界面，以此来满足程序员那显而易见的需求。而 Python 则会迂腐的指导程序员如何正确完成操作，即便它已经明确地知道程序员想要干什么（因为它给出了错误信息嘛）。这就是一个非常清晰而又短小的解释最小惊奇原则的例子。&lt;/p&gt;

&lt;p&gt;最小惊奇原则最终在 Ruby 社区失宠的原因是：这条原则本身是非常主观的。最小惊奇原则，惊奇谁呢？显然是 Matz，以及那些和 Matz 具有相似思维方式的人。但是，随着 Ruby 社区的逐渐壮大，和 Matz 思维方式相左的人数比例也越来越多，这也成为了邮件列表里那些毫无意义的争论之源。因此，这条原则最终淡出了人们的视线，以避免“甲男是否对乙物是否惊奇”的争论无处不在。&lt;/p&gt;

&lt;p&gt;但这跟 Rails 又有什么关系呢？其实，Rails 最初就是基于一个与最小惊奇（Matz）原则相似的原则设计的。这个原则就是 DHH 的 &lt;strong&gt;璀璨微笑原则&lt;/strong&gt; ，简单来说就是：接口设计的最大考量是如何让我尽可能地开怀大笑。当我把这条原则写出来的时候，连我自己都觉得这听起来有些滑稽和自恋。&lt;/p&gt;

&lt;p&gt;然而，正是由于这种最初的深度自恋才造就了 Ruby 或 Rails 这样的作品，并且这两个项目都是从单个作者的思想中迸发出来的。当然，这样说有将我自己的创作动机强加到 Matz 身上之嫌，因此我将声明缩小到我所知道的范围：我创作 Rails 纯粹是为了我自己。第一条也是最首要的原则就是为了能让我微笑。尽管 Rails 有各种各样的功能，但这些功能的最终目的都是为了能让我更好的享受人生，是为了帮助我改善为网络信息系统需求所争论不休的日常生活。&lt;/p&gt;

&lt;p&gt;就像 Matz 一样，有时我也会做出一些愚蠢的决定来实现我自己的理念。其中的一个例子就是 Inflector，一个恰可以完成类到表映射的类（包含规则和不规则的情况），譬如 Person 类对应到 People 表、Analysis 对应到 Analyses，Comment 对应到 Comments。这种行为现在已成了 Rails 中毋庸置疑的一部分，但当我们还在宣扬该原则及其重要性的早期，争议的怒火曾经肆意蔓延。&lt;/p&gt;

&lt;p&gt;另外一个例子是“为了减少了些许实现的工作量，却几乎触发了大量程序员的恐慌”，如：&lt;code&gt;Array#second&lt;/code&gt; 到 &lt;code&gt;#fifth&lt;/code&gt;（以及额外增加 &lt;code&gt;#forty_two&lt;/code&gt;）。这些访问器别名曾严重冒犯了一位非常直言不讳的支持者，他认为这些过度设计（以及额外的那个 &lt;code&gt;#forty_two&lt;/code&gt; 都快接近文明的终点了。这是一个关于 42 的梗，请自行搜索答案，&lt;img title=":smile:" alt="😄" src="https://twemoji.ruby-china.com/2/svg/1f604.svg" class="twemoji"&gt; ）完全可以改写成 &lt;code&gt;Array#[1]&lt;/code&gt;、&lt;code&gt;Array#[2]&lt;/code&gt;（和 &lt;code&gt;Array[41]&lt;/code&gt;）。&lt;/p&gt;

&lt;p&gt;但是，时至今日上述两个决定依然能令我开怀。我非常享受可以在测试用例或终端里输入 &lt;code&gt;people.third&lt;/code&gt;。这并不符合逻辑，也不高效，甚至有些病态。
但却能使我持续开怀，并由此充满信念并丰富我的人生，继而证明在服务了 Rails 12 年之后依然参与其中是完全正确的选择。&lt;/p&gt;

&lt;p&gt;和性能优化不一样，开心优化很难衡量。这使得开心优化几乎成了不科学的无谓之举。即使有些人并未完全放弃，但也觉得这是无关紧要的事情。因为程序员一直被教导要执着并攻克于可被衡量的事物，也就是那些可以明确指出 A 要比 B 好的事物。&lt;/p&gt;

&lt;p&gt;尽管对开心的追求很难在微观角度加以衡量，但从宏观角度来看却很清晰。Ruby on Rails 社区中的很多人明显是因为这样追求的才聚集于此的。他们因拥有更好的，更能带来满足感的职业生涯而骄傲。而这条原则正好处于这些情感的汇集之地，可想而知它的成功是必然的。&lt;/p&gt;

&lt;p&gt;因此，我们可以得出以下结论：尽可能让程序员开心可能是造就 Ruby on Rails 最关键的因素，它应该陪伴 Rails 一直走下去。&lt;/p&gt;
&lt;h2 id="约定优于配置"&gt;约定优于配置&lt;/h2&gt;
&lt;p&gt;一条 Rails 早期广为流传的箴言是这样的：你并不是唯一美丽的雪花。如果能放弃那些毫无意义的个人喜好，你就可以跳出诸多无谓选择的牢笼，在那些真正重要的领域快速前进。&lt;/p&gt;

&lt;p&gt;有谁会在乎你的数据库主键采用什么格式吗？选择 id，postId，posts_id 或 pid 真的那么重要吗？这真的值得反复讨论才做出决定吗？当然不。&lt;/p&gt;

&lt;p&gt;Rails 的部分使命就是，帮助那些创建网络信息系统的开发者在日益庞大并一再出现的决策丛林中劈荆斩棘。其实这些成千上万的的决策只需要做一次就够了，如果有人能帮你做，那就再好不过了。&lt;/p&gt;

&lt;p&gt;约定优于配置，不仅可以让我们避免许多无谓的思考，而且为更深层的抽象提供了肥沃的土壤。如果我们可以遵循 Person 类到 people 表的映射约定，那么我们也能用相同的约定来为 has_many :people 定义的关联找到 Person 类。优良约定的威力就在于：每个广泛使用它们的领域都会受益颇丰。&lt;/p&gt;

&lt;p&gt;不仅专家可以借此提升生产力，新手们的入门门槛也可以大大降低。Rails 中包含了如此之多的约定，以至于新手们即使都没有察觉到它们的存在就可以从中获益。就算不了解每件事情的底细，也可能创建出伟大的应用。&lt;/p&gt;

&lt;p&gt;如果你的框架仅仅是一本厚厚的教科书，而你的新应用只不过是一张白纸，你是不可能成功的。仅仅是找出从哪里以及如何开始就会花费你大量的精力。估计项目开始的大半时间基本上就耗在寻找哪个才是正确的入口上了。&lt;/p&gt;

&lt;p&gt;即使你已经了解了所有组件是如何一起工作的，这种情况也不会有所好转。但是，如果每次变动都有一个明确清晰的应对话，我们就可以快速地略过应用中的大部分工作。因为这些内容和其它应用中曾出现过的内容是相同或类似。所谓各得其所，物尽其用就是这个样子。从这种意义上来讲，约束甚至让那些最有能力的人获得了解放。&lt;/p&gt;

&lt;p&gt;当然和其它事物一样，约定的力量也并不是没有风险的。Rails 如此简洁就可以完成如此多的事情，这很容易就让人觉得应用中的每个部分都可以由预定的模版来完成。但是大部分值得创建的应用总会包含一些本身特有的元素，尽管它们可能只占 %5 或 1%，但总会有的。&lt;/p&gt;

&lt;p&gt;因此，最难的部分是知道何时我们应该打破约定。那么什么时候值得偏离正轨呢？我认为，大多数想成为唯一美丽雪花的冲动都是不明智的，并且大大低估了脱离 Rails 的成本。但是仅打破该打破的那一小部分应该是没有问题的，当然你的仔细斟酌每个细节。&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;因此，在 Rails 中我们决定舍弃小利：程序员在工具箱里挑选每件工具的权利，来换取更大的利益：一整套更好的工具箱。这个决定回报颇丰：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;人多势众&lt;/strong&gt;：当大家都用默认的方式使用 Rails 时，我们就拥有了共同的体验。教授和帮助他人就会变得容易的多，同时讨论也有了共同的基础。就好比我们都在昨晚 7 点看了相同的节目，第二天我们的讨论就有了共同话题。这种共同的体验进而促成了更有凝聚力的社区。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;日益完善的基础工具箱&lt;/strong&gt;：Rails 作为一个全栈框架包含了许多活动组件，它们之间如何协同工作与它们单独运行具有相同的重要性。软件工程的痛点大部分不是来自于组件内部，而是组件之前的相互协作。当我们都在一起努力工作修复这些大家都会碰到的痛点（因相同的的配置和使用方式而获得一致的错误）时，这些痛点就会越来越少。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;按需替换&lt;/strong&gt;：尽管 Rails 是一个主厨推荐的技术栈，你仍然有机会替换掉其中的某些框架或类库。只是不建议你这么做。当你需要为某些特定的场景开发出一套清晰的个性化工具箱时，再来考虑这些决定吧。&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;即使那些最博学多才且经验丰富的 Rails 程序员也不可能抵触菜单上的每道菜（如果是的话，那他们就不会选择继续使用 Rails 了）。因此，他们会谨慎的挑选替代者，并和其他人一齐享受剩下的部分。&lt;/p&gt;
&lt;h2 id="多元化"&gt;多元化&lt;/h2&gt;
&lt;p&gt;大家都对挑选并遵循单个核心理念来构建自己的基础架构具有强烈的诉求。这个诉求如此纯粹，以至于程序员都会自然而然地被它所吸引。&lt;/p&gt;

&lt;p&gt;Rails 不遵循上述理念。它不是单件完美剪裁的衣服，它是一床棉被，是诸多不同理念和模式的组合体。如果将它们分开来一一对比的话，其中许多通常看来还是相互抵触的。当然，我们也不会这么去做。这又不是仅有一个赢家的超级理念锦标赛。&lt;/p&gt;

&lt;p&gt;来看看 Rails MVC 模式中用来创建 View 的模板。默认情况下，所有从模板里抽取出来的 Helper 不过是一堆方法，甚至拥有同一个命名空间。是不是感到有点震惊和恐惧，这不就是 PHP 嘛！&lt;/p&gt;

&lt;p&gt;但我认为，PHP 处理这些方法的方式是正确的，因为它们之间很少相互调用，就像那些从 View 模板中抽取出来的方法一样。以此为目的，使用单个命名空间来包含这些方法不仅是理性的选择，而且是一个很棒的选择。&lt;/p&gt;

&lt;p&gt;我们偶尔也会想用更加面向对象的方式来创建 View。MVP 模式中的 Presenter 就是这样一剂解决方法之间相互依赖的良药。Presenter 中封装了一系列相互独立的方法和这些方法要处理的数据。但事实证明这种情况并不常见。&lt;/p&gt;

&lt;p&gt;相较而言，我们将 MVC 中的 Model 看做面向对象思想的精髓所在。领域建模的乐趣就在于为对象挑选合适的名称、提高一致性和降低耦合程度。这是和 View 层完全不同的场景，因此需要另辟蹊径。&lt;/p&gt;

&lt;p&gt;即便如此，我们也不会遵循单一理念的信条。Rails concern (定制过的 Ruby mixin) 经常用来扩展 Model。它可以和 Active Record 模式完美结合，让那些混入的方法可以直接存取它们要处理的数据。&lt;/p&gt;

&lt;p&gt;就算是 Active Record 模式最基本的理念也会冒犯某些纯粹主义者。因为，我们将与数据库相关的操作和业务逻辑直接混合在了一起。毫无边界的融合！没错，因为这是已经证明的切实可行的方法，可用于构建需要经常访问数据库，以存取领域模型状态的网络应用。&lt;/p&gt;

&lt;p&gt;Rails 拥有如此理想的灵活性，使得它可以处理各式各样的问题。而大多数单一模式仅在问题的某一领域运转良好，当超出其范围时就显得力所不及了。但是，通过将多个模式叠加应用，我们就可以做到全面地覆盖。最终框架的健壮性和能力将远超任何单一模式所能达到的高度。&lt;/p&gt;

&lt;p&gt;目前，理论上来说维持众多编程模式多元共存关系的代价是高昂的。想要用好 Rails，仅了解面向对象编程是不够的，最好还能有面向过程编程和函数式编程的经验。&lt;/p&gt;

&lt;p&gt;上述原则也同样适用于 Rails 中的其他子语言。我们不会为你提供过多保护，以便让你不必学习那些必须掌握的知识。比如，在 View 中使用 JavaScript 或偶尔用 SQL 来构建复杂查询。至少这类保护不会以尽可能完善为目标。&lt;/p&gt;

&lt;p&gt;降低学习曲线的方式就是让大家容易上手，在了解框架的每个细节之前，就可以做出一些有真正价值的东西。这也是为什么我们会有一个快速搭建 Hello World 方法的原因。万事俱备就等你来尝试了。&lt;/p&gt;

&lt;p&gt;Rails 的思路是，通过让从业者尽早地创建真正有价值的东西，来鼓励他们快速成长。让他们觉得学习 Rails 是一种愉悦的过程，而不是一种障碍。&lt;/p&gt;
&lt;h2 id="推崇优美的代码"&gt;推崇优美的代码&lt;/h2&gt;
&lt;p&gt;写代码并不仅仅是为了让计算机或其他程序员易于理解，而且还要享受那种沐浴在优美代码中的愉悦感。从美学角度来说，令人愉悦的代码本身就很有价值，应该值得我们花精力去追求。这并不意味着优美的代码应该胜过其他考量，但至少应该在优先考量中占有一席之地。&lt;/p&gt;

&lt;p&gt;那么什么才算优美的代码呢？在 Ruby 中，通常指的是 Ruby 固有语法和自定义 DSL 力量的交集。这是一条模糊的定义，但非常值得一试。&lt;/p&gt;

&lt;p&gt;下面是一个取自 Active Record 的简单例子：&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;Project&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="n"&gt;belongs_to&lt;/span&gt; &lt;span class="ss"&gt;:account&lt;/span&gt;
  &lt;span class="n"&gt;has_many&lt;/span&gt; &lt;span class="ss"&gt;:participants&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;class_name: &lt;/span&gt;&lt;span class="s1"&gt;'Person'&lt;/span&gt;
  &lt;span class="n"&gt;validates_presence_of&lt;/span&gt; &lt;span class="ss"&gt;:name&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它看起来和 DSL 非常相似，其实不过是一个类定义，加上三个带 symbol 参数和选项的类方法调用。这里并没有什么魔幻的东西，不过确实优美而又简洁。简单几行声明就带来了如此强大的洪荒之力。&lt;/p&gt;

&lt;p&gt;这种优美部分应该归功于之前提到过的那些原则，如约定优于配置。当调用 &lt;code&gt;belongs_to :account&lt;/code&gt; 
时，我们假定 projects 表中存在一个名字为 account_id 的外键。当必须为 partcipants 关联指定 class_name 为 Person 时，我们只需定义 Person 类即可，并可以由此推断出外键和其它相关设定。&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;CreateAccounts&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Migration&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;change&lt;/span&gt;
    &lt;span class="n"&gt;create_table&lt;/span&gt; &lt;span class="ss"&gt;:accounts&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;integer&lt;/span&gt; &lt;span class="ss"&gt;:queenbee_id&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timestamps&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个例子体现了框架威力的精髓。这里程序员遵循某种约定定义了一个类，这个类继承自 &lt;code&gt;ActiveRecord::Migration&lt;/code&gt; 并实现了一个 change 方法。剩下的事情就可以交给框架去处理了，并且框架知道该调用这个 change 方法来处理。&lt;/p&gt;

&lt;p&gt;这样程序员只需要写很少的代码就可以很好地完成工作了。上述代码不仅支持通过调用 &lt;code&gt;rails db:migrate&lt;/code&gt; 
为数据库添加一张新表，同时也支持通过调用另外一个命令从数据库中删除这张表。这和工程师自己实现这些功能，并将它们整合为工作流的方式完全不同。&lt;/p&gt;

&lt;p&gt;优美的代码有时看起来也会显得晦涩难懂。但优美的代码不应该是为了尽可能地短小精干，而应该更加关注阅读的流畅性。&lt;/p&gt;

&lt;p&gt;以下两条语句是等价的：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;people&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;include?&lt;/span&gt; &lt;span class="n"&gt;person&lt;/span&gt;
&lt;span class="o"&gt;...&lt;/span&gt;      
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;person&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;in?&lt;/span&gt; &lt;span class="n"&gt;people&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但它们在语境和侧重点上存在细微的差异。第一条语句侧重点在集合，因为它是主语。第二条语句中，主语明显是 person。这两条语句在长度上并没有太大的差别，但是我认为第二条要优美的多，特别是当它用在判断条件是关于 person 的时候，这会让我感到由衷的开心。&lt;/p&gt;
&lt;h2 id="提供开发利器"&gt;提供开发利器&lt;/h2&gt;
&lt;p&gt;Ruby 本身就包含了许多开发利器。不是碰巧，而是设计如此。其中最著名的应属 Monkey Patching: 一种可以修改现有类和方法的能力。&lt;/p&gt;

&lt;p&gt;这个能力经常被嘲讽为：太简单了，即便是最普通的程序员也可以轻松掌握。以至于那些来自限制性较为严格语言阵营的人曾经常幻想：太过信任程序员可以驾驭这项能力最终会毁了 Ruby 这门语言。&lt;/p&gt;

&lt;p&gt;因为，如果什么都可以修改的话，那还有什么可以阻止你重写 String 的 capitalize 方法，让 &lt;code&gt;“something bold”.capitalize&lt;/code&gt; 返回 &lt;code&gt;“Something Bold”&lt;/code&gt;，而不是 &lt;code&gt;“Something bold”&lt;/code&gt; 呢？这在你自己本地的应用中可能没有什么问题，但所有那些依赖于原始实现的辅助代码可能就要遭殃了。&lt;/p&gt;

&lt;p&gt;那么有什么解决方案吗？答案是：没有。在 Ruby 中，只要有好的理由没有什么可以阻止你用利器来扫除障碍。我们会通过约定、劝说和教育来推行好的理念，而不是通过禁止使用厨房中的利器，并坚持让每个人使用勺子来切西红柿。&lt;/p&gt;

&lt;p&gt;由于 Monkey Patching 的负面效应创造了诸如 &lt;code&gt;2.days.ago&lt;/code&gt;（返回两天前的日期）般的壮举。你可能会觉得这是一桩亏本的买卖。也就是说宁可没有 &lt;code&gt;2.days.ago&lt;/code&gt;，你也不想修改语言的原始实现。如果这就是你的观点，那么也许 Ruby 并不适合你。&lt;/p&gt;

&lt;p&gt;也许你从那些出于安全考虑而放弃自由的人口中听说过：正是可以修改内核类和方法的能力毁了 Ruby 这门语言。然而事实刚好相反，Ruby 的蓬勃发展正是由于它为程序员提供了全新的不同看法：完全可以相信程序员可以驾驭利器。&lt;/p&gt;

&lt;p&gt;当然仅相信是不够的，还应该教授他们使用这些利器的方法。如此一来还有利于提升整个行业水平，当然前提是大多数程序员都想成为更好的程序员，并有能力在使用利器的同时避免割伤自己的手指。这真是一个梦寐以求的想法，也是一个有悖于许多程序员对其它程序员初衷（不相信其它程序员）的想法。&lt;/p&gt;

&lt;p&gt;当讨论开发利器的重要性时，对象总是其它程序员。我从未听过有程序员说：”我不确信自己可以驾驭这种能力，请让我远离它！“经常听到的却是“我觉得其他程序员可能在滥用利器”。但这种专制的想法从未出现在我的脑海中。&lt;/p&gt;

&lt;p&gt;正是这些开发利器将我们吸引到了 Rails 周围。Rails 提供的利器尽管没有 Ruby 提供的那么锋利，但其中有些还是颇具威力的。我们不会因为在工具箱里放置了这些利器而感到抱歉。事实上，我们对工程师们敢于尝试的渴望持有足够的信念，并为之自豪。&lt;/p&gt;

&lt;p&gt;Rails 的许多功能一直因“太过随意”而饱受争议。当下最流行的一个例子是：Concern。其实它就是在 Ruby 本身内建的 Module 功能上添加了一层薄薄的语法糖，允许使用单个类来封装多个相关又可以独立理解的业务（也就是 Concern 这个名字的由来）。&lt;/p&gt;

&lt;p&gt;Concern 功能备受指责的原因是，通过它程序员可以很容易将一些不相关的东西塞到对象中。说的没错，Concern 确实可以这样使用。&lt;/p&gt;

&lt;p&gt;一种谬论认为，如果不提供类似于 Concern 的功能（即使是最温和的用法也会导致设计思想的部分分离），就可以让程序员处在通往幸福的康庄大道上。但是，如果你连让对象保持整洁都做不到的话，谈何可以写出优雅的代码呢？&lt;/p&gt;

&lt;p&gt;尚未学会如何使用利器的程序员就不可能做出甜美的糕点。注意这里的用词：尚未。我相信每个人自己都会有一条成为 Ruby 和 Rails 称职程序员的道路，即使这条道路并不完全顺利。我说的称职是指：有足够的知识知道，何时以及如何根据不同的场景使用不同的工具，有时甚至危险的工具。&lt;/p&gt;

&lt;p&gt;这不是在推脱帮助他们达成称职程序员的责任。语言和框架应该是有耐心的导师，愿意帮助和指导每个人成为专家。唯一可靠的道路就是不断犯错：错误地使用工具，些许血泪教训。没有捷径。&lt;/p&gt;

&lt;p&gt;Ruby on Rails 是大厨以及那些想要成为大厨的人的厨房。也许刚开始你只是洗洗碗，但可以逐渐成长，最终可以运营整个厨房。在这个过程中，别相信任何人告诉你的：你无法驾驭业内最好的工具。&lt;/p&gt;
&lt;h2 id="面向综合应用"&gt;面向综合应用&lt;/h2&gt;
&lt;p&gt;Rails 可用于许多场景，但最初目的是创建一个整合系统：&lt;a href="https://m.signalvnoise.com/the-majestic-monolith-29166d022228#.umpdeapqa" rel="nofollow" target="_blank" title=""&gt;Majestic Monolith&lt;/a&gt;！即一个可以解决所有问题的完整系统。这就意味着 Rails 需要考虑从前端需要即时更新的 JavasScript 到生产环境下数据库如何进行版本迁移的所有细节。&lt;/p&gt;

&lt;p&gt;如上所述，这确实是一个非常宽广的领域，但尚在我力所能及的范围之内。Rails 会专门寻找全栈工程师来创建这个整合系统。当然，目的不是将那些专注于某个领域的专家排除在外，而是因为整个全栈的团队可以更好的创建具有长远意义的事物。&lt;/p&gt;

&lt;p&gt;正是专注于提高个体程序员的开发能力，让我们想要创建这样一个整合系统。在整合系统里，我们可以拿掉很多不必要的抽象，减少各层之间的重复（就像服务端和客户端共享的模版），最重要的是可以避免让应用成为分布式系统（在确实有必要之前）。&lt;/p&gt;

&lt;p&gt;许多系统开发的复杂性来自于引入组件之间的新边界，这些边界限定了组件之间相互调用的方式。对象之间的本地调用要比远程微服务之间的调用简单得多。那些冒险启用分布式系统的人会发现，这是一个充满失败调用、网络延时和依赖更新周期的全新地狱。&lt;/p&gt;

&lt;p&gt;有时候这种分布仅仅是简单的需求。比如你为 web 应用创建了一个 API 接口，以供其他人通过 HTTP 调用，那你就乖乖等着收拾烂摊子吧（尽管处理请求要比发送请求要容易的多。因为在发送请求时，别人的错误也会导致你自己的错误）。但对你自己来说，这至少不是一次愉快的开发经历。&lt;/p&gt;

&lt;p&gt;更糟的情况是，系统可能过早地被拆分成了服务，甚至是更差的微服务。这通常来自于一个错误的认知：如果你想要个现代网络应用的话，只需要简单地多次的构建系统，一次在服务端，一次在基于 JavaScript MVC 的客户端，以及每个移动端的本地应用等。这不是基本规律，也完全没有必要。&lt;/p&gt;

&lt;p&gt;其实完全可以在多个 app 间共享整个应用的大部分内容。相同的 Controller 和 View 可以用于桌面 web 应用，也可以内嵌到手机 app 的本地应用中。也就是尽可能将开发集中在同一个整合系统中。&lt;/p&gt;

&lt;p&gt;而且这样做完全不必牺牲速度、用户体验或其他任何误导你过早拆分系统的因素。&lt;/p&gt;

&lt;p&gt;这就是我们一直追寻的大一统：可自行调配、适用于多平台应用开发、易于理解和使用的单个整合系统。&lt;/p&gt;
&lt;h2 id="演进优于稳定"&gt;演进优于稳定&lt;/h2&gt;
&lt;p&gt;当一个系统存在已经超过 10 年，如 Rails，一般都会自然僵化。每一处修改都有各种可能给他人带来灾难，因为他们可能依赖于过去的实现。而且，对这些人来说，他们的要求完全是合理的。&lt;/p&gt;

&lt;p&gt;但如果我们太过迁就保守派的意见，我们将永远看不到事情的另一面。因此，我们必须偶尔打破陈规，以改变事情演进发展的方式。正是这种演进方式才让 Rails 得以存活，并可能在未来（数）十年继续蓬勃发展。&lt;/p&gt;

&lt;p&gt;永远都是说起来容易做起来难，特别是当你自己的应用因 Rails 的重大版本升级（不向下兼容）而崩溃时。然而正是这些时刻，才让我们谨记 &lt;a href="#%E6%BC%94%E8%BF%9B%E4%BC%98%E4%BA%8E%E7%A8%B3%E5%AE%9A" title=""&gt;演进优于稳定&lt;/a&gt; 的价值所在，并为我们带来修复崩溃应用的力量，从而继续保持与时俱进。&lt;/p&gt;

&lt;p&gt;这并不代表我们就可以不管三七二十一地给别人带来不必要的或过份的伤害。Rails 2.x 到 3 的重大升级就是这样一个艰难的决定，它所带来的噩梦还依然徘徊在那些亲身经历过的人的心头，经久不散。它让许多人在 2.x 版本停留了很长一段时间，有些人甚至对此深恶痛绝到失去了理智。但，从大局来看这个决定还是值得肯定的。&lt;/p&gt;

&lt;p&gt;这些就是我们一直要做的艰难抉择。Rails 是否会因为今天的改变而在未来五年内变的更好？Rails 是否会因为接纳其他解决方案，如异步任务队列或 WebSocket，而变的更好？如果答案是肯定的，那就啥都别说了，让我们卷起袖子干活吧。&lt;/p&gt;

&lt;p&gt;其实这项工作并不仅限于 Rails 本身，而应该在广大 Ruby 社区推行。Rails 应该站在帮助 Ruby 进步的前沿，推动广大社区成员尽快使用最新版本。&lt;/p&gt;

&lt;p&gt;就目前情况来看，我们做的还不错。从我开始实践这个信条开始，我们已经经历 Ruby 1.6、1.7、1.8、1.9、2.0、2.1、2.1、2.2，直到目前最新的 2.3。一路走来伴随着许多重大的变迁，但 Rails 始终是 Ruby 的坚强后盾，它帮助每个人尽快地适应新版本的变迁。这是 Rails 作为 Ruby 主要推广者的部分权力和义务。&lt;/p&gt;

&lt;p&gt;同样，这一点也适用于工具链上的其它辅助工具。Bundler 曾经是一个充满争议的理念，但是经过 Rails 的不懈努力的推广，它已经成了可以和 Rails 相提并论的基石，并成为人们理所当然的选择。像 Asset Pipeline 和 Spring 这样的连续处理指令组件其实也一样。这三个组件全都经历过或正在经历演进的痛苦，但是这些工具的长远意义促使我们一直推动他们前进。&lt;/p&gt;

&lt;p&gt;最终，进步还是要取决于人以及他们想要做出改变的意愿。这就是为什么在 Rails Core 或 Rails Commiters 类似的小组中没有终身职位一说。这两个小组的成员都是那些为推动框架进步而积极工作的人。有些人可能只会坚持几年，不管怎样我们会永远感谢他们做出的贡献，但另外一些人可能会为之持续数十年。&lt;/p&gt;

&lt;p&gt;因此，我们会一直欢迎和鼓励新的成员加入社区，这对我们来说至关重要。我们需要新的血液和理念来激荡出更好的进步。&lt;/p&gt;
&lt;h2 id="兼收并蓄"&gt;兼收并蓄&lt;/h2&gt;
&lt;p&gt;Rails 因包含诸多富有争议的理念而闻名。如果我们要求每个人无时无刻遵循所有的信条，那么 Rails 将会很快沦为孤芳自赏的小群体。所以我们绝不会这样做。&lt;/p&gt;

&lt;p&gt;我们需要不同的意见。我们需要不同的语法。我们需要多样的思想和成员。在这个思想的大熔炉中，我们可以提炼出最好的精华部分给大家分享。在此过程中，许多人以代码或深思熟虑的论点贡献了他们的意见。&lt;/p&gt;

&lt;p&gt;这篇信条其实描述的是一种理想状态，日常生活中的实际情况要微妙（也更有趣）的多。Rails 可以在同一个帐篷里容纳下如此庞大的一个社区，究其原因是 Rails 很少或者根本没有试剑石（非一即二的考验）。&lt;/p&gt;

&lt;p&gt;RSpec（一个我经常表达不满的测试 DSL 框架）的持续成功就是一个完美的佐证。尽管我可以为此吐槽到口干舌燥：为什么我觉得这不是正确的测试方式。但依然不能妨碍它大获成功。这点才是最为至关重要的。&lt;/p&gt;

&lt;p&gt;Rails API 的出现同样如此。尽管我个人是关注和倾力的重点是带有 View 的整合系统，但对那些想要做前后端分离的人来说，毫无疑问这是 Rails 可以改进的地方。因此，我们应该欣然接受 Rails API 成为 Rails 的第二使命，而且我相信它配得上如此重要的地位。&lt;/p&gt;

&lt;p&gt;当然，拥有同一个帐篷并不意味着需要迎合所有人的所有需求。它仅仅意味着欢迎所有人加入这个大家庭，并带来他们自己的想法。我们不必牺牲我们的灵魂和价值观来让他们加入我们，我们只是知道如何将不同的想法混合在一起，来产生新的美妙思想。&lt;/p&gt;

&lt;p&gt;当然这样一个帐篷不是唾手可得的，还需要很多工作来让它受到大家的欢迎，特别是你的目标不仅仅是吸引那些已经在社区里的人。因此，降低入门门槛总是我们需要慎重考虑的工作首选。&lt;/p&gt;

&lt;p&gt;你永远不会知道：修正文档中拼写错误的人是否最终可能会创作出下一项伟大功能。但是，你有机会向每个做出微不足道贡献的人表示感激，从而激励他们继续努力工作。&lt;/p&gt;</description>
      <author>freefishz</author>
      <pubDate>Fri, 07 Oct 2016 23:08:31 +0800</pubDate>
      <link>https://ruby-china.org/topics/31249</link>
      <guid>https://ruby-china.org/topics/31249</guid>
    </item>
  </channel>
</rss>
