2017-12-02 271 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

我確實走了使用共享「基礎」控制器的路線,然後不同的關聯從它繼承。它允許我清理幾個查找方法,但仍然允許每個方法都具有靈活性。 –

相關問題