Как да премахнете наследяването на единична маса от вашия монолит

Наследяването е лесно - докато не се наложи да се справите с техническия дълг и данъците.

Когато преди пет години се появи основната кодова база на Learn, наследяването на единична таблица (STI) беше доста популярно. Екипът на Flatiron Labs навремето се включи в него - използвайки го за всичко - от оценки и учебни програми до събития и съдържание на емисиите в нашата нарастваща система за управление на обучението. И това беше чудесно - свърши работата. Това позволи на инструкторите да предоставят учебни програми, да проследяват напредъка на учениците и да създадат ангажиращо потребителско изживяване.

Но както много публикации в блога посочиха (този, този и този например), STI не мащабира много добре, особено след като данните растат и новите подкласове започват да варират в голяма степен от техните суперкласове и един от друг. Както може би се досещате, същото се случи и в нашата кодова база! Училището ни се разширяваше и подкрепяхме все повече функции и видове уроци. С течение на времето моделите започнаха да се раздуват и мутират и вече не отразяват правилната абстракция за домейна.

Изживяхме в това пространство известно време, давайки на този код широко място и да го закърпим само когато е необходимо. И тогава дойде време за рефактор.

През последните няколко месеца се заех с мисия за премахване на един особено неприятен случай на STI, който включваше нееднозначно наречения модел на съдържанието. Колкото и да е първоначална настройка на STI, всъщност е доста трудно да се премахне.

И така, в тази публикация ще разгледам малко за STI, ще посоча някакъв контекст за нашия домейн, ще очертая обхвата на работата и ще обсъдя стратегиите, които използвах за безопасно разгръщане на промените, като минимизирам повърхността за сериозни щети, докато изкопах ядрото на нашето приложение.

Всичко за наследяването на една маса (STI)

Накратко, Наследяването на единична таблица в Rails ви позволява да съхранявате няколко типа класове в една и съща таблица. В Active Record името на класа се съхранява като тип в таблицата. Например, може да имате Lab, Readme и Project всички на живо в таблицата със съдържание:

клас Lab <Съдържание; край
клас Readme <Съдържание; край
клас Проект <Съдържание; край

В този пример лаборатории, четене и проекти са всички видове съдържание, които могат да бъдат свързани с урок.

Схемата на нашата таблица със съдържание изглеждаше малко така, така че можете да видите, че типът е просто запазен в таблицата.

create_table "content", force:: cascade do | t |
  t.integer "учебен план_id",
  t.string "тип",
  t.text "markdown_format",
  t.string "заглавие",
  t.integer "track_id",
  t.integer "github_repository_id"
край

Определяне на обхвата на работата

Съдържанието се разпространява из приложението, понякога объркващо. Например, това описва връзките в модела на урока.

клас Урок <Учебна програма
  has_many: съдържание, -> {ред (порядък:: asc)}
  has_one: съдържание, Foreign_key:: учебен план_id
  has_many: readmes, Foreign_key:: curriculum_id
  has_one: лаборатория, Foreign_key:: учебен план_id
  has_one: readme, Foreign_key:: учебен план_id
  has_many: dodano_repos, чрез:: съдържание
край

Объркани ли сте? Така беше и аз. Това беше само един модел от мнозина, който трябваше да променя.

Така с моите брилянтни и талантливи съотборници (Кейт Травърс, Стивън Нунес и Спенсър Роджърс), аз обмислих по-добър дизайн, за да помогна за намаляване на объркването и да улесня тази система.

Нов дизайн

Концепцията, която Content се опитваше да представи, беше посредник между GithubRepository и Lesson.

Всяко парче от „каноничното“ съдържание на урока е свързано с хранилище в GitHub. Когато уроците бъдат публикувани или „разгърнати“ на учениците, ние правим копие от това хранилище на GitHub и даваме на учениците връзка към него. Връзката между урок и разгърнатата версия се нарича AssignedRepo.

Така че има хранилища на GitHub и в двата края на уроците: каноничната версия и разгърнатата версия.

Съдържание на клас 
клас AssignedRepo 

В един момент уроците успяха да съдържат няколко части съдържание, но в нашия сегашен свят това вече не е така. Вместо това има различни видове уроци, които могат да представят себе си, като разгледат файловете, включени в свързаните с тях хранилища.

И така, това, което решихме да направим, е да заменим Content с нова концепция, наречена CanonicalMaterial, и да дадем на AssignedRepo директно препратка към свързания с него урок, вместо да преминаваме през Content.

Система от стара до нова система, където червените пунктирани линии показват пътища, маркирани за оттегляне

Ако това ви звучи объркващо и ви харесва много работа, това е така. Ключовото извличане обаче е, че трябваше да заменим модел в доста голяма кодова база и в крайна сметка се променяше някъде в сферата на 6000 реда код.

Ключовото извличане обаче е, че трябваше да заменим модел в доста голяма кодова база и в крайна сметка се променяше някъде в сферата на 6000 реда код.

Стратегии за рефакторинг и замяна на НТИ

Новият модел

Първо създадохме нова таблица, наречена canonical_materials и създадохме новия модел и асоциации.

клас CanonicalMaterial 

Към таблицата с учебни програми добавихме и чужд ключ от canonical_material_id, за да може урока да поддържа препратка към него.

Към таблицата на присвоените_репорта добавихме колона урок-урок.

Двойни записи

След като бяха въведени новите таблици и колони, започнахме едновременно да пишем на старите таблици и новите, така че да не се налага да изпълняваме задача за допълнително запълване повече от веднъж. Всеки път, когато нещо се опита да създаде или актуализира ред за съдържание, ние също ще създадем или актуализираме canonical_material.

Например:

lesson.build_content (
  'repo_name' => име на repo.name,
  'github_repository_id' => repo_id,
  'markdown_format' => repo.readme
)

урок.canonical_material = repo.canonical_material
lesson.save

Това ни позволи да положим основите за премахване на Съдържанието в крайна сметка.

Запълване

Следващата стъпка в процеса беше запълването на данните. Ние написахме рейк задачи, за да попълним нашите таблици и да гарантираме, че CanonicalMaterial съществува за всеки GithubRepository и че всеки урок има CanonicalMaterial. И тогава изпълнихме задачите на нашия производствен сървър.

В този кръг от рефакторинг предпочетохме да имаме валидни данни, за да можем да направим чиста почивка със съществуващия начин на правене на нещата. Друга жизнеспособна опция обаче е да напишете код, който все още поддържа по-стари модели. Според нашия опит поддържането на код, който поддържа наследствено мислене, е по-объркващо и скъпо, отколкото да се попълва отново и да се уверят, че данните са валидни.

Според нашия опит поддържането на код, който поддържа наследствено мислене, е по-объркващо и скъпо, отколкото да се попълва отново и да се уверят, че данните са валидни.

Замяна

И тогава започна забавната част. За да направим подмяната възможно най-безопасна, използвахме функционални флагове за изпращане на тъмен код в по-малки PR-та, което ни позволи да създадем по-бърз цикъл за обратна връзка и да разберем по-рано дали нещата се нарушават. Използвахме скъпоценния камък, който използваме и за разработване на стандартни функции, за да направим това.

Какво да търсите

Една от най-трудните части за извършване на подмяната беше чистият брой неща, които да търсите. Думата "съдържание" за съжаление е супер общовалидна, така че беше невъзможно да се извърши просто, глобално търсене и замяна, така че аз имах тенденция да търся по-обхват, опитвайки се да отчитам вариациите.

Когато премахвате STI, това трябва да търсите:

  • Формите за единствено и множествено число на модела, включително всички негови подкласове, методи, полезни методи, асоциации и запитвания.
  • Твърдо кодирани SQL заявки
  • Контрольори
  • Serializers
  • Прегледи

Например за съдържание, което означаваше да търсите:

  • : съдържание - за асоциации и запитвания
  • : съдържание - за асоциации и запитвания
  • .joins (: content) - за заявки за присъединяване, които трябва да бъдат уловени от предишното търсене
  • .includes (: content) - за нетърпеливо зареждане на асоциации от втори ред, които също трябва да бъдат обхванати от предишното търсене
  • съдържание: - за вложени заявки
  • съдържание: - отново, повече вложени заявки
  • content_id - за заявки директно чрез id
  • .content - обаждания на метод
  • .contents - обаждания от метода за събиране
  • .build_content - полезен метод, добавен от свързването has_one и pripada_ към
  • .create_content - полезен метод, добавен от has_one и pripada_ към асоциация
  • .content_ids - полезен метод, добавен от асоциацията has_many
  • Съдържание - самото име на клас
  • content - обикновеният низ за всички твърдо кодирани референции или SQL заявки

Вярвам, че това е доста изчерпателен списък за съдържание. И тогава направих същото за лаборатория, readme и проект. Можете да видите, че тъй като Rails е толкова гъвкава и добавя много полезни методи, че е трудно да се намерят всички места, които даден модел в крайна сметка се използва.

Как всъщност да заменим изпълнението, след като сте намерили всички участници

След като всъщност намерите всички сайтове за обаждания на модела, който се опитвате да замените или премахнете, ще трябва да пренапишете нещата. Като цяло процесът, който следвахме, беше

  1. Заменете поведението на метода в дефиницията или променете метода в сайта за повикване
  2. Напишете нови методи и ги извикайте зад флаг за функция в сайта за повикване
  3. Прекъснете зависимостите от асоциациите с методи
  4. Повдигнете грешки зад флаг на функция, ако не сте сигурни в метод
  5. Размяна в обекти, които имат същия интерфейс

Ето примери за всяка стратегия.

1а. Заменете поведението или заявката на метода

Някои от замените са доста ясни. Поставяте флага на функцията, за да кажете „извикайте този код вместо този друг код, когато този флаг е включен.“

Така че вместо да задаваме заявки въз основа на съдържанието, тук заявяваме на базата на canonical_material.

1b. Променете метода в сайта за повикване

Понякога е по-лесно да замените метода в сайта за повикване, за да стандартизирате извиканите методи. (Трябва да стартирате своя тестов пакет и / или да напишете тестове, когато направите това.) Това може да отвори пътя към по-нататъшно рефакторинг.

Този пример демонстрира как да се прекъсне зависимостта от колоната canonical_id, която скоро вече няма да съществува. Забележете, че сме заместили метода в сайта за повикване, без да го поставяме зад флаг на функция. Правейки това рефакторинг, забелязахме, че сме изтръгнали canonical_id на повече от едно място, така че ние обгърнахме логиката да го направим в друг метод, с който да веригираме върху други заявки. Методът в сайта за повикване бе променен, но поведението не се промени, докато флагът на функцията не беше включен.

2. Напишете нови методи и ги извикайте зад флаг за функция в сайта за повикване

Тази стратегия е свързана с подмяната на метода, само в тази, ние пишем нов метод и го наричаме зад флаг на функция в сайта за повикване. Той беше особено полезен за метод, който се извикваше само на едно място. Освен това ни даде възможност да дадем на метода по-добър подпис - винаги полезен.

3. Прекъснете зависимостите от асоциациите с методи

В този следващ пример, трак има_бройни лаборатории. Тъй като знаем, че асоциацията has_many добавя полезни методи, заменихме този, който най-често се нарича и премахнахме линията has_many: labs. Този метод съответства на същия интерфейс, така че всичко, което се обажда на метода преди включването на функцията, ще продължи да работи.

4. Повишете грешки зад флаг на функция, ако не сте сигурни в метод

Понякога не бяхме сигурни дали сме пропуснали сайт за обаждания. Така че, вместо просто да премахнем трудно методи за отстраняване, ние умишлено повдигнахме грешки, за да можем да ги уловим по време на фазата на ръчно тестване. Това ни даде по-добър начин да определим къде се нарича метод.

5. Размяна в обекти, които имат същия интерфейс

Тъй като искахме да се освободим от лабораторната асоциация, преписахме ли изпълнението на лабораторията? метод. Вместо да проверим за наличието на лабораторен запис, ние разменихме в canonical_material, делегирахме обаждането и накарахме този обект да отговори на същия метод.

Това бяха най-полезните стратегии за преодоляване на зависимости и замяна на нови обекти в нашия монолит на Rails. След като прегледахме стотиците дефиниции и сайтове за обаждания, ги заменихме или пренаписахме едно по едно. Това е досаден процес, който не пожелавам на никого, но в крайна сметка беше изключително полезен, за да направим нашата кодова база по-четлива и за премахване на стар код, който седеше наоколо и не правеше нищо. Бяха нужни няколко разочароващи и изтеглящи косата седмици, но след като сме заменили по-голямата част от препоръките, започнахме да правим ръчно тестване.

Тестване и ръчно тестване

Тъй като промените засегнаха функции в цялата кодова база, някои от които не бяха тествани, беше трудно да се гарантира QA със сигурност, но ние направихме всичко възможно. Извършихме ръчно тестване на нашия QA сървър, който улови много грешки и крайни случаи. И тогава продължихме напред и за по-критични пътеки, написахме нови тестове.

Разгърнете се, отидете на живо и почистете

След като преминахме QA, обърнахме флагчето си и оставихме системата да се уреди. След като бяхме сигурни, че е стабилна, премахнахме флаговете на функциите и старите кодови пътища от кодовата база. Това, за съжаление, беше по-трудно от очакваното, тъй като доведе до пренаписването на голяма част от тестовия набор, предимно фабрики, които имплицитно разчитаха на модела Съдържание. В ретроспекция това, което можехме да направим, беше да напишем два теста, докато рефакторирахме, един за текущия код и един за кода зад флаг на функция.

Като последна стъпка, която тепърва предстои, трябва да архивираме данни и да изхвърлим неизползваните таблици.

И това, приятели, е един от начините да се отървете от разпръснатото наследяване на единична маса във вашия монолит Rails. Може би и този случай ще ви помогне.

Имате ли други начини за премахване на STI или рефакторинг? Любопитно ни е да знаем. Уведомете ни в коментарите.

Също така, ние се наемаме! Присъединете се към нашия екип. Ние сме готини, обещавам.

Ресурси и допълнително четене

  • Наследяване на водачи за релси
  • Как и кога да използваме наследяване на единична маса в релси от Eugene Wang (Flatiron Grad!)
  • Рефакторинг на нашите Rails App извън наследяване на една маса
  • Наследяване на единична маса спрямо полиморфни асоциации в релси
  • Наследяване на единична маса с помощта на релси 5.02

За да научите повече за училището Flatiron, посетете уебсайта, следвайте ни във Facebook и Twitter и ни посетете на предстоящи събития в близост до вас.

Flatiron School е горд член на семейство WeWork. Вижте нашите блогове за технологични сестри WeWork Technology и създаване на среща.