Bangun Komponen Rel Ramping Dengan Objek Ruby Lama Biasa
Diterbitkan: 2022-03-11Situs web Anda mendapatkan daya tarik, dan Anda berkembang pesat. Ruby/Rails adalah bahasa pemrograman pilihan Anda. Tim Anda lebih besar dan Anda telah menyerah pada "model gemuk, pengontrol kurus" sebagai gaya desain untuk aplikasi Rails Anda. Namun, Anda tetap tidak ingin meninggalkan penggunaan Rails.
Tidak masalah. Hari ini, kita akan membahas cara menggunakan praktik terbaik OOP untuk membuat kode Anda lebih bersih, lebih terisolasi, dan lebih terpisah.
Apakah Aplikasi Anda Layak Di-Refactoring?
Mari kita mulai dengan melihat bagaimana Anda harus memutuskan apakah aplikasi Anda adalah kandidat yang baik untuk pemfaktoran ulang.
Berikut adalah daftar metrik dan pertanyaan yang biasanya saya tanyakan pada diri sendiri untuk menentukan apakah kode saya perlu refactoring atau tidak.
- Tes unit lambat. Tes unit PORO biasanya berjalan cepat dengan kode yang terisolasi dengan baik, jadi tes yang berjalan lambat seringkali dapat menjadi indikator desain yang buruk dan tanggung jawab yang terlalu banyak.
- Model atau pengontrol FAT. Model atau pengontrol dengan lebih dari 200 baris kode (LOC) umumnya merupakan kandidat yang baik untuk refactoring.
- Basis kode yang terlalu besar. Jika Anda memiliki ERB/HTML/HAML dengan lebih dari 30.000 LOC atau kode sumber Ruby (tanpa GEMs ) dengan lebih dari 50.000 LOC, ada kemungkinan Anda harus melakukan refactor.
Coba gunakan sesuatu seperti ini untuk mengetahui berapa banyak baris kode sumber Ruby yang Anda miliki:
find app -iname "*.rb" -type f -exec cat {} \;| wc -l
Perintah ini akan mencari semua file dengan ekstensi .rb (file ruby) di folder / app, dan mencetak jumlah baris. Harap dicatat bahwa jumlah ini hanya perkiraan karena baris komentar akan dimasukkan dalam total ini.
Opsi lain yang lebih tepat dan lebih informatif adalah dengan menggunakan stats
tugas Rails rake yang menampilkan ringkasan singkat baris kode, jumlah kelas, jumlah metode, rasio metode terhadap kelas, dan rasio baris kode per metode:
bundle exec rake stats +----------------------+-------+-----+-------+---------+-----+-------+ | Name | Lines | LOC | Class | Methods | M/C | LOC/M | +----------------------+-------+-----+-------+---------+-----+-------+ | Controllers | 195 | 153 | 6 | 18 | 3 | 6 | | Helpers | 14 | 13 | 0 | 2 | 0 | 4 | | Models | 120 | 84 | 5 | 12 | 2 | 5 | | Mailers | 0 | 0 | 0 | 0 | 0 | 0 | | Javascripts | 45 | 12 | 0 | 3 | 0 | 2 | | Libraries | 0 | 0 | 0 | 0 | 0 | 0 | | Controller specs | 106 | 75 | 0 | 0 | 0 | 0 | | Helper specs | 15 | 4 | 0 | 0 | 0 | 0 | | Model specs | 238 | 182 | 0 | 0 | 0 | 0 | | Request specs | 699 | 489 | 0 | 14 | 0 | 32 | | Routing specs | 35 | 26 | 0 | 0 | 0 | 0 | | View specs | 5 | 4 | 0 | 0 | 0 | 0 | +----------------------+-------+-----+-------+---------+-----+-------+ | Total | 1472 |1042 | 11 | 49 | 4 | 19 | +----------------------+-------+-----+-------+---------+-----+-------+ Code LOC: 262 Test LOC: 780 Code to Test Ratio: 1:3.0
- Bisakah saya mengekstrak pola berulang di basis kode saya?
Pemisahan dalam Aksi
Mari kita mulai dengan contoh dunia nyata.
Anggap saja kami ingin menulis aplikasi yang melacak waktu untuk pelari. Di halaman utama, pengguna dapat melihat waktu mereka masuk.
Setiap entri waktu memiliki tanggal, jarak, durasi, dan info "status" tambahan yang relevan (misalnya cuaca, jenis medan, dll.), dan kecepatan rata-rata yang dapat dihitung saat diperlukan.
Kami membutuhkan halaman laporan yang menampilkan kecepatan dan jarak rata-rata per minggu.
Jika kecepatan rata-rata untuk entri lebih tinggi dari kecepatan rata-rata keseluruhan, kami akan memberi tahu pengguna dengan SMS (untuk contoh ini kami akan menggunakan Nexmo RESTful API untuk mengirim SMS).
Beranda akan memungkinkan Anda untuk memilih jarak, tanggal, dan waktu yang dihabiskan untuk jogging untuk membuat entri yang mirip dengan ini:
Kami juga memiliki halaman statistics
yang pada dasarnya adalah laporan mingguan yang mencakup kecepatan rata-rata dan jarak yang ditempuh per minggu.
- Anda dapat memeriksa sampel online di sini.
Kode
Struktur direktori app
terlihat seperti:
⇒ tree . ├── assets │ └── ... ├── controllers │ ├── application_controller.rb │ ├── entries_controller.rb │ └── statistics_controller.rb ├── helpers │ ├── application_helper.rb │ ├── entries_helper.rb │ └── statistics_helper.rb ├── mailers ├── models │ ├── entry.rb │ └── user.rb └── views ├── devise │ └── ... ├── entries │ ├── _entry.html.erb │ ├── _form.html.erb │ └── index.html.erb ├── layouts │ └── application.html.erb └── statistics └── index.html.erb
Saya tidak akan membahas model User
karena tidak ada yang istimewa karena kami menggunakannya dengan Rancangan untuk mengimplementasikan otentikasi.
Adapun model Entry
, berisi logika bisnis untuk aplikasi kita.
Setiap Entry
adalah milik User
.
Kami memvalidasi keberadaan atribut distance
, time_period
, date_time
dan status
untuk setiap entri.
Setiap kali kami membuat entri, kami membandingkan kecepatan rata-rata pengguna dengan rata-rata semua pengguna lain dalam sistem, dan memberi tahu pengguna melalui SMS menggunakan Nexmo (kami tidak akan membahas bagaimana perpustakaan Nexmo digunakan, meskipun saya ingin untuk mendemonstrasikan kasus di mana kami menggunakan perpustakaan eksternal).
- Contoh inti
Perhatikan, bahwa model Entry
berisi lebih dari logika bisnis saja. Ini juga menangani beberapa validasi dan panggilan balik.
entries_controller.rb
memiliki tindakan CRUD utama (meskipun tidak ada pembaruan). EntriesController#index
mendapatkan entri untuk pengguna saat ini dan mengurutkan catatan berdasarkan tanggal dibuat, sementara EntriesController#create
membuat entri baru. Tidak perlu membahas yang jelas dan tanggung jawab EntriesController#destroy
:
- Contoh inti
Sementara statistics_controller.rb
bertanggung jawab untuk menghitung laporan mingguan, StatisticsController#index
mendapatkan entri untuk pengguna yang masuk dan mengelompokkannya berdasarkan minggu, menggunakan metode #group_by
yang terdapat dalam kelas Enumerable di Rails. Kemudian mencoba untuk menghias hasil menggunakan beberapa metode pribadi.
- Contoh inti
Kami tidak banyak membahas pandangan di sini, karena kode sumbernya sudah cukup jelas.
Di bawah ini adalah tampilan daftar entri untuk pengguna yang masuk ( index.html.erb
). Ini adalah templat yang akan digunakan untuk menampilkan hasil tindakan indeks (metode) di pengontrol entri:
- Contoh inti
Perhatikan bahwa kita menggunakan partial render @entries
, untuk menarik kode yang dibagikan ke dalam template parsial _entry.html.erb
sehingga kita dapat menjaga kode kita KERING dan dapat digunakan kembali:
- Contoh inti
Hal yang sama berlaku untuk _form
parsial. Alih-alih menggunakan kode yang sama dengan tindakan (baru dan edit), kami membuat formulir parsial yang dapat digunakan kembali:
- Contoh inti
Adapun tampilan halaman laporan mingguan, statistics/index.html.erb
menunjukkan beberapa statistik dan melaporkan kinerja mingguan pengguna dengan mengelompokkan beberapa entri:
- Contoh inti
Dan akhirnya, helper untuk entri, entries_helper.rb
, menyertakan dua helper readable_time_period
dan readable_speed
yang seharusnya membuat atribut lebih mudah dibaca secara manusiawi:
- Contoh inti
Tidak ada yang mewah sejauh ini.
Sebagian besar dari Anda akan berpendapat bahwa refactoring ini bertentangan dengan prinsip KISS dan akan membuat sistem lebih rumit.
Jadi apakah aplikasi ini benar-benar membutuhkan refactoring?
Sama sekali tidak , tapi kami akan mempertimbangkannya untuk tujuan demonstrasi saja.
Lagi pula, jika Anda memeriksa bagian sebelumnya, dan karakteristik yang menunjukkan aplikasi perlu pemfaktoran ulang, menjadi jelas bahwa aplikasi dalam contoh kami bukan kandidat yang valid untuk pemfaktoran ulang.
Lingkaran kehidupan
Jadi mari kita mulai dengan menjelaskan struktur pola Rails MVC.
Biasanya dimulai dengan browser yang membuat permintaan, seperti https://www.toptal.com/jogging/show/1
.
Server web menerima permintaan dan menggunakan routes
untuk mengetahui controller
mana yang akan digunakan.
Controller melakukan pekerjaan parsing permintaan pengguna, pengiriman data, cookie, sesi, dll, dan kemudian meminta model
untuk mendapatkan data.
models
adalah kelas Ruby yang berbicara dengan database, menyimpan dan memvalidasi data, menjalankan logika bisnis, dan sebaliknya melakukan pekerjaan berat. Tampilan adalah apa yang dilihat pengguna: HTML, CSS, XML, Javascript, JSON.
Jika kita ingin menunjukkan urutan siklus hidup permintaan Rails, akan terlihat seperti ini:
Apa yang ingin saya capai adalah menambahkan lebih banyak abstraksi menggunakan objek ruby tua biasa (PORO) dan membuat polanya seperti berikut untuk create/update
tindakan:
Dan sesuatu seperti berikut untuk tindakan list/show
:
Dengan menambahkan abstraksi PORO, kami akan memastikan pemisahan penuh antara tanggung jawab SRP, sesuatu yang Rails tidak kuasai dengan baik.
Pedoman
Untuk mencapai desain baru, saya akan menggunakan pedoman yang tercantum di bawah ini, tetapi harap perhatikan bahwa ini bukan aturan yang harus Anda ikuti ke T. Anggap saja sebagai pedoman fleksibel yang membuat refactoring lebih mudah.
- Model ActiveRecord dapat berisi asosiasi dan konstanta, tetapi tidak ada yang lain. Jadi itu berarti tidak ada panggilan balik (gunakan objek layanan dan tambahkan panggilan balik di sana) dan tidak ada validasi (gunakan objek Formulir untuk menyertakan penamaan dan validasi model).
- Pertahankan Pengendali sebagai lapisan tipis dan selalu panggil objek Layanan. Beberapa dari Anda akan bertanya mengapa menggunakan pengontrol sama sekali karena kami ingin terus memanggil objek layanan untuk memuat logika? Nah, pengontrol adalah tempat yang baik untuk memiliki perutean HTTP, penguraian parameter, otentikasi, negosiasi konten, memanggil layanan atau objek editor yang tepat, penangkapan pengecualian, pemformatan respons, dan mengembalikan kode status HTTP yang tepat.
- Layanan harus memanggil objek Kueri, dan tidak boleh menyimpan status. Gunakan metode instan, bukan metode kelas. Seharusnya ada sangat sedikit metode publik yang sesuai dengan SRP.
- Kueri harus dilakukan di objek kueri. Metode objek kueri harus mengembalikan objek, hash, atau larik, bukan asosiasi ActiveRecord.
- Hindari menggunakan Pembantu dan gunakan dekorator sebagai gantinya. Mengapa? Sebuah perangkap umum dengan pembantu Rails adalah bahwa mereka dapat berubah menjadi tumpukan besar fungsi non-OO, semua berbagi namespace dan menginjak satu sama lain. Tetapi jauh lebih buruk adalah bahwa tidak ada cara yang bagus untuk menggunakan segala jenis polimorfisme dengan pembantu Rails — menyediakan implementasi yang berbeda untuk konteks atau jenis yang berbeda, pembantu over-riding atau sub-kelas. Saya pikir kelas pembantu Rails umumnya harus digunakan untuk metode utilitas, bukan untuk kasus penggunaan tertentu, seperti memformat atribut model untuk semua jenis logika presentasi. Buat mereka tetap ringan dan berangin.
- Hindari menggunakan kekhawatiran dan gunakan Dekorator/Delegator sebagai gantinya. Mengapa? Bagaimanapun, kekhawatiran tampaknya menjadi bagian inti dari Rails dan dapat MENGERINGKAN kode ketika dibagikan di antara beberapa model. Meskipun demikian, masalah utamanya adalah kekhawatiran tidak membuat objek model lebih kohesif. Kode hanya lebih terorganisir. Dengan kata lain, tidak ada perubahan nyata pada API model.
- Cobalah untuk mengekstrak Objek Nilai dari model untuk menjaga kode Anda lebih bersih dan untuk mengelompokkan atribut terkait.
- Selalu berikan satu variabel instan per tampilan.
Pemfaktoran ulang
Sebelum kita mulai, saya ingin membahas satu hal lagi. Ketika Anda memulai refactoring, biasanya Anda akhirnya bertanya pada diri sendiri: "Apakah refactoring itu benar-benar bagus?"
Jika Anda merasa Anda membuat lebih banyak pemisahan atau isolasi antara tanggung jawab (bahkan jika itu berarti menambahkan lebih banyak kode dan file baru), maka ini biasanya merupakan hal yang baik. Bagaimanapun, memisahkan aplikasi adalah praktik yang sangat baik dan memudahkan kita untuk melakukan pengujian unit yang tepat.
Saya tidak akan membahas hal-hal, seperti memindahkan logika dari pengontrol ke model, karena saya berasumsi Anda sudah melakukannya, dan Anda merasa nyaman menggunakan Rails (biasanya Skinny Controller dan model FAT).
Demi menjaga ketat artikel ini, saya tidak akan membahas pengujian di sini, tetapi bukan berarti Anda tidak boleh menguji.
Sebaliknya, Anda harus selalu memulai dengan tes untuk memastikan semuanya baik-baik saja sebelum melanjutkan. Ini adalah suatu keharusan, terutama ketika refactoring.
Kemudian kita dapat menerapkan perubahan dan memastikan semua tes lulus untuk bagian kode yang relevan.
Mengekstraksi Objek Nilai
Pertama, apa itu objek nilai?
Martin Fowler menjelaskan:
Objek Nilai adalah objek kecil, seperti uang atau objek rentang tanggal. Properti utama mereka adalah bahwa mereka mengikuti semantik nilai daripada semantik referensi.
Terkadang Anda mungkin menghadapi situasi di mana sebuah konsep layak mendapatkan abstraksinya sendiri dan kesetaraannya tidak didasarkan pada nilai, tetapi pada identitas. Contohnya termasuk Ruby's Date, URI, dan Pathname. Ekstraksi ke objek nilai (atau model domain) adalah kemudahan yang luar biasa.
Mengapa mengganggu?
Salah satu keuntungan terbesar dari objek Nilai adalah ekspresi yang mereka bantu capai dalam kode Anda. Kode Anda akan cenderung jauh lebih jelas, atau setidaknya bisa jika Anda memiliki praktik penamaan yang baik. Karena Objek Nilai adalah abstraksi, itu mengarah ke kode yang lebih bersih dan lebih sedikit kesalahan.
Kemenangan besar lainnya adalah kekekalan. Kekekalan objek sangat penting. Saat kami menyimpan kumpulan data tertentu, yang dapat digunakan dalam objek nilai, saya biasanya tidak ingin data itu dimanipulasi.
Kapan ini berguna?
Tidak ada jawaban tunggal, satu ukuran untuk semua. Lakukan yang terbaik untuk Anda dan apa yang masuk akal dalam situasi apa pun.
Lebih dari itu, ada beberapa pedoman yang saya gunakan untuk membantu saya membuat keputusan itu.
Jika Anda memikirkan sekelompok metode terkait, dengan objek Nilai mereka lebih ekspresif. Ekspresifitas ini berarti bahwa objek Nilai harus mewakili kumpulan data yang berbeda, yang dapat disimpulkan oleh pengembang rata-rata Anda hanya dengan melihat nama objek.
Bagaimana ini dilakukan?
Objek nilai harus mengikuti beberapa aturan dasar:
- Objek nilai harus memiliki banyak atribut.
- Atribut harus tidak berubah sepanjang siklus hidup objek.
- Kesetaraan ditentukan oleh atribut objek.
Dalam contoh kita, saya akan membuat objek nilai EntryStatus
untuk mengabstraksi atribut Entry#status_weather
dan Entry#status_landform
ke kelas mereka sendiri, yang terlihat seperti ini:
- Contoh inti
Catatan: Ini hanyalah Objek Ruby Lama Biasa (PORO) yang tidak mewarisi dari ActiveRecord::Base
. Kami telah mendefinisikan metode pembaca untuk atribut kami dan menugaskannya pada inisialisasi. Kami juga menggunakan mixin yang sebanding untuk membandingkan objek menggunakan metode (<=>).
Kita dapat memodifikasi model Entry
untuk menggunakan objek nilai yang kita buat:
- Contoh inti
Kami juga dapat memodifikasi metode EntryController#create
untuk menggunakan objek nilai baru yang sesuai:
- Contoh inti
Ekstrak Objek Layanan
Jadi apa itu objek Layanan?
Tugas objek Layanan adalah menyimpan kode untuk bagian logika bisnis tertentu. Berbeda dengan gaya "model gemuk" , di mana sejumlah kecil objek berisi banyak, banyak metode untuk semua logika yang diperlukan, menggunakan objek Layanan menghasilkan banyak kelas, yang masing-masing melayani satu tujuan.

