Computer >> คอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Ruby

การใช้ Service Objects ใน Ruby on Rails

บทความนี้ได้รับการแก้ไขจากลักษณะเดิมใน Playbook Thirty-nine - A Guide to Shipping Interactive Web Apps with Minimal Tooling และปรับแต่งให้เหมาะกับโพสต์ของผู้เยี่ยมชมนี้สำหรับ AppSignal

มีฟังก์ชันมากมายที่แอปของคุณต้องจัดการ แต่ตรรกะนั้นไม่จำเป็นต้องอยู่ในตัวควบคุมหรือแม้แต่โมเดลเสมอไป ตัวอย่างบางส่วน ได้แก่ การชำระเงินด้วยรถเข็น การลงทะเบียนสำหรับเว็บไซต์ หรือการสมัครรับข้อมูล

คุณสามารถรวมตรรกะทั้งหมดนี้ไว้ในตัวควบคุมได้ แต่คุณจะต้องทำซ้ำตัวเองโดยเรียกตรรกะเดียวกันในทุกที่เหล่านั้น คุณสามารถใส่ตรรกะในแบบจำลอง แต่บางครั้ง คุณต้องเข้าถึงสิ่งต่าง ๆ ที่พร้อมใช้งานในตัวควบคุมอย่างง่าย เช่น ที่อยู่ IP หรือพารามิเตอร์ใน URL สิ่งที่คุณต้องการคือวัตถุบริการ

งานของอ็อบเจ็กต์บริการคือการห่อหุ้มฟังก์ชันการทำงาน ดำเนินการบริการเดียว และจัดเตรียมจุดล้มเหลวเพียงจุดเดียว การใช้ออบเจ็กต์บริการยังช่วยป้องกันไม่ให้นักพัฒนาต้องเขียนโค้ดเดียวกันซ้ำแล้วซ้ำอีกเมื่อใช้ในส่วนต่างๆ ของแอปพลิเคชัน

ออบเจ็กต์บริการเป็นเพียงออบเจ็กต์ Old Ruby ธรรมดา ("PORO") เป็นเพียงไฟล์ที่อยู่ภายใต้ไดเร็กทอรีเฉพาะ เป็นคลาส Ruby ที่ส่งคืนการตอบสนองที่คาดเดาได้ สิ่งที่ทำให้การตอบสนองคาดเดาได้นั้นเกิดจากส่วนสำคัญสามส่วน ออบเจ็กต์บริการทั้งหมดควรเป็นไปตามรูปแบบเดียวกัน

  • มีวิธีการเริ่มต้นพร้อมอาร์กิวเมนต์ params
  • มีเมธอดสาธารณะแบบเดียวที่ชื่อว่า call
  • คืน OpenStruct ด้วยความสำเร็จ? และเพย์โหลดหรือข้อผิดพลาด

OpenStruct คืออะไร

มันเหมือนกับลูกสมุนของชั้นเรียนและแฮช คุณสามารถคิดได้ว่าเป็นคลาสขนาดเล็กที่สามารถรับคุณลักษณะตามอำเภอใจได้ ในกรณีของเรา เราใช้มันเป็นโครงสร้างข้อมูลชั่วคราวที่จัดการเพียงสองแอตทริบิวต์

หากความสำเร็จ true , จะส่งคืนส่วนของข้อมูล

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

หากความสำเร็จคือ false จะส่งกลับข้อผิดพลาด

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

นี่คือตัวอย่างของออบเจ็กต์บริการที่เข้าถึงและดึงข้อมูลจาก API ใหม่ของ AppSignals ซึ่งขณะนี้อยู่ในรุ่นเบต้า

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 มีไฟล์บริการที่เกี่ยวข้อง

การจัดระเบียบออบเจ็กต์บริการในลักษณะนี้ช่วยให้คาดการณ์ได้ดีขึ้นเมื่อนำไปใช้จริง และดูได้ง่ายๆ ว่าแอปกำลังทำอะไรจากมุมมอง 1 หมื่นฟุต

มาดู StripeServices . กัน ไดเรกทอรี ไดเร็กทอรีนี้มีออบเจ็กต์บริการแต่ละรายการสำหรับการโต้ตอบกับ Stripes API อีกครั้ง สิ่งเดียวที่ไฟล์เหล่านี้ทำคือนำข้อมูลจากแอปพลิเคชันของเราและส่งไปยัง Stripe หากคุณจำเป็นต้องอัปเดตการเรียก API ใน StripeService วัตถุที่สร้างการสมัครรับข้อมูล คุณมีที่เดียวเท่านั้นที่จะทำสิ่งนั้น

