Java Developer? Przejd┼║ na wy┼╝szy poziom wiedzy ­čöą­čĺ¬ ┬áSprawd┼║

Team Leader? Podnie┼Ť efektywno┼Ť─ç swojego zespo┼éu ­čĹî┬áSprawd┼║

Fail Fast ­čöą w Java

utworzone przez Clean Code, Java

Uwielbiam kod, kt├│ry nie zaskakuje. Kiedy praca na modelu domenowym u┼éatwia rozwi─ůzywanie problemu. Kiedy poj─Öcie w systemie jest sp├│jne i nie czyhaj─ů na mnie dodatkowe pu┼éapki zwi─ůzane z tym, ┼╝e zaraz u┼╝yj─Ö istniej─ůcego ju┼╝ fragmentu.

W┼éa┼Ťciwie w takim celu powstaje model dziedzinowy (domain model) – ┼╝eby za pomoc─ů zdefiniowanych poj─Ö─ç, kt├│re wyra┼╝aj─ů: agregaty, parametry procesu lub jego wynik, pomog┼éy mi ich u┼╝y─ç w przewidywalny spos├│b.

Dlatego wol─Ö, ┼╝eby niew┼éa┼Ťciwie u┼╝yty kod wywr├│ci┼é si─Ö od razu, ni┼╝ ukry┼é problem, kt├│rego skutki objawi─ů si─Ö w innym, dalszym miejscu w procesie.

Takie podej┼Ťcie/technika nazywa si─Ö Fail fast.

Przyk┼éad: Rozwa┼╝my zastosowanie tej techniki na przyk┼éadzie naliczania rabatu dla ca┼ékowitej kwoty produktu, aby uzyska─ç kwot─Ö ko┼äcow─ů do zap┼éaty. Aby obliczy─ç ko┼äcow─ů kwot─Ö do zap┼éaty, nale┼╝y od kwoty ca┼ékowitej odj─ů─ç kwot─Ö rabatu.

W tym artykule przeczytasz o:

  • Fail fast w standardowej bibliotece Java
  • Heizenbugs
  • Fail fast w modelu domenowym
  • Fail fast na przyk┼éadzie nulla
  • Krytycznych danych aplikacji

Nie piszę tutaj o iteratorach kolekcji typu Fail Fast, tylko generalnie o zagadnieniu na paru przykładach.

Fail fast w standardowej bibliotece Java

Zaprezentuj─Ö przyk┼éad Fail Fast na obiekcie w standardowej bibliotece Java. Wiele z obiekt├│w sprawdza parametry wej┼Ťciowe podczas tworzenia obiekt├│w. Rozwa┼╝my nast─Öpuj─ůcy przyk┼éad:

BigDecimal totalPrice = new BigDecimal("100.00");
BigDecimal discount = new BigDecimal("incorrect data");

BigDecimal finalPrice = totalPrice.subtract(discount);

Zdefiniowane zosta┼éy potrzebne obiekty: kwota ca┼ékowita (totalPrice) oraz kwota rabatu (discount). Kwoty zosta┼éy zaprezentowane jako obiekty BigDecimal. Na takich obiektach mo┼╝emy przeprowadzi─ç operacj─Ö matematyczn─ů, odejmuj─ůc jedn─ů kwot─Ö od innej.

Problem pojawia si─Ö wtedy, kiedy jeden z obiekt├│w zawiera niepoprawn─ů warto┼Ť─ç. Je┼╝eli by si─Ö tak sta┼éo, metoda substract nie powiod┼éaby si─Ö, poniewa┼╝ nie mo┼╝e operowa─ç na niepoprawnych danych. Co nale┼╝y wtedy zrobi─ç? Zg┼éosi─ç wyj─ůtek dzia┼éania metody?

Kiedy uruchomisz powy┼╝szy fragment kodu, zauwa┼╝ysz nast─Öpuj─ůcy wyj─ůtek zg┼éoszony w 2 linijce kodu:

java.lang.NumberFormatException: Character i is neither a decimal digit number, decimal point, nor „e” notation exponential mark.

Jest to przyk┼éad podej┼Ťcia Fail fast, czyli zg┼éoszenia b┼é─Ödu jak najszybciej jest to mo┼╝liwe.

Daje nam to nast─Öpuj─ůce korzy┼Ťci:

  • B┼é─ůd jest zg┼éaszany ju┼╝ przy pr├│bie utworzenia niepoprawnego obiektu BigDecimal (np. z napisu, a nie z liczby).
  • Od momentu utworzenia obiektu, wszystkie operacje na nim wykonywane zwracaj─ů ju┼╝ poprawne wyniki. Nie trzeba sprawdza─ç wynik├│w po┼Ťrednich.

