Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

RubyonRailsでのサービスオブジェクトの使用

この記事は、Playbook39の元の外観から変更されています-最小限のツールでインタラクティブなWebアプリを出荷するためのガイド 、AppSignalのこのゲスト投稿に合うように調整されています。

アプリが処理する必要のある機能はたくさんありますが、そのロジックは必ずしもコントローラーやモデルに属しているとは限りません。たとえば、カートでのチェックアウト、サイトへの登録、サブスクリプションの開始などがあります。

このすべてのロジックをコントローラーに含めることもできますが、繰り返し続けて、それらすべての場所で同じロジックを呼び出します。ロジックをモデルに入れることもできますが、IPアドレスや、URLのパラメーターなど、コントローラーで簡単に利用できるものにアクセスする必要がある場合があります。必要なのはサービスオブジェクトです。

サービスオブジェクトの役割は、機能をカプセル化し、1つのサービスを実行し、単一障害点を提供することです。また、サービスオブジェクトを使用すると、アプリケーションのさまざまな部分で使用するときに、開発者が同じコードを何度も作成する必要がなくなります。

サービスオブジェクトは、単なる古いRubyオブジェクト(「PORO」)です。これは、特定のディレクトリの下にある単なるファイルです。これは、予測可能な応答を返すRubyクラスです。応答を予測可能にするのは、3つの重要な部分によるものです。すべてのサービスオブジェクトは同じパターンに従う必要があります。

  • params引数を使用した初期化メソッドがあります。
  • callという名前の単一のパブリックメソッドがあります。
  • 成功したOpenStructを返しますか?ペイロードまたはエラーのいずれか。

OpenStructとは何ですか?

これは、クラスとハッシュの発案のようなものです。任意の属性を受け取ることができるミニクラスと考えることができます。この例では、2つの属性のみを処理する一種の一時的なデータ構造として使用しています。

成功が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を自由に使用して、予測可能な応答を返します。ロジックのアーキテクチャパターンはすべて同一であるため、これはテストの作成に関して非常に価値があります。

モジュールとは何ですか?

モジュールを使用すると、名前の間隔が広がり、他のクラスとの衝突を防ぐことができます。これは、すべてのクラスで同じメソッド名を使用でき、特定の名前空間の下にあるために衝突しないことを意味します。

モジュール名のもう1つの重要な部分は、アプリ内でファイルがどのように編成されているかです。サービスオブジェクトは、プロジェクトのサービスフォルダーに保持されます。上記のサービスオブジェクトの例。モジュール名はAppServicesです。 、AppServicesに分類されます サービスディレクトリ内のフォルダ。

サービスディレクトリを複数のフォルダに整理します。各フォルダには、アプリケーションの特定の部分の機能が含まれています。

たとえば、CloudflareServices ディレクトリは、Cloudflareでサブドメインを作成および削除するための特定のサービスオブジェクトを保持します。 WistiaサービスとZapierサービスは、それぞれのサービスファイルを保持しています。

このようにサービスオブジェクトを整理すると、実装に関しては予測可能性が向上し、アプリが何をしているのかを1万フィートのビューから一目で簡単に確認できます。

StripeServicesを掘り下げてみましょう ディレクトリ。このディレクトリは、StripesAPIと対話するための個々のサービスオブジェクトを保持します。繰り返しますが、これらのファイルが行うのは、アプリケーションからデータを取得してStripeに送信することだけです。 StripeServiceでAPI呼び出しを更新する必要がある場合 サブスクリプションを作成するオブジェクトの場合、それを実行できる場所は1つだけです。

送信するデータを収集するすべてのロジックは、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

とても簡単ですよね?しかし、おそらくあなたは考えているでしょう、この小さなファイルはやり過ぎです。上記と同様のファイルの別の例を見てみましょう。ただし、今回は、StripeConnectを介してマルチテナントアプリケーションで使用できるようにファイルを拡張しました。

ここで物事が面白くなります。ここでは例としてMavenseedを使用していますが、これと同じロジックがSportKeeperでも実行されます。マルチテナントアプリは、site_id列で区切られた、テーブルを共有する単一のモノリスです。各テナントはStripeConnectを介してStripeに接続し、次にStripeアカウントIDを取得して、テナントのアカウントに保存します。

同じStripeAPI呼び出しを使用して、接続されたアカウントの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は本当に美しい言語であり、サービスオブジェクトの使用はこれを本当に際立たせます。

私たちのアプリケーションでサービスファイルが果たす価値をご覧いただければ幸いです。これらは、予測可能であるだけでなく、簡単に保守できるロジックを整理するための非常に簡潔な方法を提供します。

P.S。 Ruby Magicの投稿をマスコミから離れたらすぐに読みたい場合は、Ruby Magicニュースレターを購読して、投稿を1つも見逃さないでください。

-

私の新しい本Playbook39を手に取って、この章などを読んでください。最小限のツールでインタラクティブなWebアプリを出荷するためのガイド 。この本では、私はトップダウンのアプローチで一般的なパターンとテクニックをカバーします。これは、ソロ開発者としての経験に基づいて、複数の高トラフィック、高収益のWebサイトアプリケーションを構築および維持することだけに基づいています。

クーポンコードappsignalrocksを使用します そして30%節約!


  1. Ruby on Railsとは何ですか?なぜそれが役立つのですか?

    Ruby on Rails(RoRの場合もある)は、最も人気のあるオープンソースのWebアプリケーションフレームワークです。 Rubyプログラミング言語で構築されています。 Railsを使用すると、単純なものから複雑なものまで、アプリケーションの構築に役立ちます。Railsで実行できることには制限がありません。 フレームワークとは何ですか? フレームワークは、ソフトウェアを作成するときに使用する特定の構造を提供するコード、ツール、およびユーティリティのコレクションです。 この構造により、コードがより整理されます。 正しく使うことを学ぶと、作業が簡単になります。 レールは正確に何を

  2. Ruby Freezeメソッド–オブジェクトの可変性を理解する

    オブジェクトが可変であるとはどういう意味ですか? 派手な言葉で混乱させないでください。「可変性 」は、オブジェクトの内部状態を変更できることを意味します。これは、凍結されたオブジェクトを除く、すべてのオブジェクトのデフォルトです。 、または特別なオブジェクトのリストの一部であるもの。 つまり、Rubyのすべてのオブジェクトが変更可能というわけではありません! 例 : 数字や記号、さらにはtrueには意味がありません またはfalse (オブジェクトでもあります)変更します。 数字の1は常に1になります。 ただし、他のオブジェクト、特に配列オブジェクトやハッシュオブジェクトなどのデー