Zapachy kodu: czym są i jak wiążą się z długiem technicznym

Zapachy kodu: czym są i jak wiążą się z długiem technicznym

Zapachy kodu to nie błędy. Program z błędami ulega awarii, zwraca błędne wyniki lub nie przechodzi testów. Program z zapachami kodu może działać bez zarzutu przez lata, a jednak każda zmiana kosztuje więcej niż powinna, każda nowa funkcja niesie ze sobą nieoczekiwane ryzyko, a każda próba refaktoryzacji ujawnia zależności, o których istnieniu nikt nie wiedział. Zapachy kodu to strukturalne cechy kodu, które przewidują przyszłe problemy: nie powodują natychmiastowej awarii, ale sprawiają, że każda przyszła zmiana jest trudniejsza, wolniejsza i bardziej niebezpieczna niż to konieczne.

Termin ten został spopularyzowany przez Martina Fowlera i Kenta Becka w książce Fowlera „Refactoring: Improving the Design of Existing Code ” (1999), w której skatalogowano 22 nazwane „smug kodu” i przypisano każdemu z nich odpowiednią technikę refaktoryzacji. Katalog ten pozostaje kanonicznym odniesieniem, a „smug” wymienione przez Fowlera, takie jak „Long Method”, „God Class”, „Duplicated Code”, „Feature Envy”, „Divergent Change”, „Shotgun Surgery” i inne, pojawiają się dziś w zestawach reguł SonarQube, narzędziach do analizy statycznej i listach kontrolnych do przeglądu kodu w całej branży.

Wyczyść zapachy kodu

SMART TS XL pomaga je mapować i naprawiać w złożonych systemach.

Więcej informacji

Czym jest zapach kodu?

Zapach kodu to powierzchowna cecha kodu źródłowego, która sugeruje głębszy problem strukturalny lub projektowy. Kod kompiluje się, przechodzi testy i generuje poprawne wyniki, ale coś w jego strukturze sprawia, że ​​jest trudniejszy do odczytania, rozszerzenia lub bezpiecznej modyfikacji niż powinien. Definicja Fowlera: „powierzchowna oznaka, która zazwyczaj odpowiada głębszemu problemowi w systemie”.

Zapachy kodu nie są naruszeniami w tym samym sensie co błąd składniowy czy nieudana asercja. Są to wskaźniki, wzorce, które doświadczeni programiści rozpoznają jako sygnały ostrzegawcze, nawet gdy nie widać natychmiastowego błędu. Niebezpieczeństwo polega na tym, że się kumulują: pojedyncza, długa metoda w 10 000-linijkowej bazie kodu to drobna niedogodność. Setki długich metod, zduplikowana logika rozproszona w dziesiątkach modułów i klasy God Classes w centrum grafu zależności to system, który stał się naprawdę trudny do bezpiecznej zmiany.

Zapachy kodu kontra błędy kontra dług techniczny

Te trzy koncepcje są ze sobą powiązane, ale odrębne, a ich mylenie prowadzi do niewłaściwego ustalenia priorytetów:

PojęcieDefinicjaNatychmiastowa porażka?Jak znaleźć
BugKod, który powoduje nieprawidłowe zachowanieTak, testy kończą się niepowodzeniem, użytkownicy zgłaszają błędyTestowanie, monitorowanie, dzienniki błędów
Zapach koduWzorzec strukturalny przewidujący przyszłe problemyNie, kod działa poprawniePrzegląd kodu, analiza statyczna
Dług technicznySkumulowany koszt przeszłych skrótów i złych decyzjiNie, ale z czasem się to łączyMetryki, analiza złożoności, szacunki nakładu pracy na refaktoryzację

Zapachy kodu to mechanizm akumulacji długu technicznego. Każda długa metoda dodana do bazy kodu to jednostka zaciągniętego długu technicznego; jej odsetki to dodatkowy czas, jaki każdy przyszły programista poświęca na jej zrozumienie, oraz każda przyszła zmiana, której celem jest uniknięcie skutków ubocznych wynikających z jej rozmiaru.

Czym jest kod zapachowy w SonarQube?

