Objek Layanan Rails: Panduan Lengkap

Diterbitkan: 2022-03-11

Ruby on Rails dikirimkan dengan semua yang Anda butuhkan untuk membuat prototipe aplikasi Anda dengan cepat, tetapi ketika basis kode Anda mulai berkembang, Anda akan mengalami skenario di mana mantra Fat Model, Skinny Controller konvensional rusak. Ketika logika bisnis Anda tidak dapat masuk ke dalam model atau pengontrol, saat itulah objek layanan masuk dan biarkan kami memisahkan setiap tindakan bisnis ke dalam objek Ruby-nya sendiri.

Contoh siklus permintaan dengan objek layanan Rails

Pada artikel ini, saya akan menjelaskan kapan objek layanan diperlukan; bagaimana cara menulis objek layanan bersih dan mengelompokkannya bersama untuk kewarasan kontributor; aturan ketat yang saya terapkan pada objek layanan saya untuk mengikatnya langsung ke logika bisnis saya; dan bagaimana tidak mengubah objek layanan Anda menjadi tempat pembuangan untuk semua kode yang Anda tidak tahu apa yang harus dilakukan.

Mengapa Saya Membutuhkan Objek Layanan?

Coba ini: Apa yang Anda lakukan ketika aplikasi Anda perlu men-tweet teks dari params[:message] ?

Jika Anda telah menggunakan Vanilla Rails sejauh ini, maka Anda mungkin telah melakukan sesuatu seperti ini:

 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

Masalahnya di sini adalah Anda telah menambahkan setidaknya sepuluh baris ke pengontrol Anda, tetapi mereka tidak benar-benar termasuk di sana. Juga, bagaimana jika Anda ingin menggunakan fungsi yang sama di pengontrol lain? Apakah Anda memindahkan ini ke keprihatinan? Tunggu, tetapi kode ini sama sekali tidak termasuk dalam pengontrol. Mengapa API Twitter tidak bisa datang dengan satu objek yang sudah disiapkan untuk saya panggil?

Pertama kali saya melakukan ini, saya merasa seperti telah melakukan sesuatu yang kotor. Pengontrol Rails saya yang sebelumnya ramping dan indah mulai menjadi gemuk dan saya tidak tahu harus berbuat apa. Akhirnya, saya memperbaiki pengontrol saya dengan objek layanan.

Sebelum Anda mulai membaca artikel ini, mari kita berpura-pura:

  • Aplikasi ini menangani akun Twitter.
  • The Rails Way berarti "cara konvensional Ruby on Rails dalam melakukan sesuatu" dan buku itu tidak ada.
  • Saya seorang ahli Rails… yang setiap hari diberitahu bahwa saya memang ahlinya, tetapi saya sulit mempercayainya, jadi anggap saja saya benar-benar ahlinya.

Apa itu Objek Layanan?

Objek layanan adalah Objek Ruby Lama Biasa (PORO) yang dirancang untuk menjalankan satu tindakan dalam logika domain Anda dan melakukannya dengan baik. Perhatikan contoh di atas: Metode kami sudah memiliki logika untuk melakukan satu hal, yaitu membuat tweet. Bagaimana jika logika ini dienkapsulasi dalam satu kelas Ruby yang dapat kita buat dan panggil metodenya? Sesuatu seperti:

 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])

Ini cukup banyak; objek layanan TweetCreator kami, setelah dibuat, dapat dipanggil dari mana saja, dan itu akan melakukan satu hal ini dengan sangat baik.

Membuat Objek Layanan

Pertama mari kita buat TweetCreator baru di folder baru bernama app/services :

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

Dan mari kita buang semua logika kita di dalam kelas Ruby baru:

 # 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

Kemudian Anda dapat memanggil TweetCreator.new(params[:message]).send_tweet di mana saja di aplikasi Anda, dan itu akan berfungsi. Rails akan memuat objek ini secara ajaib karena ia memuat semua secara otomatis di bawah app/ . Verifikasi ini dengan menjalankan:

 $ 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

Ingin tahu lebih banyak tentang cara kerja autoload ? Baca Panduan Pemuatan Otomatis dan Pemuatan Ulang Konstanta.

Menambahkan Gula Sintaks untuk Membuat Objek Layanan Rails Lebih Sedikit

