Nasza strona używa cookies. Korzystając ze strony, wyrażasz zgodę na używanie cookies, zgodnie z aktualnymi ustawieniami przeglądarki. Rozumiem

Fail-fast jako jedna z najbardziej przydatnych technik programowania

Dowiedz się, dlaczego filozofia fail-fast w programowaniu może być bardziej przydatna niż pisanie dobrych testów.

To jest część druga z serii artykułów o fail-fast - moim zdaniem jednej z najbardziej przydatnych technik programowania, bardziej nawet pożytecznych niż pisanie testów. W tym artykule będę kontynuował temat zakładając, że zapoznałeś się z częścią pierwszą serii. Jeżeli tego nie uczyniłeś, to proszę zrób to, zanim przejdziesz do kolejnego akapitu na tej stronie. Pierwszą część tej serii znajdziesz tutaj.


O jakie dwie magiczne linijki kodu chodziło?

Otóż załóżmy, że w naszym systemie istnieje klasa logiki biznesowej o nazwie Nadplata. Ta klasa ma wiele pól, ale dla naszych rozważań istotne są tylko trzy: id, amount oraz hasZwrotBeenIssued. Posiada też wiele metod, ale nas interesuje tylko jedna: createZwrot. Oto kod źródłowy tej klasy... oczywiście tylko fragmentów istotnych dla naszych rozważań.

@Entity
@Table(name = "NADPLATY")
@Access(AccessType.PROPERTY)
public class Nadplata
{
	private Long id;
	private Integer version;

	private BigDecimal amount;
	private boolean hasZwrotBeenIssued;

	// Pozostałe pola pominięte ze względu na zwięzłość

	// Konstruktory również pominięte
	
	public Zwrot createZwrot()
	{
		final Zwrot result = new Zwrot(amount);
		
		this.hasZwrotBeenIssued = true;
		
		return result;
	}

	// Pozostałe metody również pominięte
}


Jak widzisz metoda createZwrot jest publiczna. W każdym miejscu w kodzie można ją bez problemu wywołać, bez żadnych ograniczeń np. dwa razy z rzędu dla tej samej nadpłaty. Co więcej, każdy może wywołać tę metodę... bez względu na iloraz inteligencji. Ta metoda jest po prostu bezbronna. Jest na łasce developerów, którzy będą z niej korzystać. Dwie linijki kodu, które nie dopuściłyby do fatalnego scenariusza opisanego w części pierwszej tej serii mogłyby wyglądać np. tak:

public Zwrot createZwrot()
{
	if ( hasZwrotBeenIssued )
		throw new IllegalStateException();
	
	final Zwrot result = new Zwrot(amount);
	
	this.hasZwrotBeenIssued = true;
	
	return result;
}


Co robią te dwie linijki? Sprawdzają, czy dla danej nadpłaty zwrot został już utworzony. Jeżeli został, to generowany jest wyjątek i proces tworzenia zwrotu jest natychmiast przerywany (to jest właśnie fail-fast). Jeżeli zaś zwrot dla danej nadpłaty nie został utworzony, proces tworzenia zwrotu jest kontynuowany bez przeszkod. Proste? Prościej się nie da.

Gdyby tylko te dwie linijki kodu znalazły się w metodzie createZwrot w klasie Nadplata, wypadki potoczyłyby się zupełnie inaczej. Oto jak by się potoczyły.

  1. Stworzenie zwrotu dla nadpłaty, dla której zwrot został już stworzony, nigdy by się nie wydarzyło - innymi słowy ACME nie przelałoby 2 milionów euro osobom i instytucjom, którym te pieniądze się nie należały.
  2. W konsekwencji 67 osób z ACME nie musiałoby przez cały tydzień wystawiać 8000 pism wyjaśniających.
  3. Moduł zwrotów nigdy nie zostałby zatrzymany, gdyż dla nadpłat, dla których nie wygenerowano dotąd zwrotu, zwroty byłyby nadal tworzone - innymi słowy ACME nie straciłoby 500 tysięcy euro z tytułu odsetek.
  4. Bilans miesiąca policzyłbym się prawidłowo, gdyż w ogóle nie doszłoby do utrwalenia w bazie danych uszkodzonych danych.
  5. Wobec tego główny analityk, Piotrek (to ten, co wydevelopował funkcjonalność tworzenia bilansu miesiąca) oraz developer baz danych (to ten co razem z głównym analitykiem poprawiał dane) - żaden z nich, w ogóle nie byłby w ten temat zaangażowany.
  6. Michał nie straciłby pracy.
  7. Nikt nie siedziałby po godzinach!