SonarQube klasyfikuje problemy z kodem na trzy kategorie: błędy (zdecydowanie błędne), luki w zabezpieczeniach (problemy bezpieczeństwa) i zapachy kodu (problemy z utrzymaniem). Zapachy kodu w SonarQube są bezpośrednio odwzorowywane na katalog Fowlera i obejmują reguły dotyczące długich metod (powyżej konfigurowalnych progów wierszy), zduplikowanych bloków, zbyt dużej liczby parametrów, złożonych wskaźników złożoności poznawczej, brakującej obsługi błędów oraz naruszeń sprzężenia architektonicznego. Reguły zapachu kodu w SonarQube to najszerzej stosowana w branży zautomatyzowana operacjonalizacja oryginalnej taksonomii Fowlera.

Zapachy kodu Martina Fowlera: klasyczna taksonomia

Oryginalne 22 zapachy kodu Fowlera, uporządkowane według kategorii, pozostają standardowym punktem odniesienia. Zestaw reguł każdego głównego narzędzia do analizy statycznej pochodzi z tej taksonomii.

Kategoria Zapachy kodu
Wzdęciakod, który osiągnął nieporęczny rozmiarDługa metoda, duża klasa, obsesja prymitywna, długa lista parametrów, skupiska danych
Osoby nadużywające orientacji obiektowej, niewłaściwe wykorzystanie zasad OOInstrukcje Switch, Pole tymczasowe, Odrzucony zapis, Klasy alternatywne z różnymi interfejsami
Czynniki zapobiegające zmianom, utrudniać zmianęZmiana rozbieżna, operacja typu shotgun, równoległe hierarchie dziedziczenia
Niezbędne, niepotrzebny kodKomentarze (nadmierne), Duplikat kodu, Leniwa klasa, Klasa danych, Martwy kod, Ogólność spekulatywna
Sprzęgła, nadmierne sprzężenieZazdrość, nieodpowiednia intymność, łańcuchy wiadomości, pośrednik

Zrozumienie, do której kategorii należy dany zapach, pomaga ustalić priorytety działań naprawczych: czynniki wzmagające i uniemożliwiające zmiany bezpośrednio korelują z wysokimi kosztami refaktoryzacji; czynniki sprzęgające bezpośrednio korelują z kruchością architektury; czynniki zbędne są najbezpieczniejsze do usunięcia.

Najczęstsze zapachy kodów: krótki przewodnik

Zapach koduJak to wyglądaRyzyko pierwotne
Zduplikowany kodTa sama logika pojawia się w wielu miejscachPoprawki błędów muszą być stosowane wszędzie, kopie różnią się z czasem
Długa metodaMetody przekraczające 20-30 wierszy z wieloma obowiązkamiDuże obciążenie poznawcze; trudno testować izolowane zachowania
Klasa Boga / Duża klasaJedna klasa, która robi wszystkoKażda zmiana funkcji dotyczy tej samej klasy: konfliktów scalania, kruchości
Długa lista parametrówMetody przyjmujące 4+ parametrówŁatwo przekazać błędne wartości, trudno odczytać miejsca wywołań
Zazdrość o funkcjęMetoda wykorzystująca dane innej klasy w większym stopniu niż własneŚcisłe sprzężenie; zmiana w jednej klasie powoduje przerwanie drugiej
Zmiana rozbieżnaJedna klasa zmodyfikowana z wielu różnych powodówNarusza zasadę odpowiedzialności indywidualnej; nieprzewidywalne skutki uboczne
Operacja strzelbowaJedna zmiana wymaga edycji w wielu klasachWysokie koszty zmian; łatwo przeoczyć wystąpienie
Martwy kodKod, który nigdy nie jest wywoływany ani osiąganyDezorientuje programistów, gromadzi się przez lata, komplikuje migrację
Pierwotna obsesjaKorzystanie z typów podstawowych (ciągi znaków, inty) zamiast obiektów domenyWalidacja rozproszona wszędzie; słaba ekspresja
Grupy danychTa sama grupa pól przekazywana wielokrotnie razemPowinien być obiektem domeny; sygnały nie posiadają abstrakcji
Ogólność spekulatywnaKod napisany z myślą o wyobrażonych przyszłych potrzebachNiepotrzebna złożoność; nikt nie rozumie, dlaczego tam jest
Niespójna obsługa błędówCiche łapanie, różne strategie wyjątkówAwarie nie są wykrywane, debugowanie trwa znacznie dłużej

Definicje i przykłady zapachu kodu

Zduplikowany kod

