加载中

At FiveStreet, we have been focusing heavily on the 'Mise en place' of our objects ( thanks to Dave Bock for the phrase ). This has been key to our early success. We have found that focusing on the placement of objects allows us to worry more about building new features while adding less fear of breaking stuff. One of our strategies for achieving this is by using interactor objects.

Interactors can be defined as the use cases of your application, for example adding a comment. They are also a great place for objects that shouldn't know about each to interact. For example, in the following code example you will see that when adding a comment we also interact with a external object, the MixpanelWrapper object. What follows is a dead simple example of how we use them in our application.

FiveStreet,我们一贯的重中之重就是要让我们的对象'Mise en place(就位)'(这个词是由Dave Bock选用的)。我们初期的成功关键就在于此。我们发现,专注于合理安排对象的位置就能够使我们在不会平添更多为弄坏东西而害怕的情况下,有更多精力来考虑添加新的特性。为达此境界,我们的策略之一就是使用interactor交互器)对象。

Interactor可以定义为你的应用的一个用例,比如,添加一条评论。在使用Interactor的地方正是对象无需互相了解就能够进行交互的地方。例如,在下面给出的例子代码中,添加一条评论还需要同一个外部对象进行交互,这个对象就是MixpanelWrapper对象。随后的代码是一个在我们的应用中对这段例子代码进行使用的、再简单不过的例子了。

Add a Comment Interactor:

  #app/interactors/agent/add_comment.rb
  class Agent::AddComment
    attr_reader :comment
    
    def initialize(agent, comment)
      @agent = agent
      @comment = comment
    end
    
    def allowed?
      touch_log = TouchLog.find(@comment.touch_log_id)
      return false if !@comment.valid? || touch_log.agent_id != @agent.id
      return true
    end
  
    def run()
      return false if !allowed?
  
      MixpanelWrapper.track("Agent: Add Comment To Prospect", { :distinct_id => @agent.email })
  
       @comment.save
    end
  
   end

The previous chunk of code is a sample of how we build interactors. All of our interactors have two main methods,#allowed?and#run. In theallowed?method we handle all of our validations. Therunmethod runs the use case and more!

The Comment Controller

  class Agent::CommentsController < ApplicationController
    def create
      comment = Comment.new(params[:comment])
      action = Agent::AddComment.new(current_agent, comment)
      if action.run()
        flash_success_on_redirect "Comment was added."
        redirect_to prospect_path(action.comment.touch_log)
      else
        flash_error_on_redirect "Please enter a comment."
        redirect_to prospect_path(action.comment.touch_log)
      end
    end
  end

This controller is pretty basic, the newish thing here is callingAgent::AddComment.new(current_agent, comment)instead of the normalcomment = Comment.new(params[:comment]); comment.save.

添加评论交互器:

  #app/interactors/agent/add_comment.rb
  class Agent::AddComment
    attr_reader :comment
    
    def initialize(agent, comment)
      @agent = agent
      @comment = comment
    end
    
    def allowed?
      touch_log = TouchLog.find(@comment.touch_log_id)
      return false if !@comment.valid? || touch_log.agent_id != @agent.id
      return true
    end
  
    def run()
      return false if !allowed?
  
      MixpanelWrapper.track("Agent: Add Comment To Prospect", { :distinct_id => @agent.email })
  
       @comment.save
    end
  
   end

之前的代码块展示了如何创建交互器。所有的交互器都有两个 main 方法,#allowed? 何 #run。在 #allowed?方法中我们处理所有的验证。run 方法则用于运行使用及其它的代码。

评论控制器

  class Agent::CommentsController < ApplicationController
    def create
      comment = Comment.new(params[:comment])
      action = Agent::AddComment.new(current_agent, comment)
      if action.run()
        flash_success_on_redirect "Comment was added."
        redirect_to prospect_path(action.comment.touch_log)
      else
        flash_error_on_redirect "Please enter a comment."
        redirect_to prospect_path(action.comment.touch_log)
      end
    end
  end

控制器非常基础,这里的新东西是调用 Agent::AddComment.new(current_agent, comment) 而非通常的 comment = Comment.new(params[:comment]);comment.save。

At a high level what are the benefits:

  • Easy to Read/Understand: When I started looking at the code base for the first I knew exactly what was happening and where to find each use case.
  • Testing is easier: Thanks to the separation of concerns we gain.
  • Low 'learning' curve: There is nothing amazingly complex going on, you just pass the objects into the interactor and make the magic happen.

What are the negatives:

  • More files: Yeah there are more files but in the end it is easier to search.
  • More prep-time: It is not the default Rails way of doing things, but adding an extra file shouldn't be hard.

在高层次上的好处是:

  • 更易于阅读/理解: 当我们首次审阅底层代码时,我能清楚知道如何运行,哪里能够找到每个用例。
  • 测试更简易: 感谢关注分离。
  • 低“学习”曲线: 没有任何复杂的事情发生,你只需要传递对象给交换器,魔法诞生了。

不利之处:

  • 更多文件:是的,文件变得更多了,不过搜索起来很简单。
  • 更多准备时间:这不是 Rails 常用的处理方法,不过额外添加文件应改不难。

How does this add business value:

As mentioned we have been extremely happy with this approach and these are some of the main reasons:
  1. First, in a sense this reminds me of Rails. Interactors allow us to have such a great separation of concern that we are able to worry more about building new features and less about breaking shit. How so? Well each interactor lives on its own, it is separated from everything else.
  2. Being in its own world lets us easily remove code that we don't need. This is especially useful in the moments where that amazing new feature actually wasn't that amazing.
  3. It also allows us to reuse use-cases easier. If I wanted to post a comment anywhere else in the system I can just call this object again.
  4. Testing is 10 times easier, this is especially nice in those moments where you have a deadline and you don't have time to write the 'perfect' method. It's ok you got your test and you can always refactor.
  5. In the future when new people join the company their biggest requirement will be to know whatAgent::AddCommentis supposed to do?

这么做会带来哪些好处:

如上所述,我们对这种做法非常满意,主要有下面几方面的原因:
  1. 首先,在某种意义上这让我想到了Rails。Interactor具有相当大的关注点分离作用,我们能够将更多精力投入添加新特性的工作中而且还不用太担心破坏已有代码。为什么能这样呢?这是因为,每个interactor都是自给自足的,它同其它的对象是完全分开的。
  2. 这些自给自足的对象使得我们可以轻松地删掉我们不再需要的代码。有时这会非常有用,比如,我们添加了一个原本我们认为很神奇的一个新特性,后来发现它并不是那么神奇。
  3. 这种方式使得我们可以更加容易的对用例进行重用。如果我想在系统中的别任何地方发布一条评论,我只需要再次调用一下这个对象。
  4. 测试比以前简单了10倍。这种方式在某些时候尤其的好,比如,在最后期限之前你的时间不够用了,没时间写出“完美的”方法了。只要有测试就ok了,以后总可以对它们进行重构。、
  5. 将来再有新人加入公司的话,他们最大的要求就是要了解,Agent::AddComment是用来做什么的?
返回顶部
顶部