Hmm.... nieźle jak na dwie linijki kodu, których dodanie zajęłoby... mniej niż 5 sekund.

Będziemy musieli podrążyć jeszcze ten przykład (zapewne w kolejnej części tej serii). Jest tutaj bowiem jeszcze kilka rzeczy, które warto omówić... najpierw jednak odrobina teorii.


Krótka rozkmina teoretyczna

Dlaczego fail-fast pomaga?

Oto bardzo krótkie wyjaśnienie. Wyobraźmy sobie, że mamy taki oto kodzik:

class ClassA {
  private final ClassB classB;
	
  public void methodA(final int arg1, final int arg2)
  {
    // walidacja argumentów wejściowych
    // walidacja stanu (tj. pól) obiektu

    ...
    final BigDecimal var1 = ...;
    final BigDecimal var2 = ...;
    final BigDecimal var3 = ...;
    
    classB.methodB(var1, var2, var3);
    ...
  }
}
public class ClassB
{
  private final ClassC classC;

  void methodB(final BigDecimal arg1, final BigDecimal arg2,
               final BigDecimal arg3)
  {
    // walidacja argumentów wejściowych
    // walidacja stanu (tj. pól) obiektu

    ...
    final Money kwotaOdsetek = ...;

    classC.methodC(kwotaOdsetek);
    ...
  }
}
public class ClassC
{
  private Money odsetki;

  void methodC(final Money odsetki)
  {
    // walidacja argumentów wejściowych
    // walidacja stanu (tj. pól) obiektu

    this.odsetki = odsetki;
  }
}


Załóżmy też, że w toku wykonywania tego kodu methodC rzuciła wyjątek w czasie walidacji argumentu wejściowego odsetki. Pytanie brzmi, gdzie jest błąd? Zastanów się chwilę. Żeby ułatwić Ci rozkminę podpowiem Ci, że masz do wyboru jedną z poniższych odpowiedzi:

  1. bug jest w metodzie methodA
  2. bug jest w metodzie methodB


And the winner is... odpowiedź nr 2.

Dlaczego tak jest? Dlaczego akurat w metodzie methodB, a nie w methodA? Otóż dlatego, że methodB zwalidowała swoje argumenty wejściowe oraz stan (czyli pola) obiektu i nie wykryła żadnych nieprawidłowości. Możemy więc założyć, że metoda ta miała idealne środowisko do tego, żeby wykonać swoją robotę dobrze. Ale najwyraźniej nie wykonała tej roboty jak trzeba, bo spreparowała taką wartość argumentu dla metody methodC, że ta powiedziała "no fu...ing way" i rzuciła wyjątek. Proste?


Jakie korzyści daje nam fail-fast?

Wyobraź sobie, że jesteś developerem metody methodC. Właśnie jest piątunio godzina 16:30. Myślisz już tylko o tym, że za chwilę wyrwiesz się z biura aby wypaść z kumplami na męski wieczór (to inna nazwa dla łojenia procentów w opór). Właśnie o tej godzinie dostajesz informację, że na produkcji jest krytyczny problem, który trzeba rozwiązać przed weekendem. Klient dostaje 500-kę. Nie wierzysz w to co słyszysz, zastanawiasz się, czy to przypadkiem nie trzynasty dzień miesiąca. Jesteś przekonany, że pokiblujesz parę ładnych godzin i kiedy dołączysz do kumpli, Ci nie będą już wiedzieli jak masz na imię. Myślisz sobie "ku... mać!".

Zaglądasz do Zdziry (to taki popularny issue tracker), odnajdujesz błąd. Patrzysz o co chodzi. Klient zamieścił zrzut ekranu na którym znajduje się aż nadto dobrze Ci znany ekran z informacją, że pojawił się błąd HTTP 500. Znajduje się tam też unikalny numer transakcji biznesowej (tzw. correlation id). Zaglądasz do logów serwera i szukasz tam wpisów opatrzonych tym właśnie numerem. O godzinie 16:55 na Twojej twarzy maluje się niedowierzanie oraz niewiarygodne szczęście. Otóż masz przed sobą stack trace, z którego wynika, że nieszczęsna 500-ka poleciała ponieważ Ty rzuciłeś IllegalArgumentException w metodzie methodC w trakcie walidacji argumentu odsetki.