Heizenbugs

Gdyby nie wprowadzi─ç takiego sprawdzenia, wtedy problem wyst─ůpi┼éby podczas wywo┼éania metody substract. Kiedy ta metoda jest wykonywana? Je┼╝eli logika naliczania rabatu mia┼éaby jaki┼Ť dodatkowy warunek (albo zestaw warunk├│w), wtedy w jaki┼Ť sytuacjach b┼é─ůd by si─Ö objawi┼é, a w jakich┼Ť nie. Oznacza┼éoby to, ┼╝e b┼é─ůd jest ukryty. A to tylko prosta operacja na obiekcie (mo┼╝e domenowym?), a nie d┼éugotrwa┼éy proces wymagaj─ůcy interakcji z mniej stabilnym systemem.

Problemy, kt├│re czasem si─Ö objawiaj─ů, a czasem nie, s─ů k┼éopotliwe do uchwycenia. Uk┼éu┼éa si─Ö nawet na nie nazwa: Heizenbugs. Gdy si─Ö patrzy na system – to dzia┼éa – a gdy si─Ö nie patrzynie dzia┼éa. Zatem czy sam proces obserwacji ma wp┼éyw na dzia┼éanie systemu? (sakrzm)

Fail fast w domain model

Przyk┼éad z BigDecimal dotyczy┼é samego formatu warto┼Ťci. W modelu domenowym same warto┼Ťci opr├│cz formatu mog─ů mie─ç pewne semantyczne znaczenie. Mowa wtedy o bogatej domenie (Rich Domain Model).

Rozwa┼╝my nast─Öpuj─ůcy przyk┼éad, naliczaj─ůcy procentowy rabat od ca┼ékowitej kwoty:

class PriceCalculator {

    private static final BigDecimal HUNDRED = BigDecimal.valueOf(100);

    public BigDecimal calculateFinalPrice(BigDecimal totalPrice, int discountPercent) {
        BigDecimal discount = totalPrice.multiply(BigDecimal.valueOf(discountPercent)).divide(HUNDRED);
        return totalPrice.subtract(discount);
    }
}

Przyjmowana kwota ca┼ékowita (totalPrice) oraz procentowy rabat (discountPercent, wyra┼╝ony jako liczba od 0-100%). Aby wyliczy─ç kwot─Ö rabatu, mno┼╝ymy ca┼ékowit─ů kwot─Ö razy udzielony rabat (kt├│ry musimy podzieli─ç przez 100%) i wychodzi nam kwota rabatu. T─Ö kwot─Ö odejmujemy od kwoty ca┼ékowitej. Proste. Piszemy test, kt├│ry potwierdza, ┼╝e kod dzia┼éa:

@Test
void calculatesFinalPriceWithPercentDiscount() {
	BigDecimal totalPrice = new BigDecimal("100.00");
	int discountPercent = 20;

	BigDecimal finalPrice = calculator.calculateFinalPrice(totalPrice, discountPercent);

	assertThat(finalPrice)
			.isEqualByComparingTo("80");
}

W┼éa┼Ťciwie to m├│g┼ébym przyj─ů─ç taki parametr rabatu, ┼╝e m├│g┼éby on troszk─Ö napsu─ç w systemie:

@Test
void calculatesFinalPriceWithPercentDiscount() {
	BigDecimal totalPrice = new BigDecimal("100.00");
	int discountPercent = 200;

	BigDecimal finalPrice = calculator.calculateFinalPrice(totalPrice, discountPercent);
}

Przyj─ů┼éem procent rabatu 200. Kwestia parametru systemowego. Do zap┼éaty za zam├│wienie jest 100 PLN. Jak─ů warto┼Ť─ç na finalPrice? Ot├│┼╝ ma warto┼Ť─ç -100 PLN. Czyli co, ujemna sprzeda┼╝?

Ten b┼é─ůd m├│g┼éby s┼éono kosztowa─ç, gdyby objawi┼é si─Ö p├│┼║niej w systemie ksi─Ögowym, bo nie zosta┼é sprawdzony na pocz─ůtku.

Dobrze, poprawny ten b┼é─ůd i wprowad┼║my drobn─ů poprawk─Ö, kt├│ra sprawdzi, czy procent udzielonego rabatu jest poprawny, czyli zawiera si─Ö pomi─Ödzy 0%, a 100%:

class PriceCalculator {

