Bez (poza) awarii aplikacji, proszę!

Opublikowany: 2020-02-12

Programiści starają się unikać awarii kodu. Jeśli ktoś użyje ich aplikacji, nie powinna się ona zepsuć ani niespodziewanie zakończyć. To jeden z najprostszych pomiarów jakości – jeśli aplikacja często się zawiesza, prawdopodobnie nie jest dobrze wykonana.

Awarie aplikacji występują, gdy program ma zamiar zrobić coś niezdefiniowanego lub złego , na przykład podzielić wartość przez zero lub uzyskać dostęp do ograniczonych zasobów na komputerze. Może to być również zrobione wprost przez programistę, który napisał aplikację. „To się nigdy nie zdarzy, więc to pominę” – to dość powszechne i nie do końca nierozsądne myślenie. Są takie przypadki, które po prostu nie mogą wystąpić, nigdy, dopóki… tak się nie stanie.

Złamane obietnice

Jednym z najczęstszych przypadków, w których wiemy, że coś nie może się zdarzyć, są interfejsy API. Uzgodniliśmy między backendem a frontendem – to jedyna odpowiedź serwera, jaką możesz otrzymać na to żądanie. Opiekunowie tej biblioteki udokumentowali to zachowanie funkcji. Funkcja nie może zrobić nic innego. Oba sposoby myślenia są poprawne, ale oba mogą powodować problemy.

Kiedy korzystasz z biblioteki, możesz polegać na narzędziach językowych, które pomogą Ci poradzić sobie ze wszystkimi możliwymi przypadkami. Jeśli w używanym języku brakuje jakiejkolwiek formy sprawdzania typu lub analizy statycznej, musisz sam się tym zająć. Mimo to możesz to sprawdzić przed wysyłką do środowiska produkcyjnego, więc nie jest to wielka sprawa. To może być trudne, ale czytasz dzienniki zmian przed aktualizacją zależności i pisaniem testów jednostkowych, prawda? Albo użyjesz, albo utworzysz bibliotekę, im bardziej rygorystyczne pisanie możesz zapewnić, tym lepiej dla twojego kodu i innych programistów.

Komunikacja backend-frontend jest nieco trudniejsza. Często jest luźno powiązany, więc zmiany po jednej stronie można łatwo przeprowadzić, nie zdając sobie sprawy, jak wpłynie to na drugą stronę. Zmiana na backendzie często może złamać twoje założenia dotyczące frontendu i oba są często dystrybuowane osobno. To musi się źle skończyć. Jesteśmy tylko ludźmi i czasami zdarza się, że nie rozumiemy drugiej strony lub zapomnieliśmy powiedzieć im o tej małej zmianie. Ponownie, to nie jest wielka sprawa przy prawidłowej obsłudze sieci – odpowiedź dekodowania nie powiedzie się i wiemy, jak sobie z tym poradzić. Nawet najlepszy kod dekodujący może mieć wpływ na zły projekt…

Funkcje cząstkowe. Zły projekt.

„Będziemy mieli tutaj dwie zmienne logiczne: 'isActive' i 'canTransfer', oczywiście nie można przenieść, gdy nie jest ona aktywna, ale to tylko szczegół.” Tutaj zaczyna się nasz zły projekt, który może mocno uderzyć. Teraz ktoś utworzy funkcję z tymi dwoma argumentami i przetworzy na jej podstawie dane. Najprostszym rozwiązaniem jest… po prostu awaria na nieprawidłowym stanie, nigdy nie powinno się to zdarzyć, więc nie powinniśmy się tym przejmować. Czasami nawet się przejmujemy i zostawiamy komentarz, aby naprawić go później lub zapytać, co powinno się stać, ale ostatecznie może zostać wysłany bez wykonywania tego zadania.

 // pseudo kod
function doTransfer(Bool isActive, Bool canTransfer) {
  Jeśli ( isActive i canTransfer ) {
    // zrób coś dla transferu dostępne
  } else if ( not isActive i not canTransfer ) {
    // zrób coś dla transferu niedostępne
  } else if ( isActive i not canTransfer ) {
    // zrób coś dla transferu niedostępne
  } else { // aka ( nie isActive i canTransfer )
    // istnieją cztery możliwe stany
    // to nie powinno mieć miejsca, transfer nie powinien być dostępny, gdy nie jest aktywny
    rozbić się()
  }
}

