dikamilo.net

Kolejny blog w sieci...

Java - konstruktory, dziedziczenie oraz metody publiczne

Temat który chciałbym dzisiaj poruszyć tyczy się języka Java a konkretnie konstruktorów oraz metod publicznych w nich wywoływanych. Wydawać by się mogło że w zasadzie nie ma o czym tutaj pisać, ale jednak jest jedna rzecz o której dość sporo osób nie wie. O tej ciekawostce, bugu czy jak to inaczej można nazwać dowiedziałem się na jednych z pierwszych zajęć z modelowania programów. Co ciekawe sporo osób które zawodowo programuje w Javie nie zdaje sobie z tego sprawy.

Za pewne nie raz zdarzało nam się pisać klasę w której konstruktor wywoływał metody publiczne tej klasy, czy to do inicjalizacji obiektu czy ustawiania danych. Jeżeli stosujemy zasadę DRY (Don't repeat yourself), to tym bardziej taka sytuacja występuje. Mamy gettery oraz settery do ustawiania danych które są oczywiście publiczne żeby miały jakiś sens, używamy ich również w konstruktorach do ustawiania danych początkowych.

Na początek przyjrzyjmy się następującej klasie:

public class Foo {
  public Foo() {
    System.out.println("Foo::Konstructor");
    doFoo();
  }

  public void doFoo() {
    System.out.println("Foo::doFoo");
  }
}

Nie ma tutaj żadnej magi. Banalny przykład do pokazania o co chodzi. Mamy tutaj konstruktor oraz metodę doFoo która jest użyta również w konstruktorze. Zarówno konstruktor jak i publiczna metoda wypisują dany komunikat w celu pokazania zachowania. Oczywiste jest że tworząc nowy obiekt Foo na konsoli pojawi się:

Foo::Konstructor
Foo::doFoo

Co się stanie gdy utworzymy nową klasę dziedziczącą po Foo oraz nadpisującą metodę doFoo ? Kod takiej implementacji prezentuje się następująco:

public class Bar extends Foo {
  public Bar() {
    System.out.println("Bar::Konstructor");
  }

  public void doFoo() {
    System.out.println("Bar::doFoo");
  }
}

Osoby które miały styczność na przykład z językiem C++ powiedzą że, najpierw wywoła się konstruktor klasy Bar, jednak jako że mamy tutaj dziedziczenie to odpalony zostanie konstruktor klasy Foo oraz wykona się jego zawartość (komunikat na konsole oraz metoda doFoo) a dopiero zawartość konstruktora klasy Bar. Tak więc wynik na konsoli powinien być następujący:

Foo::Konstructor
Foo::doFoo
Bar::Konstructor

I wszystko było by w porządku gdyby nie jeden mały szkopuł. Niestety, Java działa troszkę inaczej i jeżeli tylko przeciążymy publiczną metodę w klasie podrzędnej to konstruktor klasy Foo wywoła właśnie tą przeciążoną metodę. Żeby wszystko było jasne wynik będzie następujący:

Foo::Konstructor
Bar::doFoo
Bar::Konstructor

Taki mechanizm nie działa na naszą korzyść. Dlaczego ? Klasa przechowująca dane powinna się o nie troszczyć. Mam tutaj na myśli że obsługa tych danych, ustawianie, pobieranie, sprawdzanie błędów powinna być realizowane w klasie która te dane przechowuje. Jeżeli w konstruktorze używamy metod publicznych - w tym przypadku bardzo często setterów to prosimy się o sesję debugowania naszej aplikacji. Jeżeli taka klasa zostanie po dziedziczona i zostaną przeciążone settery, co więcej owe settery będą realizować całkiem inną funkcjonalność to nasz obiekt nie zostanie utworzony poprawnie. Oczywiście podaję tutaj przykład z setterami ale może to być dowolna publiczna metoda.

Przykładowy kod:

public class Foo {
  private int value;

  public Foo(int value) {
    System.out.println("Foo:Constructor");
    setValue(value);
  }

  public void setValue(int value) {
    System.out.println("Foo::setValue");
    if (value < 1) {
      throw new IllegalArgumentException();
    }
    this.value = value;
  }

  public int getValue() {
    return value;
  }
}

public class Bar extends Foo {
  public Bar(int value) {
    super(value);
    System.out.println("Bar:Constructor");
  }

  public void setValue(int value) {
    System.out.println("Bar::setValue");
  }
}

public class Main {
  public static void main(String[] args) {
    Foo foo = new Foo(2);
    Bar bar = new Bar(2);
    System.out.println(foo.getValue());
    System.out.println(bar.getValue());
  }
}

Oraz wyniki dla foo oraz bar:

// dla Foo foo = new Foo(2);
Foo:Constructor
Foo::setValue
2

// dla Bar bar = new Bar(2);
Foo:Constructor
Bar::setValue
Bar:Constructor
0

Przykład jest trywialnie prosty. Klasa Foo zajmuje się ustawianiem oraz zwracaniem danych które przechowuje, w tym wypadku jest to pole value. Dodatkowo setter sprawdza czy przekazana wartość jest większa od 1, jeżeli jest mniejsza to rzucany jest wyjątek. Klasa Bar dziedziczy po Foo oraz przeciąża metodę setValue całkowicie ignorując ustawianie wartości. Efekt widać na powyższym listingu.

Rozwiązanie takiego problemu jest dość proste:

  • Ustawiamy setter na private
  • Tworzymy publiczną metodę doSetValue która wywołuje prywatnego settera
  • W konstruktorze używamy prywatnych setterów
  • Klient ma dostęp do settera za pomocą metody doSetValue

Takie rozwiązanie wydaje się najbardziej eleganckie ze wszystkich jakie rozważałem. Najlepiej unikać wywołań publicznych funkcji w konstruktorach, w przypadku setterów które tak czy inaczej muszą się tam znaleźć (przypominam o zasadzie DRY) można zastosować pomysł z doSetX.

Tagi