Rails Servis Nesneleri: Kapsamlı Bir Kılavuz

Yayınlanan: 2022-03-11

Ruby on Rails, uygulamanızı hızlı bir şekilde prototiplemek için ihtiyacınız olan her şeyle birlikte gelir, ancak kod tabanınız büyümeye başladığında, geleneksel Fat Model, Skinny Controller mantrasının bozulduğu senaryolarla karşılaşırsınız. İş mantığınız ne bir modele ne de bir denetleyiciye sığamadığında, o zaman hizmet nesneleri devreye girer ve her iş eylemini kendi Ruby nesnesine ayırmamıza izin verin.

Rails hizmet nesneleri ile örnek bir istek döngüsü

Bu yazımda servis nesnesinin ne zaman gerekli olduğunu anlatacağım; temiz hizmet nesneleri yazma ve bunları katkıda bulunanların akıl sağlığı için birlikte gruplandırma hakkında nasıl gidileceği; hizmet nesnelerime onları doğrudan iş mantığıma bağlamak için koyduğum katı kurallar; ve ne yapacağınızı bilmediğiniz tüm kodlar için hizmet nesnelerinizi nasıl çöplüğe çevirmeyeceğinizi.

Neden Servis Nesnelerine İhtiyacım Var?

Bunu deneyin: Uygulamanızın params[:message] gelen metni tweetlemesi gerektiğinde ne yaparsınız?

Şimdiye kadar Vanilla Rails kullanıyorsanız, muhtemelen şöyle bir şey yapmışsınızdır:

 class TweetController < ApplicationController def create send_tweet(params[:message]) end private def send_tweet(tweet) client = Twitter::REST::Client.new do |config| config.consumer_key = ENV['TWITTER_CONSUMER_KEY'] config.consumer_secret = ENV['TWITTER_CONSUMER_SECRET'] config.access_token = ENV['TWITTER_ACCESS_TOKEN'] config.access_token_secret = ENV['TWITTER_ACCESS_SECRET'] end client.update(tweet) end end

Buradaki sorun, denetleyicinize en az on satır eklemiş olmanızdır, ancak bunlar gerçekten oraya ait değildir. Ayrıca, aynı işlevi başka bir denetleyicide kullanmak isterseniz ne olur? Bunu bir endişeye mi taşıyorsun? Bekle, ama bu kod gerçekten kontrolörlere ait değil. Twitter API'si neden benim aramam için hazırlanmış tek bir nesneyle gelmiyor?

Bunu ilk yaptığımda, kirli bir şey yapmış gibi hissettim. Daha önce, güzelce ince olan Rails kontrolörlerim şişmanlamaya başlamıştı ve ne yapacağımı bilmiyordum. Sonunda, denetleyicimi bir hizmet nesnesiyle düzelttim.

Bu makaleyi okumaya başlamadan önce, şöyle yapalım:

  • Bu uygulama bir Twitter hesabını yönetir.
  • Rails Way, “bir şeyleri yapmanın geleneksel Ruby on Rails yolu” anlamına gelir ve kitap mevcut değildir.
  • Ben bir Rails uzmanıyım… ki bana her gün öyle olduğu söylenir, ama buna inanmakta güçlük çekiyorum, o yüzden gerçekten öyleymişim gibi davranalım.

Hizmet Nesneleri Nelerdir?

Hizmet nesneleri, etki alanı mantığınızda tek bir eylemi yürütmek ve bunu iyi yapmak için tasarlanmış Düz Eski Yakut Nesneleridir (PORO). Yukarıdaki örneği ele alalım: Bizim yöntemimiz zaten tek bir şey yapma mantığına sahip, o da bir tweet oluşturmak. Ya bu mantık, somutlaştırabileceğimiz ve bir yöntem çağırabileceğimiz tek bir Ruby sınıfı içinde kapsüllenmişse? Gibi bir şey:

 tweet_creator = TweetCreator.new(params[:message]) tweet_creator.send_tweet # Later on in the article, we'll add syntactic sugar and shorten the above to: TweetCreator.call(params[:message])

Bu oldukça fazla; TweetCreator hizmet nesnemiz oluşturulduktan sonra herhangi bir yerden çağrılabilir ve bu tek şeyi çok iyi yapar.

Hizmet Nesnesi Oluşturma

Önce app/services adlı yeni bir klasörde yeni bir TweetCreator oluşturalım:

 $ mkdir app/services && touch app/services/tweet_creator.rb

Ve tüm mantığımızı yeni bir Ruby sınıfına dökelim:

 # app/services/tweet_creator.rb class TweetCreator def initialize(message) @message = message end def send_tweet client = Twitter::REST::Client.new do |config| config.consumer_key = ENV['TWITTER_CONSUMER_KEY'] config.consumer_secret = ENV['TWITTER_CONSUMER_SECRET'] config.access_token = ENV['TWITTER_ACCESS_TOKEN'] config.access_token_secret = ENV['TWITTER_ACCESS_SECRET'] end client.update(@message) end end

Ardından, uygulamanızın herhangi bir yerinde TweetCreator.new(params[:message]).send_tweet ve çalışacaktır. Rails, app/ altındaki her şeyi otomatik olarak yüklediği için bu nesneyi sihirli bir şekilde yükleyecektir. Bunu çalıştırarak doğrulayın:

 $ rails c Running via Spring preloader in process 12417 Loading development environment (Rails 5.1.5) > puts ActiveSupport::Dependencies.autoload_paths ... /Users/gilani/Sandbox/nazdeeq/app/services

autoload nasıl çalıştığı hakkında daha fazla bilgi edinmek ister misiniz? Otomatik Yükleme ve Sabitleri Yeniden Yükleme Kılavuzu'nu okuyun.

Rails Hizmet Nesnelerinin Daha Az Emilmesini Sağlamak için Sözdizimsel Şeker Ekleme

Bak, bu teoride harika bir his ama TweetCreator.new(params[:message]).send_tweet sadece bir ağız dolusu. Gereksiz kelimelerle çok ayrıntılı… HTML'ye çok benziyor (ba-dum tiss! ). Gerçekten de, HAML etraftayken insanlar neden HTML kullanıyor? Hatta Slim'i. Sanırım bu başka bir zaman için başka bir makale. Eldeki göreve geri dönün:

TweetCreator güzel bir kısa sınıf adıdır, ancak nesneyi somutlaştırma ve yöntemi çağırma konusundaki ekstra zorluk çok uzun! Ruby'de bir şeyi çağırmak ve verilen parametrelerle hemen kendi kendine çalışmasını sağlamak için bir öncelik olsaydı… Ah bir dakika, var! Bu Proc#call .

Proccall , yöntemi çağırma semantiğine yakın bir şey kullanarak bloğun parametrelerini params içindeki değerlere ayarlayarak bloğu çağırır. Blokta değerlendirilen son ifadenin değerini döndürür.

 aproc = Proc.new {|scalar, values| values.map {|value| valuescalar } } aproc.call(9, 1, 2, 3) #=> [9, 18, 27] aproc[9, 1, 2, 3] #=> [9, 18, 27] aproc.(9, 1, 2, 3) #=> [9, 18, 27] aproc.yield(9, 1, 2, 3) #=> [9, 18, 27]

belgeler

Bu kafanızı karıştırıyorsa, açıklamama izin verin. Bir proc , verilen parametrelerle kendisini yürütmek için -ed olarak call . Yani, TweetCreator bir proc olsaydı, onu TweetCreator.call(message) ile çağırabilirdik ve sonuç, hantal eski TweetCreator.new(params[:message]).send_tweet oldukça benzeyen TweetCreator.new(params[:message]).call ile eşdeğer olurdu. TweetCreator.new(params[:message]).send_tweet .

Öyleyse hizmet nesnemizin daha çok bir proc gibi davranmasını sağlayalım!

İlk olarak, muhtemelen bu davranışı tüm hizmet nesnelerimizde yeniden kullanmak istediğimiz için, Rails Way'den ödünç alalım ve ApplicationService adında bir sınıf oluşturalım:

 # app/services/application_service.rb class ApplicationService def self.call(*args, &block) new(*args, &block).call end end

Orada ne yaptığımı gördün mü? Ona ilettiğiniz argümanlar veya bloklarla sınıfın yeni bir örneğini oluşturan call adında bir sınıf yöntemi ekledim ve örneğe çağrı call yaptım. Tam olarak istediğimiz şey! Yapılacak son şey, TweetCreator sınıfımızdaki yöntemi call olarak yeniden adlandırmak ve sınıfın ApplicationService öğesinden miras almasını sağlamaktır:

 # app/services/tweet_creator.rb class TweetCreator < ApplicationService attr_reader :message def initialize(message) @message = message end def call client = Twitter::REST::Client.new do |config| config.consumer_key = ENV['TWITTER_CONSUMER_KEY'] config.consumer_secret = ENV['TWITTER_CONSUMER_SECRET'] config.access_token = ENV['TWITTER_ACCESS_TOKEN'] config.access_token_secret = ENV['TWITTER_ACCESS_SECRET'] end client.update(@message) end end

Son olarak, denetleyicideki hizmet nesnemizi çağırarak bunu tamamlayalım:

 class TweetController < ApplicationController def create TweetCreator.call(params[:message]) end end

Akıl Sağlığı İçin Benzer Hizmet Nesnelerini Gruplama

Yukarıdaki örnekte yalnızca bir hizmet nesnesi vardır, ancak gerçek dünyada işler daha karmaşık hale gelebilir. Örneğin, yüzlerce hizmetiniz varsa ve bunların yarısı, örneğin başka bir Twitter hesabını takip eden bir Follower hizmetine sahip olmak gibi ilgili ticari işlemlerse? Dürüst olmak gerekirse, bir klasör 200 benzersiz görünümlü dosya içeriyorsa çıldırırdım, o kadar iyi ki Rails Way'den kopyalayabileceğimiz başka bir model var - yani, ilham kaynağı olarak kullanın: ad alanı.

Diğer Twitter profillerini takip eden bir hizmet nesnesi oluşturmakla görevlendirildiğimizi varsayalım.

Bir önceki hizmet nesnemizin adına bakalım: TweetCreator . Kulağa bir kişi ya da en azından bir organizasyondaki bir rol gibi geliyor. Tweet oluşturan biri. Hizmet nesnelerimi sanki onlarmış gibi adlandırmayı seviyorum: bir kuruluştaki roller. Bu kuralı takiben yeni nesnemi arayacağım: ProfileFollower .

Şimdi, bu uygulamanın en üst düzey yöneticisi olduğum için, hizmet hiyerarşimde bir yönetim pozisyonu oluşturacağım ve bu pozisyonların her ikisinin de sorumluluğunu bu pozisyona devredeceğim. Bu yeni yönetici pozisyonuna TwitterManager vereceğim.

Bu yönetici yönetmekten başka bir şey yapmadığı için onu bir modül yapalım ve servis nesnelerimizi bu modülün altına yerleştirelim. Klasör yapımız şimdi şöyle görünecek:

 services ├── application_service.rb └── twitter_manager ├── profile_follower.rb └── tweet_creator.rb

Ve hizmet nesnelerimiz:

 # services/twitter_manager/tweet_creator.rb module TwitterManager class TweetCreator < ApplicationService ... end end
 # services/twitter_manager/profile_follower.rb module TwitterManager class ProfileFollower < ApplicationService ... end end

Çağrılarımız artık TwitterManager::TweetCreator.call(arg) ve TwitterManager::ProfileManager.call(arg) olacak.

Veritabanı İşlemlerini Yönetecek Hizmet Nesneleri

Yukarıdaki örnekte API çağrıları yapılmıştır, ancak tüm çağrılar bir API yerine veritabanınıza yapıldığında hizmet nesneleri de kullanılabilir. Bu, özellikle bazı iş eylemlerinin bir işleme sarılmış birden çok veritabanı güncellemesi gerektirmesi durumunda yararlıdır. Örneğin, bu örnek kod, gerçekleşen bir döviz değişimini kaydetmek için hizmetleri kullanır.

 module MoneyManager # exchange currency from one amount to another class CurrencyExchanger < ApplicationService ... def call ActiveRecord::Base.transaction do # transfer the original currency to the exchange's account outgoing_tx = CurrencyTransferrer.call( from: the_user_account, to: the_exchange_account, amount: the_amount, currency: original_currency ) # get the exchange rate rate = ExchangeRateGetter.call( from: original_currency, to: new_currency ) # transfer the new currency back to the user's account incoming_tx = CurrencyTransferrer.call( from: the_exchange_account, to: the_user_account, amount: the_amount * rate, currency: new_currency ) # record the exchange happening ExchangeRecorder.call( outgoing_tx: outgoing_tx, incoming_tx: incoming_tx ) end end end # record the transfer of money from one account to another in money_accounts class CurrencyTransferrer < ApplicationService ... end # record an exchange event in the money_exchanges table class ExchangeRecorder < ApplicationService ... end # get the exchange rate from an API class ExchangeRateGetter < ApplicationService ... end end

Hizmet Nesnemden Ne Döndürürüm?

Hizmet nesnemizi nasıl call tartıştık, ancak nesne ne döndürmeli? Buna yaklaşmanın üç yolu vardır:

  • true veya false
  • Bir değer döndür
  • Bir Numaralandırma Döndür

true veya false

Bu basit: Bir eylem istendiği gibi çalışıyorsa, true ; aksi takdirde false :

 def call ... return true if client.update(@message) false end

Bir Değer Döndür