    private static final BigDecimal HUNDRED = BigDecimal.valueOf(100);

    public BigDecimal calculateFinalPrice(BigDecimal totalPrice, int discountPercent) {
        validatePercentDiscount(discountPercent);

        BigDecimal discount = totalPrice.multiply(BigDecimal.valueOf(discountPercent)).divide(HUNDRED);
        return totalPrice.subtract(discount);
    }

    private static void validatePercentDiscount(int discountPercent) {
        if (discountPercent > 100 || discountPercent < 0) {
            throw new IllegalArgumentException("Discount should be between 0 and 100%. Actual value is: " + discountPercent + "%");
        }
    }
}

No i oczywi┼Ťcie testy:

@Test
void raisesInvalidPercentDiscountAbove100() {
	BigDecimal totalPrice = new BigDecimal("100.00");
	int discountPercent = 200;

	assertThatCode(() -> calculator.calculateFinalPrice(totalPrice, discountPercent))
			.isInstanceOf(IllegalArgumentException.class)
			.hasMessage("Discount should be between 0 and 100%. Actual value is: 200%");
}

@Test
void raisesInvalidPercentDiscountBelow0() {
	BigDecimal totalPrice = new BigDecimal("100.00");
	int discountPercent = -50;

	assertThatCode(() -> calculator.calculateFinalPrice(totalPrice, discountPercent))
			.isInstanceOf(IllegalArgumentException.class)
			.hasMessage("Discount should be between 0 and 100%. Actual value is: -50%");
}

Po sprawie? Po sprawie. Ale czekaj… niepoprawna warto┼Ť─ç parametru rabatu nadal kr─ů┼╝y sobie po systemie i sprawdzana jest tylko w momencie pr├│by naliczenia tego rabatu. Czyli taki troch─Ö Heizenbug.

Co w przypadku, kiedy warto┼Ť─ç rabatu jest u┼╝ywana w innym miejscu w kodzie? Albo dopiero b─Ödzie, a kto┼Ť zapomni sprawdzi─ç tej warto┼Ťci i problem objawi si─Ö ponownie w innym procesie?

Fail fast w modelu domenowym (Value Object)

Dlatego zastosujemy technik─Ö Fail fast w naszym modelu dziedzinowym. Zdefiniujemy Value Object reprezentuj─ůcy rabat (Discount). Zdefiniujemy regu┼é─Ö biznesow─ů, kt├│ra m├│wi o tym, ┼╝e rabat procentowy mo┼╝e mie┼Ťci─ç si─Ö od 0%-100%. Taka regu┼éa b─Ödzie sprawdzana ju┼╝ przy tworzeniu obiektu, zatem warto┼Ť─ç nie b─Ödzie kr─ů┼╝y┼éa po systemie:

class Discount {

    private static final BigDecimal HUNDRED = BigDecimal.valueOf(100);
    private final BigDecimal percent;

    Discount(int percent) {
        if (percent > 100 || percent < 0) {
            throw new IllegalArgumentException("Discount should be between 0 and 100%. Actual value is: " + percent + "%");
        }

        this.percent = BigDecimal.valueOf(percent);
    }

    public static Discount ofPercent(int percent) {
        return new Discount(percent);
    }

    public BigDecimal applyTo(BigDecimal value) {
        BigDecimal discount = value.multiply(percent).divide(HUNDRED);
        return value.subtract(discount);
    }
}

Oraz testy:

@Test
void appliesDiscountToPrice() {
	BigDecimal totalPrice = new BigDecimal("100.00");
	Discount discount = Discount.ofPercent(20);

	assertThat(discount.applyTo(totalPrice))
			.isEqualByComparingTo("80");
}

@Test
void validatesInvalidPercentDiscountAbove100() {
	assertThatCode(() -> Discount.ofPercent(200))
			.isInstanceOf(IllegalArgumentException.class)
			.hasMessage("Discount should be between 0 and 100%. Actual value is: 200%");
}

@Test
void validatesInvalidPercentDiscountBelow0() {
	assertThatCode(() -> Discount.ofPercent(-50))
			.isInstanceOf(IllegalArgumentException.class)
			.hasMessage("Discount should be between 0 and 100%. Actual value is: -50%");
}

Tym sposobem uniemo┼╝liwili┼Ťmy utworzenie niepoprawnego obiektu reprezentuj─ůcego rabat. Dzi─Öki temu:

  • Logika zwi─ůzana z poprawno┼Ťci─ů rabatu zawarta jest w jednym miejscu. Nie trzeba powiela─ç sprawdzania warto┼Ťci w wielu miejscach.
  • Raz utworzony obiekt rabatu jest zawsze poprawny.
  • Extra benefit: rabat potrafi zrobi─ç co┼Ť wi─Öcej, ni┼╝ przechowa─ç warto┼Ť─ç, np. przeliczy─ç kwot─Ö

