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.
- 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. - 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%!