Sådan fjernes enkeltbordsarv fra dine skinner Monolith

Arv er let - indtil du er nødt til at håndtere teknisk gæld og skatter.

Da Lears vigtigste codebase kom til for fem år siden, var Single Table Inheritance (STI) temmelig populær. Flatiron Labs-teamet gik på det tidspunkt alt sammen - brugte det til alt fra vurderinger og læseplan til aktivitetsfoderbegivenheder og indhold i vores voksende læringsstyringssystem. Og det var fantastisk - det fik jobbet gjort. Det gjorde det muligt for instruktører at levere pensum, spore studerendes fremskridt og skabe en spændende brugeroplevelse.

Men som mange blogindlæg har påpeget (denne, denne og for eksempel denne), skalerer STI ikke super godt, især når dataene vokser, og nye underklasser begynder at variere vidt fra deres superklasser og hinanden. Som du måske havde gætt, skete det samme i vores codebase! Vores skole udvidede, og vi understøttede flere og flere funktioner og lektyper. Med tiden begyndte modellerne at oppustes og mutere og afspejler ikke længere den rette abstraktion for domænet.

Vi boede i det rum i et stykke tid, gav denne kode en bred køje og lappede den kun, når det var nødvendigt. Og så kom tiden til refaktor.

I løbet af de sidste par måneder begyndte jeg på en mission om at fjerne et særligt knust forekomst af STI, en, der involverede den noget tvetydigt navngivne indholdsmodel. Så let som STI er at indstille oprindeligt, er det faktisk temmelig svært at fjerne.

Så i dette indlæg vil jeg dække lidt om STI, give en vis kontekst om vores domæne, skitsere omfanget af arbejdet og diskutere strategier, jeg har brugt til sikkert at implementere ændringer, samtidig med at minimere overfladearealet for alvorlig skade, mens jeg sløjt kernen af vores app.

Om enkelttabelarv (STI)

Kort fortalt giver Single Table Inheritance in Rails dig mulighed for at gemme flere typer klasser i samme tabel. I Aktiv post gemmes klassens navn som typen i tabellen. For eksempel har du muligvis et Lab, Readme og Project alle live i indholdstabellen:

klasse Lab 

I dette eksempel er laboratorier, readmes og projekter alle typer indhold, der kan knyttes til en lektion.

Vores indholdstabels skema lignede lidt sådan, så du kan se, at typen lige er gemt i tabellen.

create_table "indhold", kraft:: kaskade do | t |
  t.integer "curriculum_id",
  t.streng "type",
  t.text "markdown_format",
  t.streng "titel",
  t.integer "track_id",
  t.integer "github_repository_id"
ende

Identificering af arbejdsomfang

Indhold spredte overalt i appen, undertiden forvirrende. For eksempel beskrev dette forholdet i Lesson-modellen.

