Dalam artikel ini, saya ingin berbicara tentang masalah apa dengan tipe data di Ruby, masalah apa yang saya temui, bagaimana mereka dapat dipecahkan dan bagaimana memastikan bahwa data yang bekerja dengan kita dapat diandalkan.

Pertama, Anda perlu memutuskan tipe data apa. Saya melihat definisi istilah yang sangat sukses, yang dapat ditemukan di
HaskellWiki .
Jenis adalah bagaimana Anda menggambarkan data yang akan dikerjakan oleh program Anda.
Tapi apa yang salah dengan tipe data di Ruby? Untuk menggambarkan masalah secara komprehensif, saya ingin menyoroti beberapa alasan.
Alasan 1. Masalah Ruby itu sendiri
Seperti yang Anda ketahui, Ruby menggunakan
pengetikan dinamis yang ketat dengan dukungan untuk apa yang disebut. mengetik bebek . Apa artinya ini?
Pengetikan yang kuat membutuhkan pengecoran eksplisit dan tidak menghasilkan casting ini sendiri, seperti yang terjadi, misalnya, dalam JavaScript. Karenanya, daftar kode berikut di Ruby akan gagal:
1 + '1' - 1 #=> TypeError (String can't be coerced into Integer)
Dalam pengetikan dinamis, pengecekan tipe berlangsung dalam runtime, yang memungkinkan kami untuk tidak menentukan tipe variabel dan menggunakan variabel yang sama untuk menyimpan nilai dari tipe yang berbeda:
x = 123 x = "123" x = [1, 2, 3]
Pernyataan berikut ini biasanya diberikan sebagai penjelasan dari istilah "mengetik bebek": jika terlihat seperti bebek, berenang seperti bebek dan dukun seperti bebek, maka kemungkinan besar ini adalah bebek. Yaitu mengetik bebek, mengandalkan perilaku objek, memberi kita fleksibilitas tambahan dalam menulis sistem kami. Misalnya, dalam contoh di bawah ini, nilai bagi kami bukan jenis argumen
collection
, tetapi kemampuannya untuk menanggapi pesan
blank?
dan
map
:
def process(collection) return if collection.blank? collection.map { |item| do_something_with(item) } end
Kemampuan untuk membuat "bebek" seperti itu adalah alat yang sangat kuat. Namun, seperti alat kuat lainnya, itu membutuhkan kehati-hatian saat menggunakannya. Ini dapat
diverifikasi oleh penelitian Rollbar , di mana mereka menganalisis lebih dari 1000 aplikasi Rail dan mengidentifikasi kesalahan yang paling umum. Dan 2 dari 10 kesalahan paling umum dihubungkan tepat dengan fakta bahwa objek tidak dapat menanggapi pesan tertentu. Dan karena itu, memeriksa perilaku objek yang diberikan oleh mengetik bebek pada kita dalam banyak kasus mungkin tidak cukup.
Kita dapat mengamati bagaimana pemeriksaan jenis ditambahkan ke bahasa dinamis dalam satu bentuk atau yang lain:
- TypeScript membawa pengecekan tipe ke pengembang JavaScript
- Petunjuk tipe ditambahkan dalam Python 3
- Dialyzer melakukan pekerjaan yang baik untuk memeriksa tipe Erlang / Elixir
- Curam dan Sorbet tambahkan pengecekan tipe dalam Ruby 2.x
Namun, sebelum berbicara tentang alat lain untuk bekerja dengan jenis yang lebih efisien di Ruby, mari kita lihat dua masalah lagi yang ingin saya temukan solusinya.
Alasan 2. Masalah umum pengembang dalam berbagai bahasa pemrograman
Mari kita mengingat kembali definisi tipe data yang saya berikan di awal artikel:
Jenis adalah bagaimana Anda menggambarkan data yang akan dikerjakan oleh program Anda.
Yaitu tipe dirancang untuk membantu kami menggambarkan data dari area subjek kami di mana sistem kami beroperasi. Namun, seringkali alih-alih beroperasi dengan tipe data yang kami buat dari area subjek kami, kami menggunakan tipe primitif, seperti angka, string, array, dll., Yang tidak mengatakan apa pun tentang area subjek kami. Masalah ini biasanya diklasifikasikan sebagai Obsesi Primitif (obsesi dengan primitif).
Berikut adalah contoh Obsesi Primitif yang khas:
price = 9.99 # vs Money = Struct.new(:amount_cents, :currency) price = Money.new(9_99, 'USD')
Alih-alih menggambarkan tipe data untuk bekerja dengan uang, angka reguler sering digunakan. Dan angka ini, seperti jenis primitif lainnya, tidak mengatakan apa-apa tentang bidang subjek kita. Menurut pendapat saya, ini adalah masalah terbesar dalam menggunakan primitif daripada membuat sistem tipe Anda sendiri, di mana tipe ini akan menggambarkan data dari area subjek kami. Kami sendiri menolak kelebihan yang bisa kami dapatkan dengan penggunaan tipe.
Saya akan berbicara tentang keuntungan ini segera setelah membahas masalah lain yang diajarkan kerangka kerja Ruby on Rails kami, terima kasih yang, saya yakin, sebagian besar dari mereka di sini datang ke Ruby.
Alasan 3. Masalah yang biasa dialami kerangka kerja Ruby on Rails kami
Ruby on Rails, atau lebih tepatnya kerangka ORM
ActiveRecord
dibangun di dalamnya, mengajarkan kita bahwa objek yang berada dalam keadaan tidak valid adalah normal. Menurut saya, ini jauh dari ide terbaik. Dan saya akan mencoba menjelaskannya.
Ambil contoh ini:
class App < ApplicationRecord validates :platform, presence: true end app = App.new app.valid? # => false
Tidak sulit untuk memahami bahwa objek
app
akan memiliki keadaan tidak valid: validasi model
App
mengharuskan objek model ini memiliki atribut
platform
, dan objek kami memiliki atribut ini kosong.
Sekarang, mari kita coba meneruskan objek ini dalam keadaan tidak valid ke layanan yang mengharapkan objek
App
sebagai argumen dan melakukan beberapa tindakan tergantung pada atribut
platform
objek ini:
class DoSomethingWithAppPlatform # @param [App] app # # @return [void] def call(app) # do something with app.platform end end DoSomethingWithAppPlatform.new.call(app)
Dalam hal ini, bahkan pemeriksaan tipe akan berlalu. Namun, karena atribut ini kosong untuk objek, tidak jelas bagaimana layanan akan menangani kasus ini. Bagaimanapun, memiliki kemampuan untuk membuat objek dalam keadaan tidak valid, kami mengutuk diri kita sendiri dengan kebutuhan untuk terus menangani kasus ketika negara yang tidak valid telah bocor ke sistem kami.
Tapi mari kita pikirkan masalah yang lebih dalam. Secara umum, mengapa kita memeriksa validitas data? Sebagai aturan, untuk memastikan bahwa keadaan yang tidak valid tidak bocor ke sistem kami. Jika sangat penting untuk memastikan bahwa keadaan yang tidak valid tidak diizinkan, lalu mengapa kita membiarkan objek dengan keadaan yang tidak valid dibuat? Terutama ketika kita berhadapan dengan objek penting seperti model ActiveRecord, yang sering merujuk pada logika bisnis root. Menurut pendapat saya, ini kedengarannya seperti ide yang sangat buruk.
Jadi, meringkas semua hal di atas, kami mendapatkan masalah berikut dalam bekerja dengan data di Ruby / Rails:
- bahasa itu sendiri memiliki mekanisme untuk memverifikasi perilaku, tetapi bukan data
- kami, seperti pengembang dalam bahasa lain, cenderung menggunakan tipe data primitif daripada membuat sistem tipe untuk area subjek kami
- Rails membiasakan kita dengan fakta bahwa keberadaan benda-benda dalam keadaan tidak valid adalah normal, meskipun solusi seperti itu sepertinya ide yang sangat buruk
Bagaimana mengatasi masalah ini?
Saya ingin mempertimbangkan salah satu solusi untuk masalah yang dijelaskan di atas, menggunakan contoh implementasi fitur nyata di Appodeal. Dalam proses pengumpulan statistik pada statistik Pengguna Aktif Harian (selanjutnya DAU) untuk aplikasi yang menggunakan Appodeal untuk memonetisasi, kami sampai pada sekitar struktur data berikut yang perlu kami kumpulkan:
DailyActiveUsersData = Struct.new( :app_id, :country_id, :user_id, :ad_type, :platform_id, :ad_id, :first_request_date, keyword_init: true )
Struktur ini memiliki semua masalah yang sama yang saya tulis di atas:
- pengecekan tipe apa pun sama sekali tidak ada, yang membuatnya tidak jelas nilai apa yang dapat diambil atribut dari struktur ini
- tidak ada deskripsi data yang digunakan dalam struktur ini, dan alih-alih jenis khusus untuk domain kami, primitif digunakan
- struktur mungkin ada dalam keadaan tidak valid
Untuk mengatasi masalah ini, kami memutuskan untuk menggunakan perpustakaan
dry-types
dry-struct
dan
dry-struct
.
dry-types
adalah sistem tipe sederhana dan dapat dikembangkan untuk Ruby, berguna untuk casting, menerapkan berbagai kendala, mendefinisikan struktur kompleks, dll.
dry-struct
adalah perpustakaan yang dibangun di atas
dry-types
yang menyediakan DSL yang nyaman untuk mendefinisikan struktur yang diketik / kelas.
Untuk menggambarkan data area subjek kami yang digunakan dalam struktur untuk mengumpulkan DAU, sistem tipe berikut ini dibuat:
module Types include Dry::Types.module AdTypeId = Types::Strict::Integer.enum(AD_TYPES.invert) EntityId = Types::Strict::Integer.constrained(gt: 0) PlatformId = Types::Strict::Integer.enum(PLATFORMS.invert) Uuid = Types::Strict::String.constrained(format: UUID_REGEX) Zero = Types.Constant(0) end
Sekarang kami telah menerima deskripsi data yang digunakan dalam sistem kami dan yang dapat kami gunakan dalam struktur. Seperti yang Anda lihat, tipe
EntityId
dan
Uuid
memiliki beberapa keterbatasan, dan tipe enumerable
AdTypeId
dan
PlatformId
hanya dapat memiliki nilai dari set tertentu. Bagaimana cara kerjanya dengan jenis ini? Pertimbangkan
PlatformId
sebagai contoh:
# enumerable- PLATFORMS = { 'android' => 1, 'fire_os' => 2, 'ios' => 3 }.freeze # , # Types::PlatformId[1] == Types::PlatformId['android'] # , # , Types::PlatformId['fire_os'] # => 2 # , Types::PlatformId['windows'] # => Dry::Types::ConstraintError
Jadi, menggunakan tipe-tipe itu sendiri sudah diketahui. Sekarang mari kita terapkan pada struktur kita. Sebagai hasilnya, kami mendapatkan ini:
class DailyActiveUsersData < Dry::Struct attribute :app_id, Types::EntityId attribute :country_id, Types::EntityId attribute :user_id, Types::EntityId attribute :ad_type, (Types::AdTypeId ǀ Types::Zero) attribute :platform_id, Types::PlarformId attribute :ad_id, Types::Uuid attribute :first_request_date, Types::Strict::Date end
Apa yang kita lihat sekarang dalam struktur data untuk DAU? Dengan menggunakan
dry-types
dry-struct
dan
dry-struct
kami menyingkirkan masalah yang terkait dengan kurangnya pemeriksaan tipe data dan kurangnya deskripsi data. Sekarang, siapa pun yang melihat struktur ini dan deskripsi jenis yang digunakan di dalamnya dapat memahami nilai apa yang dapat diambil setiap atribut.
Adapun masalah dengan objek dalam keadaan tidak valid,
dry-struct
menyelamatkan kita dari ini: jika kita mencoba menginisialisasi struktur dengan nilai-nilai tidak valid, kita akan mendapatkan kesalahan sebagai hasilnya. Dan untuk kasus-kasus di mana kebenaran data sangat penting (dan dalam kasus pengumpulan DAU, ini kasusnya dengan kami), menurut pendapat saya, mendapatkan pengecualian jauh lebih baik daripada mencoba menangani data yang tidak valid nanti. Selain itu, jika proses pengujian telah ditetapkan untuk Anda (dan ini persis sama dengan kami), maka dengan probabilitas tinggi kode yang menghasilkan kesalahan tersebut tidak akan mencapai lingkungan produksi.
Dan selain ketidakmampuan menginisialisasi objek dalam keadaan tidak valid,
dry-struct
juga tidak memungkinkan perubahan objek setelah inisialisasi. Berkat kedua faktor ini, kami mendapatkan jaminan bahwa objek dari struktur tersebut akan berada dalam kondisi yang valid dan Anda dapat dengan aman mengandalkan data ini di mana pun di sistem Anda.
Ringkasan
Pada artikel ini saya mencoba menjelaskan masalah yang mungkin Anda temui saat bekerja dengan data di Ruby, serta berbicara tentang alat yang kami gunakan untuk menyelesaikan masalah ini. Dan berkat penerapan alat-alat ini, saya benar-benar berhenti khawatir tentang kebenaran data yang sedang kami kerjakan. Bukankah itu sempurna? Bukankah ini tujuan dari instrumen apa pun - untuk memudahkan hidup kita dalam beberapa aspek? Dan menurut saya,
dry-types
dry-struct
dan
dry-struct
tugasnya dengan sempurna!