Patrzysz na kolejną linijkę stack trace'a i widzisz, że wywołanie Twojej metody przyszło z metody methodB. Przeglądasz historię w GIT-cie i patrzysz kto dodał w methodB wywołanie Twojej metody. To Wacek! Bez krzty współczucia przepinasz na niego zgłoszenie w Zdzirze i mówisz, że ma buga w swoim kodzie do rozwiązania przed weekendem. Wyłączasz komputer i wychodzisz z pracy. Po co masz siedzieć - błąd nie jest przecież w Twoim kodzie.

Tak więc fail-fast uratował Ci wieczór i porządnego kaca następnego dnia. Jest za co być wdzięcznym! To jest właśnie pierwsza najważniejsza korzyść ze stosowania tej techniki programowania. Fail-fast chroni nasze developerskie tyłki przed rozwiązywaniem cudzych bugów.

Popatrzmy jednak co się dzieje dalej. Wacek siada do kodu. Też słyszał o fail-fast. Jego metoda, methodB, również w pierwszych liniach kodu waliduje argumenty wejściowe oraz stan obiektu. Tak więc Wacek wie, że nie będzie długo szukał buga. Skoro walidacja argumentów i stanu obiektu nie rzuciły wyjątku to znaczy, że wszystko było w porządku w momencie wywołania metody methodB. To znaczy, że bug znajduje się w linijkach kodu znajdujących się za walidacją argumentów i stanu obiektu, a przed wywołaniem metody methodC.

public class ClassB
{
  private final ClassC classC;

  void methodB(final BigDecimal arg1, final BigDecimal arg2,
               final BigDecimal arg3)
  {
    // walidacja argumentów wejściowych
    // walidacja stanu (tj. pól) obiektu

    ... // 20 liniejk kodu - to tutaj jest bug!!!
    final Money kwotaOdsetek = ...; // albo tutaj!!!

    classC.methodC(kwotaOdsetek);
    ...
  }
}


Tych linijek jest 21 - myśli sobie "bułka z masłem". Po dwóch minutach już widzi źródło problemu, a po kolejnych dwóch ma już wyrzeźbioną poprawkę. On również będzie miał udany weekend.

To jest druga cecha fail-fast o której należy wspomnieć - zawęża rozmiar kodu źródłowego jaki musimy przejrzeć, żeby znaleźć buga. W tym przypadku Wacek musiał jedynie przejrzeć kod metody methodB. Gdyby methodB nie zwalidowała na wejściu swoich argumentów i stanu pól obiektu, wówczas buga musiałby poszukiwać nie tylko w metodzie methodB, ale również w methodA. Dzięki fail-fast obszar poszukiwania buga znajuduje się pomiędzy tym blokiem fail-fast, który wygenerował wyjątek, a blokiem fail-fast bezpośrednio poprzednim w stosie wywołań. Im więcej walidacji fail-fast dodamy do kodu, tym mniejsze są te bloki i mniejszy zakres kodu, w którym musimy szukać buga.


Co dalej?

Cóż, nie wyczerpałem jeszcze tematu. Muszę się jedna zatrzymać bo artykuł jest już dość długi i obawiam się, że za chwilę stanie się nieprzyswajalny. W kolejnej części planuję opisać dokładniej gdzie umieszczać bloki fail-fast i co powinny zawierać. Tymczasem.... niech moc będzie z Tobą!


"One more thing..."

A może masz ochotę na naprawdę mocne szkolenie?

W styczniu, został uruchomiony produkcyjnie największy system księgowy w Polsce. Rocznie przechodzi przez niego 330 miliardów złotych (!), co dzień korzysta z niego 30 tysięcy (!) użytkowników, realizuje milion (!) księgowań na godzinę (!), a główna tabela księgowa zawiera 8 miliardów(!) rekordów. To moje dziecko, to ja go zaprojektowałem. W czasie szkolenie przedstawię Ci architekturę tego systemu oraz różne techniki, które z powodzeniem stosuję od 16 lat. Jeżeli jesteś zainteresowany, to zajrzyj tutaj.

Rozpocznij dyskusję