klasse lektion  {rækkefølge (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: tildelte_repos, gennem:: indhold
ende

Forvirret? Det var jeg også. Og det var bare en model af mange, som jeg var nødt til at ændre.

Så med mine strålende og talentfulde holdkammerater (Kate Travers, Steven Nunez og Spencer Rogers) brainstormede jeg et bedre design for at hjælpe med at skære ned på forvirring og gøre dette system lettere at udvide.

Et nyt design

Konceptet, som Content forsøgte at repræsentere, var en mellemmand mellem et GithubRepository og en Lesson.

Hvert stykke "kanonisk" lektionsindhold er knyttet til et depot på GitHub. Når lektioner offentliggøres eller "distribueres" til studerende, laver vi en kopi af det GitHub-arkiv og giver studerende et link til det. Forbindelsen mellem en lektion og den implementerede version kaldes en AssignedRepo.

Så der er GitHub-lagre i begge ender af lektioner: den kanoniske version og den implementerede version.

klasse Indhold 
klasse AssignedRepo 

På et tidspunkt kunne lektioner have flere stykker indhold, men i vores nuværende verden er det ikke længere tilfældet. I stedet er der forskellige slags lektioner, som kan se på sig selv ved at se på filerne, der er inkluderet i deres tilknyttede oplagringssteder.

Så hvad vi besluttede at gøre var at erstatte Content med et nyt koncept kaldet CanonicalMaterial, og give AssignedRepo en direkte henvisning til dens tilknyttede lektion i stedet for at gennemgå Content.

Gammelt til nyt systemdiagram, hvor røde stiplede linjer indikerer stier, der er markeret til afskrivning

Hvis det lyder forvirrende og kan lide en masse arbejde, er det fordi det er. Den vigtigste afhentning er dog, at vi var nødt til at erstatte en model i en temmelig stor kodebase, og endte med at ændre et sted inden for 6000 kodelinjer.

Den vigtigste afhentning er dog, at vi var nødt til at erstatte en model i en temmelig stor kodebase, og endte med at ændre et sted inden for 6000 kodelinjer.

Strategier til refactoring og udskiftning af STI

Den nye model

Først oprettede vi en ny tabel kaldet canonical_materials og skabte den nye model og foreninger.

klasse CanonicalMaterial 

Vi tilføjede også en fremmed nøgle af canonical_material_id til læseplanstabellen, så en lektion kunne opretholde en henvisning til den.

I tabellen tildelte_repos tilføjede vi en lektion_id-kolonne.

Dual Writes

Efter at de nye tabeller og kolonner var på plads, begyndte vi at skrive til de gamle tabeller og de nye samtidig, så vi ikke behøver at køre en udfyldningsopgave mere end én gang. Hver gang noget forsøgte at oprette eller opdatere en indholdsrække, ville vi også oprette eller opdatere et kanonisk_materiale.

For eksempel:

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

lesson.canonical_material = repo.canonical_material
lesson.save

Dette gjorde det muligt for os at lægge grundlaget for i sidste ende at fjerne indhold.

opfyldning

Det næste trin i processen var at udfylde dataene. Vi skrev rakeopgaver for at udfylde vores borde og sikre, at der eksisterede et CanonicalMaterial for hvert GithubRepository, og at hver lektion havde et CanonicalMaterial. Og så kørte vi opgaverne på vores produktionsserver.

I denne runde med refactoring foretrækkede vi at have gyldige data, så vi kunne gøre en ren pause med den arvede måde at gøre tingene på. En anden bæredygtig mulighed er imidlertid at skrive kode, der stadig understøtter ældre modeller. Efter vores erfaring har det været mere forvirrende og dyrt at opretholde kode, der understøtter ældre tænkning, end det har været at udfylde og sikre, at dataene er gyldige.

Efter vores erfaring har det været mere forvirrende og dyrt at opretholde kode, der understøtter ældre tænkning, end det har været at udfylde og sikre, at dataene er gyldige.

Udskiftning

Og så begyndte den sjove del. For at gøre udskiftningen så sikker som muligt brugte vi funktionsflag til at sende mørk kode i mindre PR'er, hvilket gjorde det muligt for os at skabe en hurtigere feedback-loop og vide hurtigere, om ting var i stykker. Vi brugte rollout-perlen, som vi også bruger til standardfunktionsudvikling, til at gøre dette.

Hvad man skal søge efter

En af de sværeste dele ved at udføre udskiftningen var det store antal ting at søge efter. Ordet "indhold" er desværre supergenerisk, så det var umuligt at foretage en enkel, global søgning og erstatte, så jeg havde en tendens til at foretage en mere scoped-søgning med forsøg på at redegøre for variationerne.

Når du fjerner STI, er dette de ting, du skal søge efter:

  • Modelens enestående og flertalsformer, inklusive alle dens underklasser, metoder, anvendelsesmetoder, foreninger og forespørgsler.
  • Hardkodede SQL-forespørgsler
  • Controllere
  • Serializers
  • visninger

For indhold betød det f.eks. Kigge efter:

  • : indhold - til foreninger og forespørgsler
  • : indhold - til foreninger og forespørgsler
  • .joins (: indhold) - til forespørgsler om sammenføjning, som skal fanges af den forrige søgning
  • .indbefatter (: indhold) - til ivrig indlæsning af andenordens tilknytning, som også skal fanges af den forrige søgning
  • indhold: - til indlejrede forespørgsler
  • indhold: - igen mere indlejrede forespørgsler
  • content_id —for spørgsmål direkte efter id
  • .indhold - metodekald
  • .indhold - opkald til indsamlingsmetode
  • .build_content - hjælpemetode tilføjet af has_one og tilhører_til foreningen
  • .create_content - hjælpemetode tilføjet af has_one og tilhører_til associering
  • .content_ids - hjælpemetode tilføjet af has_many-foreningen
  • Indhold - selve klassens navn
  • indhold - den almindelige streng til eventuelle hardkodede referencer eller SQL-forespørgsler

Jeg tror, ​​det er en temmelig omfattende liste over indhold. Og så gjorde jeg det samme for lab, readme og project. Du kan se, at fordi Rails er så fleksibel og tilføjer mange hjælpemetoder, er det svært at finde alle de steder, hvor en model ender med at blive brugt.

Sådan udskiftes implementeringen faktisk, når du har fundet alle de opkaldte

Når du faktisk har fundet alle opkaldsstederne i den model, du prøver at erstatte eller fjerne, kommer du til at omskrive tingene. Generelt var processen, vi fulgte,

  1. Udskift metodeadfærd i definitionen, eller ændre metoden på opkaldsstedet
  2. Skriv nye metoder, og kald dem bag et funktionsflag på opkaldsstedet
  3. Afbryd afhængigheder af foreninger med metoder
  4. Ræk fejl bag et funktionsflag, hvis du ikke er sikker på en metode
  5. Byt ind objekter, der har den samme grænseflade

Her er eksempler på hver strategi.

1a. Udskift metodens opførsel eller forespørgsel

Nogle af udskiftningerne er ret ligetil. Du sætter funktionsflagget på plads for at sige "kalde denne kode i stedet for denne anden kode, når dette flag er tændt."

Så i stedet for forespørgsel baseret på indhold, spørger vi her baseret på kanonisk_materiale.

1b. Skift metoden på opkaldsstedet

Nogle gange er det lettere at udskifte metoden på opkaldsstedet for at standardisere de opkaldte metoder. (Du skal køre din testsuite og / eller skrive test, når du gør dette.) Hvis du gør det, kan du åbne stien for yderligere refactoring.

Dette eksempel viser, hvordan man afhænger afhængigheden af ​​kolonnen canonical_id, som snart ikke længere findes. Bemærk, at vi udskiftede metoden på opkaldsstedet uden at lægge den bag et funktionsflag. Da vi gjorde denne refactoring, bemærkede vi, at vi plukkede canonical_id på mere end et sted, så vi indpakket logikken for at gøre det på en anden metode, som vi kunne kæde til andre spørgsmål. Metoden på opkaldsstedet blev ændret, men opførslen ændrede sig ikke, før funktionsflagget blev tændt.

2. Skriv nye metoder, og kald dem bag et funktionsflag på opkaldsstedet

Denne strategi er relateret til udskiftning af metoden, kun i denne, vi skriver en ny metode og kalder den bag et funktionsflag på opkaldsstedet. Det var især nyttigt til en metode, der kun blev kaldt et sted. Det gjorde os også i stand til at give metoden en bedre signatur - altid nyttig.

3. Afbryd afhængigheder af foreninger med metoder

I dette næste eksempel har et nummer mange laboratorier. Fordi vi ved, at has_many-foreningen tilføjer hjælpemetoder, erstattede vi den, der mest kaldes og fjernede has_many: labs-linjen. Denne metode er i overensstemmelse med den samme grænseflade, så alt, der kalder metoden, før funktionen blev tændt, ville fortsætte med at arbejde.

4. Læg fejl bag et funktionsflag, hvis du er usikker på en metode

Der var nogle gange, hvor vi ikke var sikre på, om vi gik glip af et opkaldssite. Så i stedet for bare hårdt at fjerne metoder til at begynde med, rejste vi bevidst fejl, så vi kunne fange dem i den manuelle testfase. Dette gav os en bedre måde at spore, hvor en metode blev kaldt.

5. Byt ind objekter, der har den samme grænseflade

Fordi vi ville slippe af med labforeningen, omskrev vi implementeringen af ​​laboratoriet? metode. I stedet for at kontrollere for tilstedeværelsen af ​​en laboratorieoptegnelse, udskiftede vi det kanoniske_materiale, delegerede opkaldet og fik det objekt til at svare på den samme metode.

Dette var de mest nyttige strategier til at bryde afhængigheder og bytte nye objekter i vores Rails monolit. Efter at have gennemgået de hundreder af definitioner og opkaldssteder, erstattede eller omskrev vi dem en efter en. Det er en kedelig proces, som jeg ikke ønsker nogen, men det var i sidste ende yderst hjælpsom til at gøre vores kodebase mere læselig og til at fjerne gammel kode, der sad og ikke gjorde noget. Det tog flere frustrerende og hårtrækkende uger at komme til slutningen, men når vi havde udskiftet størstedelen af ​​referencerne, begyndte vi at foretage manuel test.

Test og manuel test

Fordi ændringerne påvirkede funktioner på tværs af hele kodebasen, hvoraf nogle ikke var under test, var det svært at QA med sikkerhed, men vi gjorde vores bedste. Vi udførte manuel test på vores QA-server, som fangede en masse bugs og edge cases. Og så gik vi videre og efter mere kritiske veje, skrev nye test.

Rul ud, gå live og rydde op

Efter at have bestået QA, vendte vi vores funktionsflag på og lader systemet afvikles. Efter at vi var sikre på, at det var stabilt, fjernede vi funktionsflagene og de gamle kodestier fra kodebasen. Dette var desværre sværere end forventet, fordi det medførte omskrivning af en masse af testpakken, for det meste fabrikker, der implicit stod til indholdsmodellen. I eftertid var det, vi kunne have gjort, at skrive to sæt tests, mens vi refaktorerede, et for den aktuelle kode og et for koden bag et funktionsflag.

Som et sidste trin, som stadig skal komme, bør vi tage sikkerhedskopi af data og droppe vores ubrugte tabeller.

Og det, venner, er en måde, du slipper for med den spredte enkeltbordsarv i din Rails-monolit. Måske hjælper denne casestudie dig også.

Har du andre måder at fjerne STI eller refactoring? Vi er nysgerrige efter at vide det. Fortæl os det i kommentarerne.

Vi ansætter også! Deltag i vores team. Vi er cool, jeg lover.

Ressourcer og yderligere læsning

  • Rails Guider Arv
  • Hvordan og hvornår man skal bruge enkeltbordsarv i skinner af Eugene Wang (Flatiron Grad!)
  • Refactoring vores rails app ud af en-tabel arv
  • Enkeltbordsarv vs. polymorfe foreninger i skinner
  • Enkelt tabel arv ved hjælp af skinner 5.02

Hvis du vil lære mere om Flatiron School, skal du besøge webstedet, følge os på Facebook og Twitter og besøge os ved kommende begivenheder i nærheden af ​​dig.

Flatiron School er et stolt medlem af WeWork-familien. Se vores søsterteknologiblogger WeWork Technology og Making Meetup.