ตรรกะทั้งหมดที่รวบรวมข้อมูลที่จะส่งจะทำในออบเจ็กต์บริการแยกต่างหาก ซึ่งอยู่ใน 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 file มีหลายสิ่งที่จำเป็นต้องเกิดขึ้น นี่คือสิ่งที่ ก่อนที่เราจะเข้าสู่สิ่งที่เกิดขึ้น:

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

จากภาพรวมระดับสูง นี่คือสิ่งที่เรากำลังดู:

เราต้องรับ ID ลูกค้า Stripe ก่อน เพื่อที่เราจะสามารถส่งไปยัง Stripe เพื่อสร้างการสมัครรับข้อมูลได้ ในตัวมันเองเป็นวัตถุบริการที่แยกจากกันโดยสิ้นเชิงที่ทำหลายสิ่งเพื่อให้สิ่งนี้เกิดขึ้น

  1. เราตรวจสอบเพื่อดูว่า stripe_customer_id ถูกบันทึกไว้ในโปรไฟล์ของผู้ใช้ หากใช่ เราจะเรียกลูกค้าจาก Stripe เพียงเพื่อให้แน่ใจว่าลูกค้ามีอยู่จริง จากนั้นส่งคืนในเพย์โหลดของ OpenStruct ของเรา
  2. หากไม่มีลูกค้า เราจะสร้างลูกค้า บันทึก stripe_customer_id จากนั้นส่งคืนในเพย์โหลดของ OpenStruct

ไม่ว่าจะด้วยวิธีใด CustomerService . ของเรา ส่งคืนรหัสลูกค้า Stripe และจะทำสิ่งที่จำเป็นเพื่อให้สิ่งนั้นเกิดขึ้น นี่คือไฟล์นั้น:

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

นี่คือสิ่งที่น่าสนใจ เราใช้ Mavenseed เป็นตัวอย่าง แม้ว่าตรรกะเดียวกันนี้จะทำงานบน SportKeeper เช่นกัน แอปที่มีผู้เช่าหลายรายของเราเป็นตารางการแชร์แบบเสาเดียว คั่นด้วยคอลัมน์ site_id ผู้เช่าแต่ละรายเชื่อมต่อกับ Stripe ผ่าน Stripe Connect จากนั้นเราจะได้รับรหัสบัญชี Stripe เพื่อบันทึกในบัญชีของผู้เช่า

เมื่อใช้การเรียก 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

หมายเหตุทางเทคนิคบางประการเกี่ยวกับไฟล์นี้:ฉันสามารถแชร์ตัวอย่างที่ง่ายกว่านี้ แต่ฉันคิดว่ามันมีค่าสำหรับคุณที่จะเห็นว่าออบเจกต์บริการมีโครงสร้างอย่างไร รวมถึงการตอบกลับด้วย

ขั้นแรก วิธีการ "โทร" มีคำสั่งช่วยเหลือและอย่างอื่น เหมือนกับการเขียนข้อความต่อไปนี้:

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

แต่วิธีการของ Ruby จะเริ่มบล็อกโดยอัตโนมัติโดยปริยาย ดังนั้นจึงไม่มีเหตุผลที่จะเพิ่มจุดเริ่มต้นและจุดสิ้นสุด คำสั่งนี้อ่านว่า “สร้างการสมัครรับข้อมูล ส่งคืนข้อผิดพลาดหากมี มิฉะนั้น ส่งคืนการสมัครสมาชิก”

เรียบง่าย กระชับ และสง่างาม Ruby เป็นภาษาที่สวยงามจริงๆ และการใช้บริการวัตถุก็เน้นให้เห็นถึงสิ่งนี้

ฉันหวังว่าคุณจะเห็นคุณค่าที่ไฟล์บริการเล่นในแอปพลิเคชันของเรา สิ่งเหล่านี้เป็นวิธีที่กระชับมากในการจัดระเบียบตรรกะของเรา ซึ่งไม่เพียงแต่คาดการณ์ได้เท่านั้น แต่ยังรักษาได้ง่ายอีกด้วย!

ป.ล. หากคุณต้องการอ่านโพสต์ Ruby Magic ทันทีที่ออกจากสื่อ สมัครรับจดหมายข่าว Ruby Magic ของเราและไม่พลาดแม้แต่โพสต์เดียว!

——

อ่านบทนี้และอื่น ๆ โดยหยิบหนังสือเล่มใหม่ของฉัน Playbook Thirty-nine - A Guide to Shipping Interactive Web Apps with Minimal Tooling . ในหนังสือเล่มนี้ ฉันใช้แนวทางจากบนลงล่างเพื่อครอบคลุมรูปแบบและเทคนิคทั่วไป โดยอิงจากประสบการณ์ตรงของฉันในฐานะนักพัฒนาคนเดียวและดูแลแอปพลิเคชันเว็บไซต์ที่มีรายได้สูงและเข้าชมสูงหลายรายการ

ใช้รหัสคูปอง appsignalrocks และประหยัด 30%!