Ten przykład może wyglądać głupio, ale czasami możesz złapać się w tego rodzaju pułapkę, która jest nieco trudniejsza do zauważenia i rozwiązania niż ta. Skończysz z czymś, co nazywa się funkcją częściową. Jest to funkcja, która jest zdefiniowana tylko dla niektórych możliwych danych wejściowych, ignorując lub zawieszając się z innymi. Należy zawsze unikać funkcji częściowych (proszę zauważyć, że w językach dynamicznie typowanych większość funkcji można traktować jako częściowe). Jeśli twój język nie może zapewnić poprawnego zachowania przy sprawdzaniu typu i analizie statycznej, po pewnym czasie może się zawiesić w nieoczekiwany sposób. Kod ciągle ewoluuje i wczorajsze założenia mogą dziś nie być aktualne.

Porażka szybko. Często zawodzi.

Jak możesz się chronić? Najlepszą obroną jest atak! Jest takie miłe powiedzenie: „Szybko przegrywaj. Często zawodzi. Ale czy nie zgodziliśmy się po prostu, że powinniśmy unikać awarii aplikacji, częściowych funkcji i złego projektu? Erlang OTP daje programistom mityczną przewagę polegającą na tym, że sam się leczy po nieoczekiwanych stanach i aktualizuje podczas działania. Mogą sobie na to pozwolić, ale nie każdy ma taki luksus. Dlaczego więc mielibyśmy zawodzić szybko i często?

Przede wszystkim, aby znaleźć te nieoczekiwane stany i zachowania . Jeśli nie sprawdzisz, czy stan Twojej aplikacji jest poprawny, może to prowadzić do jeszcze gorszych wyników niż awaria!

Po drugie, aby pomóc innym programistom współpracować na tej samej bazie kodu . Jeśli jesteś teraz sam w projekcie, może być ktoś inny po tobie. Możesz zapomnieć o niektórych założeniach i wymaganiach. Dosyć powszechne jest nie czytanie dostarczonej dokumentacji, dopóki wszystko nie działa, lub w ogóle nie dokumentuje się wewnętrznych metod i typów. W tym stanie ktoś wywołuje jedną z dostępnych funkcji z nieoczekiwaną, ale poprawną wartością. Załóżmy na przykład, że mamy funkcję „czekaj”, która przyjmuje dowolną wartość całkowitą i czeka przez tę liczbę sekund. A jeśli ktoś poda do niego „-17”? Jeśli nie ulegnie awarii natychmiast po wykonaniu tej czynności, może to spowodować poważne błędy i nieprawidłowe stany. Czy to czeka w nieskończoność, czy wcale?

Najważniejszą częścią celowego zawieszania się jest robienie tego z wdziękiem . Jeśli zawiesisz aplikację, musisz podać pewne informacje, aby umożliwić diagnozę. Jest to dość łatwe, gdy używasz debugera, ale powinieneś mieć jakiś sposób zgłaszania awarii aplikacji bez niego. Możesz użyć systemów rejestrowania, aby zachować te informacje między uruchomieniami aplikacji lub przeglądać je zewnętrznie.

Drugą najważniejszą częścią celowego zawieszania się jest uniknięcie tego w środowisku produkcyjnym…

Nie zawiedź. Kiedykolwiek.

W końcu wyślesz swój kod. Nie da się tego zrobić perfekcyjnie, często nawet myślenie o zapewnieniu gwarancji poprawności jest zbyt kosztowne. Należy jednak upewnić się, że nie będzie się źle zachowywał ani nie zawieszał. Jak możesz to osiągnąć, skoro już zdecydowaliśmy się rozbijać szybko i często?

Ważną częścią celowego zawieszania się jest robienie tego tylko w środowiskach nieprodukcyjnych . Należy używać potwierdzeń, które są usuwane w kompilacjach produkcyjnych aplikacji. Pomoże to w rozwoju i pozwoli wykryć problemy, nie wpływając na użytkowników końcowych. Jednak czasami lepiej jest zawiesić się, aby uniknąć nieprawidłowych stanów aplikacji. Jak możemy to osiągnąć, jeśli wykonaliśmy już funkcje cząstkowe?

Uniemożliwić przedstawienie niezdefiniowanych i nieprawidłowych stanów i powrót do prawidłowych stanów w inny sposób. To może wydawać się proste, ale wymaga dużo przemyślenia i pracy. Bez względu na to, ile to jest, zawsze jest to mniej niż szukanie błędów, wprowadzanie tymczasowych poprawek i… denerwowanie użytkowników. Automatycznie zmniejszy prawdopodobieństwo wystąpienia niektórych funkcji częściowych.

 // pseudo kod
