At fjerne en forældet funktion fra en kodebase er konceptuelt set en af de enkleste ting en udvikler kan gøre. Slet definitionen, bekræft at intet bruger den, og commit. I praksis er det trinnet "bekræft at intet bruger den", der bryder sammen, for enhver funktion, der har eksisteret længe nok til at blive forældet. Funktionen kan kaldes fra kode, der blev skrevet for år siden af en person, der ikke længere er en del af teamet, i et repository, der modtager ændringer sjældent, af et sprog eller framework, som det nuværende team ikke ejer. Den kan kaldes indirekte via en wrapper, via refleksion eller via en runtime-forsendelsesmekanisme, der ikke vises i nogen statisk kaldgraf. Den kan refereres til i genereret kode, i testscaffolding eller i en konfigurationsfil, der udløser den ved navn. Udvikleren, der markerer den som forældet, og udvikleren, der til sidst fjerner den, har muligvis ingen måde at vide noget af dette på uden et værktøj, der er i stand til at opbygge en komplet, tværgående repository-kalderinventar.
Find alle opkaldere, før du fjerner noget
SMART TS XL opbygger en tværsproget opkaldsgraf, der identificerer alle, der ringer til enhver funktion, før du foretager en ændring.
Klik herOmkostningerne ved at begå dette forkert er umiddelbare og konkrete. En funktion, der fjernes med ufuldstændig registrering af kaldere, forårsager runtime-fejl i de systemer, der stadig er afhængige af den. I en monolit med en enkelt implementering er fejlfladen begrænset. I et distribueret system med flere tjenester, der hver især implementeres uafhængigt, kaskaderer fejlene: den tjeneste, der leverede funktionen, opdateres, forbrugerne bliver ikke, og brud opstår under runtime i produktion i systemer, der kan ejes af forskellige teams. I et mainframe-miljø, hvor COBOL-programmer kalder delte værktøjsparagraffer ved navn, manifesterer fejlen sig muligvis ikke, før et specifikt batchjob kører, hvilket kan være ugentligt eller månedligt, hvilket gør den manglende reference usynlig gennem normale testcyklusser. Som undersøgt i den bredere kontekst af håndtering af forældet kode , forværres risiciene over tid: forældet kode, der fjernes ufuldstændigt, er farligere end forældet kode, der forbliver på plads, fordi fjernelsen skaber illusionen af fuldstændighed, mens de resterende kaldere fortsætter med at køre mod en definition, der ikke længere eksisterer.
Denne artikel er en praktisk guide til opkaldsregistrering før fjernelse af funktioner: hvad en komplet opkaldsliste kræver, hvorfor de værktøjer, som udviklere bruger først, er strukturelt utilstrækkelige, hvordan forskellige typer opkaldsrelationer kræver forskellige analysemetoder, og hvordan ægte tværsystemoptælling af opkald ser ud i virksomhedsskala-kodebaser, der blander sprog, platforme og arkiver.
Hvorfor det er sværere at finde den, der ringer, end det ser ud til
Overfladeversionen af caller discovery er velkendt for enhver udvikler: højreklik på et funktionsnavn i et IDE, vælg "Find alle referencer" eller "Vis kaldhierarki", og gennemgå resultaterne. Dette fungerer pålideligt inden for rammerne af et enkelt projekt, der er indlæst i en enkelt IDE-instans. I det øjeblik kodebasen udvider dette omfang, bliver resultaterne ufuldstændige på måder, der ikke er synlige i outputtet. IDE'en angiver ikke, hvilke kaldere den ikke fandt, fordi den ikke indekserede de lagre, der indeholder dem. Udvikleren ser et resultatsæt, der ser komplet ud, og fortsætter i overensstemmelse hermed.
Dette er det strukturelle problem med opkaldsregistrering i stor skala: de værktøjer, som udviklere bruger mest flydende, er begrænset af deres indekseringsomfang, og i store, distribuerede, flersprogede systemer dækker dette omfang en brøkdel af de steder, hvor en given funktion kan kaldes. Udviklerens tillid til resultaternes fuldstændighed er omvendt proportional med søgningens faktiske fuldstændighed. I en lille kodebase med kun ét sprog er IDE-kaldhierarki virkelig pålideligt. I et virksomhedssystem, der spænder over flere lagre, sprog og implementeringsmiljøer, er det systematisk misvisende. Som analyseret i sammenhæng med kodeentropi og refaktoreringsrisiko , kan ældre moduler afhænge af forældede grænseflader, mens nyere tjenester stadig kalder rutiner, der oprindeligt var designet til tidligere miljøer, og disse tværsystemkaldsrelationer er netop dem, som IDE-afgrænset søgning ikke kan se.
For at forstå de specifikke årsager til, at opkaldsregistrering mislykkes, skal man undersøge hver enkelt hovedopkaldstype: direkte opkald, indirekte opkald, tværsproglige kald og dynamisk afsendelse. Hver type mislykkes af forskellige årsager og kræver forskellige analyseteknikker for at kunne løse problemet korrekt.
Direkte kald på tværs af arkivgrænser
Direkte kald er den enkleste kaldstype: én funktion kalder eksplicit en anden ved navn. Inden for et enkelt repository håndterer IDE'er disse pålideligt. På tværs af repository-grænser mislykkes analysen, fordi IDE'ens indeksering ikke spænder over grænsen. Hvis repository A definerer en delt hjælpefunktion, og repositories B, C og D hver især importerer og kalder den, ser IDE'en for et af disse repositories kun kaldene inden for sit eget indekserede omfang.
Dette kaldmønster for flere arkiver er normen snarere end undtagelsen i mikroservicearkitekturer, hvor delte biblioteker udgives som pakker og forbruges på tværs af snesevis af tjenester. Biblioteksvedligeholderen, der udfaser en funktion i den delte pakke, skal vide, hvilke forbrugertjenester der stadig kalder den. Deres IDE ved intet om forbrugerne. Pakkeadministratoren ved, hvilke tjenester der afhænger af pakken, men ikke hvilken specifik funktion i pakken hver tjeneste kalder. Kortlægning fra "denne tjeneste bruger version X af denne pakke" til "denne tjeneste kalder denne specifikke udfasede funktion" kræver indeksering af hver forbrugers kildekode og løsning af kaldet til den specifikke funktionsdefinition.
Indirekte opkald: Indpakninger, delegerede og facader
En funktion kan ikke kaldes direkte af dens forbrugere. Den kan kaldes via en wrapper-funktion, der giver yderligere logføring, fejlhåndtering eller parametertransformation. Den kan tildeles en delegat eller en funktionspointer og kaldes via delegaten. Den kan registreres i et serviceregister eller et plugin-framework og kaldes ved navn via en dispatch-mekanisme. I hvert af disse tilfælde returnerer en direkte søgning efter kald til den forældede funktion et ufuldstændigt resultat, fordi de faktiske kaldere kalder wrapperen eller dispatcheren, ikke selve den forældede funktion.
Wrapper-medieret kald er særligt almindeligt i store kodebaser, hvor tværgående bekymringer som logging, autorisation og gentagelseslogik er lagdelt omkring kernefunktioner gennem wrapper-mønstre. En forældet funktion, der er pakket ind af et logging-værktøj, kaldes effektivt af alle kaldere af wrapperen, ikke af nogen kode, der indeholder den forældede funktions navn. Identificering af disse kaldere kræver sporing gennem wrapperen: logging-værktøjet kalder den forældede funktion, og alle kaldere af logging-værktøjet er derfor en indirekte kalder af den forældede funktion. Denne rekursive gennemgang af kaldsgrafen er det, der adskiller en grundig kalderopgørelse fra en referencesøgning på overfladeniveau.
Overvej et repræsentativt Java-eksempel, hvor en forældet metode tilgås via et delegeringslag:
Java
// Deprecated in the core service
@Deprecated
public BillingResult calculateLegacyFee(Account account) {
// original implementation
}
// Facade that delegates; not visible as a direct caller in a simple reference search
public BillingResult computeFee(Account account) {
return calculateLegacyFee(account); // indirect caller
}
// Actual consumer; calls computeFee, unaware of the underlying deprecated method
public void processMonthlyBilling(List<Account> accounts) {
accounts.forEach(a -> computeFee(a)); // two hops from the deprecated function
}
En søgning efter "Find alle referencer" calculateLegacyFee afkast computeFee som den eneste opkalder. Den vender ikke tilbage processMonthlyBilling, som er den reelle forbruger af den forældede adfærd. En komplet opkaldsliste kræver, at man gennemløber opkaldsgrafen opstrøms gennem computeFee at identificere hver sti, der i sidste ende aktiverer den forældede metode.
Krydssproglige kald
Krydssprogskald er den kategori, hvor standardværktøjer til registrering af kaldere fejler mest. Når en Java-tjeneste kalder et COBOL-program ved navn gennem et middleware-lag, når et Python-script kalder en lagret procedure, der ombryder en forældet funktion, eller når et JCL-job kalder et program ved sit PROGNAME, der internt kalder et forældet afsnit, vises ingen af disse relationer i et enkelt sprogs kaldgraf. Hvert sprogs værktøj ser kun sin egen side af kaldet.
I mainframe-miljøer er tværfaglige kald strukturelle og udbredte. En JCL-jobstrøm angiver navnet på det COBOL-program, det udfører. COBOL-programmet kalder afsnit og underprogrammer ved navn. Hjælpeafsnit defineret i kopibiblioteker deles på tværs af mange programmer. Når et afsnit i et kopibibliotek er forældet, kræver det at finde alle kaldere forståelse af COBOL-kaldsrelationerne (hvilke programmer inkluderer kopibiblioteket, og hvilke kalder afsnittet), JCL-kaldsrelationerne (hvilke job kalder disse programmer) og eventuelle tværfaglige grænseflader (Java eller SQL, der interagerer med disse programmer). Intet enkelt værktøj spænder over alle disse relationer. Som undersøgt i analysen af statisk analyse på ældre systemer , kan statiske analyseværktøjer, der er bygget til moderne miljøer, ikke se det fulde billede af, hvordan ældre programmer udløses, kaldes og forbindes, når kaldsrelationerne spænder over JCL-, COBOL- og tværfaglige grænseflader samtidigt.
Dynamisk forsendelse og refleksion
Nogle kaldere kalder en funktion ikke ved dens bogstavelige navn i kildekoden, men gennem en mekanisme, der løser funktionen under kørsel: refleksion i Java eller .NET, getattr/__call__ i Python, sen binding i COBOL til og med CALL identifier, dynamisk forsendelse via polymorfi eller invocation-by-string i plugin-frameworks og konfigurationsdrevne systemer. Disse kaldere indeholder ikke den forældede funktions navn i nogen form, som statisk analyse pålideligt kan detektere.
En konfigurationsfil, der angiver et funktionsnavn som en streng, der indlæses under kørsel og bruges til at kalde funktionen gennem refleksion, er en kalder, der ikke optræder nogen steder i nogen kildekodeanalyse. Et plugin-framework, der opdager og kalder registrerede handlere via grænseflade, er en kalder, der kun vises i kaldgrafen som et kald til afsendelsesmekanismen, ikke som et kald til en specifik handler. Identificering af disse kaldere kræver en kombination af statisk analyse for at finde dynamiske afsendelsesmønstre, kørselssporing for at observere faktiske kald og manuel inspektion af konfigurations- og registreringslogik, der bestemmer, hvilke funktioner der sendes til. Som diskuteret i undersøgelsen af statisk analyse på obfuskeret og genereret kode , skal statisk analyse, når udførelsesstier ikke udtrykkes direkte i kildekoden, rekonstruere de sandsynlige stier fra strukturelle mønstre snarere end direkte tekstreferencer, og disse rekonstruktioner kræver sprogbevidst analyse af selve afsendelsesmekanismen.
De værktøjer, udviklerne først griber fat i, og hvor de stopper
Der er en forudsigelig rækkefølge af værktøjer, som udviklere bruger, når de forsøger at finde opkaldere, og et lige så forudsigeligt punkt, hvor hvert enkelt værktøj holder op med at give pålidelige resultater. Det er vigtigt at forstå denne rækkefølge, fordi hvert værktøjs output ser komplet ud, selv når det ikke er det.
IDE-kaldshierarki: Pålidelig inden for ét projekt
IDE-kaldhierarkifunktioner er det mest naturlige første skridt. IntelliJ IDEA, Visual Studio, VS Code og Eclipse tilbyder alle en form for "find alle kaldere" eller "vis kaldhierarki", der rekursivt opregner kalderne for en valgt funktion inden for det indekserede omfang af det aktuelle projekt eller arbejdsområde. For en funktion, der udelukkende bruges i ét repository og ét sprog, er disse funktioner nøjagtige og tilstrækkelige.
Begrænsningen er eksplicit i omfanget: "inden for det indekserede omfang". Kaldere i andre arkiver, i andre sprogkørselstider eller i tjenester, der afhænger af denne kode via en pakkehåndtering snarere end via direkte projektreference, er uden for omfanget. IDE'en angiver ikke, hvad den ikke søgte efter. Udvikleren modtager et resultatsæt og har ingen indsigt i, hvor mange yderligere arkiver der findes, som ikke blev indekseret, hvor mange af dem der bruger den forældede funktion, eller om resultatet "nul kaldere" reelt betyder nul kaldere eller "nul kaldere inden for den del af systemet, som dette værktøj kan se".
grep og tekstsøgning: Bred, men strukturelt blind
Når der er mistanke om, at IDE-søgning er ufuldstændig, er næste trin normalt tekstsøgning: grep på tværs af de tilgængelige kildemapper eller platformsøgning via GitHub- eller GitLab-kodesøgning. Dette udvider omfanget betydeligt og finder kaldere i andre arkiver, hvis disse arkiver er tilgængelige. Det strukturelle problem er, at tekstsøgning finder strenge, ikke kald. Den returnerer alle forekomster af funktionsnavnet, inklusive kommentarer, der nævner funktionen, dokumentationsstrenge, logmeddelelser, der navngiver funktionen til fejlfindingsformål, og strenglitteraler, der indeholder funktionsnavnet, men ikke kalder det. Den overser også kaldere, hvor funktionsnavnet adskiller sig fra søgestrengen: kaldere gennem aliasser, gennem delvist matchede navne i COBOL, hvor navne kan være forkortet, eller gennem dynamisk kald, hvor navnet samles under kørsel.
Resultatsættet fra tekstsøgning kræver manuel filtrering for at bestemme, hvilke forekomster der er faktiske opkaldssteder, hvilke der er dokumentationsreferencer, og hvilke der er falske positiver fra strengkollisioner. I et stort system er denne filtrering i sig selv en betydelig indsats, og filtreringsprocessen kan ikke verificere fuldstændigheden: hvis en opkalder blev overset, fordi den bruger et andet navn, indeholder det filtrerede resultatsæt ingen indikation af udeladelsen.
Compiler-advarsler og @Deprecated Annotationer
Moderne sprog og værktøjskæder tilbyder annotationsmekanismer for forældede funktioner, der genererer advarsler, når forældede funktioner kaldes. Javas @Deprecated annotation kombineret med -Xlint:deprecation producerer advarsler under kompilering på kaldssites. C#'er [Obsolete] Attributten genererer advarsler under byggeprocessen. Gos konvention med at navngive forældede funktioner og dokumentere dem i godoc genererer ikke automatisk advarsler. Disse mekanismer er værdifulde, men begrænsede på en specifik måde: de fungerer kun for kaldere, der kompilerer mod den samme kodebase, som forældelsen er annoteret i.
En opkalder, der bruger en ældre version af biblioteket, ældre end @Deprecated annotation modtager ingen advarsel. En kalder, der bruger en binær artefakt i stedet for at kompilere fra kildekoden, modtager ingen advarsel. En kalder i et andet sprog, der kalder via en tværsproget grænseflade, modtager ingen advarsel. Og afgørende er det, at de advarsler, der produceres under kompilering, er lokale for compilerens visning: de advarer om de kald, den ser, ikke om kald i andre arkiver, der kompileres separat. Brug af compileradvarsler som den eneste mekanisme til at opdage kaldere i et multiservice-system overser alle kaldere, der kompilerer uafhængigt, hvilket er den normale tilstand i mikroservicearkitekturer.
Statiske analyseværktøjer: Bedre, men begrænset til omfanget
Specialbyggede statiske analyseværktøjer giver en mere præcis optælling af opkaldere end IDE'er for deres målsprog og kan ofte krydse grænser for repositories, hvis de er konfigureret til at indeksere flere kodebaser. De bygger korrekte opkaldsgrafer i stedet for at stole på tekstmatchning, håndterer aliasing og indirekte kald bedre end IDE-søgning og kan køres i CI-pipelines for at fange nye opkaldere, når de tilføjes. De er den mest kapable enkeltsprogede tilgang, der er tilgængelig.
Begrænsningen er den samme omfangsgrænse, der begrænser IDE'er, men nu på værktøjsniveau: et statisk Java-analyseværktøj indekserer ikke COBOL-programmer, en COBOL-analysator indekserer ikke Java-tjenester, og ingen af dem indekserer JCL-jobstrømme. I et system, hvor den forældede funktion er et COBOL-værktøj, der kaldes fra COBOL-programmer, der kaldes fra JCL-job, og hvis dataoutput forbruges af Java-tjenester, ser hvert statisk analyseværktøj ét fragment af kaldsrelationen. Som undersøgt i forbindelse med essentielle refaktoreringsteknikker kræver identifikation af alle udførelsesstier til en given del af koden, inklusive sjældne fejltilstande og fallback-grene, den slags komplet kaldsgrafkortlægning, som enkeltsprogede værktøjer ikke kan konstruere på tværs af sproggrænser.
Hvad en komplet opkaldsliste faktisk kræver
En komplet opgørelse over kaldere for en forældet funktion i et virksomhedssystem er ikke et søgeresultat. Det er en struktureret opregning af alle udførelsesstier, hvorigennem den forældede funktion kan nås, inklusive stier, der er direkte, indirekte, tværsproglige og dynamisk afsendte. Opbygning af denne opregning kræver adskillige funktioner, som intet enkelt standardværktøj tilbyder.
En samlet tværsproglig kaldgraf. Kaldgrafen skal spænde over alle sprog i systemet. Et kald fra en JCL-procedure til et COBOL-program, et kald fra COBOL-programmet til et delt hjælpeparagraf og et kald fra en Java-tjeneste til det samme COBOL-program gennem en middleware-grænseflade skal alle være noder og kanter i den samme graf. Den forældede funktion er en node i denne graf, og en optælling af kaldere er en gennemløb af alle indgående kanter, direkte og transitive, uanset hvilket sprog de stammer fra.
Rekursiv gennemgang gennem den fulde kaldgraf. Direkte kaldere er kun det første lag. En komplet inventarisering kræver, at man følger kaldgrafen opstrøms gennem indirekte kaldere, wrapperfunktioner og facadelag, indtil gennemgangen når funktioner, der ikke har deres egne kaldere, som er de sande indgangspunkter for kaldkæderne. Enhver funktion i en sti, der slutter ved den forældede funktion, er en kalder i den relevante forstand: fjernelse af den forældede funktion vil afbryde enhver sti, der passerer gennem den.
Indeksering på tværs af arkiver. Kaldgrafen skal indeholde kode fra alle arkiver, der potentielt kan kalde funktionen, inklusive arkiver, der er afhængige af funktionen via et delt bibliotek eller en delt pakke. Dette kræver indeksering af alle arkiver samtidigt og løsning af importrelationer på tværs af arkiver for at forbinde kald i ét arkiv til definitioner i et andet.
Detektion af indirekte kaldmønstre. Analysen skal identificere kald foretaget via refleksion, dynamisk afsendelse, funktionspointere, delegerede og strengbaseret kald i konfigurationsfiler. Disse kræver mønsterbaseret detektion snarere end direkte kaldkantopløsning: at finde de dynamiske afsendelsesmekanismer i koden og bestemme, hvilke funktioner de kan afsende til under hvilke betingelser.
Sondring mellem aktive kaldere og kun-test eller døde kaldere. Ikke alle kaldere kræver det samme svar. En kalder, der kun findes i en testfixture for selve den forældede funktion, skal fjernes som en del af oprydningen, ikke migreres. En kalder i kode, der i sig selv er blevet identificeret som død kode gennem brugsanalyse, er ikke en blokering for fjernelse af funktioner. Forståelse af disse sondringer kræver, at kalder-optællingen kombineres med information om, hvilke kodestier der rent faktisk er aktive. Som beskrevet i undersøgelsen af detektion af død kode gennem statisk analyse , kan utilgængelig kode og ubrugte funktioner forblive i årevis i missionskritiske systemer på grund af ufuldstændig dokumentation eller usikkerhed om historiske afhængigheder, og kalderbeholdningen for en forældet funktion skal skelne mellem kaldere, der selv er aktive, og kaldere, der selv er døde.
Frafald til fjernelse-processen: En struktureret tilgang
At behandle fjernelse af forældede funktioner som en enkeltstående hændelse snarere end en struktureret proces er kilden til de fleste fejl i forbindelse med caller-discovery. Den korrekte tilgang behandler fjernelse som det sidste trin i en flerfaset proces, der begynder længe før nogen kode slettes.
Fase 1: Markér og mål
Det første trin er at annotere funktionen som forældet ved hjælp af sprogets indbyggede mekanisme (@Deprecated i Java, [Obsolete] i C#, #[deprecated] i Rust eller den tilsvarende metode) og etablering af et baseline antal opkaldere. Denne baseline er ikke resultatet af en enkelt søgning; den er resultatet af indeksering af alle kendte kodebaser, der muligvis kalder funktionen, og optælling af resultaterne. Baseline-modellen tjener to formål: den kvantificerer migreringsomfanget, og den giver en reference, som fremskridt kan måles i forhold til, efterhånden som opkaldere migreres.
Grundlinjen bør organiseres efter opkaldstype og placering:
| Opkaldskategori | Tælle | Prioritet | Ejer |
|---|---|---|---|
| Direkte opkaldere i samme arkiv | N | Høj | Nuværende hold |
| Direkte opkaldere i afhængige tjenester | N | Høj | Serviceejere |
| Opkaldere via wrapper-funktioner | N | Medium | Indpakningsejere |
| Kaldere i genereret kode eller frameworkkode | N | Medium | Rammeteamet |
| Opkaldere i testkode | N | Lav | Nuværende hold |
| Opkaldere i død kode | N | Kun oprydning | Nuværende hold |
Fase 2: Underret og migrer
Med en komplet opkaldsliste bliver migreringen en organiseret indsats snarere end en reaktiv. Hver opkaldsejer får besked om de specifikke opkaldsplaceringer: ikke "du kalder muligvis denne funktion", men "du kalder denne funktion på linje 247 af BillingService.java, linje 82 af AccountProcessor.java, og i integrationstesten på linje 14 af BillingServiceTest.java"Dette niveau af specificitet er, hvad opkaldsbeholdningen muliggør, og hvad generiske advarsler om udfasning ikke kan give."
Det er vigtigt at angive en migreringssti sammen med notifikationen. Udfasningen bør omfatte dokumentation af erstatningsfunktionen, en beskrivelse af eventuelle adfærdsmæssige forskelle mellem den gamle og den nye implementering, og hvor ændringen ikke er triviel, et kodeeksempel, der viser før og efter. For kaldere i andre teams' kodebaser bør tidslinjen for migreringen forhandles eksplicit snarere end annonceres ensidigt, fordi disse teams har deres egne prioriteter og leveringsforpligtelser. Som undersøgt i forbindelse med databaserefaktorering på tværs af afhængige systemer , er indfasning af forbrugere af den nye struktur, før den gamle udfases, den disciplin, der forhindrer, at ændringer, der ikke fungerer, fremstår som uventede hændelser.
Fase 3: Overvåg opkaldsantallet
Mellem baseline-målingen og den planlagte fjernelsesdato bør antallet af opkaldere overvåges kontinuerligt. Hver gang en opkalder migrerer til erstatningsfunktionen, falder antallet. Fjernelsesportalen nås, når antallet når nul for aktive opkaldere (test- og dead-code-opkaldere kan fjernes samtidig med selve funktionen). Kontinuerlig overvågning kræver, at optællingen af opkaldere køres efter en tidsplan som en del af CI-pipelinen, og ikke er afhængig af en engangsopgørelse, der bliver forældet, når koden ændres.
Overvågningen registrerer også nye opkaldere, der tilføjes i udfasningsperioden. I store organisationer er det almindeligt, at der skrives ny kode, der kalder en udfaset funktion i løbet af migreringsvinduet, enten fordi udvikleren ikke var klar over udfasningen, fordi en kodegennemgang overså den, eller fordi en automatiseret kodegenerator producerer kode, der kalder den udfasede funktion. CI-niveau opkaldsdetektion for den udfasede funktion, der er konfigureret til at fejle på nye opkaldssites, forhindrer antallet af opkaldere i at vokse, mens migreringen er i gang.
Fase 4: Bekræft fuldstændighed før fjernelse
Umiddelbart før funktionen fjernes, bør optællingen af antallet af kaldere køres en sidste gang mod det fulde omfang af alle kendte kodebaser. Denne sidste kontrol fungerer som sikkerhedsporten: den bekræfter, at antallet af kaldere har nået nul for aktive kaldere, og identificerer eventuelle tilføjelser i det sene stadie, der ikke blev fanget af CI-overvågningen. På dette tidspunkt bør inventaret også verificere fraværet af dynamiske kaldere: konfigurationsfiler, der refererer til funktionen via strenge, refleksionsbaserede registreringer og eventuelle andre indirekte kaldmekanismer, der blev identificeret under den indledende analyse.
Verifikationen bør omfatte afhængighedsgrafen for alle delte biblioteker eller pakker, der eksponerer den forældede funktion. Hvis funktionen er en del af en offentlig API, der forbruges af eksterne parter, skal tidslinjen for fjernelse tage højde for eksterne forbrugere, som muligvis ikke kan nås via intern kodeanalyse. For interne systemer dækker verifikationen alle indekserede kodebaser. For offentligt publicerede API'er dækker verifikationen det kendte sæt af forbrugere plus en defineret solnedgangsperiode, hvor eksterne forbrugere skal migrere.
Hvordan opkaldsregistrering fungerer forskelligt i ældre og mainframe-miljøer
De ovenfor beskrevne udfordringer gælder for alle store softwaresystemer, men de er særligt udtalte i mainframe- og ældre miljøer, fordi opkaldsrelationerne i disse miljøer udtrykkes gennem mekanismer, som moderne værktøjer til registrering af opkald ikke er designet til at analysere.
I COBOL-miljøer kaldes funktioner via CALL-sætninger, der kan referere til målet via en literal streng, via et dataelement, der indeholder programnavnet, eller via en procedurepointer. Literal-string-tilfældet kan løses via statisk analyse; dataelement-tilfældet kræver dataflowanalyse for at bestemme, hvilken værdi dataelementet kan have på kaldetidspunktet; og procedurepointer-tilfældet kræver sporing af, hvordan pointeren er tildelt. Hver af disse kaldmekanismer vises forskelligt i kildekoden og kræver forskellig analyse for at løse dem.
I JCL-miljøer kaldes programmer ved navn i EXEC PGM=-sætninger. Programnavnet er en streng, der knyttes til et kompileret modul i et indlæsningsbibliotek. Sporing af kaldere af et COBOL-program via JCL kræver parsing af JCL for at udtrække programnavne, knytte disse navne til de kompilerede COBOL-programmer, der implementerer dem, og finde ud af, hvilke COBOL-paragrafer i disse programmer der kalder det forældede værktøj. Denne flertrinsløsning ligger helt uden for rammerne af hverken en COBOL-analysator eller en JCL-analysator, der arbejder isoleret.
Delte kopibøger er et særligt vigtigt tilfælde i COBOL-miljøer. Et forældet afsnit defineret i en kopibog kan inkluderes i mange programmer via COPY-sætninger. Afsnittet duplikeres ikke fysisk i hvert program; det inkluderes ved kompilering. En analyse, der tæller forekomster af afsnitsnavnet i kildefiler uden at løse kopibogsinkluderinger, vil både overtælle (finde afsnitsdefinitionen i selve kopibogen) og undertælle (overse det faktum, at hvert program, der inkluderer kopibogen, har adgang til afsnittet). Korrekt registrering af kaldere kræver forståelse af, hvilke programmer der inkluderer hvilke kopibøger, og hvilke afsnit i disse kopibøger de rent faktisk kalder. Forholdet mellem hardcodede referencer og deres downstream-forbrugere illustrerer, hvorfor det er vigtigt at løse disse programniveau-kaldsrelationer før enhver strukturel ændring: hvad der synes at være en simpel strengreference, kan være den eneste mekanisme, hvorigennem snesevis af programmer når kritisk funktionalitet.
Hvordan SMART TS XL Opbygger den komplette opkaldsliste
SMART TS XL konstruerer en samlet kaldgraf på tværs af alle sprog, platforme og repositories i det indekserede miljø. COBOL-programmer, JCL-jobstrømme, Java-tjenester, .NET-applikationer, SQL-lagrede procedurer, Python-scripts og andre kildeartefakter parses alle ved hjælp af sprogspecifik analyse til en fælles krydsreferencegraf. Hver funktion, afsnit, procedure, metode og programenhed er en node i den graf. Hver kaldrelation, uanset om det er en COBOL CALL-sætning, en Java-metodekaldelse, en JCL EXEC PGM eller en SQL EXEC, er en typebestemt kant. Grafen repræsenterer systemets komplette kaldtopologi, ikke en delvis visning pr. sprog.
Når en funktion er markeret til fjernelse, SMART TS XLs opkaldsoptælling gennemgår opkaldsgrafen, der kommer ind fra målfunktionsnoden, og indsamler alle opkaldere på alle niveauer i opkaldshierarkiet. Gennemgangen er rekursiv og følger grafen gennem wrapper-funktioner, facadelag og mellemliggende værktøjer, indtil den når funktioner uden opkaldere, som repræsenterer de sande indgangspunkter for opkaldskæderne. Resultaterne er organiseret efter sprog, efter arkiv, efter opkaldstype og efter opkaldsdybde, hvilket giver teamet en struktureret oversigt, der adskiller direkte opkaldere fra indirekte opkaldere og aktive opkaldere fra opkaldere med død kode.
Platformens effektanalysefunktion udvider dette til en struktureret ændrings-effektrapport: ikke kun hvilke funktioner, der kalder den forældede funktion, men også hvilke programmer, tjenester, batchjob og JCL-procedurer, der er påvirket på alle niveauer i afhængighedskæden. Denne rapport er den artefakt, der gør processen fra forældelse til fjernelse handlingsrettet: den navngiver ejerne, identificerer de specifikke kaldssteder og kvantificerer omfanget af den migrering, der kræves, før fjernelsen kan fortsætte sikkert. Som undersøgt i detaljerne om effektanalyse til virksomhedsændringsstyring , er evnen til at opregne berørte komponenter, før der foretages en strukturel ændring, det grundlæggende krav for sikker drift af komplekse, sammenkoblede virksomhedssystemer.
SMART TS XL understøtter også den løbende overvågningsfase af udfasningsprocessen. Fordi krydsreferencegrafen løbende opdateres, efterhånden som ændringer i kildekoden indekseres, er antallet af kald for en udfaset funktion altid aktuelt. Integration af CI-pipeline tillader automatiske kontroller at mislykkes ved nye kald til udfasede funktioner, hvilket håndhæver migreringsdisciplinen på det tidspunkt, hvor ny kode introduceres, i stedet for at opdage overtrædelser bagefter. Denne kombination af indledende optælling, migreringsvejledning og kontinuerlig overvågning dækker hele livscyklussen for en udfaset funktion fra annotering til sikker fjernelse.
Fjernelse af funktioner uden fortrydelse
Forskellen mellem en funktionsfjernelse, der går problemfrit, og en, der udløser produktionsfejl, er næsten altid en forskel i fuldstændigheden af caller-registreringen. Selve fjernelsen er triviel: slet definitionen og implementer. Forberedelsen er der, hvor arbejdet er, og forberedelsen er kun så god som den caller-inventar, den er baseret på.
I systemer, hvor kaldgrafen er overfladisk, ensproget og indeholdt i et enkelt repository, er IDE-kaldshierarki og compileradvarsler tilstrækkelig forberedelse. I systemer, hvor kaldgrafen spænder over flere sprog, flere repositories, flere platforme og potentielt flere årtiers kode, dækker disse værktøjer en lille og ukendt del af den faktiske kalderoverflade. Gabet mellem det, de returnerer, og det, der rent faktisk kalder funktionen, er der, hvor produktionsfejl opstår.
Specialbygget tværsproglig og tværbaseret optælling af kaldere er ikke en forfinelse af udviklerens arbejdsgang til fjernelse af funktioner. Det er en forudsætning for at udføre arbejdsgangen sikkert i ethvert system, der er komplekst nok til at have akkumuleret den slags tværsystemkaldsrelationer, som forældede funktioner i virksomhedskodebaser rutinemæssigt bærer. Enhver forældet funktion, der fjernes uden en komplet kalderopgørelse, er en udgivelse, der indeholder et ukendt antal runtime-fejl, der venter på den specifikke udførelsessti, der når den manglende definition. Eliminering af denne ukendte er, hvad struktureret kalderopdagelse er til for.