برنامه نویسی

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
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید


مثال‌های بالا نشان‌دهنده مواردی است که من استفاده معتبر از یک لایه سرویس می‌دانم – یا به‌طور دقیق‌تر، فرآیندهای تجاری. آنها موارد واضحی را نشان می دهند که در آن انتزاع ارزش واقعی اضافه می کند: گردش کار پیچیده، یکپارچه سازی خدمات خارجی، و قوانین تجاری غنی از دامنه و غیره.
اما نکته کلیدی فقط در مورد زمان استفاده از این الگوها نیست – بلکه در مورد زیر سوال بردن رویکردهای پیش فرض ما در معماری است. خیلی وقت‌ها، ما به دنبال اشیاء خدماتی می‌رویم زیرا «اینطوری کار می‌شود» یا به این دلیل که خوانده‌ایم «مدل‌های چاق بد هستند». در عوض، ما باید:

  • ساده شروع کنید – مستقیماً در مدل ها و کنترلرها
  • بگذارید پیچیدگی انتزاع را هدایت کند – نه برعکس
  • به جای «خدمات» عمومی، به فرآیندها و گردش کار فکر کنید.
  • الگوهای تثبیت شده را زیر سوال ببرید – فقط به این دلیل که همه آن را انجام می دهند، آن را برای مورد خاص شما مناسب نمی کند

! مهمتر از آن، اگر بخشی از یک تیم بزرگ هستید، یک مورد را ایجاد کنید و به توافق برسید متحد شده است ابتدا نزدیک شوید داشتن نیمی از پایگاه کد خود با استفاده از اشیاء خدمات سنتی و نیمی دیگر از گردش‌های کاری فرآیند گرا احتمالاً مشکلات بیشتری نسبت به حل آن ایجاد می‌کند. تصمیمات معماری باید تصمیمات تیمی باشد که توسط قراردادها و مستندات واضح پشتیبانی شود.
به یاد داشته باشید: هر لایه انتزاعی یک معامله است. مطمئن شوید که ارزش کافی برای توجیه هزینه پیچیدگی دریافت می کنید.

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا