Bagaimana Menghilangkan Warisan Meja Tunggal dari Monolith Rails Anda

Warisan mudah - sampai Anda harus berurusan dengan utang teknis dan pajak.

Ketika basis kode utama Learn muncul lima tahun lalu, Single Table Inheritance (STI) cukup populer. Tim Flatiron Labs pada saat itu membahas semuanya - menggunakannya untuk semuanya mulai dari penilaian dan kurikulum hingga kegiatan yang memberi makan kegiatan dan konten dalam sistem manajemen pembelajaran kami yang sedang berkembang. Dan itu luar biasa— itu menyelesaikan pekerjaan. Itu memungkinkan instruktur untuk menyampaikan kurikulum, melacak kemajuan siswa, dan menciptakan pengalaman pengguna yang menarik.

Tetapi seperti yang ditunjukkan oleh banyak posting blog (yang ini, yang ini, dan yang ini misalnya), IMS tidak berskala super baik, terutama ketika data tumbuh dan subkelas baru mulai sangat bervariasi dari superclasses dan satu sama lain. Seperti yang Anda duga, hal yang sama terjadi pada basis kode kami! Sekolah kami berkembang dan kami mendukung lebih banyak fitur dan tipe pelajaran. Seiring waktu, model mulai menggembung dan bermutasi dan tidak lagi mencerminkan abstraksi yang tepat untuk domain.

Kami tinggal di ruang itu untuk sementara waktu, memberikan kode itu tempat tidur yang luas, dan menambalnya hanya jika diperlukan. Dan kemudian tiba saatnya untuk refactor.

Selama beberapa bulan terakhir, saya memulai sebuah misi untuk menghapus satu contoh STI yang sulit dipahami, salah satu yang melibatkan model Konten yang agak ambigu bernama. Semudah mengatur STI pada awalnya, sebenarnya cukup sulit untuk dihilangkan.

Jadi, dalam posting ini, saya akan membahas sedikit tentang IMS, memberikan beberapa konteks tentang domain kami, menguraikan ruang lingkup pekerjaan, dan membahas strategi yang saya gunakan untuk menerapkan perubahan dengan aman sambil meminimalkan area permukaan untuk kerusakan serius sementara saya memusnahkan inti dari aplikasi kami.

Tentang Single Table Inheritance (STI)

Secara singkat, Tabel Tunggal Warisan di Rails memungkinkan Anda untuk menyimpan beberapa jenis kelas di tabel yang sama. Dalam Rekaman Aktif, nama kelas disimpan sebagai tipe dalam tabel. Misalnya, Anda mungkin memiliki Lab, Readme, dan Project semua langsung di tabel konten:

kelas Lab 

Dalam contoh ini, laboratorium, pembacaan, dan proyek adalah semua jenis konten yang dapat dikaitkan dengan pelajaran.

Skema tabel konten kami tampak sedikit seperti ini, sehingga Anda dapat melihat bahwa jenisnya baru saja disimpan di tabel.

create_table "content", force:: cascade do | t |
  t.integer "curriculum_id",
  t.string "jenis",
  t.text "markdown_format",
  t.string "judul",
  t.integer "track_id",
  t.integer "github_repository_id"
akhir

Mengidentifikasi Lingkup Pekerjaan

Konten tergeletak di seluruh aplikasi, terkadang membingungkan. Sebagai contoh, ini menggambarkan hubungan dalam model Pelajaran.

kelas Pelajaran  {order (ordinal:: asc)}
  has_one: content, foreign_key:: curriculum_id
  has_many: readmes, foreign_key:: curriculum_id
  has_one: lab, foreign_key:: curriculum_id
  has_one: readme, foreign_key:: curriculum_id
  has_many: ditugaskan_repos, melalui:: konten
akhir

Bingung? Begitu juga saya. Dan itu hanya satu model dari banyak yang harus saya ubah.

Jadi dengan rekan tim saya yang brilian dan berbakat (Kate Travers, Steven Nunez, dan Spencer Rogers), saya melakukan brainstorming desain yang lebih baik untuk membantu mengurangi kebingungan dan membuat sistem ini lebih mudah diperluas.

Desain Baru

Konsep yang coba diwakili oleh Konten adalah perantara antara GithubRepository dan Lesson.

