2017-12-02 269 views
0

这是我努力寻找良好解决方案的那些“真实世界项目”问题之一。Rails 4+:具有多个关联的单个资源,控制器组织

我有一个task模型与几种不同的资源相关联,我需要允许来自每个这些关联资源的CRUD能力。

例如,一个project有很多tasks,我需要更新tasksproject的上下文中。

此外,每个project有许多milestones,并且每个milestone也可以有很多tasks

这种情况下,task可能会或可能不会与milestone相关联。

class Project < ApplicationRecord 
    has_many :milestones 
    has_many :tasks 
end 

class Milestone < ApplicationRecord 
    belongs_to :project # required 
    has_many :tasks 
end 

class Task < ApplicationRecord 
    belongs_to :project # required 
    belongs_to :milestone # optional 
end 

我有命名空间我的控制器更好的组织和控制。这使我的路线,如:

# routes.rb 
resources :projects do 
    namespace :projects do 
    resources :milestones # app/controllers/projects/milestones_controller.rb 
    resources :tasks # app/controllers/projects/tasks_controller.rb 
    end 
end 

resources :milestones do 
    namespace :milestones do 
    resources :tasks # app/controllers/milestones/tasks_controller.rb 
    end 
end 

我也是用了很多AJAX的,使接口快(与Turbolinks),所以我的行动遵循<action>.js.erb格式,其中使用JavaScript来更新页面,而不是页面刷新。请注意,我为task表单也使用了对话框/弹出式界面,因此我不能只进行整个页面刷新。

虽然这个“工作”,它结束了我有很多重复代码的情况。

# projects/tasks_controller.rb 
def new 
    @project = Project.find(params[:project_id]) 
    @task = @project.tasks.new 
end 

def create 
    @project = Project.find(params[:project_id]) 
    @task = @project.tasks.new(task_params) 

    if @task.save 
    # create.js.erb 
    else 
    render js: "alert('error');" # example... 
    end 
end 

# app/views/projects/tasks/create.js.erb 
$("#tasks_for_<%= dom_id(@project) %>").append("<%=j render(partial: 'projects/tasks/task') %>"); 

# milestones/tasks_controller.rb 
def new 
    @milestone = Milestone.find(params[:milestone_id]) 
    @task = @milestone.tasks.new 
end 

def create 
    @milestone = Milestone.find(params[:milestone_id]) 
    @task = @milestone.tasks.new(task_params) 

    if @task.save 
    # create.js.erb 
    else 
    render js: "alert('error');" # example... 
    end 
end 

# app/views/milestones/tasks/create.js.erb 
$("#tasks_for_<%= dom_id(@milestone) %>").append("<%=j render(partial: 'milestones/tasks/task') %>"); 

这只是一些示例代码,来自实际系统的代码显示了更多的重复。如您所知,每个不同资源中有很多重复代码,与tasks资源的交互稍有不同。

是否有一些标准格式或Rails功能可以帮助构建由其他资源操纵的资源?

我怎样才能减少这种重复?它直接导致一个复杂的系统,每当我添加或更改某个功能时,我都必须在3个以上的不同地方进行更改。

+0

这正是为什么你应该使用演示者,服务,经理等(所有普通的老红宝石对象)。 – jvillian

+0

@jvillian:我确实使用演示者和服务对象以及其他PORO。但我不确定这些将如何帮助减少上述控制器和视图的重复?问题在于3个以上的地方使用相同代码的90%,我不确定如何干掉代码,但仍然允许10%的差异。 –

回答

0

让我们假设你有一个ServiceBase看起来是这样的:

# services/service_base.rb 
class ServiceBase 

    attr_accessor :args, 
       :controller 

    class << self 

    def call(args={}) 
     new(args).call 
    end 

    end # Class Methods 

    #====================================================================== 
    # Instance Methods 
    #====================================================================== 

    def initialize(args) 
     @args = args 
    end 

    private 

    def params 
     controller.params 
    end 

    def assign_args 
     args.each do |k,v| 
     class_eval do 
      attr_accessor k 
     end 
     send("#{k}=",v) 
     end 
    end 

end 

然后一个Tasks::NewService,看起来是这样的:

# services/tasks/new_service.rb 
class Tasks::NewService < ServiceBase 

    def call 
     assign_args 
     @haser = haser_klass.find(haser_id) 
     @haser.tasks.new 
    end 

    private 

    def haser_klass 
     haser_base.constantize 
    end 

    def haser_instance_name 
     haser_base.downcase 
    end 

    def haser_base 
     controller.class.name.split("::")[0].singularize 
    end 

    def haser_id 
     params["#{haser_instance_name}_id".to_sym] 
    end 

end 

然后,在你的控制器,你应该能够做类似的事:

# milestones/tasks_controller.rb 
Milestones::TasksController < ApplicationController 

    def new 
    @task = Tasks::NewService.call(controller: self) 
    end 

end 

# projects/tasks_controller.rb 
Projects::TasksController < ApplicationController 

    def new 
    @task = Tasks::NewService.call(controller: self) 
    end 

end 

如果你有一个Tasks::CreateService,看起来像:

# services/tasks/create_service.rb 
class Tasks::CreateService < ServiceBase 

    delegate :render, 
      to: :controller 

    def call 
     assign_args 
     @haser = haser_klass.find(haser_id) 
     @task = @haser.tasks.new(task_params) 
     if @task.save 
     # create.js.erb 
     else 
     render js: "alert('error');" # example... 
     end  
    end 

    private 

    def task_params 
     send("#{haser_instance_name}_params") 
    end 

    def milestone_params 
     params.require(:milestone).permit(:foo) 
    end 

end 

然后,在你的控制器,你应该能够做这样的事情:如果你到了一个地步,

# milestones/tasks_controller.rb 
Milestones::TasksController < ApplicationController 

    def new 
    @task = Tasks::NewService.call(controller: self) 
    end 

    def create 
    Tasks::CreateService.call(controller: self) 
    end 

end 

# projects/tasks_controller.rb 
Projects::TasksController < ApplicationController 

    def new 
    @task = Tasks::NewService.call(controller: self) 
    end 

    def new 
    Tasks::CreateService.call(controller: self) 
    end 

end 

,抽象后,您的控制器有相同的方法,那么你可以开始做其他花哨的东西,如Milestones::TasksControllerProjects::TasksController都从具有所有共享方法的ApplicationController以外的公共控制器继承。所以,假设你没有使用TasksController。然后,也许你的控制器可能类似于:

# tasks_controller.rb 
TasksController < ApplicationController 

    def new 
    @task = Tasks::NewService.call(controller: self) 
    end 

    def create 
    Tasks::CreateService.call(controller: self) 
    end 

end  

# milestones/tasks_controller.rb 
Milestones::TasksController < TasksController; end 

# projects/tasks_controller.rb 
Projects::TasksController < TasksController; end 

现在,你只需要在一个地方的变化:在您的服务。当然,如果你不想使用这些服务,你总是可以重写控制器方法。

+0

我确实走了使用共享“基础”控制器的路线,然后不同的关联从它继承。它允许我清理几个查找方法,但仍然允许每个方法都具有灵活性。 –

相关问题