Computer >> 컴퓨터 >  >> 프로그램 작성 >> Ruby

Ruby on Rails에서 서비스 객체 사용

<블록 인용>

이 문서는 Playbook Thirty-9 - 최소한의 도구로 대화형 웹 앱을 제공하기 위한 가이드의 원래 모습에서 수정되었습니다. , 이 AppSignal 게스트 게시물에 맞게 조정되었습니다.

앱이 처리해야 하는 많은 기능이 있지만 해당 로직이 반드시 컨트롤러나 모델에 속할 필요는 없습니다. 몇 가지 예에는 장바구니로 결제, 사이트 등록 또는 구독 시작이 포함됩니다.

이 모든 논리를 컨트롤러에 포함할 수 있지만 모든 위치에서 동일한 논리를 호출하면서 자신을 계속 반복하게 될 것입니다. 모델에 논리를 넣을 수 있지만 때로는 IP 주소 또는 URL의 매개변수와 같이 컨트롤러에서 쉽게 사용할 수 있는 항목에 액세스해야 합니다. 필요한 것은 서비스 개체입니다.

서비스 개체의 작업은 기능을 캡슐화하고 하나의 서비스를 실행하며 단일 실패 지점을 제공하는 것입니다. 서비스 개체를 사용하면 개발자가 애플리케이션의 다른 부분에서 사용될 때 동일한 코드를 반복해서 작성하지 않아도 됩니다.

서비스 객체는 평범한 오래된 루비 객체("PORO")입니다. 특정 디렉토리 아래에 있는 파일일 뿐입니다. 예측 가능한 응답을 반환하는 Ruby 클래스입니다. 응답을 예측 가능하게 만드는 것은 세 가지 핵심 부분 때문입니다. 모든 서비스 개체는 동일한 패턴을 따라야 합니다.

  • params 인수가 있는 초기화 메서드가 있습니다.
  • call이라는 단일 공개 메서드가 있습니다.
  • 성공적으로 OpenStruct를 반환합니까? 페이로드 또는 오류입니다.

OpenStruct란 무엇입니까?

클래스와 해시의 아이디어와 같습니다. 임의의 속성을 받을 수 있는 미니 클래스로 생각할 수 있습니다. 우리의 경우 두 가지 속성만 처리하는 일종의 임시 데이터 구조로 사용하고 있습니다.

성공이 true인 경우 , 데이터 페이로드를 반환합니다.

OpenStruct.new({success ?:true, payload: 'some-data'})

성공이 false인 경우 , 오류를 반환합니다.

OpenStruct.new({success ?:false, error: 'some-error'})

다음은 현재 베타 버전인 AppSignals 새 API에 도달하여 데이터를 가져오는 서비스 개체의 예입니다.

module AppServices
 
  class AppSignalApiService
 
    require 'httparty'
 
    def initialize(params)
      @endpoint   = params[:endpoint] || 'markers'
    end
 
    def call
      result = HTTParty.get("https://appsignal.com/api/#{appsignal_app_id}/#{@endpoint}.json?token=#{appsignal_api_key}")
    rescue HTTParty::Error => e
      OpenStruct.new({success?: false, error: e})
    else
      OpenStruct.new({success?: true, payload: result})
    end
 
    private
 
      def appsignal_app_id
        ENV['APPSIGNAL_APP_ID']
      end
 
      def appsignal_api_key
        ENV['APPSIGNAL_API_KEY']
      end
 
  end
end

AppServices::AppSignalApiService.new({endpoint: 'markers'}).call을 사용하여 위의 파일을 호출합니다. . 저는 OpenStruct를 자유롭게 사용하여 예측 가능한 응답을 반환합니다. 이는 로직의 모든 아키텍처 패턴이 동일하기 때문에 테스트를 작성할 때 매우 중요합니다.

모듈이란 무엇입니까?

