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