Ruby on Rails: لایه سرویس شما یک دروغ است

اگر شما یک توسعه دهنده Rails هستید، احتمالاً کدی را دیده اید (یا نوشته اید) که شبیه این است:
class UserService
def self.create(params)
user = User.new(params)
if user.save
UserMailer.welcome_email(user).deliver_later
user
else
false
end
end
end
# In your controller
def create
@user = UserService.create(user_params)
if @user
redirect_to @user, notice: 'User was successfully created.'
else
render :new
end
end
آشنا به نظر می رسد؟ همه ما آنجا بوده ایم. همه ما این اشیاء سرویس را نوشته ایم زیرا “این کاری است که توسعه دهندگان خوب Rails انجام می دهند.” اما اجازه دهید از شما چیزی بپرسم: آن لایه سرویس واقعاً چه ارزشی را اضافه کرد؟
به ما گفته شده است که لایه های سرویس:
- منطق کسب و کار را از مدل ها جدا کنید
- کد را قابل آزمایش تر کنید
- کنترلرها را نازک نگه دارید
- از اصل مسئولیت واحد پیروی کنید
وقتی سرویس شما فقط تایپ اضافی است
بسیاری از پایگاههای کد Rails مملو از اشیاء خدماتی هستند که کاری جز فراخوانی روش پراکسی به مدلهای ActiveRecord انجام نمیدهند. بیایید به یک مثال در دنیای واقعی نگاه کنیم:
class CommentService
def self.create_for_post(post, user, content)
Comment.create(
post: post,
user: user,
content: content
)
end
end
اینجا چه چیزی به دست آوردیم؟ هیچ چیز به جز یک فایل اضافی برای نگهداری و یک لایه اضافی برای پرش در هنگام اشکال زدایی وجود ندارد. این سرویس به معنای واقعی کلمه فقط از پارامترهای Comment.create عبور می کند. حتی بدتر از آن، ما در مقایسه با کار مستقیم با مدل، عملکرد خود را از دست دادهایم – دیگر به API غنی ActiveRecord برای رسیدگی به خطاهای اعتبارسنجی و تماسهای برگشتی دسترسی نداریم.
وقتی واقعاً به یک لایه سرویس نیاز دارید
بیایید واضح بگوییم: اشیاء خدمات همیشه بد نیستند. (در واقع من یک مقاله جداگانه در مورد بازنگری اشیاء خدمات نوشته ام: https://dev.to/alexander_shagov/ruby-on-rails-rethinking-service-objects-4l0b)
1. سازماندهی عملیات مجتمع
module Orders
class ProcessingWorkflow
include Dry::Transaction
step :start_processing
step :process_payment
step :allocate_inventory
step :create_shipping_label
step :send_confirmation
step :complete_processing
private
def start_processing(input)
order = input[:order]
order.update!(status: 'processing')
Success(input)
rescue => e
Failure([:processing_failed, e.message])
end
def process_payment(input)
order = input[:order]
payment = Payments::Gateway.new.charge(
amount: order.total,
token: order.payment_token
)
Success(input.merge(payment: payment))
rescue => e
Failure([:payment_failed, e.message])
end
def allocate_inventory(input)
# ...
end
def create_shipping_label(input)
# ...
end
def send_confirmation(input)
OrderMailer.confirmation(input[:order]).deliver_later
Success(input)
rescue => e
Failure([:notification_failed, e.message])
end
def complete_processing(input)
order = input[:order]
order.update!(status: 'processed')
Success(input)
rescue => e
Failure([:completion_failed, e.message])
end
end
end
2. رسیدگی به خدمات خارجی
module Subscriptions
class StripeWorkflow
include Dry::Transaction
step :validate_subscription
step :create_stripe_customer
step :create_stripe_subscription
step :update_user_records
private
def validate_subscription(input)
contract = Subscriptions::ValidationContract.new
result = contract.call(input)
result.success? ? Success(input) : Failure([:validation_failed, result.errors])
end
def create_stripe_customer(input)
# ... stripe code
end
def create_stripe_subscription(input)
# ... stripe code
end
def update_user_records(input)
user = input[:user]
user.update!(
stripe_customer_id: input[:customer].id,
stripe_subscription_id: input[:subscription].id,
subscription_status: input[:subscription].status
)
Success(input)
rescue => e
Failure([:record_update_failed, e.message])
end
end
end
3. قوانین پیچیده کسب و کار
module Loans
class ApplicationProcess
include Dry::Transaction
step :validate_application
step :check_credit_score
step :evaluate_debt_ratio
step :calculate_risk_score
step :determine_approval
step :process_result
private
def validate_application(input)
contract = Loans::ApplicationContract.new
result = contract.call(input)
result.success? ? Success(input) : Failure([:validation_failed, result.errors])
end
def check_credit_score(input)
application = input[:application]
if application.credit_score < 600
Failure([:credit_score_too_low, "Credit score below minimum requirement"])
else
Success(input)
end
end
def evaluate_debt_ratio(input)
calculator = Loans::DebtRatioCalculator.new(input[:application])
ratio = calculator.compute
if ratio > 0.43
Failure([:debt_ratio_too_high, "Debt-to-income ratio exceeds maximum"])
else
Success(input.merge(debt_ratio: ratio))
end
end
def calculate_risk_score(input)
# ...
end
def determine_approval(input)
# ...
end
def process_result(input)
application = input[:application]
if input[:approved]
rate_calculator = Loans::InterestRateCalculator.new(
application: application,
risk_score: input[:risk_score]
)
application.update!(
status: 'approved',
interest_rate: rate_calculator.compute,
approval_date: Time.current
)
LoanMailer.approval_notice(application).deliver_later
else
application.update!(status: 'rejected')
LoanMailer.rejection_notice(application).deliver_later
end
Success(input)
rescue => e
Failure([:processing_failed, e.message])
end
end
end
مثالهای بالا نشاندهنده مواردی است که من استفاده معتبر از یک لایه سرویس میدانم – یا بهطور دقیقتر، فرآیندهای تجاری. آنها موارد واضحی را نشان می دهند که در آن انتزاع ارزش واقعی اضافه می کند: گردش کار پیچیده، یکپارچه سازی خدمات خارجی، و قوانین تجاری غنی از دامنه و غیره.
اما نکته کلیدی فقط در مورد زمان استفاده از این الگوها نیست – بلکه در مورد زیر سوال بردن رویکردهای پیش فرض ما در معماری است. خیلی وقتها، ما به دنبال اشیاء خدماتی میرویم زیرا «اینطوری کار میشود» یا به این دلیل که خواندهایم «مدلهای چاق بد هستند». در عوض، ما باید:
- ساده شروع کنید – مستقیماً در مدل ها و کنترلرها
- بگذارید پیچیدگی انتزاع را هدایت کند – نه برعکس
- به جای «خدمات» عمومی، به فرآیندها و گردش کار فکر کنید.
- الگوهای تثبیت شده را زیر سوال ببرید – فقط به این دلیل که همه آن را انجام می دهند، آن را برای مورد خاص شما مناسب نمی کند
! مهمتر از آن، اگر بخشی از یک تیم بزرگ هستید، یک مورد را ایجاد کنید و به توافق برسید متحد شده است ابتدا نزدیک شوید داشتن نیمی از پایگاه کد خود با استفاده از اشیاء خدمات سنتی و نیمی دیگر از گردشهای کاری فرآیند گرا احتمالاً مشکلات بیشتری نسبت به حل آن ایجاد میکند. تصمیمات معماری باید تصمیمات تیمی باشد که توسط قراردادها و مستندات واضح پشتیبانی شود.
به یاد داشته باشید: هر لایه انتزاعی یک معامله است. مطمئن شوید که ارزش کافی برای توجیه هزینه پیچیدگی دریافت می کنید.