Psst… Interesuj─ůcy artyku┼é?

Je┼╝eli podoba Ci si─Ö ten artyku┼é i chcesz takich wi─Öcej – do┼é─ůcz do newslettera. Nie omin─ů Ci─Ö materia┼éy tego typu.

.

Fail fast na przykładzie nulla

W Java obiekty s─ů przekazywane przez referencj─Ö, nie przez warto┼Ť─ç. Oznacza to, ┼╝e kto┼Ť m├│g┼éby przekaza─ç pust─ů referencj─Ö, albo przekazujemy j─ů nie┼Ťwiadomie z innego miejsca (np. z bazy danych, je┼╝eli warto┼Ť─ç nie istnieje, jest zwracany null).

Swoj─ů drug─ů, polecam przeczyta─ç o One Billion Dollar Mistake.

Opr├│cz sprawdzenia poprawno┼Ťci obiekt├│w domenowych mo┼╝na sprawdzi─ç, czy przekazany obiekt nie jest nullem. Mo┼╝na by by┼éo zastosowa─ç nast─Öpuj─ůc─ů konstrukcj─Ö:

public BigDecimal applyTo(BigDecimal value) {
	Objects.requireNonNull(value, "value is null");

	BigDecimal discount = value.multiply(percent).divide(HUNDRED);
	return value.subtract(discount);
}

Zabezpieczamy si─Ö przed tym, ┼╝e jaki┼Ť przekazany argument nie jest nullem. Czyli takie Fail Fast. Je┼╝eli ten argument by┼éby u┼╝yty jeszcze w jakiej┼Ť logice (instrukcje warunkowe) to mo┼╝na by powiedzie─ç, ┼╝e pozbywamy si─Ö Heizenbug├│w.

Zastan├│wmy si─Ö jednak, co to podej┼Ťcie nam daje. Tak naprawd─Ö kod zg┼éosi wyj─ůtek jedynie dwie linijki wy┼╝ej. Gdyby nie by┼éo tego sprawdzenia, wywr├│ci┼éby si─Ö dok┼éadnie z takim skutkiem nieco dalej.

Chc─ůc si─Ö zabezpieczy─ç przed tego typu sytuacjami, musieliby┼Ťmy paranoicznie dodawa─ç takie sprawdzenie dla ka┼╝dego argumentu ka┼╝dej metody w kodzie. Czy to by poprawi┼éo czytelno┼Ť─ç? Nie.

Brak null w modelu domenowym?

Rozwi─ůzaniem mo┼╝e by─ç um├│wienie si─Ö w zespole, ┼╝e og├│le nie zwracamy null w modelu dziedzinowym. Je┼╝eli co┼Ť faktycznie mo┼╝e nie istnie─ç, udekorujmy tak─ů warto┼Ť─ç w Optional, a ka┼╝d─ů inn─ů sytuacj─Ö uznajemy za b┼é─ůd do naprawy. Wtedy kod staje si─Ö bardzo prosty, zak┼éadamy, ┼╝e referencja istnieje.

Sytuacj─ů, w kt├│rej widz─Ö zastosowanie sprawdzania to konstruowanie nowych obiekt├│w, aby zapewni─ç ich sp├│jno┼Ť─ç. Gdyby┼Ťmy tego nie zrobili, m├│g┼éby powsta─ç niesp├│jny obiekt. Na przyk┼éad:

class Price {
    private final BigDecimal value;

    Price(BigDecimal value) {
        this.value = requireNonNull(value, "value cannot be null");
    }
}

Dependency Injection

A co z Dependency Injection i sprawdzeniem wstrzykni─Ötych referencji do konstruktora klasy typu Service, Controller itd? S─ů to elementy systemu, kt├│re rozwi─ůzywanie zale┼╝no┼Ťci deleguj─ů do frameworka (Inversion of Control na poziomie konstruktora).

Chodzi o nast─Öpuj─ůcy przyk┼éad:

class BasketDetailsService {
    private final BasketRepository basketRepository;
    private final DiscountPolicy discountPolicy;
    private final CouponRepository couponRepository;

