Computer >> Máy Tính >  >> Lập trình >> Ruby

Sử dụng các đối tượng dịch vụ trong Ruby on Rails

Bài viết này đã được sửa đổi so với hình thức ban đầu trong Playbook Ba mươi chín - Hướng dẫn vận chuyển ứng dụng web tương tác với công cụ tối thiểu và được điều chỉnh để phù hợp với bài đăng của khách này cho AppSignal.

Có rất nhiều chức năng mà ứng dụng của bạn cần xử lý, nhưng logic đó không nhất thiết phải thuộc về bộ điều khiển hoặc thậm chí là mô hình. Một số ví dụ bao gồm thanh toán bằng giỏ hàng, đăng ký trang web hoặc bắt đầu đăng ký.

Bạn có thể đưa tất cả logic này vào bộ điều khiển, nhưng bạn sẽ tiếp tục lặp lại chính mình, gọi cùng một logic ở tất cả những nơi đó. Bạn có thể đặt logic trong một mô hình, nhưng đôi khi, bạn cần truy cập vào những thứ dễ dàng có sẵn trong bộ điều khiển, chẳng hạn như địa chỉ IP hoặc một tham số trong URL. Những gì bạn cần là một đối tượng dịch vụ.

Công việc của một đối tượng dịch vụ là đóng gói chức năng, thực thi một dịch vụ và cung cấp một điểm lỗi duy nhất. Việc sử dụng các đối tượng dịch vụ cũng ngăn các nhà phát triển phải viết đi viết lại cùng một mã khi nó được sử dụng trong các phần khác nhau của ứng dụng.

Một đối tượng dịch vụ chỉ là một Đối tượng Ruby Cũ ("PORO"). Nó chỉ là một tệp nằm trong một thư mục cụ thể. Đó là một lớp Ruby trả về một phản hồi có thể dự đoán được. Điều làm cho phản ứng có thể dự đoán được là do ba phần quan trọng. Tất cả các đối tượng dịch vụ phải tuân theo cùng một mẫu.

  • Có phương thức khởi tạo với đối số params.
  • Có một phương thức công khai được đặt tên là cuộc gọi.
  • Trả lại một OpenStruct thành công? và một trọng tải hoặc một lỗi.

OpenStruct là gì?

Nó giống như đứa con tinh thần của một lớp học và một hàm băm. Bạn có thể coi nó như một lớp nhỏ có thể nhận các thuộc tính tùy ý. Trong trường hợp của chúng tôi, chúng tôi đang sử dụng nó như một loại cấu trúc dữ liệu tạm thời chỉ xử lý hai thuộc tính.

Nếu thành công là true , nó trả về một khối lượng dữ liệu.

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

Nếu thành công là false , nó trả về một lỗi.

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

Dưới đây là ví dụ về đối tượng dịch vụ tiếp cận và lấy dữ liệu từ API mới của AppSignals, hiện đang ở giai đoạn thử nghiệm.

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

Bạn sẽ gọi tệp ở trên bằng AppServices::AppSignalApiService.new({endpoint: 'markers'}).call . Tôi sử dụng OpenStruct một cách tự do để trả về một phản hồi có thể dự đoán được. Điều này thực sự có giá trị khi nói đến các bài kiểm tra viết vì tất cả các mẫu kiến ​​trúc của logic đều giống hệt nhau.

Mô-đun là gì?

Việc sử dụng các mô-đun cung cấp cho chúng ta khoảng cách giữa các tên và tránh va chạm với các lớp khác. Điều này có nghĩa là bạn có thể sử dụng các tên phương thức giống nhau trong tất cả các lớp và chúng sẽ không xung đột vì chúng nằm trong một không gian tên cụ thể.

Một phần quan trọng khác của tên mô-đun là cách tổ chức tệp trong ứng dụng của chúng tôi. Các đối tượng dịch vụ được giữ trong một thư mục dịch vụ trong dự án. Ví dụ về đối tượng dịch vụ ở trên, với tên mô-đun của AppServices , thuộc AppServices trong thư mục dịch vụ.

