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を取得する必要があります。それ自体は、これを実現するために多くのことを行う完全に別個のサービスオブジェクトです。
-
stripe_customer_id
かどうかを確認します ユーザーのプロファイルに保存されます。そうである場合は、顧客が実際に存在することを確認するためにStripeから顧客を取得し、OpenStructのペイロードで返します。 - 顧客が存在しない場合は、顧客を作成し、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%節約!
-
Ruby on Railsとは何ですか?なぜそれが役立つのですか?
Ruby on Rails(RoRの場合もある)は、最も人気のあるオープンソースのWebアプリケーションフレームワークです。 Rubyプログラミング言語で構築されています。 Railsを使用すると、単純なものから複雑なものまで、アプリケーションの構築に役立ちます。Railsで実行できることには制限がありません。 フレームワークとは何ですか? フレームワークは、ソフトウェアを作成するときに使用する特定の構造を提供するコード、ツール、およびユーティリティのコレクションです。 この構造により、コードがより整理されます。 正しく使うことを学ぶと、作業が簡単になります。 レールは正確に何を
-
Ruby Freezeメソッド–オブジェクトの可変性を理解する
オブジェクトが可変であるとはどういう意味ですか? 派手な言葉で混乱させないでください。「可変性 」は、オブジェクトの内部状態を変更できることを意味します。これは、凍結されたオブジェクトを除く、すべてのオブジェクトのデフォルトです。 、または特別なオブジェクトのリストの一部であるもの。 つまり、Rubyのすべてのオブジェクトが変更可能というわけではありません! 例 : 数字や記号、さらにはtrueには意味がありません またはfalse (オブジェクトでもあります)変更します。 数字の1は常に1になります。 ただし、他のオブジェクト、特に配列オブジェクトやハッシュオブジェクトなどのデー