Usunięcie przestarzałej funkcji z bazy kodu jest koncepcyjnie jedną z najprostszych rzeczy, jakie może zrobić programista. Usuń definicję, upewnij się, że nic jej nie używa, zatwierdź. W praktyce, w przypadku każdej funkcji, która istniała wystarczająco długo, aby ją przestarzałą, krok „upewnij się, że nic jej nie używa” to punkt, w którym proces się załamuje. Funkcja może być wywoływana z kodu napisanego lata temu przez osobę, która nie jest już członkiem zespołu, w repozytorium, które rzadko otrzymuje zmiany, przez język lub framework, którego obecny zespół nie jest właścicielem. Może być wywoływana pośrednio przez wrapper, refleksję lub mechanizm dystrybucji w czasie wykonywania, który nie pojawia się w żadnym statycznym grafie wywołań. Może być odwoływana w wygenerowanym kodzie, w rusztowaniu testów lub w pliku konfiguracyjnym, który ją wyzwala po nazwie. Programista, który oznacza ją jako przestarzałą, i programista, który ostatecznie ją usunie, mogą nie mieć o tym pojęcia bez narzędzia umożliwiającego zbudowanie kompletnego, międzyrepozytoryjnego spisu wywołujących.
Znajdź każdego dzwoniącego, zanim cokolwiek usuniesz
SMART TS XL tworzy międzyjęzykowy wykres wywołań, który identyfikuje każdego wywołującego daną funkcję przed wprowadzeniem zmiany.
Kliknij tutajKoszt popełnienia błędu jest natychmiastowy i konkretny. Funkcja usunięta z niekompletnym rozpoznaniem wywołującego powoduje awarie w czasie wykonywania w systemach, które nadal od niej zależą. W monolicie z pojedynczym wdrożeniem powierzchnia awarii jest ograniczona. W systemie rozproszonym z wieloma usługami, z których każda jest wdrażana niezależnie, awarie następują kaskadowo: usługa udostępniająca funkcję jest aktualizowana, odbiorcy nie, a awaria pojawia się w czasie wykonywania w systemach produkcyjnych, które mogą być własnością różnych zespołów. W środowisku mainframe, w którym programy COBOL wywołują współdzielone akapity narzędziowe po nazwie, awaria może nie ujawnić się do momentu uruchomienia określonego zadania wsadowego, które może być wykonywane co tydzień lub co miesiąc, sprawiając, że brakujące odwołanie będzie niewidoczne w normalnych cyklach testowania. Jak analizuje się w szerszym kontekście zarządzania przestarzałym kodem , ryzyko kumuluje się z czasem: przestarzały kod, który jest niekompletnie usuwany, jest bardziej niebezpieczny niż przestarzały kod pozostawiony na miejscu, ponieważ usunięcie tworzy iluzję kompletności, podczas gdy pozostałe wywołujące nadal działają w oparciu o definicję, która już nie istnieje.
W tym artykule znajdziesz praktyczny przewodnik po wyszukiwaniu wywołujących przed usunięciem funkcji: czego wymaga kompletny spis wywołujących, dlaczego narzędzia, po które sięgają w pierwszej kolejności programiści, są niewystarczające pod względem strukturalnym, w jaki sposób różne typy relacji wywołań wymagają różnych podejść analitycznych oraz jak wygląda rzeczywiste międzysystemowe wyliczanie wywołujących w bazach kodu na skalę przedsiębiorstwa, które łączą języki, platformy i repozytoria.
Dlaczego odkrywanie rozmówców jest trudniejsze, niż się wydaje
Wersja wykrywania wywołań na poziomie powierzchni jest znana każdemu programiście: wystarczy kliknąć prawym przyciskiem myszy nazwę funkcji w środowisku IDE, wybrać opcję „Znajdź wszystkie odwołania” lub „Pokaż hierarchię wywołań” i przejrzeć wyniki. Działa to niezawodnie w zakresie pojedynczego projektu załadowanego w pojedynczej instancji IDE. W momencie, gdy baza kodu wykracza poza ten zakres, wyniki stają się niekompletne w sposób niewidoczny w wynikach. IDE nie wskazuje, których wywołań nie znaleziono, ponieważ nie zindeksowało repozytoriów, które je zawierają. Programista widzi zestaw wyników, który wydaje się kompletny i kontynuuje działanie zgodnie z nim.
To jest problem strukturalny z wykrywaniem wywołujących na dużą skalę: narzędzia, z których programiści korzystają najsprawniej, są ograniczone przez zakres indeksowania, a w dużych, rozproszonych, wielojęzycznych systemach zakres ten obejmuje ułamek miejsc, w których dana funkcja może zostać wywołana. Pewność programisty co do kompletności wyników jest odwrotnie proporcjonalna do faktycznej kompletności wyszukiwania. W małej, jednojęzycznej bazie kodu hierarchia wywołań IDE jest naprawdę niezawodna. W systemie korporacyjnym obejmującym wiele repozytoriów, języków i środowisk wdrożeniowych jest ona systematycznie myląca. Analizując w kontekście entropii kodu i ryzyka refaktoryzacji , starsze moduły mogą zależeć od przestarzałych interfejsów, podczas gdy nowsze usługi nadal wywołują procedury pierwotnie zaprojektowane dla starszych środowisk, a te międzysystemowe relacje wywołań to właśnie te, których wyszukiwanie ograniczone IDE nie widzi.
Zrozumienie konkretnych przyczyn niepowodzenia wykrywania rozmówcy wymaga przeanalizowania każdego z głównych typów połączeń: połączeń bezpośrednich, pośrednich, wywołań międzyjęzykowych i dynamicznego wysyłania. Każde z nich kończy się niepowodzeniem z innych przyczyn i wymaga innych technik analizy, aby rozwiązać problem.
Bezpośrednie połączenia przez granice repozytorium
Wywołania bezpośrednie to najprostszy typ wywołania: jedna funkcja jawnie wywołuje inną po nazwie. W obrębie jednego repozytorium środowiska programistyczne (IDE) obsługują je niezawodnie. Analiza między repozytorium a repozytorium kończy się niepowodzeniem, ponieważ indeksowanie IDE nie obejmuje granic. Jeśli repozytorium A definiuje współdzieloną funkcję narzędziową, a repozytoria B, C i D importują ją i wywołują, środowisko programistyczne dla dowolnego z tych repozytoriów widzi tylko wywołania w obrębie własnego zakresu indeksowania.
Ten wzorzec wywołań wielorepozytoryjnych jest normą, a nie wyjątkiem w architekturach mikrousług, gdzie biblioteki współdzielone są publikowane jako pakiety i wykorzystywane w dziesiątkach usług. Opiekun biblioteki, który wycofuje funkcję w pakiecie współdzielonym, musi wiedzieć, które usługi korzystające z niej nadal ją wywołują. Jego IDE nie wie nic o konsumentach. Menedżer pakietów wie, które usługi zależą od pakietu, ale nie wie, którą konkretną funkcję w pakiecie wywołuje każda usługa. Mapowanie z „ta usługa używa wersji X tego pakietu” na „ta usługa wywołuje tę konkretną, wycofaną funkcję” wymaga indeksowania kodu źródłowego każdego konsumenta i przypisania wywołania do konkretnej definicji funkcji.
Połączenia pośrednie: opakowania, delegaci i fasady
Funkcja nie może być wywoływana bezpośrednio przez swoich odbiorców. Może być wywoływana za pośrednictwem funkcji opakowującej, która zapewnia dodatkowe rejestrowanie, obsługę błędów lub transformację parametrów. Może być przypisana do delegata lub wskaźnika do funkcji i wywoływana za pośrednictwem delegata. Może być zarejestrowana w rejestrze usług lub infrastrukturze wtyczek i wywoływana po nazwie za pośrednictwem mechanizmu dyspozytora. W każdym z tych przypadków bezpośrednie wyszukiwanie wywołań funkcji przestarzałej zwraca niekompletny wynik, ponieważ faktyczne wywołania wywołują wrapper lub dyspozytor, a nie samą funkcję przestarzałą.
Wywołanie pośredniczone przez wrapper jest szczególnie powszechne w dużych bazach kodu, gdzie zagadnienia takie jak logowanie, autoryzacja i logika ponawiania prób są nakładane na funkcje podstawowe za pomocą wzorców wrapperów. Wycofana funkcja opakowana przez narzędzie rejestrujące jest w rzeczywistości wywoływana przez każdego wywołującego wrapper, a nie przez kod zawierający nazwę wycofywanej funkcji. Identyfikacja tych wywołujących wymaga prześledzenia wrappera: narzędzie rejestrujące wywołuje wycofywaną funkcję, a zatem każdy wywołujący narzędzie rejestrujące jest pośrednim wywołującym wycofywaną funkcję. To rekurencyjne przeglądanie grafu wywołań odróżnia dogłębną inwentaryzację wywołujących od powierzchownego wyszukiwania referencji.
Rozważmy przykładowy przykład w języku Java, w którym dostęp do przestarzałej metody odbywa się poprzez warstwę delegacji:
Jawa
// 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
}
Wyszukiwanie „Znajdź wszystkie odniesienia” calculateLegacyFee powraca computeFee jako jedyny dzwoniący. Nie wraca processMonthlyBilling, który jest prawdziwym konsumentem wycofanego zachowania. Pełny spis dzwoniących wymaga przejścia przez graf wywołań w górę strumienia computeFee aby zidentyfikować każdą ścieżkę, która ostatecznie wywołuje przestarzałą metodę.
Wywołania międzyjęzykowe
Wywołania międzyjęzykowe to kategoria, w której standardowe narzędzia do wykrywania wywołań zawodzą najzupełniej. Gdy usługa Java wywołuje program COBOL po nazwie za pośrednictwem warstwy pośredniczącej, gdy skrypt Pythona wywołuje procedurę składowaną, która opakowuje przestarzałą funkcję, lub gdy zadanie JCL wywołuje program po nazwie PROGNAME, który wewnętrznie wywołuje przestarzały akapit, żadna z tych relacji nie pojawia się w grafie wywołań żadnego pojedynczego języka. Narzędzia każdego języka widzą tylko swoją własną stronę wywołania.
W środowiskach mainframe wywołania międzyjęzykowe są strukturalne i wszechobecne. Strumień zadań JCL określa nazwę wykonywanego programu COBOL. Program COBOL wywołuje akapity i podprogramy według nazwy. Akapity narzędziowe zdefiniowane w bibliotekach kopii są współdzielone przez wiele programów. Gdy akapit w bibliotece kopii jest przestarzały, znalezienie wszystkich wywołujących wymaga zrozumienia relacji wywołań COBOL (które programy zawierają bibliotekę kopii i które wywołują akapit), relacji wywołań JCL (które zadania wywołują te programy) oraz wszelkich interfejsów międzyjęzykowych (Java lub SQL, które oddziałują z tymi programami). Żadne pojedyncze narzędzie nie obejmuje wszystkich tych relacji. Jak sprawdzono w analizie analizy statycznej w starszych systemach , narzędzia do analizy statycznej zbudowane dla nowoczesnych środowisk nie są w stanie uzyskać pełnego obrazu tego, jak starsze programy są wyzwalane, wywoływane i połączone, gdy relacje wywołań obejmują jednocześnie JCL, COBOL i interfejsy międzysystemowe.
Dynamiczna wysyłka i refleksja
Niektórzy wywołujący wywołują funkcję nie za pomocą jej nazwy literowej w kodzie źródłowym, ale za pomocą mechanizmu, który rozwiązuje funkcję w czasie wykonywania: refleksji w Javie lub .NET, getattr/__call__ w Pythonie, późne wiązanie w COBOL-u poprzez CALL identifier, dynamiczne wysyłanie poprzez polimorfizm lub wywoływanie za pomocą ciągu znaków w frameworkach wtyczek i systemach sterowanych konfiguracją. Te wywołania nie zawierają nazwy przestarzałej funkcji w żadnej formie, którą analiza statyczna może niezawodnie wykryć.
Plik konfiguracyjny, który określa nazwę funkcji jako ciąg znaków, ładowany w czasie wykonywania i używany do wywołania funkcji poprzez refleksję, jest obiektem wywołującym, który nie pojawia się nigdzie w żadnej analizie kodu źródłowego. Struktura wtyczek, która odkrywa i wywołuje zarejestrowane procedury obsługi przez interfejs, jest obiektem wywołującym, który pojawia się na grafie wywołań tylko jako wywołanie mechanizmu dyspozytorskiego, a nie jako wywołanie konkretnego obiektu obsługującego. Identyfikacja tych obiektów wywołujących wymaga połączenia analizy statycznej w celu znalezienia wzorców dynamicznej dyspozytorskiej, śledzenia w czasie wykonywania w celu obserwacji rzeczywistych wywołań oraz ręcznej inspekcji logiki konfiguracji i rejestracji, która określa, do których funkcji są wysyłane. Jak omówiono w badaniu analizy statycznej zaciemnionego i wygenerowanego kodu , gdy ścieżki wykonywania nie są wyrażone bezpośrednio w kodzie źródłowym, analiza statyczna musi zrekonstruować prawdopodobne ścieżki ze wzorców strukturalnych, a nie bezpośrednich odniesień tekstowych, a te rekonstrukcje wymagają analizy mechanizmu dyspozytorskiego z uwzględnieniem języka.
Narzędzia, po które programiści sięgają w pierwszej kolejności i na czym poprzestają
Istnieje przewidywalna sekwencja narzędzi, z których korzystają programiści, próbując wykryć osoby dzwoniące, oraz równie przewidywalny moment, w którym każde z nich przestaje dostarczać wiarygodnych wyników. Zrozumienie tej sekwencji jest ważne, ponieważ dane wyjściowe każdego narzędzia wydają się kompletne, nawet jeśli tak nie jest.
Hierarchia wywołań IDE: niezawodność w ramach jednego projektu
Funkcje hierarchii wywołań IDE to najbardziej naturalny pierwszy krok. IntelliJ IDEA, Visual Studio, VS Code i Eclipse oferują funkcje „znajdź wszystkich wywołujących” lub „pokaż hierarchię wywołań”, które rekurencyjnie wyliczają wywołujących wybraną funkcję w indeksowanym zakresie bieżącego projektu lub obszaru roboczego. W przypadku funkcji używanej wyłącznie w jednym repozytorium i jednym języku, funkcje te są dokładne i wystarczające.
Ograniczenie jest wyraźnie widoczne w zakresie: „w obrębie indeksowanego zakresu”. Wywołujące w innych repozytoriach, w środowiskach wykonawczych innych języków lub w usługach zależnych od tego kodu za pośrednictwem menedżera pakietów, a nie poprzez bezpośrednie odwołanie do projektu, znajdują się poza zakresem. IDE nie wskazuje, czego nie przeszukało. Deweloper otrzymuje zestaw wyników i nie ma wglądu w liczbę dodatkowych repozytoriów, które nie zostały zindeksowane, liczbę z nich korzystających z przestarzałej funkcji, ani w to, czy wynik „zero wywołujących” oznacza faktycznie zero wywołujących, czy „zero wywołujących w części systemu, którą narzędzie widzi”.
grep i wyszukiwanie tekstowe: szerokie, ale strukturalnie ślepe
Gdy podejrzewa się, że wyszukiwanie w środowisku IDE jest niekompletne, kolejnym krokiem jest zazwyczaj wyszukiwanie tekstowe: użycie polecenia grep w dostępnych katalogach źródłowych lub przeszukanie platformy za pomocą kodu w GitHub lub GitLab. To znacznie poszerza zakres i znajduje wywołujące funkcje w innych repozytoriach, jeśli są one dostępne. Problem strukturalny polega na tym, że wyszukiwanie tekstowe znajduje ciągi znaków, a nie wywołania. Zwraca ono każde wystąpienie nazwy funkcji, w tym komentarze, które ją wspominają, ciągi znaków dokumentacji, komunikaty dziennika z nazwą funkcji dla celów debugowania oraz literały ciągu znaków zawierające nazwę funkcji, ale jej nie wywołujące. Pomija również wywołujące funkcje, których nazwa różni się od wyszukiwanego ciągu znaków: wywołujące funkcje poprzez aliasy, częściowo dopasowane nazwy w COBOL-u, gdzie nazwy mogą być skrócone, lub poprzez dynamiczne wywołanie, gdzie nazwa jest asemblowana w czasie wykonywania.
Zbiór wyników wyszukiwania tekstowego wymaga ręcznego filtrowania w celu określenia, które wystąpienia są rzeczywistymi miejscami wywołań, które odwołaniami do dokumentacji, a które fałszywie dodatnimi wynikami kolizji ciągów znaków. W dużym systemie samo filtrowanie jest dużym wysiłkiem, a proces filtrowania nie jest w stanie zweryfikować kompletności: jeśli wywołujący został pominięty z powodu użycia innej nazwy, przefiltrowany zbiór wyników nie zawiera informacji o pominięciu.
Ostrzeżenia kompilatora i @Deprecated Adnotacje
Nowoczesne języki i łańcuchy narzędzi zapewniają mechanizmy adnotacji deprecjacji, które generują ostrzeżenia w przypadku wywołania funkcji uznanych za przestarzałe. @Deprecated adnotacja połączona z -Xlint:deprecation generuje ostrzeżenia w czasie kompilacji w miejscach wywołań. C# [Obsolete] Atrybut generuje ostrzeżenia w czasie kompilacji. Konwencja języka Go, polegająca na nazywaniu funkcji uznanych za przestarzałe i dokumentowaniu ich w dokumencie godoc, nie generuje automatycznie ostrzeżeń. Mechanizmy te są cenne, ale mają pewne ograniczenia: działają tylko w przypadku wywołań, które kompilują się w oparciu o tę samą bazę kodu, w której adnotacja o wycofaniu jest zastrzeżona.
Osoba dzwoniąca, która używa starszej wersji biblioteki, starszej niż @Deprecated adnotacja, nie otrzymuje żadnego ostrzeżenia. Wywołujący, który używa artefaktu binarnego zamiast kompilować ze źródła, nie otrzymuje żadnego ostrzeżenia. Wywołujący w innym języku, który wywołuje za pośrednictwem interfejsu międzyjęzykowego, nie otrzymuje żadnego ostrzeżenia. Co najważniejsze, ostrzeżenia generowane podczas kompilacji są lokalne dla widoku kompilatora: ostrzegają o wywołaniach, które widzi, a nie o wywołaniach w innych repozytoriach, które są kompilowane oddzielnie. Używanie ostrzeżeń kompilatora jako jedynego mechanizmu wykrywania wywołujących w systemie wielousługowym pomija wszystkie wywołujące, które kompilują się niezależnie, co jest normalną sytuacją w architekturach mikrousług.
Narzędzia do analizy statycznej: lepsze, ale ograniczone zakresem
Specjalnie zaprojektowane narzędzia do analizy statycznej zapewniają dokładniejsze wyliczanie wywołujących niż środowiska IDE dla języka docelowego i często przekraczają granice repozytoriów, jeśli są skonfigurowane do indeksowania wielu baz kodu. Tworzą poprawne grafy wywołań zamiast polegać na dopasowywaniu tekstu, lepiej radzą sobie z aliasingiem i wywołaniami pośrednimi niż wyszukiwanie w środowisku IDE oraz mogą być uruchamiane w potokach CI, aby wychwytywać nowe wywołujące w miarę ich dodawania. Stanowią one najbardziej wydajne dostępne podejście w jednym języku.
Ograniczenie jest takie samo, jak w przypadku środowisk IDE, ale teraz na poziomie narzędzia: narzędzie do analizy statycznej Javy nie indeksuje programów COBOL, analizator COBOL nie indeksuje usług Java, ani żadne z nich nie indeksuje strumieni zadań JCL. W systemie, w którym przestarzałą funkcją jest narzędzie COBOL wywoływane z programów COBOL, które są wywoływane z zadań JCL i których dane wyjściowe są wykorzystywane przez usługi Java, każde narzędzie do analizy statycznej widzi jeden fragment relacji wywołania. Jak to zbadano w kontekście podstawowych technik refaktoryzacji , identyfikacja wszystkich ścieżek wykonania do danej sekcji kodu, w tym rzadkich błędów i gałęzi awaryjnych, wymaga kompletnego mapowania grafu wywołań, którego narzędzia jednojęzyczne nie są w stanie zbudować w różnych językach.
Czego tak naprawdę wymaga kompletny spis dzwoniących
Kompletny spis wywołujących dla wycofanej funkcji w systemie korporacyjnym nie jest wynikiem wyszukiwania. Jest to ustrukturyzowane wyliczenie wszystkich ścieżek wykonania, którymi można dotrzeć do wycofanej funkcji, w tym ścieżek bezpośrednich, pośrednich, międzyjęzykowych i dynamicznie przydzielanych. Stworzenie tego wyliczenia wymaga kilku funkcji, których nie oferuje żadne standardowe narzędzie.
Ujednolicony graf wywołań międzyjęzykowych. Graf wywołań musi obejmować wszystkie języki w systemie. Wywołanie z procedury JCL do programu COBOL, wywołanie z programu COBOL do akapitu współdzielonego narzędzia oraz wywołanie z usługi Java do tego samego programu COBOL za pośrednictwem interfejsu middleware muszą być węzłami i krawędziami w tym samym grafie. Wycofana funkcja jest węzłem w tym grafie, a wyliczenie wywołujących to przejście wszystkich krawędzi przychodzących, bezpośrednich i przechodnich, niezależnie od języka, z którego pochodzą.
Rekurencyjne przechodzenie przez pełny graf wywołań. Bezpośrednie funkcje wywołujące to tylko pierwsza warstwa. Pełna inwentaryzacja wymaga prześledzenia grafu wywołań w górę strumienia, poprzez pośrednie funkcje wywołujące, funkcje opakowujące i warstwy fasady, aż do momentu, gdy przejście dotrze do funkcji, które nie mają własnych funkcji wywołujących, które są prawdziwymi punktami wejścia do łańcuchów wywołań. Każda funkcja na ścieżce, która kończy się na funkcji przestarzałej, jest funkcją wywołującą w odpowiednim sensie: usunięcie funkcji przestarzałej spowoduje przerwanie każdej ścieżki, która przez nią przechodzi.
Indeksowanie międzyrepozycyjne. Graf wywołań musi zawierać kod z każdego repozytorium, które potencjalnie mogłoby wywołać funkcję, w tym z repozytoriów zależnych od funkcji za pośrednictwem biblioteki współdzielonej lub pakietu. Wymaga to jednoczesnego indeksowania wszystkich repozytoriów i rozwiązania relacji importu międzyrepozycyjnego w celu połączenia wywołań w jednym repozytorium z definicjami w innym.
Wykrywanie wzorców pośrednich wywołań. Analiza musi identyfikować wywołania wykonywane poprzez refleksję, dynamiczne rozsyłanie, wskaźniki do funkcji, delegaty i wywołania oparte na ciągach znaków w plikach konfiguracyjnych. Wymaga to wykrywania opartego na wzorcach, a nie bezpośredniego rozwiązywania krawędzi wywołań: znajdowania mechanizmów dynamicznego rozsyłania w kodzie i określania, do których funkcji mogą one wysyłać wywołania w określonych warunkach.
Rozróżnienie między aktywnymi wywołującymi a wywołującymi tylko testowymi lub martwymi. Nie wszystkie wywołujące wymagają tej samej odpowiedzi. Wywołujący, który istnieje tylko w fixturze testowej dla samej przestarzałej funkcji, musi zostać usunięty w ramach czyszczenia, a nie migrowany. Wywołujący w kodzie, który sam został zidentyfikowany jako martwy kod poprzez analizę użycia, nie blokuje usunięcia funkcji. Zrozumienie tych rozróżnień wymaga połączenia wyliczenia wywołujących z informacją o tym, które ścieżki kodu są faktycznie aktywne. Jak szczegółowo opisano w badaniu wykrywania martwego kodu poprzez analizę statyczną , nieosiągalny kod i nieużywane funkcje mogą utrzymywać się latami w systemach o znaczeniu krytycznym z powodu niekompletnej dokumentacji lub niepewności co do historycznych zależności, a inwentarz wywołujących dla przestarzałej funkcji musi rozróżniać między wywołującymi, którzy sami są aktywni, a wywołującymi, którzy sami są nieaktywni.
Proces od wycofania do usunięcia: podejście strukturalne
Traktowanie usuwania przestarzałych funkcji jako pojedynczego zdarzenia, a nie ustrukturyzowanego procesu, jest przyczyną większości błędów wykrywania funkcji wywołujących. Prawidłowe podejście traktuje usuwanie jako ostatni krok w wieloetapowym procesie, który rozpoczyna się na długo przed usunięciem jakiegokolwiek kodu.
Faza 1: Zaznacz i zmierz
Pierwszym krokiem jest oznaczenie funkcji jako przestarzałej przy użyciu wbudowanego mechanizmu języka (@Deprecated w Javie, [Obsolete] w C#, #[deprecated] w języku Rust lub odpowiednim odpowiedniku) i ustalenie bazowej liczby wywołujących. Ta bazowa liczba nie jest wynikiem pojedynczego wyszukiwania; jest wynikiem indeksowania każdej znanej bazy kodu, która może wywołać funkcję, i zliczania wyników. Linia bazowa służy dwóm celom: określa zakres migracji i stanowi punkt odniesienia, względem którego można mierzyć postęp migracji wywołujących.
Dane bazowe powinny być uporządkowane według typu osoby dzwoniącej i jej lokalizacji:
| Kategoria dzwoniącego | Liczyć | Priorytet | Właściciel |
|---|---|---|---|
| Bezpośrednie osoby dzwoniące w tym samym repozytorium | N | Wysoki | Obecna drużyna |
| Dzwoniący bezpośrednio w usługach zależnych | N | Wysoki | Właściciele usług |
| Wywołujący za pomocą funkcji opakowującej | N | Średni | Właściciele opakowań |
| Wywołujący w generowanym kodzie lub kodzie ramowym | N | Średni | Zespół Framework |
| Wywołujący w kodzie tylko testowym | N | Niski | Obecna drużyna |
| Dzwoniący w martwym kodzie | N | Tylko sprzątanie | Obecna drużyna |
Faza 2: Powiadomienie i migracja
Dzięki kompletnemu spisowi dzwoniących migracja staje się zorganizowanym działaniem, a nie reaktywnym. Każdy właściciel dzwoniącego jest powiadamiany o konkretnych lokalizacjach połączeń: nie „możesz wywołać tę funkcję”, ale „wywołujesz tę funkcję w wierszu 247”. BillingService.java, linia 82 AccountProcessor.javai w teście integracyjnym w wierszu 14 BillingServiceTest.java.” Taki poziom szczegółowości jest możliwy dzięki spisowi wywołujących, a nie mogą go zapewnić ogólne ostrzeżenia o wycofaniu.
Podanie ścieżki migracji wraz z powiadomieniem jest niezbędne. Wycofanie powinno obejmować dokumentację funkcji zastępczej, opis wszelkich różnic w zachowaniu między starą a nową implementacją, a w przypadku nietrywialnych zmian – przykład kodu pokazujący stan przed i po. W przypadku użytkowników w bazach kodu innych zespołów, harmonogram migracji powinien być negocjowany jawnie, a nie ogłaszany jednostronnie, ponieważ zespoły te mają własne priorytety i zobowiązania do dostarczenia. Jak wyjaśniono w kontekście refaktoryzacji baz danych w systemach zależnych , stopniowe wprowadzanie użytkowników nowej struktury przed wycofaniem starej jest sposobem, który zapobiega pojawianiu się zmian powodujących przerwy w pracy jako nieoczekiwanych incydentów.
Faza 3: Monitorowanie liczby dzwoniących
Pomiędzy pomiarem bazowym a planowaną datą usunięcia, liczba wywołujących powinna być stale monitorowana. Za każdym razem, gdy wywołujący migruje do funkcji zastępczej, liczba ta maleje. Bramka usuwania jest osiągana, gdy liczba aktywnych wywołujących spada do zera (wywołujące tylko testowe i z martwym kodem mogą być usuwane jednocześnie z samą funkcją). Ciągłe monitorowanie wymaga uruchamiania enumeracji wywołujących zgodnie z harmonogramem w ramach procesu CI, a nie polegania na jednorazowym spisie, który staje się nieaktualny wraz ze zmianami w kodzie.
Monitorowanie wychwytuje również nowe wywołania dodane w okresie wycofywania funkcji. W dużych organizacjach często zdarza się, że nowy kod wywołuje wycofaną funkcję w trakcie migracji, ponieważ programista nie był świadomy wycofania funkcji, przegląd kodu go przeoczył lub automatyczny generator kodu generuje kod wywołujący wycofaną funkcję. Wykrywanie wywołań wycofanej funkcji na poziomie CI, skonfigurowane tak, aby nie występowały w nowych miejscach wywołań, zapobiega wzrostowi liczby wywołań podczas migracji.
Faza 4: Sprawdź kompletność przed usunięciem
Bezpośrednio przed usunięciem funkcji, należy przeprowadzić enumerację wywołujących po raz ostatni, w pełnym zakresie wszystkich znanych baz kodu. Ta ostateczna kontrola pełni funkcję bramki bezpieczeństwa: potwierdza, że liczba wywołujących osiągnęła zero dla aktywnych wywołań i identyfikuje wszelkie dodania na późnym etapie, które nie zostały wykryte przez monitorowanie CI. Na tym etapie inwentaryzacja powinna również zweryfikować brak dynamicznych wywołujących: plików konfiguracyjnych odwołujących się do funkcji za pomocą ciągu znaków, rejestracji opartych na refleksji oraz wszelkich innych pośrednich mechanizmów wywołań zidentyfikowanych podczas wstępnej analizy.
Weryfikacja powinna obejmować graf zależności wszystkich bibliotek współdzielonych lub pakietów, które udostępniają wycofaną funkcję. Jeśli funkcja jest częścią publicznego interfejsu API wykorzystywanego przez podmioty zewnętrzne, harmonogram usuwania musi uwzględniać zewnętrznych konsumentów, którzy mogą nie być dostępni poprzez wewnętrzną analizę kodu. W przypadku systemów wewnętrznych weryfikacja obejmuje każdą indeksowaną bazę kodu. W przypadku publicznie publikowanych interfejsów API weryfikacja obejmuje znany zbiór konsumentów oraz zdefiniowany okres wygaśnięcia, w którym zewnętrzni konsumenci muszą dokonać migracji.
Jak inaczej działa wykrywanie rozmówców w środowiskach starszych i komputerów mainframe
Opisane powyżej wyzwania dotyczą każdego dużego systemu oprogramowania, ale stają się szczególnie dotkliwe w środowiskach mainframe i starszych, ponieważ powiązania między wywołaniami w tych środowiskach są wyrażane za pomocą mechanizmów, których analiza za pomocą nowoczesnych narzędzi do wykrywania wywołujących nie jest możliwa.
W środowiskach COBOL funkcje są wywoływane za pomocą instrukcji CALL, które mogą odwoływać się do celu za pomocą ciągu literałowego, elementu danych zawierającego nazwę programu lub wskaźnika do procedury. Przypadek ciągu literałowego można rozwiązać za pomocą analizy statycznej; przypadek elementu danych wymaga analizy przepływu danych w celu określenia, jaką wartość element danych może posiadać w momencie wywołania; a przypadek wskaźnika do procedury wymaga śledzenia sposobu przypisania wskaźnika. Każdy z tych mechanizmów wywołań pojawia się inaczej w kodzie źródłowym i wymaga innej analizy do rozwiązania.
W środowiskach JCL programy są wywoływane według nazwy w instrukcjach EXEC PGM=. Nazwa programu to ciąg znaków, który mapuje skompilowany moduł w bibliotece load. Śledzenie wywołań programu COBOL za pomocą JCL wymaga analizy składniowej JCL w celu wyodrębnienia nazw programów, mapowania tych nazw na skompilowane programy COBOL, które je implementują, oraz ustalenia, które akapity COBOL w tych programach wywołują przestarzałe narzędzie. To wieloetapowe rozwiązanie całkowicie wykracza poza zakres działania analizatora COBOL ani analizatora JCL działającego w izolacji.
Współdzielone kopie są szczególnie ważnym przypadkiem w środowiskach COBOL. Przestarzały akapit zdefiniowany w kopii może być dołączony do wielu programów za pomocą instrukcji COPY. Akapit nie jest fizycznie duplikowany w każdym programie; jest dołączony w momencie kompilacji. Analiza zliczająca wystąpienia nazwy akapitu w plikach źródłowych bez rozstrzygania inkluzji w kopiach spowoduje zarówno przeliczenie (znalezienie definicji akapitu w samej kopii), jak i zaniżenie (pominięcie faktu, że każdy program zawierający kopię ma dostęp do akapitu). Prawidłowe wykrywanie wywołujących wymaga zrozumienia, które programy zawierają które kopie i które akapity w tych kopiach faktycznie wywołują. Relacja między zakodowanymi na stałe odwołaniami a ich dalszymi odbiorcami ilustruje, dlaczego rozstrzyganie tych relacji wywołań na poziomie programu jest niezbędne przed jakąkolwiek zmianą strukturalną: to, co wydaje się prostym odwołaniem do ciągu znaków, może być jedynym mechanizmem, poprzez który dziesiątki programów osiągają krytyczną funkcjonalność.
W jaki sposób SMART TS XL Buduje kompletny spis dzwoniących
SMART TS XL Konstruuje ujednolicony graf wywołań dla każdego języka, platformy i repozytorium w indeksowanym środowisku. Programy COBOL, strumienie zadań JCL, usługi Java, aplikacje .NET, procedury składowane SQL, skrypty Pythona i inne artefakty źródłowe są analizowane za pomocą analizy specyficznej dla języka w celu utworzenia wspólnego grafu odwołań. Każda funkcja, akapit, procedura, metoda i jednostka programu jest węzłem w tym grafie. Każda relacja wywołania, niezależnie od tego, czy jest to instrukcja COBOL CALL, wywołanie metody Java, JCL EXEC PGM czy SQL EXEC, jest krawędzią typu. Graf reprezentuje pełną topologię wywołań systemu, a nie częściowy widok dla każdego języka.
Gdy funkcja jest oznaczona do usunięcia, SMART TS XLWyliczenie wywołujących w programie 's przechodzi przez graf wywołań przychodzących z węzła funkcji docelowej, zbierając każdego wywołującego na każdym poziomie hierarchii wywołań. Przechodzenie jest rekurencyjne, podążając za grafem przez funkcje opakowujące, warstwy fasady i narzędzia pośrednie, aż dotrze do funkcji bez wywołujących, które reprezentują rzeczywiste punkty wejścia łańcuchów wywołań. Wyniki są uporządkowane według języka, repozytorium, typu wywołującego i głębokości wywołania, co zapewnia zespołowi ustrukturyzowany inwentarz, który oddziela wywołujących bezpośrednio od pośrednich oraz wywołujących aktywnie od wywołujących z martwym kodem.
Funkcja analizy wpływu platformy rozszerza tę funkcję o ustrukturyzowany raport o wpływie zmian: nie tylko wskazuje, które funkcje wywołują funkcję wycofaną, ale także które programy, usługi, zadania wsadowe i procedury JCL są nią dotknięte na każdym poziomie łańcucha zależności. Raport ten jest artefaktem, który umożliwia realizację procesu od wycofania do usunięcia: wskazuje właścicieli, identyfikuje konkretne lokalizacje wywołań i określa zakres migracji wymaganej przed bezpiecznym przeprowadzeniem usuwania. Jak opisano szczegółowo w analizie wpływu w zarządzaniu zmianami w przedsiębiorstwie , możliwość enumeracji dotkniętych komponentów przed wprowadzeniem zmiany strukturalnej jest fundamentalnym wymogiem bezpiecznego działania złożonych, połączonych systemów przedsiębiorstwa.
SMART TS XL Obsługuje również fazę bieżącego monitorowania procesu wycofywania funkcji. Ponieważ graf referencyjny jest stale aktualizowany w miarę indeksowania zmian w kodzie źródłowym, liczba wywołań dla wycofywanej funkcji jest zawsze aktualna. Integracja z potokiem CI pozwala automatycznym kontrolom na eliminowanie błędów przy nowych wywołaniach wycofywanych funkcji, wymuszając dyscyplinę migracji w momencie wprowadzania nowego kodu, zamiast wykrywania naruszeń po fakcie. To połączenie początkowej enumeracji, wskazówek dotyczących migracji i ciągłego monitorowania obejmuje cały cykl życia wycofywanej funkcji, od adnotacji do bezpiecznego usunięcia.
Usuwanie funkcji bez żalu
Różnica między usunięciem funkcji, które przebiega bezproblemowo, a takim, które powoduje awarie produkcyjne, prawie zawsze polega na różnicy w kompletności wykrywania wywołujących. Samo usunięcie jest banalnie proste: usuń definicję i wdróż. Praca polega na przygotowaniu, a przygotowanie jest tak dobre, jak inwentarz wywołujących, na którym się opiera.
W systemach, w których graf wywołań jest płytki, jednojęzyczny i zawarty w jednym repozytorium, hierarchia wywołań IDE i ostrzeżenia kompilatora stanowią wystarczające przygotowanie. W systemach, w których graf wywołań obejmuje wiele języków, wiele repozytoriów, wiele platform i potencjalnie wiele dekad kodu, narzędzia te pokrywają niewielki i niepoznawalny ułamek rzeczywistej powierzchni wywołań. Różnica między tym, co zwracają, a tym, co faktycznie wywołuje funkcję, jest źródłem błędów produkcyjnych.
Celowo stworzona międzyjęzykowa i międzyrepozytoryjna enumeracja wywołujących nie jest udoskonaleniem procesu pracy programisty w zakresie usuwania funkcji. Jest to warunek wstępny bezpiecznego wykonywania tego procesu w dowolnym systemie na tyle złożonym, aby zgromadzić relacje wywołań międzysystemowych, które rutynowo występują w przypadku wycofanych funkcji w bazach kodu przedsiębiorstw. Każda wycofana funkcja, która jest usuwana bez kompletnego spisu wywołujących, to wersja zawierająca nieznaną liczbę błędów środowiska wykonawczego oczekujących na konkretną ścieżkę wykonania prowadzącą do brakującej definicji. Eliminacja tej niewiadomej jest celem strukturalnego wykrywania wywołujących.