Dengar, secara teori ini terasa hebat, tetapi TweetCreator.new(params[:message]).send_tweet hanyalah seteguk. Ini terlalu bertele-tele dengan kata-kata yang berlebihan… seperti HTML (ba-dum tiss! ). Namun, dalam semua keseriusan, mengapa orang menggunakan HTML saat HAML ada? Atau bahkan Langsing. Saya kira itu artikel lain untuk lain waktu. Kembali ke tugas yang ada:

TweetCreator adalah nama kelas pendek yang bagus, tetapi kelemahan ekstra dalam membuat objek dan memanggil metode terlalu panjang! Jika saja ada prioritas di Ruby untuk memanggil sesuatu dan mengeksekusinya sendiri segera dengan parameter yang diberikan… oh tunggu, ada! Ini Proc#call .

Proccall memanggil blok, mengatur parameter blok ke nilai dalam params menggunakan sesuatu yang dekat dengan metode pemanggilan semantik. Ini mengembalikan nilai ekspresi terakhir yang dievaluasi di blok.

 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]

Dokumentasi

Jika ini membingungkan Anda, izinkan saya menjelaskannya. Sebuah proc dapat call -ed untuk mengeksekusi dirinya sendiri dengan parameter yang diberikan. Artinya, jika TweetCreator adalah proc , kita dapat menyebutnya dengan TweetCreator.call(message) dan hasilnya akan setara dengan TweetCreator.new(params[:message]).call , yang terlihat sangat mirip dengan TweetCreator.new(params[:message]).send_tweet lama kita yang berat TweetCreator.new(params[:message]).send_tweet .

Jadi mari kita buat objek layanan kita berperilaku lebih seperti proc !

Pertama, karena kita mungkin ingin menggunakan kembali perilaku ini di semua objek layanan kita, mari pinjam dari Rails Way dan buat kelas bernama ApplicationService :

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

Apakah Anda melihat apa yang saya lakukan di sana? Saya menambahkan metode kelas yang disebut call yang membuat instance baru dari kelas dengan argumen atau blok yang Anda berikan padanya, dan memanggil call pada instance. Persis apa yang kami inginkan! Hal terakhir yang harus dilakukan adalah mengganti nama metode dari kelas TweetCreator kami menjadi call , dan mewarisi kelas dari ApplicationService :

 # 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

Dan akhirnya, mari kita selesaikan ini dengan memanggil objek layanan kita di controller:

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

Mengelompokkan Objek Layanan Serupa untuk Sanity

Contoh di atas hanya memiliki satu objek layanan, tetapi di dunia nyata, semuanya bisa menjadi lebih rumit. Misalnya, bagaimana jika Anda memiliki ratusan layanan, dan setengahnya adalah tindakan bisnis terkait, misalnya, memiliki layanan Follower yang mengikuti akun Twitter lain? Sejujurnya, saya akan menjadi gila jika sebuah folder berisi 200 file yang tampak unik, untungnya ada pola lain dari Rails Way yang bisa kita salin—maksud saya, gunakan sebagai inspirasi: namespace.

Anggap saja kita telah ditugaskan untuk membuat objek layanan yang mengikuti profil Twitter lainnya.

Mari kita lihat nama objek layanan kami sebelumnya: TweetCreator . Kedengarannya seperti seseorang, atau setidaknya, peran dalam sebuah organisasi. Seseorang yang membuat Tweet. Saya suka memberi nama objek layanan saya seolah-olah hanya itu: peran dalam suatu organisasi. Mengikuti konvensi ini, saya akan memanggil objek baru saya: ProfileFollower .

Sekarang, karena saya penguasa tertinggi aplikasi ini, saya akan membuat posisi manajerial dalam hierarki layanan saya dan mendelegasikan tanggung jawab untuk kedua layanan ini ke posisi itu. Saya akan menyebut posisi manajerial baru ini TwitterManager .

Karena manajer ini tidak melakukan apa-apa selain mengelola, mari kita menjadikannya sebagai modul dan menyarangkan objek layanan kita di bawah modul ini. Struktur folder kita sekarang akan terlihat seperti:

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

Dan objek layanan kami:

 # 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

Dan panggilan kita sekarang akan menjadi TwitterManager::TweetCreator.call(arg) , dan TwitterManager::ProfileManager.call(arg) .

Objek Layanan untuk Menangani Operasi Basis Data

Contoh di atas membuat panggilan API, tetapi objek layanan juga dapat digunakan saat semua panggilan ditujukan ke database Anda, bukan ke API. Ini sangat membantu jika beberapa tindakan bisnis memerlukan beberapa pembaruan basis data yang dibungkus dalam suatu transaksi. Misalnya, kode contoh ini akan menggunakan layanan untuk mencatat pertukaran mata uang yang terjadi.

 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