    public BasketDetailsService(BasketRepository basketRepository, DiscountPolicy discountPolicy, CouponRepository couponRepository) {
        this.basketRepository = requireNonNull(basketRepository, "basketRepository == null");
        this.discountPolicy = requireNonNull(discountPolicy, "discountPolicy == null");;
        this.couponRepository = requireNonNull(couponRepository, "couponRepository == null");;
    }
}

Tutaj r├│wnie┼╝ zalecam usp├│jnienie podej┼Ťcia w projekcie/zespole i rozwa┼╝enia wad i zalet.

Ja na to zagadnienie patrz─Ö do┼Ť─ç pragmatycznie. Je┼╝eli u┼╝ywany framework, kt├│rym pos┼éugujemy si─Ö do rozwi─ůzywania zale┼╝no┼Ťci, weryfikuje, czy obiekty s─ů powi─ůzane/powstrzykiwane poprawnie – wtedy wed┼éug mnie nie ma potrzeby weryfikowania tego ponownie. By┼éaby to nadmiarowa cz─Ö┼Ť─ç kodu.

Psst… Interesuj─ůcy artyku┼é?

Je┼╝eli podoba Ci si─Ö ten artyku┼é i chcesz takich wi─Öcej – do┼é─ůcz do newslettera. Nie omin─ů Ci─Ö materia┼éy tego typu.

.

Krytyczne dane dla aplikacji

Technik─Ö Fail fast mo┼╝na stosowa─ç r├│wnie┼╝ dla dost─Öpu do krytycznych danych aplikacji.

Dajmy na to, że podczas uruchamiania aplikacji na przykład:

  • pobierane s─ů dane s┼éownikowe ze ┼║r├│de┼é danych
  • albo nape┼éniany jest kluczowy cache, kt├│ry pozwala szybko pracowa─ç na danych, albo prze┼é─ůczy─ç si─Ö na scenariusz alternatywny w przypadku awarii np. komunikacji z innym serwisem
  • albo brakuje jakiego┼Ť ustawienia w pliku konfiguracyjnym

Wtedy mo┼╝e lepiej nie uruchomi─ç aplikacji i zg┼éosi─ç b┼é─ůd, ni┼╝ przyj─ů─ç dane, kt├│re w dalszej kolejno┼Ťci spowoduj─ů niestabilno┼Ť─ç obs┼éugiwanych proces├│w.

Podsumowanie

Czym jest Fail fast

Fail fast to technika, w kt├│rej b┼é─ůd jest zg┼éaszany od razu w widoczny spos├│b. Dzi─Öki temu w oprogramowaniu nie jest propagowany niepoprawny stan, kt├│ry ukrywa problem. Nie trzeba sprawdza─ç przekazanych warto┼Ťci w kodzie, podczas ich u┼╝ywania.

Jak mog─Ö zastosowa─ç podej┼Ťcie Fail fast w Domain-Driven-Design

Pewne poj─Öcia domenowe mo┼╝na modelowa─ç jako obiekty warto┼Ťci (Value Objects). Podczas ich tworzenia w konstruktorze, mo┼╝esz sprawdzi─ç ich semantyczne znaczenie za pomoc─ů dost─Öpnych regu┼é. Na przyk┼éad modeluj─ůc procentow─ů zni┼╝k─Ö, mo┼╝na sprawdzi─ç, czy warto┼Ť─ç zawiera si─Ö w przedziale od 0%-100% i nie dopu┼Ťci─ç do utworzenia niepoprawnego obiektu.

Wpis kt├│ry czytasz to zaledwie fragment wiedzy zawartej w Programie szkoleniowym Java Developera od SoftwareSkill. Mamy do przekazania sporo usystematyzowanej wiedzy z zakresu kluczowych kompetencji i umiej─Ötno┼Ťci Java Developera. Program sk┼éada si─Ö z kilku modu┼é├│w w cotygodniowych dawkach wiedzy w formie video.

Podoba Ci się ten artykuł? Weź więcej.

Je┼╝eli uwa┼╝asz ten materia┼é za warto┼Ťciowy i chcesz wi─Öcej tre┼Ťci tego typu ÔÇô nie przegap ich i otrzymuj je prosto na swoj─ů skrzynk─Ö. Nawi─ů┼╝my kontakt.

.

Gdyby┼Ť potrzebowa┼é jeszcze wi─Öcej:

Jeste┼Ť Java Developerem?

Przejd┼║ na wy┼╝szy poziom wiedzy 
„Droga do Seniora” ­čöą­čĺ¬

Jeste┼Ť Team Leaderem? Masz zesp├│┼é?

Podnie┼Ť efektywno┼Ť─ç i wiedz─Ö swojego zespo┼éu ­čĹî

Dyskusja