모듈을 사용하면 이름 간격이 제공되고 다른 클래스와 충돌하는 것을 방지할 수 있습니다. 즉, 모든 클래스에서 동일한 메서드 이름을 사용할 수 있으며 특정 네임스페이스 아래에 있기 때문에 충돌하지 않습니다.

모듈 이름의 또 다른 핵심 부분은 앱에서 파일이 구성되는 방식입니다. 서비스 개체는 프로젝트의 서비스 폴더에 보관됩니다. 모듈 이름이 AppServices인 위의 서비스 개체 예 , AppServices에 속함 서비스 디렉토리의 폴더입니다.

내 서비스 디렉토리를 여러 폴더로 구성합니다. 각 폴더에는 애플리케이션의 특정 부분에 대한 기능이 포함되어 있습니다.

예를 들어, CloudflareServices 디렉터리에는 Cloudflare에서 하위 도메인을 만들고 제거하기 위한 특정 서비스 개체가 있습니다. Wistia 및 Zapier 서비스에는 각각의 서비스 파일이 있습니다.

이와 같이 서비스 개체를 구성하면 구현과 관련하여 더 나은 예측 가능성을 얻을 수 있으며 10,000피트 보기에서 앱이 수행하는 작업을 한 눈에 쉽게 확인할 수 있습니다.

StripeServices를 살펴보겠습니다. 예배 규칙서. 이 디렉토리에는 Stripes API와 상호 작용하기 위한 개별 서비스 개체가 있습니다. 다시 말하지만, 이 파일이 하는 유일한 일은 애플리케이션에서 데이터를 가져와 Stripe로 보내는 것입니다. StripeService에서 API 호출을 업데이트해야 하는 경우 구독을 생성하는 개체는 한 곳에서만 할 수 있습니다.

전송할 데이터를 수집하는 모든 논리는 AppServices에 있는 별도의 서비스 개체에서 수행됩니다. 예배 규칙서. 이 파일은 애플리케이션에서 데이터를 수집하고 외부 API와 인터페이스하기 위해 해당 서비스 디렉터리로 보냅니다.

다음은 시각적인 예입니다. 새 구독을 시작하는 사람이 있다고 가정해 보겠습니다. 모든 것은 컨트롤러에서 비롯됩니다. 다음은 SubscriptionsController입니다. .

class SubscriptionsController < ApplicationController
 
  def create
    @subscription = Subscription.new(subscription_params)
 
    if @subscription.save
 
      result = AppServices::SubscriptionService.new({
        subscription_params: {
          subscription: @subscription,
          coupon: params[:coupon],
          token: params[:stripeToken]
        }
      }).call
 
      if result && result.success?
        sign_in @subscription.user
        redirect_to subscribe_welcome_path, success: 'Subscription was successfully created.'
      else
        @subscription.destroy
        redirect_to subscribe_path, danger: "Subscription was created, but there was a problem with the vendor."
      end
 
    else
      redirect_to subscribe_path, danger:"Error creating subscription."
    end
  end
end

먼저 인앱 구독을 만들고 성공하면 그와 stripeToken 및 쿠폰과 같은 항목을 AppServices::SubscriptionService라는 파일로 보냅니다. .

AppServices::SubscriptionService에서 파일에는 몇 가지 일이 필요합니다. 무슨 일이 일어나고 있는지 알아보기 전에 다음과 같은 개체가 있습니다.

module AppServices
  class SubscriptionService
 
    def initialize(params)
      @subscription     = params[:subscription_params][:subscription]
      @token            = params[:subscription_params][:token]
      @plan             = @subscription.subscription_plan
      @user             = @subscription.user
    end
 
    def call
 
      # create or find customer
      customer ||= AppServices::StripeCustomerService.new({customer_params: {customer:@user, token:@token}}).call
 
      if customer && customer.success?
 
        subscription ||= StripeServices::CreateSubscription.new({subscription_params:{
          customer: customer.payload,
          items:[subscription_items],
          expand: ['latest_invoice.payment_intent']
        }}).call
 
        if subscription && subscription.success?
          @subscription.update_attributes(
            status: 'active',
            stripe_id: subscription.payload.id,
            expiration: Time.at(subscription.payload.current_period_end).to_datetime
          )
          OpenStruct.new({success?: true, payload: subscription.payload})
        else
          handle_error(subscription&.error)
        end
 
      else
        handle_error(customer&.error)
      end
 
    end
 
    private
 
      attr_reader :plan
 
      def subscription_items
        base_plan
      end
 
      def base_plan
        [{ plan: plan.stripe_id }]
      end
 
      def handle_error(error)
        OpenStruct.new({success?: false, error: error})
      end
  end