Hizmet nesneniz bir yerden veri alıyorsa, muhtemelen bu değeri döndürmek istersiniz:

 def call ... return false unless exchange_rate exchange_rate end

Bir Enum ile yanıtlayın

Hizmet nesneniz biraz daha karmaşıksa ve farklı senaryoları ele almak istiyorsanız, hizmetlerinizin akışını kontrol etmek için numaralandırmalar ekleyebilirsiniz:

 class ExchangeRecorder < ApplicationService RETURNS = [ SUCCESS = :success, FAILURE = :failure, PARTIAL_SUCCESS = :partial_success ] def call foo = do_something return SUCCESS if foo.success? return FAILURE if foo.failure? PARTIAL_SUCCESS end private def do_something end end

Ardından uygulamanızda şunları kullanabilirsiniz:

 case ExchangeRecorder.call when ExchangeRecorder::SUCCESS foo when ExchangeRecorder::FAILURE bar when ExchangeRecorder::PARTIAL_SUCCESS baz end

Hizmet Nesnelerini app/services lib/services içine koymalı mıyım?

Bu özneldir. İnsanların görüşleri, hizmet nesnelerini nereye koyacakları konusunda farklılık gösterir. Bazıları onları lib/services içine koyarken, bazıları app/services oluşturur. Ben ikinci kampa düşüyorum. Rails'in Başlangıç ​​Kılavuzu, lib/ klasörünü “uygulamanız için genişletilmiş modüller” koyacağınız yer olarak tanımlar.

Benim düşünceme göre, "genişletilmiş modüller", çekirdek etki alanı mantığını kapsamayan ve genellikle projeler arasında kullanılabilen modüller anlamına gelir. Rastgele bir Yığın Taşması cevabının akıllıca sözleriyle, oraya "potansiyel olarak kendi mücevheri olabilecek" bir kod koyun.

Servis Nesneleri İyi Bir Fikir mi?

Kullanım durumunuza bağlıdır. Bakın—şu anda bu makaleyi okuyor olmanız, bir modele veya denetleyiciye tam olarak ait olmayan bir kod yazmaya çalıştığınızı gösteriyor. Yakın zamanda hizmet nesnelerinin nasıl bir anti-kalıp olduğuyla ilgili bu makaleyi okudum. Yazarın görüşleri var ama saygılarımla katılmıyorum.

Başka bir kişinin hizmet nesnelerini aşırı kullanması, onların doğası gereği kötü oldukları anlamına gelmez. Girişim Nazdeeq'te, ActiveRecord olmayan modellerin yanı sıra hizmet nesneleri de kullanıyoruz. Ancak, nereye gidenler arasındaki fark benim için her zaman açıktı: ActiveRecord olmayan modellerde gerçekten kalıcılık gerektirmeyen kaynakları tutarken tüm iş eylemlerini hizmet nesnelerinde tutuyorum. Günün sonunda, hangi modelin sizin için iyi olduğuna karar vermek size kalmış.

Ancak, genel olarak hizmet nesnelerinin iyi bir fikir olduğunu düşünüyor muyum? Kesinlikle! Kodumu düzenli bir şekilde düzenli tutuyorlar ve PORO'ları kullanmam konusunda bana güven veren şey, Ruby'nin nesneleri sevmesidir. Hayır, cidden, Ruby nesneleri sever . Çılgınca, tamamen çılgınca, ama buna bayıldım! Konuşma konusu olan mesele:

 > 5.is_a? Object # => true > 5.class # => Integer > class Integer ?> def woot ?> 'woot woot' ?> end ?> end # => :woot > 5.woot # => "woot woot"

Görmek? 5 tam anlamıyla bir nesnedir.

Birçok dilde sayılar ve diğer ilkel türler nesne değildir. Ruby, tüm türlerine yöntemler ve örnek değişkenler vererek Smalltalk dilinin etkisini takip eder. Bu, kişinin Ruby kullanımını kolaylaştırır, çünkü nesnelere uygulanan kurallar tüm Ruby'ye uygulanır. Ruby-lang.org

Bir Hizmet Nesnesini Ne Zaman Kullanmamalıyım?