function doTransfer(stan stanu) {
  przełącznik ( stan ) {
    case State.canTransfer {
      // zrób coś dla transferu dostępne
    }
    case State.cannotTransfer {
      // zrób coś dla transferu niedostępne
    }
    sprawa Stan.nieaktywny {
      // zrób coś dla transferu niedostępne
    }
    // Nie można przedstawić transferu jako dostępnego bez bycia aktywnym
    // są tylko trzy możliwe stany
  }
}

Jak uniemożliwić nieprawidłowe stany? Wybierzmy dwa z poprzednich przykładów. W przypadku naszych dwóch zmiennych binarnych 'isActive' i 'canTransfer' możemy zmienić te dwie w jedno wyliczenie. Będzie on wyczerpująco reprezentować wszystkie możliwe i ważne stany. Nawet wtedy ktoś może wysyłać niezdefiniowane zmienne, ale jest to o wiele łatwiejsze w obsłudze. Będzie to nieprawidłowa wartość, która nie zostanie zaimportowana do naszego programu, zamiast przekazania nieprawidłowego stanu, co utrudni wszystko.

Naszą funkcję oczekiwania można również ładnie ulepszyć w przypadku silnie wpisanych języków. Możemy zmusić go do używania tylko liczb całkowitych bez znaku na wejściu. Samo to rozwiąże wszystkie nasze problemy, ponieważ nieprawidłowe argumenty zostaną usunięte przez kompilator. Ale co, jeśli twój język nie ma typów? Mamy kilka możliwych rozwiązań. Po pierwsze – po prostu crash, ta funkcja jest niezdefiniowana dla liczb ujemnych i nie będziemy robić rzeczy nieważnych lub niezdefiniowanych. Podczas testów będziemy musieli znaleźć nieprawidłowe użycie. Bardzo ważne będą tutaj testy jednostkowe (które i tak powinniśmy zrobić). Po drugie – może to być ryzykowne, ale w zależności od kontekstu może być przydatne. Możemy wrócić do prawidłowych wartości, zachowując asercję w kompilacjach nieprodukcyjnych, aby naprawić nieprawidłowe stany, jeśli to możliwe. Może to nie być dobre rozwiązanie dla takich funkcji, ale jeśli zamiast tego podamy wartość bezwzględną liczby całkowitej, unikniemy awarii aplikacji. W zależności od konkretnego języka, dobrym pomysłem może być również zgłoszenie/zgłoszenie jakiegoś błędu/wyjątku. Jeśli to możliwe, warto się wycofać, nawet jeśli użytkownik zobaczy błąd, jest to znacznie lepsze doświadczenie niż awaria.

Weźmy jeszcze jeden przykład. Jeśli stan danych użytkownika w Twojej aplikacji frontendowej może być w niektórych przypadkach nieprawidłowy, może być lepiej wymusić wylogowanie i ponownie pobrać prawidłowe dane z serwera, zamiast ulegać awarii. Użytkownik może i tak zostać do tego zmuszony lub zostać złapany w niekończącą się pętlę awaryjną. Po raz kolejny – w takich sytuacjach w środowiskach nieprodukcyjnych powinniśmy asertować i zawieszać się, ale nie pozwól, aby Twoi użytkownicy byli zewnętrznymi testerami.

Streszczenie

Nikt nie lubi zawieszających się i niestabilnych aplikacji. Nie lubimy ich tworzyć ani używać. Szybkie niepowodzenie z twierdzeniami, które zapewniają użyteczną diagnozę podczas tworzenia i testów, szybko wykryją wiele problemów. Powrót do prawidłowych stanów w środowisku produkcyjnym sprawi, że Twoja aplikacja będzie o wiele bardziej stabilna. Uniemożliwienie przedstawienia nieprawidłowych stanów wyeliminuje całą klasę problemów. Daj sobie trochę więcej czasu na zastanowienie się przed opracowaniem, jak pozbyć się nieprawidłowych stanów i cofnąć się do nich, a także trochę więcej podczas pisania, aby uwzględnić pewne asercje. Już dziś możesz zacząć ulepszać swoje aplikacje!

Czytaj więcej:

  • Projekt według umowy
  • Algebraiczny typ danych