Najczęstszy i najdroższy problem z kodem w dużych systemach. Duplikacja powstaje w wyniku kopiowania i wklejania, presji czasu oraz pracy zespołów w silosach, które niezależnie rozwiązują ten sam problem. Bezpośrednią konsekwencją jest podatek konserwacyjny: każda zmiana współdzielonej logiki musi zostać zastosowana do każdej kopii.

Jawa

// ServiceA -- discount calculation
double calculateDiscount(double amount) {
    if (amount > 1000) return amount * 0.1;
    return 0;
}

// ServiceB -- same logic, copied and forgotten
double computeDiscount(double value) {
    if (value > 1000) return value * 0.1;
    return 0;
}

Gdy reguła biznesowa ulega zmianie (próg wzrasta do 1500, a stawka do 12%), jedna kopia zostaje zaktualizowana, a druga nie. Dwa moduły nie zgadzają się teraz co do fundamentalnej logiki biznesowej, a rozbieżność ujawnia się w środowisku produkcyjnym podczas audytu, a nie podczas testów.

Poprawka : Wyodrębnij logikę współdzieloną do pojedynczej funkcji, klasy narzędziowej lub biblioteki współdzielonej, do której odwołują się obaj wywołujący.

Długa metoda

Metoda, która z czasem rozrosła się poza swoje pierwotne przeznaczenie, absorbując dodatkowe obowiązki. Obciążenie poznawcze związane z czytaniem metody składającej się z 200 wierszy kodu różni się jakościowo od czytania dwudziestu metod składających się z 10 wierszy kodu, a nie tylko ilościowo. Długie metody są trudne do przetestowania, ponieważ wykonują zbyt wiele czynności, aby testować je w izolacji, i trudne do zrozumienia, ponieważ czytelnik musi zachować cały kontekst wykonania w pamięci roboczej.

Próg wykrywalności : Metody powyżej 20-30 wierszy wymagają weryfikacji; powyżej 50 wierszy refaktoryzacja jest prawie zawsze uzasadniona. W COBOL-u akapity przekraczające 100 poleceń są równoważne.

pyton

class OrderProcessor:
    def process_order(self, order):
        # Validate order -- 40 lines
        # Calculate discounts -- 30 lines
        # Update inventory -- 25 lines
        # Send notification emails -- 20 lines
        # Generate invoice -- 35 lines
        # 150+ lines total
        pass

Każda odpowiedzialność w tej metodzie powinna być oddzielną klasą lub funkcją. Ich połączenie oznacza, że ​​każda przyszła aktualizacja fakturowania, stanu zapasów lub powiadomień grozi destabilizacją całego procesu przetwarzania zamówień.

Klasa Boga

Klasa, która skumulowała obowiązki w wielu domenach, naruszając w tak poważny sposób zasadę pojedynczej odpowiedzialności, że staje się centrum ciężkości bazy kodu: wszystko od niej zależy, a wprowadzenie w niej jakichkolwiek zmian wymaga zrozumienia wszystkich jej aspektów.

Sygnał wykrycia : Klasa zawierająca ponad 20–30 metod publicznych lub taka, której nazwa zawiera słowa kluczowe „Manager”, „Processor”, „Handler”, „Utils” lub „Helper” stosowane do wielu niezwiązanych ze sobą domen.

Zmiana rozbieżna

Klasa, która jest modyfikowana z wielu różnych, niezwiązanych ze sobą powodów. Za każdym razem, gdy zmienia się schemat bazy danych, edytujesz tę klasę. Za każdym razem, gdy zmieniają się zasady cenowe, edytujesz tę klasę. Za każdym razem, gdy zmienia się format powiadomień, edytujesz tę klasę. Ta klasa ma zbyt wiele obowiązków i powinna zostać podzielona.

Definicja : Jedna klasa, która ciągle się zmienia z różnych powodów. Odwrotność chirurgii strzelbowej.

Operacja strzelbowa

Pojedyncza zmiana koncepcyjna, która wymaga edycji w wielu różnych klasach. Zmiana stawki podatkowej wymaga modyfikacji obliczeń back-endowych, walidacji front-endowych, wyzwalacza bazy danych, zadania wsadowego i zapytania raportowego w pięciu różnych miejscach. Pominięcie któregokolwiek z nich powoduje niespójne działanie.

sql

-- Tax logic duplicated across queries
SELECT amount * 0.05 FROM invoices;
SELECT amount * 0.05 FROM payments;
SELECT amount * 0.05 FROM reports;

Zmiana z 0.05 na 0.07 wymaga teraz znalezienia każdego wystąpienia w plikach SQL, procedurach składowanych i kodzie aplikacji.

Zazdrość o funkcję

Metoda, która spędza więcej czasu korzystając z danych i metod innej klasy niż z własnych. Sygnalizuje to, że zachowanie prawdopodobnie należy do innej klasy.

Jawa

// In ReportGenerator -- envious of Customer's data
double calculateCustomerRating(Customer customer) {
    return customer.getOrderCount() * customer.getAverageOrderValue()
           / customer.getDaysSinceRegistration();
}
// This logic belongs in Customer, not ReportGenerator

Martwy kod

Kod, który istnieje w repozytorium, ale nigdy nie jest wywoływany przez żadną ścieżkę wykonania w środowisku produkcyjnym. Martwy kod gromadzi się latami, ponieważ funkcje są usuwane, zastępowane lub restrukturyzowane bez usuwania starego kodu. Powoduje to zakłócenia w przeglądzie kodu, dezorientuje programistów w procesie wdrażania do bazy kodu, komplikuje analizę migracji i czasami jest przypadkowo reaktywowany.

Wykrywanie:Narzędzia do analizy statycznej, w tym SonarQube, Knip (dla TypeScript/JavaScript) i SMART TS XL zidentyfikuj niedostępne funkcje, niewywołane metody i nieużywane zmienne w całej bazie kodu.

Naruszenia zasady DRY