Setiap bagian dari konten pelajaran "kanonik" ditautkan ke repositori di GitHub. Ketika pelajaran diterbitkan atau "digunakan" untuk siswa, kami membuat salinan repositori GitHub itu dan memberi siswa tautan ke sana. Tautan antara pelajaran dan versi yang digunakan disebut AssignedRepo.

Jadi ada repositori GitHub di kedua ujung pelajaran: versi kanonik dan versi yang digunakan.

Konten kelas 
class AssignedRepo 

Pada satu titik, pelajaran bisa memiliki banyak konten, tetapi di dunia kita saat ini, itu tidak lagi terjadi. Sebaliknya, ada berbagai macam pelajaran, yang dapat mengintrospeksi diri dengan melihat file-file yang termasuk dalam repositori terkait.

Jadi, apa yang kami putuskan untuk lakukan adalah mengganti Konten dengan konsep baru yang disebut CanonicalMaterial, dan memberikan AssignedRepo referensi langsung ke pelajaran terkaitnya alih-alih melalui Konten.

Diagram Sistem Lama ke Baru, di mana garis putus-putus merah menunjukkan jalur yang ditandai untuk penghentian

Jika itu terdengar membingungkan dan seperti banyak pekerjaan, itu karena itu. Kuncinya adalah bahwa kita harus mengganti model dalam basis kode yang cukup besar, dan akhirnya mengubah suatu tempat di bidang 6000 baris kode.

Kuncinya adalah bahwa kita harus mengganti model dalam basis kode yang cukup besar, dan akhirnya mengubah suatu tempat di bidang 6000 baris kode.

Strategi untuk Refactoring dan Mengganti IMS

Model Baru

Pertama, kami membuat tabel baru yang disebut canonical_materials dan membuat model dan asosiasi baru.

kelas CanonicalMaterial 

Kami juga menambahkan kunci asing canonical_material_id ke tabel kurikulum, sehingga Pelajaran dapat mempertahankan referensi untuk itu.

Pada tabel ditugaskan_repo, kami menambahkan kolom lesson_id.

Tulisan Ganda

Setelah tabel dan kolom baru dipasang, kami mulai menulis ke tabel lama dan yang baru secara bersamaan sehingga kami tidak perlu menjalankan tugas pengisian ulang lebih dari sekali. Setiap kali sesuatu mencoba membuat atau memperbarui baris konten, kami juga membuat atau memperbarui materi canonical_m.

Sebagai contoh:

lesson.build_content (
  'repo_name' => repo.name,
  'github_repository_id' => repo_id,
  'markdown_format' => repo.readme
)

lesson.canonical_material = repo.canonical_material
pelajaran.save

Ini memungkinkan kami untuk meletakkan dasar untuk akhirnya menghapus Konten.

Mengisi kembali

Langkah selanjutnya dalam proses ini adalah mengisi ulang data. Kami menulis tugas menyapu untuk mengisi tabel kami dan memastikan bahwa CanonicalMaterial ada untuk setiap GithubRepository dan bahwa setiap Pelajaran memiliki Material Canonical. Dan kemudian kami menjalankan tugas di server produksi kami.

Dalam putaran refactoring ini, kami lebih suka memiliki data yang valid sehingga kami dapat membuat terobosan bersih dengan cara lama dalam melakukan sesuatu. Namun, opsi lain yang layak adalah menulis kode yang masih mendukung model lama. Dalam pengalaman kami, lebih membingungkan dan mahal untuk mempertahankan kode yang mendukung pemikiran sebelumnya daripada untuk mengisi ulang dan memastikan data tersebut valid.

Dalam pengalaman kami, lebih membingungkan dan mahal untuk mempertahankan kode yang mendukung pemikiran sebelumnya daripada untuk mengisi ulang dan memastikan data tersebut valid.

Penggantian

Dan kemudian bagian yang menyenangkan dimulai. Untuk membuat penggantian seaman mungkin, kami menggunakan bendera fitur untuk mengirimkan kode gelap dalam PR yang lebih kecil, yang memungkinkan kami untuk membuat lingkaran umpan balik yang lebih cepat dan mengetahui lebih cepat jika ada masalah. Kami menggunakan permata peluncuran, yang juga kami gunakan untuk pengembangan fitur standar, untuk melakukan ini.

Apa yang Dicari

Salah satu bagian tersulit dalam melakukan penggantian adalah banyaknya hal yang harus dicari. Kata "konten" sayangnya super generik, jadi tidak mungkin untuk melakukan pencarian dan penggantian yang sederhana dan global, jadi saya cenderung melakukan pencarian yang lebih luas dengan mencoba menjelaskan variasi.

