Papa Carlo처럼 컨트롤러를 조각하세요.

19310 단어 railsshowdevruby
Rails 개발자인 우리는 컨트롤러를 얇게 유지하는 경향이 있습니다(그리고 models fat – 오, 잠깐, it's not true anymore ; 이제 fat services 😉).

다양한 추상화 계층을 추가합니다.

쿼리 매개변수 기반 필터를 처리할 때 여전히 다음과 같이 작성해야 합니다.

class EventsController < ApplicationController
  def index
    events = Event.all.
      page(params[:page] || 1).
      order(sort_params)

    events = events.where(
      type: params[:type_filter]
    ) if params[:type_filter].in?(%w[published draft])

    events = events.future if params[:time_filter] == "future"
    # NOTE: `searched` is a scope or class method defined on the Event model
    events = events.searched(params[:q]) if params[:q].present?

    render json: events
  end

  def sort_params
    sort_by = params[:sort_by].in?(%w[id name started_at]) ?
              params[:sort_by] :
              :started_at
    sort_order = params[:sort].in?(%w[asc desc]) ? params[:sort] : :desc
    { sort_by => sort_order }
  end
end

날씬하지 않은 컨트롤러가 있음에도 불구하고 읽기, 테스트 및 유지 관리가 어려운 코드가 있습니다.

새로운 보석interactors(러시아어로 "손 평면"을 의미)을 사용하여 이 컨트롤러(파파 카를로가 통나무에서 부라티노policies를 조각한 것처럼)를 조각할 수 있는 방법을 보여주고 싶습니다.

form objects은 해시 기반 매개변수로 구동되는 데이터 변환을 위한 범용 도구입니다.

좋아요, 이상하게 들리네요 😕

위의 예를 살펴보겠습니다. 데이터(활성 레코드 관계, Event.all )를 가져와서 사용자 입력(params 개체)에 따라 변환합니다.

컨트롤러 외부 어딘가에서 이 변환을 추출할 수 있다면 어떨까요?

"이 추상화의 요점은 무엇입니까?"

Russian Pinocchio이 있습니다.
  • 코드를 더 읽기 쉽게 만듭니다(논리 분기 감소)
  • .
  • 코드를 테스트하기 쉽게 만듭니다(및 Rubanok )
  • .
  • 코드를 재사용할 수 있도록 합니다(예: 정렬 및 페이지 매김 논리가 다른 컨트롤러에서도 사용될 가능성이 있음).

  • 먼저 Rubanok을 추가할 때 위의 컨트롤러가 어떻게 보이는지 보여드리겠습니다.

    class EventsController < ApplicationController
      def index
        events = planish Event.all
        render json: events
      end
    end
    

    그게 다야. 이보다 더 얇을 수는 없습니다(좋아요, render json: planish(Event.all) ).
    planish 메서드 아래에 무엇이 숨겨져 있습니까?

    구성 원칙보다 관례를 활용하는 Rails 전용 방법(btw, Rubanok 자체는 Rails-free임)이며 다음과 같이 전개될 수 있습니다.

    def index
      events = EventsPlane.call(Event.all, params.to_unsafe_h)
      render json: events
    end
    

    그리고 EventsPlane 클래스는 모든 마법 변환이 일어나는 곳입니다:

    class EventsPlane < Rubanok::Plane
      TYPES = %w[draft published].freeze
      SORT_FIELDS = %w[id name started_at].freeze
      SORT_ORDERS = %w[asc desc].freeze
    
      map :page, activate_always: true do |page: 1|
        raw.page(page)
      end
    
      map :type_filter do |type_filter:|
        next raw.none unless TYPES.include?(type_filter)
    
        raw.where(type: type_filter)    
      end
    
      match :time_filter do
        having "future" do
          raw.future
        end
    
        default { |_time_filter| raw.none }
      end
    
      map :sort_by, :sort do |sort_by: "started_at", sort: "desc"|
        next raw unless SORT_FIELDS.include?(sort_by) &&
          SORT_ORDERS.include?(sort)
        raw.order(sort_by => sort)
      end
    
      map :q do |q:|
        raw.searched(q)
      end
    end
    

    평면 클래스는 전달된 raw에 따라 데이터를 변환하는 방법(params 메서드를 통해 액세스 가능)을 설명합니다.
  • map를 사용하여 키를 추출하고 해당 값이 비어 있지 않은 경우(즉, 빈 문자열은 무시됨) 변환을 적용합니다. 여기서 Ruby 키워드 인수 기본값을 사용할 수 있습니다. 멋지죠?
  • match를 사용하여 변압기를 선택할 때도 값을 고려하십시오.

  • 이제 비행기에 대한 테스트를 따로 작성할 수 있습니다.

    describe EventsPlane do
      let(:input) { Event.all }
      # add default transformations
      let(:output) { input.page(1).order(started_at: :desc) }
      let(:params) { {} }
    
      # we match the resulting SQL query and do not make real queries
      # at all–our tests are fast!
      subject { described_class.call(input, params).to_sql }
    
      specify "q=?" do
        params[:q] = "wood"
    
        expect(subject).to eq(output.searched("wood").to_sql)
      end
    
      specify "type_filter=<valid>" do
        params[:type_filter] = "draft"
    
        expect(subject).to eq(output.where(type: "draft").to_sql)
      end
    
      specify "type_filter=<invalid>" do
        params[:type_filter] = "unpublished"
    
        expect(subject).to eq(output.none.to_sql)
      end
    
      # ...
    end
    

    컨트롤러/요청 테스트에서 필요한 것은 특정 평면이 사용되었는지 확인하는 것입니다.

    describe EventsController do
      subject { get :index }
    
      specify do
        expect { subject }.to have_planished(Event.all).
          with(EventsPlane)
      end
    end
    

    따라서 Rubanok은 컨트롤러를 조각하는 데 적합하지만 범용이라고 말했습니다. GraphQL 예제로 증명해 보겠습니다!

    module GraphAPI
      module Types
        class Query < GraphQL::Schema::Object
          field :profiles, Types::Profile.connection_type, null: false do
            argument :city, Int, required: false
            argument :home, Int, required: false
            argument :tags, [ID], required: false
            argument :q, String, required: false
          end
    
          def profiles(**params)
            ProfilesPlane.call(Profile.all, params)
          end
        end
      end
    end
    

    이제 막 마른 체형을 발명한 것 같습니다 🙂

    자세한 내용은 Rubanok 저장소를 확인하고 자유롭게 아이디어를 제안하세요!

    추신 유사한 아이디어(PORO 방식이지만)를 구현하지만 ActiveRecord에 초점을 맞추고 테스트 지원이 부족한 이전 gemseveral reasons이 있습니다.

    추신 대규모 애플리케이션에서 코드를 구성하는 데 사용하는 다른 추상화가 궁금하십니까? 또는 make tests faster 과 같은 다른 게시물과 Rubanok filterer 과 같은 프로젝트를 확인하십시오.


    "Clowne: Clone Ruby models with a smile"에서 더 많은 개발 기사를 읽어보세요!

    좋은 웹페이지 즐겨찾기