Zasada DRY (Don't Repeat Yourself) głosi, że każdy element wiedzy musi mieć pojedynczą, jednoznaczną reprezentację w systemie. Naruszenia DRY są główną przyczyną duplikacji kodu, skupisk danych i wielu scenariuszy operacji typu shotgun. Gdy logika biznesowa jest reprezentowana w wielu miejscach, reprezentacje te nieuchronnie się rozchodzą. DRY to zasada; duplikacja kodu to zapach wskazujący na naruszenie.

pyton

# DRY violation: same validation logic in three places
def validate_email_in_registration(email):
    return "@" in email and "." in email

def validate_email_in_profile_update(email):
    return "@" in email and "." in email

def validate_email_in_checkout(email):
    return "@" in email and "." in email

# DRY-compliant: one function, three callers
def is_valid_email(email):
    return "@" in email and "." in email

Progi detekcji: kiedy kod staje się zapachem?

Wykrywanie zapachu kodu wymaga mierzalnych progów. Poniżej przedstawiono powszechnie używane wskaźniki i wartości wskazujące na zapach wymagający uwagi:

metrycznyCo mierzyPróg ostrzegawczyPróg krytyczny
Złożoność cyklomatycznaLiczba gałęzi decyzyjnych w metodziePrzede 10Przede 20
Metoda Długość (linie)Liczba wierszy w metodzie/funkcjiPrzede 20Przede 50
Liczba parametrówLiczba parametrów akceptowanych przez metodęPrzede 4Przede 7
Długość zajęćLiczba wierszy w klasiePrzede 200Przede 500
Współczynnik duplikacjiProcent kodu, który jest zduplikowanyPowyżej 3%Powyżej 10%
Złożoność poznawczaJak trudny jest do zrozumienia kodPrzede 15Przede 25
Sprzężenie aferentne (Ca)Liczba klas zależnych od tej klasyPrzede 15Przede 30
Sprzęgło eferentne (Ce)Liczba zajęć w tej klasie zależy odPrzede 15Przede 30

Progi te można konfigurować w SonarQube, a większość platform do analizy statycznej umożliwia tworzenie niestandardowych reguł opartych na tych metrykach. Klasy i metody progów krytycznych to cele refaktoryzacji o najwyższym priorytecie: są najbardziej prawdopodobnymi źródłami przyszłych defektów i najbardziej kosztownymi w utrzymaniu komponentami.

Narzędzia do wykrywania zapachu kodu

Automatyczne wykrywanie to jedyne skalowalne podejście do identyfikacji podejrzanych kodów w dużych bazach kodu. Ręczny przegląd wychwytuje jedynie ułamek tego, co wykrywają narzędzia automatyczne, a ręczny przegląd nie jest skalowalny do starszych systemów z milionami wierszy kodu.

NarzędziePodstawowy językCo wykrywa
SonarQube / SonarCloudJava, Python, JS/TS, C# i innePełna taksonomia zapachów Fowlera, punkty zapalne bezpieczeństwa, duplikacje
Checkstyle + PMDJavaNaruszenia stylu, duplikaty, metryki złożoności
ESLint + typescript-eslintJavaScript, TypeScriptDługie funkcje, złożoność, nieużywany kod
Pylint + RadonPythonWskaźnik złożoności, stylu i łatwości utrzymania
ReSharper / RiderC#Nadmiarowy kod, długie metody, problemy ze sprzężeniem
ClippyRdzaNaruszenia idiomatyczne, typowe wzorce będące zapachami kodu w Rust
kodKlimatWielojęzyczneZłożoność, duplikacja, wynik łatwości utrzymania
SMART TS XLCOBOL, JCL, Java, Python, RPG, SQL, .NETDuplikacja międzyjęzykowa, martwy kod, sprzężenie, dryf zależności

W Rust kod śmierdzi są wychwytywane głównie przez Clippy, który wymusza idiomatyczne wzorce Rust. Do najczęstszych zapachów specyficznych dla Rust należą: niepotrzebne klonowanie, niewłaściwe użycie unwrap() w ścieżkach produkcyjnych, nadmiernie zagnieżdżonych wyrażeniach dopasowania i funkcjach, które powinny zwracać Result ale zamiast tego użyj paniki.

Zapachy kodu i dług techniczny: powiązanie

Dług techniczny to skumulowany koszt wcześniejszych decyzji, które faworyzowały szybkość nad jakością. Błędy w kodzie to mechanizm, poprzez który ten dług manifestuje się w strukturze kodu. Zależność jest bezpośrednia: każdy nieadresowany błąd w kodzie to jednostka długu technicznego, a stopa procentowa to dodatkowy czas, jaki każda przyszła zmiana musi poświęcić na jego obejście.

Jak opisano w kontekście analizy wpływu na zarządzanie zmianą oprogramowania , problemy strukturalne, o których wspomina zapach kodu, takie jak nadmierne powiązania, zduplikowana logika, akumulacja martwego kodu, bezpośrednio zwiększają zakres każdej zmiany, ponieważ utrudniają wyizolowanie wpływu danej zmiany.

Wyjaśnij dług techniczny w kategoriach „smrodu kodu” : jeśli baza kodu ma 40% duplikacji, każda poprawka błędu kosztuje 1.4 raza więcej niż powinna. Jeśli podstawowa klasa przetwarzania jest klasą boską, od której wszystko zależy, każde dodanie funkcji wymaga zrozumienia i przetestowania całej klasy. Jeśli obsługa błędów jest niespójna, każdy incydent produkcyjny wymaga więcej czasu na analizę, ponieważ sygnały awarii są zawodne. Dług techniczny nie jest abstrakcyjny, lecz sumą tych narastających nieefektywności.

Badania CISQ konsekwentnie wskazują, że programiści spędzają 30-40% swojego czasu na pracy nad długiem technicznym, zamiast tworzyć nowe funkcjonalności. Gęstość zapachu kodu jest najbardziej bezpośrednim miernikiem tego, ile długu się nagromadziło.

W jaki sposób SMART TS XL Wykrywa zapachy kodu w skali przedsiębiorstwa

Poszczególne narzędzia, takie jak SonarQube i Clippy, działają w ramach jednego języka. W środowiskach korporacyjnych, gdzie programy COBOL zapisują dane do zestawów danych odczytywanych przez usługi Java, gdzie strumienie zadań JCL wywołują programy w wielu językach, a ta sama logika biznesowa została niezależnie zduplikowana w trzech różnych systemach napisanych w ciągu trzech różnych dekad, narzędzia jednojęzyczne nie są w stanie zapewnić pełnego obrazu.

SMART TS XL'S statyczna analiza kodu wykrywa jednocześnie nieprawidłowe kody we wszystkich językach w środowisku: zduplikowaną logikę między kopią COBOL a klasą narzędziową Java, martwy kod w programach RPG, którego nie wywołuje żadne zadanie JCL, wzorce God Class w programach COBOL, gdzie pojedynczy akapit wykonuje pracę pięćdziesięciu, oraz niespójne wzorce obsługi błędów w różnych językach.

Możliwość mapowania zależności aplikacji identyfikuje architektoniczne problemy, których poszczególne narzędzia na poziomie plików nie widzą: które komponenty mają najsilniejsze sprzężenie aferentne (od których jest najwięcej zależności, najwyższe ryzyko uszkodzenia czegoś w przypadku wprowadzenia zmian), gdzie występują zależności cykliczne między modułami, które powinny być niezależne, oraz gdzie zduplikowana logika biznesowa była niezależnie utrzymywana w różnych systemach, bez świadomości istnienia drugiej kopii.

Funkcja analizy wpływu umożliwia wdrożenie działań w przypadku wystąpienia nieprzyjemnych zapachów: przed refaktoryzacją jakiegokolwiek komponentu o wysokim stopniu sprzężenia, analiza wpływu wylicza każdy zależny komponent, który wymaga przetestowania, walidacji lub aktualizacji. To przekształca „paraliż refaktoryzacji”, którego zespoły doświadczają w dużych, „śmierdzących” bazach kodu, w ustrukturyzowany, zakresowy program naprawczy, w którym każda zmiana ma zdefiniowany zakres, a nie nieznane ryzyko.

W przypadku zespołów realizujących programy modernizacji starszych wersji oprogramowania, analiza zapachu kodu stanowi podstawę planu modernizacji: martwy kod jest eliminowany przed rozpoczęciem migracji (zmniejszając jej zakres), zduplikowana logika jest konsolidowana w kanoniczne implementacje, komponenty o największym powiązaniu są modernizowane na końcu (po zajęciu się wszystkim, co od nich zależy), a klasy Boga są rozkładane przed konwersją na nowy język, ponieważ konwersja klasy Boga do języka Java powoduje utworzenie klasy Boga w języku Java.

Rozwiązywanie problemów z zapachami kodu: Struktura priorytetyzacji

Nie wszystkie smrody kodu wymagają natychmiastowej refaktoryzacji. Właściwym podejściem jest priorytetyzacja oparta na ryzyku:

Priorytet 1: Błędy w komponentach o wysokiej częstotliwości zmian. Kod, który często się zmienia i charakteryzuje się wysoką złożonością lub sprzężeniem, generuje najwięcej defektów. Te komponenty generują najwyższy koszt w przeliczeniu na zmianę i generują najwięcej incydentów produkcyjnych. Napraw je w pierwszej kolejności.

Priorytet 2: Zapachy na granicach architektonicznych. Klasy Boga i komponenty o wysokim stopniu sprzężenia, od których wszystko zależy, są najbardziej niebezpieczne do zmiany, ale też najdroższe do pozostawienia bez zmian. Wymagają one najdokładniejszej analizy wpływu przed refaktoryzacją.

Priorytet 3: Duplikacja kodu w różnych systemach. Gdy ta sama logika biznesowa istnieje w wielu systemach, zmiany muszą być skoordynowane we wszystkich kopiach jednocześnie. Konsolidacja tej duplikacji zmniejsza obciążenie związane z koordynacją i zapobiega rozbieżnościom.

Priorytet 4, Usuwanie martwego kodu. Martwy kod to najbezpieczniejsza kategoria, którą należy uwzględnić: jego usunięcie nie może zakłócić działania, a jedynie ujawnić wcześniej ukryte zależności. Należy go usunąć przed jakąkolwiek migracją lub konwersją, aby uniknąć marnowania czasu na konwersję kodu, który nigdy nie zostanie wywołany.

Priorytet 5, Styl i zapachy strukturalne w obszarach niskiego ryzyka. Długie metody i listy parametrów w stabilnym kodzie o niskim współczynniku zmian można rozwiązać w sposób doraźny, gdy sąsiedni kod wymaga zmiany z innych powodów, jednocześnie refaktoryzując otaczające zapachy.

Dyscyplina polegająca na systematycznym wykrywaniu, mierzeniu i reagowaniu na problemy z kodem, które już spowodowały awarię produkcji, a nie na reaktywnym ich wykrywaniu, odróżnia zespoły programistyczne, które utrzymują stałe tempo dostarczania oprogramowania, od tych, które stopniowo zwalniają wraz z rozwojem systemów.