2026'da Rails Service Objects: Tasarım Kalıpları, PORO ve Teknik Mülakat Soruları

PORO örnekleri, Result sınıfları ve temiz mimari kalıpları ile Rails service objects için kapsamlı rehber. Üretim uygulamaları için pratik kod.

2026'da Rails Service Objects: Tasarım Kalıpları, PORO ve Teknik Mülakat Soruları

Rails service objects, iş mantığını controller ve modellerden ayırarak Plain Old Ruby Objects (PORO) sınıflarına aktarır. Bu kalıp, Rails uygulamalarının büyüdükçe bakımını kolaylaştırır ve mülakatçılar adayların temiz mimari anlayışını değerlendirmek için bu konuyu sıkça test eder.

Mülakatçıların beklentileri

Bir service object tek bir iş operasyonunu kapsüller. Sınıf tek bir public metoda sahiptir (genellikle call), bağımlılıkları constructor üzerinden alır ve öngörülebilir bir sonuç döndürür. Bunu neden önemli olduğunu açıklayabilen adaylar, sadece nasıl uygulanacağını bilenlerden öne çıkar.

Service Objects Neden Fat Model ve Fat Controller Sorunlarını Çözer

Rails mantığın bir yere konulmasını teşvik eder. Yönlendirme olmadan, ekipler bunu modellere iter ("fat models, skinny controllers") ta ki ActiveRecord sınıfları binlerce satıra ulaşana kadar. Diğerleri bunu controllerlarda bırakır ve actionları izole test etmeyi imkansız hale getirir.

Service objects üçüncü bir yol sunar: domain mantığı, Rails'den açıkça ihtiyaç duydukları dışında hiçbir şeye bağımlı olmayan özel sınıflarda yaşar. Bu ayrışma, kodu tam framework yüklenmeden test edilebilir, farklı giriş noktaları arasında taşınabilir (controllerlar, arka plan işleri, konsol) ve her sınıf tek bir şey yaptığı için okunabilir kılar.

Rails Guides service objects önermez, ancak topluluk bu kalıp etrafında standartlaşmıştır. Rails 8'in basitliğe vurgusu, Solid Queue ve Solid Cache'in dış bağımlılıkların yerini alması ile PORO'ları daha da çekici kılar: daha az gem, daha fazla saf Ruby.

Üretime Hazır Bir Service Object'in Anatomisi

Bir service object üç şeye ihtiyaç duyar: bağımlılıkları alan bir constructor, tek bir public call metodu ve başarı veya başarısızlık sinyali veren bir dönüş tipi.

ruby
# app/services/users/create_account.rb
module Users
  class CreateAccount
    def initialize(user_repo: User, mailer: UserMailer)
      @user_repo = user_repo
      @mailer = mailer
    end

    def call(params)
      user = @user_repo.new(params)
      return Result.failure(:validation_failed, user.errors) unless user.valid?

      user.save!
      @mailer.welcome_email(user).deliver_later
      Result.success(user)
    rescue ActiveRecord::RecordNotUnique
      Result.failure(:email_taken, "Email already registered")
    end
  end
end

Constructor varsayılan değerlerle user_repo ve mailer kabul eder. Üretim kodu varsayılanları kullanır; testler mockları enjekte eder. call metodu doğrular, kaydeder, email gönderir ve her sonucu bir Result nesnesine sarar.

Gem Kullanmadan Minimal Bir Result Sınıfı Oluşturma

Bazı ekipler hemen dry-monads'a yönelir. Gem sağlamdır, ancak öğrenme eğrisi ekler. 30 satırlık bir Result sınıfı çoğu ihtiyacı karşılar:

ruby
# app/services/result.rb
class Result
  attr_reader :value, :error, :code

  def initialize(success:, value: nil, error: nil, code: nil)
    @success = success
    @value = value
    @error = error
    @code = code
  end

  def success? = @success
  def failure? = !@success

  def self.success(value) = new(success: true, value: value)
  def self.failure(code, error = nil) = new(success: false, code: code, error: error)

  def on_success
    yield(value) if success?
    self
  end

  def on_failure
    yield(code, error) if failure?
    self
  end
end

Bu Result on_success ve on_failure ile zincirlemeyi destekler, yapılandırılmış hata kodları sunar ve sıfır bağımlılık gerektirir. Controllerlar bunu temiz bir şekilde tüketir:

ruby
# app/controllers/users_controller.rb
class UsersController < ApplicationController
  def create
    result = Users::CreateAccount.new.call(user_params)

    result
      .on_success { |user| redirect_to user, notice: "Account created" }
      .on_failure { |code, error| render :new, status: :unprocessable_entity }
  end

  private

  def user_params
    params.require(:user).permit(:email, :password, :name)
  end
end
dry-monads ne zaman kullanılmalı

Halihazırda dry-rb ekosistemini kullanan veya birden fazla operasyonu zincirlemek için Do notasyonuna ihtiyaç duyan ekipler dry-monads'tan faydalanır. Gem, Ruby 3.x'te pattern matching desteği ile Success, Failure ve Maybe tiplerini sağlar.

Niyeti İşaret Eden İsimlendirme Kuralları

İki kural hakim:

StilÖrnekNe zaman kullanılır
Fiil cümlesiCreateAccount, SendInvoice, RefundPaymentDurum değiştiren komutlar
-or sonekli isimAccountCreator, InvoiceSender, PaymentRefunderDaha az yaygın, bazı ekipler tercih eder

Fiil cümlesi stili doğal okunur: Users::CreateAccount.new.call(params). -or soneki çalışır ama netlik katmadan hece ekler.

Dizin yapısı da önemlidir. Domain'e göre gruplama, ilgili service objectleri bir arada tutar:

text
app/services/
  users/
    create_account.rb
    reset_password.rb
    update_profile.rb
  orders/
    place_order.rb
    cancel_order.rb
    calculate_totals.rb
  result.rb

Ruby on Rails mülakatlarında başarılı olmaya hazır mısın?

İnteraktif simülatörler, flashcards ve teknik testlerle pratik yap.

Test Edilebilir Service Objects için Dependency Injection

Sabit kodlanmış bağımlılıklar testi zorlaştırır. Bu service object veritabanına erişmeden ve gerçek emailler göndermeden test edilemez:

ruby
# Kaçının: sabit kodlanmış bağımlılıklar
class CreateAccount
  def call(params)
    user = User.create!(params)        # Doğrudan model çağrısı
    UserMailer.welcome(user).deliver   # Doğrudan mailer çağrısı
  end
end

Constructor üzerinden enjeksiyon bunu çözer:

ruby
# Daha iyi: enjekte edilebilir bağımlılıklar
class CreateAccount
  def initialize(user_repo: User, mailer: UserMailer)
    @user_repo = user_repo
    @mailer = mailer
  end

  def call(params)
    user = @user_repo.create!(params)
    @mailer.welcome(user).deliver_later
    Result.success(user)
  end
end

Testler artık sahte nesneler kullanır:

ruby
# spec/services/users/create_account_spec.rb
RSpec.describe Users::CreateAccount do
  let(:fake_repo) { class_double(User) }
  let(:fake_mailer) { class_double(UserMailer) }
  let(:service) { described_class.new(user_repo: fake_repo, mailer: fake_mailer) }

  it "returns success with valid params" do
    user = instance_double(User, valid?: true)
    allow(fake_repo).to receive(:new).and_return(user)
    allow(user).to receive(:save!)
    allow(fake_mailer).to receive_message_chain(:welcome_email, :deliver_later)

    result = service.call(email: "test@example.com", password: "secure123")

    expect(result).to be_success
  end
end

Veritabanı yok, email yok, hızlı çalıştırma. RSpec dokümantasyonu tip doğrulamalı mocklar için class_double ve instance_doubleı kapsar.

Rails Service Objects Mülakat Soruları

Bu sorular mid-level ve senior Rails mülakatlarında karşımıza çıkar. Kod örnekleri ile somut cevaplar hazırlanmalıdır.

S: İş mantığı ne zaman modelde, ne zaman service objectte olmalı?

Modeller validasyonların, ilişkilerin, scopeların ve tek kayıt davranışlarının sahibidir. Service objectler birden fazla modeli kapsayan, dış çağrılar gerektiren veya açık transaction sınırlarına ihtiyaç duyan operasyonları yönetir. User modeli email formatını doğrular; CreateAccount servisi kullanıcıyı oluşturur, hoş geldiniz emaili gönderir ve varsayılan ayarları yapar.

S: Service objectlerde hatalar exception olmadan nasıl yönetilir?

Başarı/başarısızlık durumlarına sahip bir Result nesnesi döndürülür. Bu, hata yönetimini çağıran tarafta açık hale getirir, exception tabanlı kontrol akışından kaçınır ve farklı başarısızlık modları için yapılandırılmış hata kodları sağlar. Exceptionlar, iş kuralı ihlalleri için değil, gerçekten istisnai durumlar (veritabanı çökmesi, ağ hatası) için uygun kalır.

S: Service object ile interactor arasındaki fark nedir?

Interactorlar (interactor gibi gemlerden) belirli bir arayüze sahip service objectlerdir: bir context hash'i alırlar, onu değiştirirler ve context.fail! üzerinden başarısızlık sinyali verirler. PORO olarak service objectler önceden belirlenmiş bir arayüze sahip değildir, bu da ekiplere daha fazla esneklik ancak daha az tutarlılık sağlar.

S: Dış API çağıran bir service object nasıl test edilir?

HTTP istemcisi bir bağımlılık olarak enjekte edilir. Testlerde hazır yanıtlar döndüren bir stub geçirilir. WebMock veya VCR gibi araçlar HTTP etkileşimlerini kaydedip yeniden oynatabilir, ancak constructor enjeksiyonu birim testlerinde ağ çağrılarından tamamen kaçınır.

Daha fazla Rails mülakat hazırlığı için RSpec ve test kalıplarını kapsayan Rails mülakat soruları rehberine bakılabilir.

Karmaşık İş Akışları için Railway-Oriented Design

Railway-oriented programlama, bir iş akışını iki paralel ray olarak ele alır: başarı ileriye devam eder, başarısızlık hemen çıkar. Her adım ya başarı rayında ilerler ya da başarısızlık rayına geçer.

ruby
# app/services/orders/place_order.rb
module Orders
  class PlaceOrder
    def initialize(
      inventory: InventoryService.new,
      payment: PaymentService.new,
      fulfillment: FulfillmentService.new
    )
      @inventory = inventory
      @payment = payment
      @fulfillment = fulfillment
    end

    def call(cart:, payment_method:)
      validate_cart(cart)
        .then { |items| reserve_inventory(items) }
        .then { |reservation| charge_payment(reservation, payment_method) }
        .then { |charge| create_fulfillment(charge) }
    end

    private

    def validate_cart(cart)
      return Result.failure(:empty_cart, "Cart is empty") if cart.items.empty?
      Result.success(cart.items)
    end

    def reserve_inventory(items)
      @inventory.reserve(items)
    end

    def charge_payment(reservation, payment_method)
      result = @payment.charge(reservation.total, payment_method)
      return result if result.failure?

      Result.success({ reservation: reservation, charge: result.value })
    end

    def create_fulfillment(data)
      @fulfillment.create(data[:reservation], data[:charge])
    end
  end
end

Result sınıfı, yalnızca başarıda devam eden bir then metoduna ihtiyaç duyar:

ruby
# app/services/result.rb'ye ekle
def then
  return self if failure?
  yield(value)
end

Envanter rezervasyonu başarısız olursa, ödeme asla çalışmaz. Ödeme başarısız olursa, teslimat asla çalışmaz. Her adım bir Result döndürür ve zincir ilk başarısızlıkta kısa devre yapar.

Service Objects Ne Zaman Anti-Pattern Olur

Service objectler gerçek sorunları çözer ama gereksiz yere çoğalabilir. Bu uyarı işaretlerine dikkat edilmelidir:

  • Tek satırlık service objectler: Eğer service sadece bir model metodunu çağırıyorsa, değer katmadan dolaylılık ekler. Mantık basit olduğunda User.authenticate(email, password), AuthenticateUser.new.call(email, password)dan daha iyidir.

  • Sadece test için var olan service objectler: Bağımlılık enjeksiyonu testi kolaylaştırır, ancak yalnızca test edilebilirlik için model metodlarını sarmak amacıyla service objectler oluşturmak, gerçek sorunun mimari değil test kurulumu olduğunu gösterir.

  • Mantıksız anemik service objectler: Davranış eklemeden sadece modellere delege eden service objectler seremondir. Netguru mühendislik blogu bu aşırı çıkarma sorununu tartışır.

Alternatif: paylaşılan model davranışı için Concerns, Money veya DateRange gibi domain kavramları için Value Objects ve tek kayıt operasyonları için model metodları ile birlikte PORO'ların seçici kullanımı.

Rails Service Object Tasarımı için Temel Çıkarımlar

  • Mantık birden fazla modeli kapsadığında, dış çağrılar gerektirdiğinde veya açık hata yönetimine ihtiyaç duyduğunda service objecte çıkarılır
  • Ham değerler veya exceptionlar değil, Result nesnesi döndüren tek bir public call metodu kullanılır
  • Üretim kullanımı için makul varsayılanlarla constructor üzerinden bağımlılıklar enjekte edilir
  • Service objectler eylemi tanımlayan CreateAccount veya PlaceOrder gibi fiil cümleleriyle isimlendirilir
  • Service objectler düz dizin yapısı yerine domain'e göre (users/, orders/) organize edilir
  • dry-monads gibi gemler eklemeden önce minimal bir Result sınıfıyla başlanır
  • Exceptionlar iş kuralı ihlalleri için değil, altyapı hataları için ayrılır
  • Modellerin doğal olarak yönettiği tek satırlık operasyonlar için service object oluşturmaktan kaçınılır

Pratik yapmaya başla!

Mülakat simülatörleri ve teknik testlerle bilgini test et.

Günün meydan okuması

Ruby on Rails kodundaki hatayı bulabilir misin?

Gerçek bir kod parçası, gizli bir hata, günde bir deneme. Denemek için hesap gerekmez.

Anthony Fillion-Maillet

Yazan:

Anthony Fillion-Maillet

SharpSkill kurucusu

10 yılı aşkın süredir fullstack geliştirici. SharpSkill’i yönetiyor ve burada yayımlanan her şeyden sorumlu.

20 Ağustos 2026 tarihinde güncellendi

Paylaş

İlgili makaleler