Bu kolay. Bu kurallara sahibim:

  1. Kodunuz yönlendirmeyi, paramları veya diğer denetleyici-y işlemlerini yapıyor mu?
    Öyleyse, bir hizmet nesnesi kullanmayın; kodunuz denetleyiciye aittir.
  2. Kodunuzu farklı denetleyicilerde paylaşmaya mı çalışıyorsunuz?
    Bu durumda bir hizmet nesnesi kullanmayın—bir endişe kullanın.
  3. Kodunuz kalıcılık gerektirmeyen bir model gibi mi?
    Eğer öyleyse, bir hizmet nesnesi kullanmayın. Bunun yerine ActiveRecord olmayan bir model kullanın.
  4. Kodunuz belirli bir iş eylemi mi? (ör. "Çöpü çıkar", "Bu metni kullanarak PDF oluştur" veya "Bu karmaşık kuralları kullanarak gümrük vergisini hesapla")
    Bu durumda, bir hizmet nesnesi kullanın. Bu kod muhtemelen denetleyicinize veya modelinize mantıksal olarak uymuyor.

Elbette bunlar benim kurallarım, dolayısıyla bunları kendi kullanım durumlarınıza uyarlayabilirsiniz. Bunlar benim için çok iyi çalıştı, ancak kilometreniz değişebilir.

İyi Hizmet Nesneleri Yazma Kuralları

Hizmet nesneleri oluşturmak için dört kuralım var. Bunlar taşa yazılmamıştır ve gerçekten onları kırmak istiyorsanız, yapabilirsiniz, ancak mantık yürütmeniz sağlam değilse, muhtemelen kod incelemelerinde değiştirmenizi isteyeceğim.

Kural 1: Hizmet Nesnesi Başına Yalnızca Bir Genel Yöntem

Hizmet nesneleri, tekil iş eylemleridir. İsterseniz genel yönteminizin adını değiştirebilirsiniz. Ben call kullanmayı tercih ediyorum, ancak Gitlab CE'nin kod tabanı onu execute çağırıyor ve diğer insanlar perform öğesini kullanabilir. Ne istersen kullan - umurumda olan tek şey nermin diyebilirsin. Tek bir hizmet nesnesi için iki genel yöntem oluşturmayın. Gerekirse onu iki nesneye ayırın.

Kural 2: Hizmet Nesnelerini Bir Şirkette Aptal Roller Gibi Adlandırın

Hizmet nesneleri, tekil eylemleridir. Bu işi yapması için şirkette bir kişiyi işe alsaydınız, onlara ne ad verirdiniz? İşleri tweet oluşturmaksa, onlara TweetCreator . Görevleri belirli tweetleri okumaksa, onlara TweetReader .

Kural 3: Birden Çok Eylem Gerçekleştirmek için Genel Nesneler Oluşturmayın

Hizmet nesneleri, tekil iş eylemleridir . İşlevi iki parçaya böldüm: TweetReader ve ProfileFollower . Yapmadığım şey, TwitterHandler adında tek bir genel nesne oluşturmak ve tüm API işlevlerini oraya dökmek. Lütfen bunu yapma. Bu, "iş eylemi" zihniyetine aykırıdır ve hizmet nesnesinin Twitter Perisi gibi görünmesini sağlar. Kodu iş nesneleri arasında paylaşmak istiyorsanız, bir BaseTwitterManager nesnesi veya modülü oluşturun ve bunu hizmet nesnelerinize karıştırın.

Kural 4: Hizmet Nesnesinin İçindeki İstisnaları İşleyin

On beşinci kez: Hizmet nesneleri, tekil iş eylemleridir. Bunu yeterince söyleyemem. Tweetleri okuyan biri varsa ya size tweeti verir ya da “Bu tweet yok” der. Benzer şekilde, hizmet nesnenizin paniklemesine, kontrol cihazınızın masasına atlamasına ve “Hata!” nedeniyle tüm işi durdurmasını söylemesine izin vermeyin. Sadece false ve denetleyicinin oradan devam etmesine izin verin.

Krediler ve Sonraki Adımlar

Bu makale, Toptal'daki harika Ruby geliştiricileri topluluğu olmadan mümkün olmazdı. Bir sorunla karşılaşırsam, topluluk şimdiye kadar tanıştığım en yardımsever yetenekli mühendis grubudur.

Hizmet nesneleri kullanıyorsanız, test sırasında belirli yanıtları nasıl zorlayacağınızı merak ediyor olabilirsiniz. Hizmet nesnesine gerçekten çarpmadan, her zaman istediğiniz sonucu döndürecek sahte hizmet nesnelerinin Rspec'te nasıl oluşturulacağına ilişkin bu makaleyi okumanızı tavsiye ederim!

Ruby hileleri hakkında daha fazla bilgi edinmek istiyorsanız, Toptaler Mate Solymosi'den Ruby DSL Oluşturma: Gelişmiş Meta Programlama Kılavuzu'nu öneririm. routes.rb dosyasının Ruby gibi hissetmediğini çözüyor ve kendi DSL'nizi oluşturmanıza yardımcı oluyor.