Pewnie w większości projektów można spotkać się z wielkim workiem na niechciane metody, czyli z tzw. klasami Utilities, które znane są również pod pojęciem Helper Classes. Czym one się charakteryzują? Tym, że zawierają tylko metody statyczne, nie przechowują żadnego stanu oraz nie może powstać żadna instancja takiej klasy. Oczywiście dobrze jest jeśli każda z nich grupuje metody o podobnych odpowiedzialnościach. Należy się jednak zastanowić czy to odpowiednia droga dla naszego kodu? Czy jednak nie kieruje nami lenistwo lub może posiadamy zbyt niską wiedzę domenową, którą należałoby zdobyć? Są to pytania, na które warto sobie odpowiedzieć i poszukać rozwiązania, aby nie tworzyć kubła na niechciany kod. Przyjrzyjmy się zatem jak może wyglądać implementacja takiej klasy.

Jak zaimplementować klasę Utility?

Implementacja takiej klasy nie jest sporym wyzwaniem, inaczej nikt by ich przecież nie pisał. Przyjmijmy zatem za cel implementację walidacji cyfry kontrolnej dla numeru PESEL. Jeżeli nie będzie się ona zgadzała to nie będziemy mogli utworzyć danego obiektu odzwierciedlającego PESEL. Natomiast w innym przypadku będzie to jak najbardziej możliwe. Jest to oczywiście uproszczona implementacja, ponieważ nie sprawdzamy wszystkich danych ukrytych w numerze PESEL, ale nie o to w tym chodzi. Zajrzyjmy, więc do kodu.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package pl.csanecki.example;

import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import pl.csanecki.example.Pesel;

final class PeselUtil {

  private PeselUtil() {
    throw new UnsupportedOperationException("Cannot create PeselUtil instance");
  }

  public static boolean isProperChecksum(String peselLiteral) {
    int[] wages = { 1, 3, 7, 9, 1, 3, 7, 9, 1, 3 };

    List<Integer> numbers = Stream.of(peselLiteral.split(""))
        .map(Integer::parseInt)
        .collect(Collectors.toList());

    int sum = IntStream.range(0, wages.length)
        .map(index -> (wages[index] * numbers.get(index)) % 10)
        .sum();

    int checksum = (10 - (sum % 10)) % 10;

    return numbers.get(numbers.size() - 1) == checksum;
  }
}

public class Main {
  
  public static void main(String[] args) {
    String peselLiteral = "02010521694";

    if (PeselUtil.isProperChecksum(peselLiteral)) {
      Pesel pesel = new Pesel(peselLiteral);
    }
  }
}

Warto zwrócić uwagę na to, że PeselUtil posiada prywatny konstruktor, który dodatkowo został zabezpieczony wyjątkiem, przez co nie możemy utworzyć instancji tej klasy. Nie posiada ona żadnych zmiennych pozwalających na trzymanie w nim stanu. Klasa została również oznaczona przez słowo kluczowe final, aby nikt czasem jej nie rozszerzył. Zawiera jedynie publiczną, statyczną metodę isProperChecksum weryfikującą cyfrę kontrolną dla numeru PESEL. W klasie Main przedstawiam sposób wykorzystania tej metody, aby po walidacji zakończonej sukces można było utworzyć obiekt klasy Pesel.

Wraz z rozwojem naszego oprogramowania klasa PeselUtil może puchnąć przez zwiększanie się w niej ilości metod walidacyjnych. Oczywiście if w Main także będzie się powiększał co doprowadzi do ciężkiego w utrzymaniu kodu. Z jakiego, więc powodu wciąż tworzymy tego typu konstrukcje i skąd skąd narodził ten pomysł?

Dlaczego klasy Utility zyskały popularność?

Prawdopodobnie idea klas Utilities została przeniesiona z programowania proceduralnego i zostaje wykorzystywana w Javie pod przykrywką programowania zorientowanego obiektowo. Korzystając z statycznych metod nie mamy możliwości trzymania stanu w obiekcie przez co dane trafiające na wejściu zostają przetworzone i zwrócone na wyjściu. Natomiast programując obiektowo tworzymy instancję klasy, która powinna zawierać dane, aby móc wykonywać na nich operacje zachowując przy tym hermetyzację. Dlaczego, więc tworzymy klasy Utilities? Bo programowanie proceduralne jest po prostu wygodniejsze i łatwiejsze do zrozumienia. Nie trzeba też pisać dużej ilości kodu, który następnie trzeba by było utrzymywać. Mamy swój worek z metodami i dorzucamy do niego kolejne rozwiązania. Istotną przyczyną pisania klas Utilities może być też strach przed biznesem, który będzie nas rugał za “tracenie czasu” na myślenie nad nazwą metody czy klasy. Albo też poczucie, że jesteśmy złymi deweloperami, bo nie dowozimy szybko nowych funkcjonalności. Co może być jednak konsekwencją takiego stylu pisania kodu?

Problemy związane z Utlities