Apa yang Saya Kembalikan dari Objek Layanan Saya?

Kami telah membahas bagaimana call objek layanan kami, tetapi apa yang harus dikembalikan oleh objek? Ada tiga cara untuk mendekati ini:

  • Kembalikan true atau false
  • Mengembalikan nilai
  • Kembalikan Enum

Kembalikan true atau false

Yang ini sederhana: Jika suatu tindakan berfungsi sebagaimana dimaksud, kembalikan true ; jika tidak, kembalikan false :

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

Mengembalikan Nilai

Jika objek layanan Anda mengambil data dari suatu tempat, Anda mungkin ingin mengembalikan nilai tersebut:

 def call ... return false unless exchange_rate exchange_rate end

Tanggapi dengan Enum

Jika objek layanan Anda sedikit lebih kompleks, dan Anda ingin menangani skenario yang berbeda, Anda bisa menambahkan enum untuk mengontrol aliran layanan Anda:

 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

Dan kemudian di aplikasi Anda, Anda dapat menggunakan:

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

Bukankah Seharusnya Saya Menempatkan Objek Layanan di lib/services Alih-alih app/services ?

Ini subjektif. Pendapat orang berbeda tentang di mana harus meletakkan objek layanan mereka. Beberapa orang meletakkannya di lib/services , sementara beberapa membuat app/services . Saya jatuh di kubu yang terakhir. Panduan Memulai Rails menjelaskan folder lib/ sebagai tempat untuk meletakkan "modul yang diperluas untuk aplikasi Anda."

Menurut pendapat saya yang sederhana, "modul yang diperluas" berarti modul yang tidak merangkum logika domain inti dan umumnya dapat digunakan di seluruh proyek. Dalam kata-kata bijak dari jawaban Stack Overflow acak, masukkan kode di sana yang "berpotensi menjadi permatanya sendiri."

Apakah Objek Layanan adalah Ide Bagus?

Itu tergantung pada kasus penggunaan Anda. Lihat—fakta bahwa Anda sedang membaca artikel ini sekarang menunjukkan bahwa Anda mencoba menulis kode yang sebenarnya tidak termasuk dalam model atau pengontrol. Saya baru-baru ini membaca artikel ini tentang bagaimana objek layanan adalah anti-pola. Penulis memiliki pendapatnya sendiri, tetapi saya dengan hormat tidak setuju.

Hanya karena beberapa orang lain menggunakan objek layanan secara berlebihan tidak berarti objek tersebut pada dasarnya buruk. Di startup saya, Nazdeeq, kami menggunakan objek layanan serta model non-ActiveRecord. Tetapi perbedaan antara apa yang terjadi selalu terlihat bagi saya: Saya menyimpan semua tindakan bisnis di objek layanan sambil menjaga sumber daya yang tidak benar-benar membutuhkan ketekunan dalam model non-ActiveRecord. Pada akhirnya, terserah Anda untuk memutuskan pola apa yang baik untuk Anda.

Namun, apakah menurut saya objek layanan secara umum adalah ide yang bagus? Sangat! Mereka menjaga kode saya terorganisir dengan rapi, dan apa yang membuat saya percaya diri dalam penggunaan PORO adalah bahwa Ruby menyukai objek. Tidak, serius, Ruby menyukai benda. Ini gila, benar-benar gila, tapi aku menyukainya! Inti masalah:

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

Melihat? 5 secara harfiah adalah sebuah objek.

Dalam banyak bahasa, angka dan tipe primitif lainnya bukanlah objek. Ruby mengikuti pengaruh bahasa Smalltalk dengan memberikan metode dan variabel instan ke semua jenisnya. Ini memudahkan penggunaan Ruby, karena aturan yang berlaku untuk objek berlaku untuk semua Ruby. Ruby-lang.org

Kapan Sebaiknya Saya Tidak Menggunakan Objek Layanan?

Yang ini mudah. Saya memiliki aturan ini:

  1. Apakah kode Anda menangani perutean, params, atau melakukan hal-hal pengontrol-y lainnya?
    Jika demikian, jangan gunakan objek layanan—kode Anda termasuk dalam pengontrol.
  2. Apakah Anda mencoba membagikan kode Anda di pengontrol yang berbeda?
    Dalam hal ini, jangan gunakan objek layanan—gunakan perhatian.
  3. Apakah kode Anda seperti model yang tidak membutuhkan ketekunan?
    Jika demikian, jangan gunakan objek layanan. Gunakan model non-ActiveRecord sebagai gantinya.
  4. Apakah kode Anda merupakan tindakan bisnis tertentu? (misalnya, "Buang sampah", "Buat PDF menggunakan teks ini", atau "Hitung bea masuk menggunakan aturan rumit ini")
    Dalam hal ini, gunakan objek layanan. Kode itu mungkin tidak cocok secara logis dengan pengontrol atau model Anda.