Mengapa? Apa saja manfaatnya?
- Memisahkan. Objek layanan membantu Anda mencapai lebih banyak isolasi antar objek.
- Visibilitas. Objek layanan (jika dinamai dengan baik) menunjukkan apa yang dilakukan aplikasi. Saya hanya bisa melirik direktori layanan untuk melihat kemampuan apa yang disediakan aplikasi.
- Model dan pengontrol pembersihan. Pengontrol mengubah permintaan (params, sesi, cookie) menjadi argumen, meneruskannya ke layanan dan mengarahkan atau merender sesuai dengan respons layanan. Sedangkan model hanya berurusan dengan asosiasi, dan ketekunan. Mengekstrak kode dari pengontrol/model ke objek layanan akan mendukung SRP dan membuat kode lebih terpisah. Tanggung jawab model kemudian hanya berurusan dengan asosiasi dan menyimpan/menghapus catatan, sedangkan objek layanan akan memiliki tanggung jawab tunggal (SRP). Ini mengarah pada desain yang lebih baik dan pengujian unit yang lebih baik.
- KERING dan Rangkullah perubahan. Saya menyimpan objek layanan sesederhana dan sekecil mungkin. Saya membuat objek layanan dengan objek layanan lain, dan saya menggunakannya kembali.
- Bersihkan dan percepat rangkaian pengujian Anda. Layanan mudah dan cepat untuk diuji karena mereka adalah objek Ruby kecil dengan satu titik masuk (metode panggilan). Layanan kompleks disusun dengan layanan lain, sehingga Anda dapat membagi pengujian dengan mudah. Juga, menggunakan objek layanan memudahkan untuk mengejek/mematikan objek terkait tanpa perlu memuat seluruh lingkungan Rails.
- Dapat dihubungi dari mana saja. Objek layanan kemungkinan akan dipanggil dari pengontrol serta objek layanan lainnya, Tugas Tertunda / Penyelamatan / Sidekiq, tugas Rake, konsol, dll.
Di sisi lain, tidak ada yang pernah sempurna. Kerugian dari objek Layanan adalah bahwa mereka dapat menjadi berlebihan untuk tindakan yang sangat sederhana. Dalam kasus seperti itu, Anda mungkin akan memperumit, alih-alih menyederhanakan, kode Anda.
Kapan Anda harus mengekstrak objek layanan?
Tidak ada aturan keras dan cepat di sini juga.
Biasanya, objek Layanan lebih baik untuk sistem menengah hingga besar; mereka dengan jumlah logika yang layak di luar operasi CRUD standar.
Jadi, setiap kali Anda berpikir bahwa cuplikan kode mungkin bukan milik direktori tempat Anda akan menambahkannya, mungkin ide yang baik untuk mempertimbangkan kembali dan melihat apakah itu harus pergi ke objek layanan sebagai gantinya.
Berikut adalah beberapa indikator kapan harus menggunakan objek Layanan:
- Tindakannya kompleks.
- Tindakan menjangkau berbagai model.
- Tindakan berinteraksi dengan layanan eksternal.
- Tindakan tersebut bukan merupakan perhatian utama dari model yang mendasarinya.
- Ada beberapa cara untuk melakukan tindakan.
Bagaimana seharusnya Anda mendesain Objek Layanan?
Merancang kelas untuk objek layanan relatif mudah, karena Anda tidak memerlukan permata khusus, tidak perlu mempelajari DSL baru, dan dapat kurang lebih mengandalkan keterampilan desain perangkat lunak yang sudah Anda miliki.
Saya biasanya menggunakan pedoman dan konvensi berikut untuk mendesain objek layanan:
- Jangan menyimpan status objek.
- Gunakan metode instan, bukan metode kelas.
- Seharusnya ada sangat sedikit metode publik (sebaiknya satu untuk mendukung SRP.
- Metode harus mengembalikan objek hasil kaya dan bukan boolean.
- Layanan berada di bawah direktori
app/services
. Saya mendorong Anda untuk menggunakan subdirektori untuk domain logika bisnis yang berat. Misalnya, fileapp/services/report/generate_weekly.rb
akan mendefinisikanReport::GenerateWeekly
sementaraapp/services/report/publish_monthly.rb
akan mendefinisikanReport::PublishMonthly
. - Layanan dimulai dengan kata kerja (dan tidak diakhiri dengan Layanan):
ApproveTransaction
,SendTestNewsletter
,ImportUsersFromCsv
. - Layanan menanggapi metode panggilan. Saya menemukan menggunakan kata kerja lain membuatnya agak berlebihan: ApproveTransaction.approve() tidak terbaca dengan baik. Juga, metode panggilan adalah metode de facto untuk lambda, procs, dan objek metode.
Jika Anda melihat StatisticsController#index
, Anda akan melihat sekelompok metode ( weeks_to_date_from
, weeks_to_date_to
, avg_distance
, dll.) digabungkan ke controller. Itu tidak bagus. Pertimbangkan konsekuensinya jika Anda ingin membuat laporan mingguan di luar statistics_controller
.
Dalam kasus kami, mari buat Report::GenerateWeekly
dan ekstrak logika laporan dari StatisticsController
:
- Contoh inti
Jadi StatisticsController#index
sekarang terlihat lebih bersih:
- Contoh inti
Dengan menerapkan pola objek Layanan, kami menggabungkan kode di sekitar tindakan spesifik yang kompleks dan mempromosikan pembuatan metode yang lebih kecil dan lebih jelas.
Pekerjaan rumah: pertimbangkan untuk menggunakan objek Value untuk WeeklyReport
alih-alih Struct
.
Ekstrak Objek Kueri dari Pengontrol
Apa itu objek Kueri?
Objek Query adalah PORO yang mewakili query database. Itu dapat digunakan kembali di berbagai tempat dalam aplikasi sementara pada saat yang sama menyembunyikan logika kueri. Ini juga menyediakan unit terisolasi yang baik untuk diuji.
Anda harus mengekstrak kueri SQL/NoSQL yang kompleks ke dalam kelasnya sendiri.
Setiap objek Kueri bertanggung jawab untuk mengembalikan kumpulan hasil berdasarkan kriteria/aturan bisnis.
Dalam contoh ini, kami tidak memiliki kueri yang rumit, jadi menggunakan objek Kueri tidak akan efisien. Namun, untuk tujuan demonstrasi, mari ekstrak kueri di Report::GenerateWeekly#call
dan buat generate_entries_query.rb
:
- Contoh inti
Dan di Report::GenerateWeekly#call
, mari kita ganti:
def call @user.entries.group_by(&:week).map do |week, entries| WeeklyReport.new( ... ) end end
dengan:
def call weekly_grouped_entries = GroupEntriesQuery.new(@user).call weekly_grouped_entries.map do |week, entries| WeeklyReport.new( ... ) end end
Pola objek kueri membantu menjaga logika model Anda benar-benar terkait dengan perilaku kelas, sekaligus menjaga pengontrol Anda tetap ramping. Karena mereka tidak lebih dari kelas Ruby lama biasa, objek kueri tidak perlu mewarisi dari ActiveRecord::Base
, dan harus bertanggung jawab tidak lebih dari mengeksekusi kueri.
Ekstrak Buat Entri ke Objek Layanan
Sekarang, mari ekstrak logika membuat entri baru ke objek layanan baru. Mari gunakan konvensi dan buat CreateEntry
:
- Contoh inti
Dan sekarang EntriesController#create
kami adalah sebagai berikut:
def create begin CreateEntry.new(current_user, entry_params).call flash[:notice] = 'Entry was successfully created.' rescue Exception => e flash[:error] = e.message end redirect_to root_path end
Pindahkan Validasi ke dalam Objek Formulir
Sekarang, di sini segalanya mulai menjadi lebih menarik.
Ingat dalam pedoman kami, kami sepakat bahwa kami ingin model berisi asosiasi dan konstanta, tetapi tidak ada yang lain (tidak ada validasi dan tidak ada panggilan balik). Jadi mari kita mulai dengan menghapus callback, dan menggunakan objek Form sebagai gantinya.
Objek Formulir adalah Objek Ruby Tua Biasa (PORO). Ia mengambil alih dari objek pengontrol/layanan di mana pun ia perlu berbicara dengan database.
Mengapa menggunakan objek Formulir?
Saat ingin memfaktorkan ulang aplikasi Anda, selalu merupakan ide yang baik untuk mengingat prinsip tanggung jawab tunggal (SRP).
SRP membantu Anda membuat keputusan desain yang lebih baik seputar tanggung jawab kelas.
Model tabel database Anda (model ActiveRecord dalam konteks Rails), misalnya, mewakili satu record database dalam kode, jadi tidak ada alasan untuk khawatir dengan apa pun yang dilakukan pengguna Anda.
Di sinilah objek Form masuk.
Objek Formulir bertanggung jawab untuk mewakili formulir dalam aplikasi Anda. Jadi setiap bidang input dapat diperlakukan sebagai atribut di kelas. Itu dapat memvalidasi bahwa atribut tersebut memenuhi beberapa aturan validasi, dan dapat meneruskan data "bersih" ke tempat yang harus dituju (misalnya, model database Anda atau mungkin pembuat kueri pencarian Anda).
Kapan sebaiknya Anda menggunakan objek Form?
- Saat Anda ingin mengekstrak validasi dari model Rails.
- Saat beberapa model dapat diperbarui dengan pengiriman formulir tunggal, Anda mungkin ingin membuat objek Formulir.
Ini memungkinkan Anda untuk meletakkan semua logika formulir (konvensi penamaan, validasi, dan sebagainya) ke dalam satu tempat.
Bagaimana Anda membuat objek Formulir?
- Buat kelas Ruby biasa.
- Sertakan
ActiveModel::Model
(di Rails 3, Anda harus menyertakan Penamaan, Konversi, dan Validasi sebagai gantinya) - Mulai gunakan kelas formulir baru Anda seolah-olah itu adalah model ActiveRecord biasa, perbedaan terbesarnya adalah Anda tidak dapat menyimpan data yang disimpan dalam objek ini.
Harap dicatat bahwa Anda dapat menggunakan permata reformasi, tetapi tetap menggunakan PORO, kami akan membuat entry_form.rb
yang terlihat seperti ini:
- Contoh inti
Dan kita akan memodifikasi CreateEntry
untuk mulai menggunakan objek Form EntryForm
:
class CreateEntry ...... ...... def call @entry_form = ::EntryForm.new(@params) if @entry_form.valid? .... else .... end end end
Catatan: Beberapa dari Anda akan mengatakan bahwa tidak perlu mengakses objek Form dari objek Service dan kita bisa memanggil objek Form langsung dari controller, yang merupakan argumen yang valid. Namun, saya lebih suka memiliki aliran yang jelas, dan itulah mengapa saya selalu memanggil objek Form dari objek Service.
Pindahkan Panggilan Balik ke Objek Layanan
Seperti yang kami setujui sebelumnya, kami tidak ingin model kami berisi validasi dan panggilan balik. Kami mengekstrak validasi menggunakan objek Form. Tetapi kami masih menggunakan beberapa panggilan balik ( after_create
dalam model Entry
compare_speed_and_notify_user
).
Mengapa kami ingin menghapus panggilan balik dari model?
Pengembang Rails biasanya mulai memperhatikan rasa sakit panggilan balik selama pengujian. Jika Anda tidak menguji model ActiveRecord Anda, Anda akan mulai merasakan sakit nanti saat aplikasi Anda berkembang dan karena lebih banyak logika diperlukan untuk memanggil atau menghindari panggilan balik.
after_*
callback terutama digunakan dalam kaitannya dengan menyimpan atau mempertahankan objek.
Setelah objek disimpan, tujuan (yaitu tanggung jawab) dari objek telah terpenuhi. Jadi, jika kita masih melihat callback dipanggil setelah objek disimpan, kemungkinan yang kita lihat adalah callback yang menjangkau di luar area tanggung jawab objek, dan saat itulah kita mengalami masalah.
Dalam kasus kami, kami mengirim SMS ke pengguna setelah kami menyimpan entri, yang sebenarnya tidak terkait dengan domain Entri.
Cara sederhana untuk menyelesaikan masalah adalah dengan memindahkan panggilan balik ke objek layanan terkait. Lagi pula, mengirim SMS untuk pengguna akhir terkait dengan Objek Layanan CreateEntry
dan bukan dengan model Entri itu sendiri.
Dalam melakukannya, kita tidak lagi harus mematikan metode compare_speed_and_notify_user
dalam pengujian kita. Kami telah membuatnya menjadi masalah sederhana untuk membuat entri tanpa memerlukan SMS untuk dikirim, dan kami mengikuti desain Berorientasi Objek yang baik dengan memastikan kelas kami memiliki satu tanggung jawab (SRP).
Jadi sekarang CreateEntry
kami terlihat seperti:
- Contoh inti
Gunakan Dekorator Alih-alih Pembantu
Meskipun kita dapat dengan mudah menggunakan koleksi Draper dari model tampilan dan dekorator, saya akan tetap menggunakan PORO untuk artikel ini, seperti yang telah saya lakukan sejauh ini.
Yang saya butuhkan adalah kelas yang akan memanggil metode pada objek yang didekorasi.
Saya dapat menggunakan method_missing
untuk mengimplementasikannya, tetapi saya akan menggunakan perpustakaan standar Ruby SimpleDelegator
.
Kode berikut menunjukkan cara menggunakan SimpleDelegator
untuk mengimplementasikan dekorator dasar kami:
% app/decorators/base_decorator.rb require 'delegate' class BaseDecorator < SimpleDelegator def initialize(base, view_context) super(base) @object = base @view_context = view_context end private def self.decorates(name) define_method(name) do @object end end def _h @view_context end end
Jadi mengapa metode _h
?
Metode ini bertindak sebagai proxy untuk konteks tampilan. Secara default, konteks tampilan adalah turunan dari kelas tampilan, kelas tampilan default adalah ActionView::Base
. Anda dapat mengakses view helper sebagai berikut:
_h.content_tag :div, 'my-div', class: 'my-class'
Untuk membuatnya lebih nyaman, kami menambahkan metode decorate
ke ApplicationHelper
:
module ApplicationHelper # ..... def decorate(object, klass = nil) klass ||= "#{object.class}Decorator".constantize decorator = klass.new(object, self) yield decorator if block_given? decorator end # ..... end
Sekarang, kita dapat memindahkan helper EntriesHelper
ke dekorator:
# app/decorators/entry_decorator.rb class EntryDecorator < BaseDecorator decorates :entry def readable_time_period mins = entry.time_period return Time.at(60 * mins).utc.strftime('%M <small>Mins</small>').html_safe if mins < 60 Time.at(60 * mins).utc.strftime('%H <small>Hour</small> %M <small>Mins</small>').html_safe end def readable_speed "#{sprintf('%0.2f', entry.speed)} <small>Km/H</small>".html_safe end end
Dan kita dapat menggunakan readable_time_period
dan readable_speed
seperti:
# app/views/entries/_entry.html.erb - <td><%= readable_speed(entry) %> </td> + <td><%= decorate(entry).readable_speed %> </td>
- <td><%= readable_time_period(entry) %></td> + <td><%= decorate(entry).readable_time_period %></td>
Struktur Setelah Refactoring
Kami berakhir dengan lebih banyak file, tetapi itu tidak selalu merupakan hal yang buruk (dan ingat bahwa, sejak awal, kami mengakui bahwa contoh ini hanya untuk tujuan demonstratif dan belum tentu merupakan kasus penggunaan yang baik untuk refactoring):
app ├── assets │ └── ... ├── controllers │ ├── application_controller.rb │ ├── entries_controller.rb │ └── statistics_controller.rb ├── decorators │ ├── base_decorator.rb │ └── entry_decorator.rb ├── forms │ └── entry_form.rb ├── helpers │ └── application_helper.rb ├── mailers ├── models │ ├── entry.rb │ ├── entry_status.rb │ └── user.rb ├── queries │ └── group_entries_query.rb ├── services │ ├── create_entry.rb │ └── report │ └── generate_weekly.rb └── views ├── devise │ └── .. ├── entries │ ├── _entry.html.erb │ ├── _form.html.erb │ └── index.html.erb ├── layouts │ └── application.html.erb └── statistics └── index.html.erb
Kesimpulan
Meskipun kami fokus pada Rails dalam posting blog ini, RoR bukanlah ketergantungan dari objek layanan yang dijelaskan dan PORO lainnya. Anda dapat menggunakan pendekatan ini dengan kerangka kerja web, seluler, atau aplikasi konsol apa pun.
Dengan menggunakan MVC sebagai arsitektur aplikasi web, semuanya tetap terhubung dan membuat Anda berjalan lebih lambat karena sebagian besar perubahan berdampak pada bagian lain dari aplikasi. Juga, ini memaksa Anda untuk berpikir di mana harus meletakkan beberapa logika bisnis – haruskah itu masuk ke model, pengontrol, atau tampilan?
Dengan menggunakan PORO sederhana, kami telah memindahkan logika bisnis ke model atau layanan yang tidak mewarisi dari ActiveRecord
, yang sudah merupakan kemenangan besar, belum lagi kami memiliki kode yang lebih bersih, yang mendukung SRP dan pengujian unit yang lebih cepat.
Arsitektur bersih bertujuan untuk menempatkan kasus penggunaan di tengah/atas struktur Anda, sehingga Anda dapat dengan mudah melihat apa yang dilakukan aplikasi Anda. Ini juga membuatnya lebih mudah untuk mengadopsi perubahan karena jauh lebih modular dan terisolasi.
Saya harap saya mendemonstrasikan bagaimana menggunakan Objek Ruby Lama Biasa dan lebih banyak abstraksi memisahkan masalah, menyederhanakan pengujian dan membantu menghasilkan kode yang bersih dan dapat dipelihara.