Mimo początkowych korzyści to i tak wszystko ma swoją cenę, którą prędzej czy później trzeba zapłacić. Oczywiście jeśli jest to świadomy wydatek to możemy lepiej się przygotować na konsekwencje naszych decyzji. Ta kwestia dotyczy także klas Utilities. Łatwo się je implementuje, ale musimy zdawać sobie sprawę z kilku problemów, które napotkamy później na swojej drodze. Oto kilka z nich, które udało mi się znaleźć:

  • wysoki coupling spowodowany brakiem abstrakcji
  • niska kohezja z powodu zgrupowania ze sobą niepowiązanych metod
  • brak możliwości testowania wybranej klasy w izolacji od klas Utilites
  • złamane podstawowe zasady stojące za SOLID
  • brak informacji, że dana klasa potrzebuje do działania klasy Utility (Inversion of Control)
  • może stać się workiem na metody, których nikt nie chce

Wraz z używaniem klas Utilities tracimy wszystkie zalety języka obiektowego. Należy walczyć samemu ze sobą i zaprzestać ich tworzenia. To na pewno będzie długa droga, ale naprawdę warto spróbować i oduczyć się tego nawyku. Sam muszę przyznać, że jestem na początku tej trasy, ale staram się to poprawić. Oczywiście jest to trudne jak widzi się w projekcie pozwolenie na taki styl pisania. Wręcz aż kusi, aby dopisać tam coś swojego. Więc w jaki sposób się przed tym bronić?

W jaki sposób walczyć z chęcią pisania takich klas?

Pierwszym co można zrobić to zacząć uczyć się języka domenowego. Zdobywając taką wiedzę możemy zacząć modelować klasy, które same decydują o swoim stanie. Będą samodzielnie podejmować decyzję czy przekazane dane są prawidłowe do stworzenia instancji danej klasy. Przeróbmy przedstawiony wcześniej przykład, aby pokazać sposób w jaki można pozbyć się, jakby się wydawało, niezbędnej klasy PeselUtil.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package pl.csanecki.example;

import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;

class Pesel {

  private String peselLiteral;

  public Pesel(String peselLiteral) {
    if (isInvalidChecksumFor(peselLiteral)) {
      throw new IllegalArgumentException("Cannot create Pesel instance");
    }

    this.peselLiteral = peselLiteral;
  }

  private boolean isInvalidChecksumFor(String peselLiteral) {
    int[] wages = { 1, 3, 7, 9, 1, 3, 7, 9, 1, 3 };

    List<Integer> numbers = Stream.of(peselLiteral.split(""))
        .map(Integer::parseInt)
        .collect(Collectors.toList());

    int sum = IntStream.range(0, wages.length)
        .map(index -> (wages[index] * numbers.get(index)) % 10)
        .sum();

    int checksum = (10 - (sum % 10)) % 10;

    return numbers.get(numbers.size() - 1) == checksum;
  }
}

public class Main {

  public static void main(String[] args) {
    String peselLiteral = "02010521694";

    Pesel pesel = new Pesel(peselLiteral);
  }
}

Jeżeli cyfra kontrolna nie będzie prawidłowa to nie powstanie instancja klasy Pesel. To sama klasa decyduje o tym czy jest możliwe stworzenie jej obiektu. Okazuje się wręcz, że zbędne są klasy pomocnicze typu PeselUtil. Wystarczy poświęcić trochę czasu na eksplorację domeny, aby w konsekwencji bronić system przed wprowadzeniem go w nieprawidłowy stan w bardziej przejrzystszy sposób.

Oczywiście wszystko sprowadza się do nazewnictwa, które trzeba przyznać jest najcięższym elementem programowania. Chyba, że posiadamy człowieka od biznesu, którego możemy zaprosić na kawę i będziemy w stanie podyskutować z nim na temat naszej aplikacji. W ten oto sposób możemy nauczyć się języka biznesowego przez co będziemy swobodniej tworzyć elementy naszego oprogramowania.

Podsumowanie

Czy należy, więc bezwzględnie unikać klas Utility? To zależy. Na pewno trzeba je w znacznym stopniu ograniczać. Nie chcemy przecież cofnąć się do programowania proceduralnego, które de facto też ma swoje zalety. Z tego powodu należy ciągle pogłębiać swoją wiedzę biznesową, aby spełniać oczekiwania klienta. Jak wspomniałem we wcześniejszym artykule, dla mnie osobiście programowanie jest narzędziem do upraszczania procesów biznesowych. Programista nie tylko musi znać się na technikaliach, ale także na sprawnym obracaniu się w domenie biznesowej.

Wracając do Utilities. Istnieją przykłady bibliotek, z których korzystamy na co dzień, a implementują ten pattern. Apache StringUtils, CollectionUtils czy nawet klasa JDK java.lang.Math są powszechnie wykorzystywane w kodzie produkcyjnym. Okazują się niezbędne i na pewno będą jeszcze długo wykorzystywane. Nie zmienia to jednak faktu, że tworząc nowy kod produkcyjny powinno się ograniczać ich implementację chociażby z wcześniej wymienionych powodów.

Źródła: