代码之家  ›  专栏  ›  技术社区  ›  Dan L

Rails 4+:具有多个关联的单个资源,控制器组织

  •  0
  • Dan L  · 技术社区  · 8 年前

    这是一个“现实世界项目”的问题,我正在努力找到一个好的解决方案。

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

    例如 project 有很多 tasks 我需要更新 任务 在 项目 .

    此外,每个 项目 有很多 milestones ,以及每个 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更新页面而不是刷新页面的格式。注意,我使用的是对话框/弹出界面 任务 表单,所以我不能只刷新整个页面。

    虽然这个“可行”,但最终会出现一种情况,即我有很多重复代码。

    # 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') %>");
    

    这只是一些示例代码,来自实际系统的代码显示了更多的重复。可以看出,在每个不同的资源中都有很多重复代码,这些代码与 任务 资源

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

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

    1 回复  |  直到 8 年前
        1
  •  0
  •   jvillian    8 年前

    假设你有一个 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
    

    如果在抽象之后,你的控制器有相同的方法,那么你可以开始做其他有趣的事情,比如have Milestones::TasksController 和 Projects::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
    

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