Saat menghapus IMS, ini adalah hal-hal yang harus Anda cari:

  • Bentuk tunggal dan jamak dari model, termasuk semua subkelasnya, metode, metode utilitas, asosiasi dan pertanyaan.
  • Kueri Hardcoded SQL
  • Pengontrol
  • Serializers
  • Tampilan

Misalnya, untuk konten, itu berarti mencari:

  • : konten - untuk asosiasi dan permintaan
  • : isi - untuk asosiasi dan pertanyaan
  • .gabung (: isi) - untuk permintaan gabung, yang harus ditangkap oleh pencarian sebelumnya
  • .includes (: content) - untuk segera memuat asosiasi orde kedua, yang juga harus ditangkap oleh pencarian sebelumnya
  • konten: - untuk permintaan bersarang
  • isi: - lagi, kueri yang lebih bersarang
  • content_id — untuk kueri langsung oleh id
  • .content - panggilan metode
  • .contents - panggilan metode pengumpulan
  • .build_content - metode utilitas ditambahkan oleh asosiasi has_one dan hers_to
  • .create_content - metode utilitas yang ditambahkan oleh asosiasi has_one dan hers_to
  • .content_ids - metode utilitas yang ditambahkan oleh asosiasi has_many
  • Konten - nama kelas itu sendiri
  • konten - string sederhana untuk referensi hardcoded atau kueri SQL

Saya percaya itu adalah daftar konten yang cukup komprehensif. Dan kemudian saya melakukan hal yang sama untuk lab, readme, dan project. Anda dapat melihatnya karena Rails sangat fleksibel dan menambahkan banyak metode utilitas, sulit untuk menemukan semua tempat yang akhirnya digunakan oleh model.

Bagaimana Sebenarnya Mengganti Implementasi Setelah Anda Menemukan Semua Penelepon

Setelah Anda benar-benar menemukan semua situs panggilan model yang Anda coba ganti atau hapus, Anda bisa menulis ulang sesuatu. Secara umum, proses yang kami ikuti adalah

  1. Ganti perilaku metode dalam definisi atau ubah metode di situs panggilan
  2. Tulis metode baru dan panggil di belakang bendera fitur di situs panggilan
  3. Hentikan ketergantungan pada asosiasi dengan metode
  4. Naikkan kesalahan di balik tanda fitur jika Anda tidak yakin tentang suatu metode
  5. Tukar objek yang memiliki antarmuka yang sama

Berikut adalah contoh dari masing-masing strategi.

1a. Ganti perilaku atau kueri metode

Beberapa penggantian cukup mudah. Anda menempatkan bendera fitur di tempat untuk mengatakan "panggil kode ini alih-alih kode lain ini ketika bendera ini aktif."

Jadi, alih-alih kueri berdasarkan konten, di sini kita kueri berdasarkan canonical_material.

1b. Ubah metode di situs panggilan

Terkadang, lebih mudah untuk mengganti metode di situs panggilan untuk membakukan metode yang dipanggil. (Anda harus menjalankan suite tes dan / atau menulis tes saat Anda melakukan ini.) Melakukan hal itu dapat membuka jalan untuk refactoring lebih lanjut.

Contoh ini menunjukkan cara memecah ketergantungan pada kolom canonical_id, yang akan segera tidak ada lagi. Perhatikan bahwa kami mengganti metode di situs panggilan tanpa meletakkannya di belakang tanda fitur. Dalam melakukan refactoring ini, kami perhatikan bahwa kami memetik canonical_id di lebih dari satu tempat, jadi kami membungkus logika untuk melakukan itu dengan metode lain yang dapat kami gunakan ke pertanyaan lain. Metode di situs panggilan diubah, tetapi perilaku tidak berubah sampai bendera fitur dihidupkan.

2. Tulis metode baru dan panggil di belakang bendera fitur di situs panggilan

Strategi ini terkait dengan penggantian metode, hanya dalam metode ini, kami menulis metode baru dan menyebutnya di belakang bendera fitur di situs panggilan. Itu sangat berguna untuk metode yang hanya dipanggil di satu tempat. Itu juga memungkinkan kami untuk memberikan metode ini tanda tangan yang lebih baik — selalu bermanfaat.

