บทความนี้ได้รับการแก้ไขจากลักษณะเดิมใน 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 เพื่อสร้างการสมัครรับข้อมูลได้ ในตัวมันเองเป็นวัตถุบริการที่แยกจากกันโดยสิ้นเชิงที่ทำหลายสิ่งเพื่อให้สิ่งนี้เกิดขึ้น
- เราตรวจสอบเพื่อดูว่า
stripe_customer_id
ถูกบันทึกไว้ในโปรไฟล์ของผู้ใช้ หากใช่ เราจะเรียกลูกค้าจาก Stripe เพียงเพื่อให้แน่ใจว่าลูกค้ามีอยู่จริง จากนั้นส่งคืนในเพย์โหลดของ OpenStruct ของเรา - หากไม่มีลูกค้า เราจะสร้างลูกค้า บันทึก 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%!