Tentu saja, ini adalah aturan saya , jadi Anda dapat menyesuaikannya dengan kasus penggunaan Anda sendiri. Ini telah bekerja sangat baik untuk saya, tetapi jarak tempuh Anda mungkin berbeda.

Aturan untuk Menulis Objek Layanan yang Baik

Saya memiliki empat aturan untuk membuat objek layanan. Ini tidak ditulis dengan batu, dan jika Anda benar- benar ingin memecahkannya, Anda bisa, tetapi saya mungkin akan meminta Anda untuk mengubahnya dalam tinjauan kode kecuali alasan Anda masuk akal.

Aturan 1: Hanya Satu Metode Publik per Objek Layanan

Objek layanan adalah tindakan bisnis tunggal . Anda dapat mengubah nama metode publik Anda jika Anda mau. Saya lebih suka menggunakan call , tetapi basis kode Gitlab CE menyebutnya execute dan orang lain dapat menggunakan perform . Gunakan apa pun yang Anda inginkan—Anda bisa menyebutnya nermin untuk semua yang saya pedulikan. Hanya saja, jangan membuat dua metode publik untuk satu objek layanan. Pecah menjadi dua objek jika perlu.

Aturan 2: Beri Nama Objek Layanan Seperti Peran Bodoh di Perusahaan

Objek layanan adalah tindakan bisnis tunggal. Bayangkan jika Anda mempekerjakan satu orang di perusahaan untuk melakukan satu pekerjaan itu, Anda akan menyebutnya apa? Jika tugas mereka adalah membuat tweet, sebut saja mereka TweetCreator . Jika tugas mereka adalah membaca tweet tertentu, panggil mereka TweetReader .

Aturan 3: Jangan Buat Objek Generik untuk Melakukan Beberapa Tindakan

Objek layanan adalah tindakan bisnis tunggal. Saya memecah fungsionalitas menjadi dua bagian: TweetReader , dan ProfileFollower . Apa yang tidak saya lakukan adalah membuat satu objek generik bernama TwitterHandler dan membuang semua fungsionalitas API di sana. Tolong jangan lakukan ini. Ini bertentangan dengan pola pikir "tindakan bisnis" dan membuat objek layanan terlihat seperti Peri Twitter. Jika Anda ingin berbagi kode di antara objek bisnis, cukup buat objek atau modul BaseTwitterManager dan campurkan ke objek layanan Anda.

Aturan 4: Menangani Pengecualian Di Dalam Objek Layanan

Untuk kesekian kalinya: Objek layanan adalah tindakan bisnis tunggal. Saya tidak bisa mengatakan ini cukup. Jika Anda memiliki seseorang yang membaca tweet, mereka akan memberi Anda tweet, atau mengatakan, "Tweet ini tidak ada." Demikian pula, jangan biarkan objek layanan Anda panik, lompat ke meja pengontrol Anda, dan suruh untuk menghentikan semua pekerjaan karena “Error!” Kembalikan saja false dan biarkan pengontrol bergerak dari sana.

Kredit dan Langkah Selanjutnya

Artikel ini tidak akan mungkin terwujud tanpa komunitas pengembang Ruby yang luar biasa di Toptal. Jika saya mengalami masalah, komunitas adalah kelompok insinyur berbakat yang paling membantu yang pernah saya temui.

Jika Anda menggunakan objek layanan, Anda mungkin bertanya-tanya bagaimana cara memaksakan jawaban tertentu saat pengujian. Saya sarankan membaca artikel ini tentang cara membuat objek layanan tiruan di Rspec yang akan selalu mengembalikan hasil yang Anda inginkan, tanpa benar-benar mengenai objek layanan!

Jika Anda ingin mempelajari lebih lanjut tentang trik Ruby, saya sarankan Membuat Ruby DSL: Panduan untuk Pemrograman Meta Tingkat Lanjut oleh sesama Toptaler Mate Solymosi. Dia menguraikan bagaimana file routes.rb tidak terasa seperti Ruby dan membantu Anda membangun DSL Anda sendiri.