end

높은 수준의 개요에서 살펴보고 있는 내용은 다음과 같습니다.

구독을 생성하기 위해 Stripe에 보낼 수 있도록 먼저 Stripe 고객 ID를 가져와야 합니다. 그 자체로 이를 수행하기 위해 여러 가지 작업을 수행하는 완전히 별개의 서비스 개체입니다.

  1. stripe_customer_id 사용자의 프로필에 저장됩니다. 그렇다면 고객이 실제로 존재하는지 확인하기 위해 Stripe에서 고객을 검색한 다음 OpenStruct의 페이로드에 반환합니다.
  2. 고객이 존재하지 않는 경우 고객을 생성하고 stripe_customer_id를 저장한 다음 OpenStruct의 페이로드에 반환합니다.

어느 쪽이든, 우리의 CustomerService Stripe 고객 ID를 반환하고 이를 수행하는 데 필요한 작업을 수행합니다. 파일은 다음과 같습니다.

module AppServices
  class CustomerService
 
    def initialize(params)
      @user               = params[:customer_params][:customer]
      @token              = params[:customer_params][:token]
      @account            = @user.account
    end
 
    def call
      if @account.stripe_customer_id.present?
        OpenStruct.new({success?: true, payload: @account.stripe_customer_id})
      else
        if find_by_email.success? && find_by_email.payload
          OpenStruct.new({success?: true, payload: @account.stripe_customer_id})
        else
          create_customer
        end
      end
    end
 
    private
 
      attr_reader :user, :token, :account
 
      def find_by_email
        result ||= StripeServices::RetrieveCustomerByEmail.new({email: user.email}).call
        handle_result(result)
      end
 
      def create_customer
        result ||= StripeServices::CreateCustomer.new({customer_params:{email:user.email, source: token}}).call
        handle_result(result)
      end
 
      def handle_result(result)
        if result.success?
          account.update_column(:stripe_customer_id, result.payload.id)
          OpenStruct.new({success?: true, payload: account.stripe_customer_id})
        else
          OpenStruct.new({success?: false, error: result&.error})
        end
      end
 
  end
end
 

여러 서비스 개체에 걸쳐 논리를 구성하는 이유를 알 수 있기를 바랍니다. 이 모든 논리를 가진 거대한 파일 하나를 상상할 수 있습니까? 안 돼요!

AppServices::SubscriptionService로 돌아가기 파일. 이제 Stripe에 보낼 수 있는 고객이 생겼습니다. 그러면 Stripe에서 구독을 생성하는 데 필요한 데이터가 완성됩니다.

이제 마지막 서비스 개체인 StripeServices::CreateSubscription을 호출할 준비가 되었습니다. 파일.

다시, StripeServices::CreateSubscription 서비스 객체는 절대 변경되지 않습니다. 데이터를 가져와 Stripe에 보내고 성공을 반환하거나 개체를 페이로드로 반환하는 단일 책임이 있습니다.

module StripeServices
 
  class CreateSubscription
 
    def initialize(params)
      @subscription_params = params[:subscription_params]
    end
 
    def call
      subscription = Stripe::Subscription.create(@subscription_params)
    rescue Stripe::StripeError => e
      OpenStruct.new({success?: false, error: e})
    else
      OpenStruct.new({success?: true, payload: subscription})
    end
 
  end
 
end

꽤 간단하죠? 하지만 당신은 아마도 이 작은 파일이 과하다고 생각할 것입니다. 위와 유사한 파일의 다른 예를 살펴보겠습니다. 하지만 이번에는 Stripe Connect를 통해 다중 테넌트 애플리케이션과 함께 사용할 수 있도록 확장했습니다.

여기에서 흥미로운 일이 발생합니다. 이 동일한 논리가 SportKeeper에서도 실행되지만 여기에서는 Mavenseed를 예로 사용하고 있습니다. 다중 테넌트 앱은 site_id 열로 구분된 단일 모놀리스 공유 테이블입니다. 각 테넌트가 Stripe Connect를 통해 Stripe에 연결하면 Stripe 계정 ID를 받아 테넌트의 계정에 저장합니다.

동일한 Stripe API 호출을 사용하여 연결된 계정의 Stripe 계정을 전달하기만 하면 Stripe가 연결된 계정을 대신하여 API 호출을 수행합니다.

따라서 우리의 StripeService는 개체는 기본 응용 프로그램 및 테넌트와 함께 이중 작업을 수행하여 동일한 파일을 호출하지만 다른 데이터를 보냅니다.

module StripeServices
 
  class CreateSubscription
 
    def initialize(params)
      @subscription_params  = params[:subscription_params]
      @stripe_account       = params[:stripe_account]
      @stripe_secret_key    = params[:stripe_secret_key] ? params[:stripe_secret_key] : (Rails.env.production? ? ENV['STRIPE_LIVE_SECRET_KEY'] : ENV['STRIPE_TEST_SECRET_KEY'])
    end
 
    def call
      subscription = Stripe::Subscription.create(@subscription_params, account_params)
    rescue Stripe::StripeError => e
      OpenStruct.new({success?: false, error: e})
    else
      OpenStruct.new({success?: true, payload: subscription})
    end
 
    private
 
      attr_reader :stripe_account, :stripe_secret_key
 
      def account_params
        {
          api_key: stripe_secret_key,
          stripe_account: stripe_account,
          stripe_version: ENV['STRIPE_API_VERSION']
        }
      end
  end
 
end

이 파일에 대한 몇 가지 기술 참고 사항:더 간단한 예를 공유할 수 있었지만 응답을 포함하여 적절한 서비스 개체가 어떻게 구성되어 있는지 보는 것이 중요하다고 생각합니다.

첫째, "call" 메소드에는 구조 및 else 문이 있습니다. 다음을 작성하는 것과 동일합니다.

def call
   begin
   rescue Stripe ::StripeError  => e
   else
   end
end

그러나 Ruby 메서드는 암시적으로 블록을 자동으로 시작하므로 시작과 끝을 추가할 이유가 없습니다. 이 문은 "구독을 생성하고 오류가 있으면 오류를 반환하고 그렇지 않으면 구독을 반환합니다."로 읽습니다.

간단하고 간결하며 우아합니다. Ruby는 진정으로 아름다운 언어이며 서비스 객체를 사용하면 이 점이 특히 강조됩니다.

서비스 파일이 우리 응용 프로그램에서 수행하는 가치를 볼 수 있기를 바랍니다. 그들은 예측할 수 있을 뿐만 아니라 쉽게 유지 관리할 수 있는 매우 간결한 논리 구성 방법을 제공합니다.

추신 Ruby Magic 게시물이 언론에 공개되는 즉시 읽고 싶다면 Ruby Magic 뉴스레터를 구독하고 게시물을 놓치지 마세요!

——

내 새 책 Playbook Thirty-9 - 최소한의 도구로 대화형 웹 앱 제공에 대한 가이드를 선택하여 이 장을 비롯한 자세한 내용을 읽으십시오. . 이 책에서 나는 단독 개발자로 구축하고 다수의 고수익 웹사이트 애플리케이션을 유지 관리하는 직접적인 경험을 바탕으로 일반적인 패턴과 기술을 다루는 하향식 접근 방식을 취합니다.

쿠폰 코드 appsignalrocks 사용 30% 절약!