Tôi sắp xếp thư mục dịch vụ của mình thành nhiều thư mục, mỗi thư mục chứa chức năng cho một phần cụ thể của ứng dụng.

Ví dụ:CloudflareServices thư mục chứa các đối tượng dịch vụ cụ thể để tạo và xóa tên miền phụ trên Cloudflare. Các dịch vụ Wistia và Zapier giữ các tệp dịch vụ tương ứng của họ.

Việc tổ chức các đối tượng dịch vụ của bạn như thế này mang lại khả năng dự đoán tốt hơn khi bắt đầu triển khai và bạn có thể dễ dàng xem nhanh ứng dụng đang làm gì từ chế độ xem 10 km.

Hãy cùng tìm hiểu về StripeServices danh mục. Thư mục này chứa các đối tượng dịch vụ riêng lẻ để tương tác với API Stripes. Một lần nữa, điều duy nhất mà các tệp này làm là lấy dữ liệu từ ứng dụng của chúng tôi và gửi đến Stripe. Nếu bạn cần cập nhật lệnh gọi API trong StripeService đối tượng tạo đăng ký, bạn chỉ có một nơi để thực hiện điều đó.

Tất cả logic thu thập dữ liệu được gửi được thực hiện trong một đối tượng dịch vụ riêng biệt, nằm trong AppServices danh mục. Các tệp này thu thập dữ liệu từ ứng dụng của chúng tôi và gửi dữ liệu đó đến thư mục dịch vụ tương ứng để giao tiếp với API bên ngoài.

Dưới đây là một ví dụ trực quan:giả sử rằng chúng tôi có một người nào đó đang bắt đầu đăng ký mới. Mọi thứ bắt nguồn từ một bộ điều khiển. Đây là 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

Trước tiên, chúng tôi sẽ tạo đăng ký trong ứng dụng và nếu thành công, chúng tôi sẽ gửi dữ liệu đó, stripeToken và những thứ như phiếu giảm giá vào một tệp có tên AppServices::SubscriptionService .

Trong AppServices::SubscriptionService tệp, có một số điều cần phải xảy ra. Đây là đối tượng đó, trước khi chúng ta đi vào những gì đang xảy ra:

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

Từ tổng quan cấp cao, đây là những gì chúng ta đang xem:

Trước tiên, chúng tôi phải lấy ID khách hàng của Stripe để có thể gửi đến Stripe để tạo đăng ký. Bản thân nó là một đối tượng dịch vụ hoàn toàn riêng biệt thực hiện một số điều để biến điều này thành hiện thực.

  1. Chúng tôi kiểm tra xem liệu stripe_customer_id được lưu trên hồ sơ của người dùng. Nếu đúng như vậy, chúng tôi truy xuất khách hàng từ Stripe chỉ để đảm bảo rằng khách hàng thực sự tồn tại, sau đó trả lại khách hàng đó dưới dạng trọng tải của OpenStruct của chúng tôi.
  2. Nếu khách hàng không tồn tại, chúng tôi tạo khách hàng, lưu stripe_customer_id, sau đó trả lại khách hàng đó trong tải trọng của OpenStruct.

Dù bằng cách nào, CustomerService của chúng tôi trả lại ID khách hàng Stripe và nó sẽ làm những gì cần thiết để biến điều đó thành hiện thực. Đây là tệp đó:

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
 

Hy vọng rằng bạn có thể bắt đầu hiểu tại sao chúng tôi cấu trúc logic của mình trên nhiều đối tượng dịch vụ. Bạn có thể tưởng tượng một tập tin khổng lồ khổng lồ với tất cả logic này không? Không thể nào!

Quay lại AppServices::SubscriptionService của chúng tôi tập tin. Giờ đây, chúng tôi có một khách hàng mà chúng tôi có thể gửi cho Stripe, họ hoàn thành dữ liệu mà chúng tôi cần để tạo đăng ký trên Stripe.

Bây giờ chúng tôi đã sẵn sàng để gọi đối tượng dịch vụ cuối cùng, StripeServices::CreateSubscription tệp.

Một lần nữa, StripeServices::CreateSubscription đối tượng dịch vụ không bao giờ thay đổi. Nó có một trách nhiệm duy nhất, đó là lấy dữ liệu, gửi đến Stripe và trả về thành công hoặc trả về đối tượng dưới dạng trọng tải.

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

Khá đơn giản phải không? Nhưng có thể bạn đang nghĩ, tệp nhỏ này quá mức cần thiết. Hãy xem một ví dụ khác về tệp tương tự như ở trên, nhưng lần này chúng tôi đã tăng cường nó để sử dụng với ứng dụng nhiều người thuê thông qua Stripe Connect.

Đây là nơi mọi thứ trở nên thú vị. Chúng tôi đang sử dụng Mavenseed làm ví dụ ở đây, mặc dù logic tương tự này cũng chạy trên SportKeeper. Ứng dụng nhiều người thuê của chúng tôi là một khối duy nhất, chia sẻ các bảng, được phân tách bằng cột site_id. Mỗi người thuê kết nối với Stripe qua Stripe Connect và sau đó chúng tôi sẽ nhận được ID tài khoản Stripe để lưu vào tài khoản của người thuê.

Sử dụng các lệnh gọi API Stripe tương tự của chúng tôi, chúng tôi có thể chỉ cần chuyển Tài khoản Stripe của tài khoản được kết nối và Stripe sẽ thực hiện lệnh gọi API thay mặt cho tài khoản được kết nối.

Vì vậy, theo một cách nào đó, StripeService của chúng tôi đối tượng đang thực hiện hai nhiệm vụ, cùng với cả ứng dụng chính và đối tượng thuê, để gọi cùng một tệp, nhưng gửi dữ liệu khác nhau.

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

Một vài lưu ý kỹ thuật về tệp này:Tôi có thể đã chia sẻ một ví dụ đơn giản hơn, nhưng tôi thực sự nghĩ rằng việc xem một đối tượng dịch vụ thích hợp được cấu trúc như thế nào, bao gồm cả các phản hồi của nó là rất hữu ích.

Đầu tiên, phương thức "call" có câu lệnh cứu và câu lệnh khác. Điều này cũng giống như cách viết sau:

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

Nhưng các phương thức Ruby tự động bắt đầu một khối một cách ngầm định, vì vậy không có lý do gì để thêm phần bắt đầu và kết thúc. Câu lệnh này có nội dung là, “tạo đăng ký, trả lại lỗi nếu có, nếu không, hãy trả lại đăng ký.”

Đơn giản, cô đọng và trang nhã. Ruby thực sự là một ngôn ngữ đẹp và việc sử dụng các đối tượng dịch vụ thực sự làm nổi bật điều này.

Tôi hy vọng rằng bạn có thể thấy giá trị mà các tệp dịch vụ đóng trong các ứng dụng của chúng tôi. Chúng cung cấp một cách rất ngắn gọn để tổ chức logic của chúng tôi, không chỉ có thể dự đoán được mà còn dễ bảo trì!

Tái bút. Nếu bạn muốn đọc các bài đăng của Ruby Magic ngay khi chúng xuất hiện trên báo chí, hãy đăng ký bản tin Ruby Magic của chúng tôi và không bao giờ bỏ lỡ một bài đăng nào!

——

Đọc chương này và hơn thế nữa bằng cách chọn cuốn sách mới của tôi Playbook Ba mươi chín - Hướng dẫn vận chuyển ứng dụng web tương tác với công cụ tối thiểu . Trong cuốn sách này, tôi thực hiện phương pháp tiếp cận từ trên xuống để đề cập đến các mẫu và kỹ thuật phổ biến, chỉ dựa trên kinh nghiệm trực tiếp của tôi khi xây dựng một nhà phát triển đơn lẻ và duy trì nhiều ứng dụng trang web có lưu lượng truy cập cao, doanh thu cao.

Sử dụng mã phiếu thưởng appsignalrocks và tiết kiệm 30%!