3. Hentikan ketergantungan pada asosiasi dengan metode

Dalam contoh berikut ini, sebuah trek memiliki banyak lab. Karena kita tahu bahwa asosiasi has_many menambahkan metode utilitas, kami mengganti yang paling umum dipanggil dan menghapus baris has_many: labs. Metode ini sesuai dengan antarmuka yang sama, sehingga apa pun yang memanggil metode sebelum fitur diaktifkan akan terus berfungsi.

4. Naikkan kesalahan di balik tanda fitur jika Anda tidak yakin tentang suatu metode

Ada beberapa kali kami tidak yakin apakah kami melewatkan situs panggilan. Jadi, alih-alih hanya metode menghapus yang sulit pada awalnya, kami sengaja meningkatkan kesalahan sehingga kami bisa menangkapnya selama fase pengujian manual. Ini memberi kami cara yang lebih baik untuk melacak di mana metode dipanggil.

5. Tukar objek yang memiliki antarmuka yang sama

Karena kami ingin menyingkirkan asosiasi lab, kami menulis ulang implementasi lab? metode. Alih-alih memeriksa keberadaan catatan lab, kami bertukar bahan canonical_m, mendelegasikan panggilan, dan membuat objek itu merespons metode yang sama.

Ini adalah strategi yang paling membantu untuk memutus ketergantungan dan bertukar objek baru di seluruh monolit Rails kami. Setelah meninjau ratusan definisi dan situs panggilan, kami mengganti atau menulis ulang mereka satu per satu. Ini adalah proses yang membosankan yang saya tidak berharap pada siapa pun, tetapi pada akhirnya sangat membantu untuk membuat basis kode kami lebih terbaca dan untuk menghapus kode lama yang tidak melakukan apa-apa. Butuh beberapa minggu yang membuat frustasi dan mencabut rambut untuk mencapai akhir, tetapi setelah kami mengganti sebagian besar referensi, kami mulai melakukan pengujian manual.

Pengujian & Pengujian Manual

Karena perubahan memengaruhi fitur di seluruh basis kode, beberapa di antaranya tidak diuji, sulit untuk QA dengan pasti, tetapi kami melakukan yang terbaik. Kami melakukan pengujian manual pada server QA kami, yang menangkap banyak bug dan kasus tepi. Dan kemudian kami pergi ke depan dan untuk jalur yang lebih kritis, menulis tes baru.

Roll Out, Go Live, & Clean Up

Setelah melewati QA, kami menyalakan bendera fitur kami dan membiarkan sistem menetap. Setelah kami yakin itu stabil, kami menghapus flag fitur dan jalur kode lama dari basis kode. Sayangnya, ini lebih sulit dari yang diharapkan karena itu mengharuskan penulisan ulang banyak test suite, sebagian besar pabrik yang secara implisit bergantung pada model Konten. Dalam retrospeksi, apa yang bisa kami lakukan adalah menulis dua set tes saat kami refactoring, satu untuk kode saat ini dan satu untuk kode di belakang bendera fitur.

Sebagai langkah terakhir, yang masih akan datang, kita harus membuat cadangan data dan menjatuhkan tabel kita yang tidak digunakan.

Dan itu, teman-teman, adalah salah satu cara Anda menyingkirkan Spread Table Inheritance di monolith Rails Anda. Mungkin studi kasus ini akan membantu Anda juga.

Apakah Anda memiliki cara lain untuk menghapus IMS atau refactoring? Kami ingin tahu. Beri tahu kami di komentar.

Kami juga merekrut! Bergabunglah dengan tim kami. Kami keren, aku janji.

Sumberdaya dan Bacaan Tambahan

  • Rails Memandu Warisan
  • Bagaimana dan Kapan Menggunakan Warisan Tabel Tunggal dalam Rel oleh Eugene Wang (Flatiron Grad!)
  • Refactoring Aplikasi Rails Kami Dari Single-Table Inheritance
  • Tabel Tunggal Warisan vs Asosiasi Polimorfik di Rails
  • Warisan Tabel Tunggal Menggunakan Rails 5.02

Untuk mempelajari lebih lanjut tentang Flatiron School, kunjungi situs web, ikuti kami di Facebook dan Twitter, dan kunjungi kami di acara mendatang di dekat Anda.

Flatiron School adalah anggota keluarga WeWork yang bangga. Lihatlah blog saudara teknologi kami WeWork Technology and Making Meetup.