Hermetyzacja danych w Javie

Hermetyzacja java

Hermetyzacja/Enkapsulacja danych w Javie

Podczas nauki i prób zrozumienia programowania obiektowego spotykamy się z terminem hermetyzacji lub enkapsulacji.

Choć, gdy pierwszy raz o nich słyszymy to przechodzą nas aż ciarki po ciele. Prawda, jednak jest taka, że nie ma czego się bać bo zagadnienie nie jest aż tak skomplikowane i właśnie w tym wpisie je wytłumaczę.

Poczas przerabiania tematu hermetyzacji w Javie warto też wspomnieć o pakietach, ponieważ właśnie one są częścią hermetyzacji w Javie.

Pakiety

Zacznijmy od pakietów, warto będzie już wiedzieć o co z nimi chodzi podczas tłumacznia enkapsulacji.

Z pewnością podczas programowania w Javie zdążyłeś już zauważyć taką linijkę kodu na samej górze klasy:

package pl.maniaq;

Co to oznacza…

Jak można sobie szybko przetłumaczyć słowo package oznacza właśnie paczka lub pakiet. Paczka – czyli inaczej możemy nazwać to zbiorem. Zbiorem czego? Zbiorem klas, enumeratorów, interfejsów itd.

To po co właściwie nam te zbiory?

Pakiety fizycznie na dysku twardy są po prostu katalogami. Takie katalogi w bardzo łątwy sposób pozwalają nam uporządkować klasy według ich przeznaczenia.

Mamy do czynienia z kontrolerami? Mamy paczkę kontrolerów. Mamy stworzone walidatory? Robimy to samo. Jest to bardzo przydatne, gdy nasza aplikacja jest trochę bardziej skomplikowane i chcemy łatwiej się odnaleźć w naszych klasach.

Tak jak wspomniałem pakiety są tak naprawdę katalogami, jednak również wspominałem, że te katalogi mają coś współnego z hermetyzacją – tak, nie zapomniałem o tym, jednak wytłumaczę to w kolejnym rozdziale.

Teraz wystarczy Ci wiedzieć, że paczki pozwalają nam utrzymać porządek w projekcie i mają coś wspólnego z tajemniczym hasłem: enkapsulacja.

Hermetyzacja

To czym w końcu jest ta hermetyzacja? Zagadnienie jest śliśle związane ogólnie z programowanie obiektowym, a dosłownie oznacza ukrywanie danych oraz funkcji obiektów dane klasy.

Uściślijmy, hermetyzacja polega na decydowaniu jak pole i metody danego obiektu mają być widoczne dla innych obiektów,

Możemy nawiązać do życia realnego – nie zawsze chcemy, aby każdy wiedział o nas i o tym co robimy. Czasami chcemy coś ukryć – i tak właśnie możemy rozumieć to w programowaniu.

Możesz jeszcze nie widzieć celu hermetyzowania klas, jednak to wszystko się wyjaśni na sam koniec artykułu, teraz zagłębmy się  w to jak enkapsulować dane w Javie.

Modyfikatory dostępu

Mówiąc o enkapsulacji w Javie musimy poznać modyfikatory dostępu – jak sama nazwa wskazuje, to one nam pozwolą modyfikować dostęp do danych.

Są to słowa kluczowe, które opisują widoczność pola, metody lub klasy – odnosząc się do tego przed czym występuję.

W Javie posiadamy nastepujące modyfikatory dostępu:

  • public
  • protected
  • private
  • no modifier

Jak widać występują cztery rodzaje, ostatni oznacza po prostu brak żadnego z powyższych.

Przejdźmy od razu do tego jak one wszystkie działają.

public

Jest to najbardziej swobodny modyfikator. Public, publiczny – możemy się, więc domyślać, że dostęp do tego jest swobodny. Prawda jest taka, że pole lub metoda oznaczona jako public jest dostępna wszędzie.

Stwórzmy sobię prostą klasę Dog – jak widać wszystkie pola, metody oraz nawet klasa są publiczne.

public class Dog {
    public String dogName;
    public String dogBreed;
    public Dog(){
        dogName = "noname";
        dogBreed = "unknown";
    }
    public Dog(String dogName, String dogBreed){
        this.dogName = dogName;
        this.dogBreed = dogBreed;
    }
    public void bark(){
        System.out.println("Woow, woow!");
    }
}

I przytoczmy również kod z funkcji main

public class Main {
    public static void main(String[] args) {
        Dog pies = new Dog("Rex", "Owczarek");
        System.out.println("Pies wabi się: "+pies.dogName + ", jego rasa to: " + pies.dogBreed + ".");
        pies.bark();
    }
}

Jak widzisz do każdego pola metody możesz odwoływać się poprzez obiekt kropkę. Możesz tak zrobić, ponieważ wszystko w klasie dog jest zadeklarowane jako public.

Wniosek jest prosty: przy użyciu public nie możemy ukryć żadnych danych.

protected

Modyfikator protected nakłada już na pola i metody pewne ograniczenia. Użycie protected udostępnia pola i metody tylko:

  • klasie
  • klasie dziedziczącej
  • innych klas w tym samym pakiecie

Edytujmy nasz poprzedni kod

public class Dog {
    protected String dogName;
    protected String dogBreed;
    public Dog(){
        dogName = "noname";
        dogBreed = "unknown";
    }
    public Dog(String dogName, String dogBreed){
        this.dogName = dogName;
        this.dogBreed = dogBreed;
    }
    public void bark(){
        System.out.println("Woow, woow!");
    }
}

Edytowałem pola jako protected, spróbujmy teraz stworzyć obiekt w main:

public class Main {
    public static void main(String[] args) {
        Dog pies = new Dog("Rex", "Owczarek");
        System.out.println("Pies wabi się: "+pies.dogName + ", jego rasa to: " + pies.dogBreed + ".");
        pies.bark();
    }
}

I teraz, czy kompilacja się powiedzie czy nie powiedzie? Odpowiedź brzmi: to zależy. Już spieszę z wyjaśnieniami.

Jeżeli klasa Dog jest w tej samem paczce co klasa Main to mamy dostęp do pól czyli program się skompiluje. W przypadku, gdy klasa Dog będzie w osobnej paczce to pole będzie niewidoczne i program się nie skompiluje. W moim przypadku obie klasy są w jednej paczce, więc mam dostęp do pól z czego wynika, że program się kompiluje.

private

Modyfikator private najbardziej ograniczna widoczność pól i metod. Oznacza to, że użycie private wiąże się z tym, że pole lub metoda będzie widoczna tylko w klasie. Nie będzie widoczna nawet w tym samym pakiecie lub klasie dziedziczącej.

Ponownie przytoczmy sobie przykład klasy Dog z polami private

public class Dog {
    private String dogName;
    private String dogBreed;
    public Dog(){
        dogName = "noname";
        dogBreed = "unknown";
    }
    public Dog(String dogName, String dogBreed){
        this.dogName = dogName;
        this.dogBreed = dogBreed;
    }
    public void bark(){
        System.out.println("Woow, woow!");
    }
}

Ponownie przytoczmy main

public class Main {
    public static void main(String[] args) {
        Dog pies = new Dog("Rex", "Owczarek");
        System.out.println("Pies wabi się: "+pies.dogName + ", jego rasa to: " + pies.dogBreed + ".");
        pies.bark();
    }
}

I czy teraz już wiesz jaki będzie rezultat działania programu? Program oczywiście się nie skompiluje, ponieważ pola są jako private. Oznacza to, będzie musieli usunąć odnośniki do pól obiektu Dog, aby kod się skompilował.

public class Main {
    public static void main(String[] args) {
        Dog pies = new Dog("Rex", "Owczarek");
        pies.bark();
    }
}

Mając taki kod program się skompiluje, oczywiście nie ma teraz możliwości wyłuskania wartości pól obiektu, ale zazwyczaj obchodzi się to innym sposobem – getterami i setterami, dzięki którym jako twórcy możemy decydować, do których danych dajemy dostęp.

brak modyfikatora

Może być taki przypadek, że nie wpiszemy żadnego modyfikatora i to również będzie poprawne jednak musimy być świadom jego konsekwencji.

Brak modyfikatora oznacza, że pola i metody są widoczne tylko w klasie i pakiecie. Coś takiego jak privatetyle, że dodatkowo pola są widoczne w pakiecie.

Ponownie będzie potrzebny nam kod klasy Dog

public class Dog {
    String dogName;
    String dogBreed;
    public Dog(){
        dogName = "noname";
        dogBreed = "unknown";
    }
    public Dog(String dogName, String dogBreed){
        this.dogName = dogName;
        this.dogBreed = dogBreed;
    }
    public void bark(){
        System.out.println("Woow, woow!");
    }
}

Ponownie potrzebujemy main

public class Main {
    public static void main(String[] args) {
        Dog pies = new Dog("Rex", "Owczarek");
        System.out.println("Pies wabi się: "+pies.dogName + ", jego rasa to: " + pies.dogBreed + ".");
        pies.bark();
    }
}

I tym razem kod się skompiluje za pierwszym razem – tylko wtedy jeżeli obie klasy są w tym samym pakiecie! U mnie, więc kod się kompiluje.

A co z klasami…

Na początku powiedziałem, ze modyfikatorów dostępu można używać także w konwencji klas – i tak jest – lecz nie wszystkich.

W przypadku klas, możemy używać tylko dwóch modifkatorów:

  • public – w 98% występuje w klasach
  • no modifer – rzadko używany, przynajmniej przeze mnie. Jeżeli nie użyjemy modyfikatora public, to cała klasa będzie tylko dostępna z poziomu pakietu.

Wniosek jest prosty: jeżeli klasa nie ma modyfikatora public to do klasy nie możemy się odnieść z poza pakietu.

Po co ta cała hermetyzacja…

Dane są ukrywane głównie z trzech powodów

1. Uodparniamy naszą klasę na błędy

Poprawnie hermetyzując dane może uniknąć wielu błędów, dobrym przykładem jest tworzenie klas tzw. immutable (np. String jest taką klasą) – czyli niezmiennych.

Wystarczy tylko ukryć pola klasy oraz wystawić metody, które będą zwracały kopię (!) pól i nasza klasa będzie już niezmienna – co może być czasami wymagane do poprawnego działania aplikacji.

2. Lepiej odzwierciedlamy rzeczywistość

Przypuśćmy, że chcemy zbudować model, który będzie symulował psa tzn. jego czynności oraz właściwości. My jednak skupimy się na jego czynnościach.

Stwórzmy sobie krótki przykład, mamy obiekt Dog

Dog dog = new Dog();

Nie skupiajmy się na właściwościach jakie możemy mu przekazać przez konstruktor, skupmy się na tym co może taki obiekt wykonywać np.

dog.eat(meat);

lub

dog.bark(message);

Skupmy się na spożywaniu posiłku, nasza klasa wystawia metodę eat, dzięki której możemy nakarmić naszego psa – jednak ten proces składa się z wielu innych czynności w organiźmie psa, o których nie musimy wiedzieć jako właściciel takiego psa.

Wywołując metodę eat, tak naprawdę w klasie może dziać się wiele innych rzeczy, do których my nie mam dostępu – mamy wystawioną gotową meotdę.

Taka metode tak naprawdę składać się z wielu czynności, o których właściciel nie ma pojęcia:

public class Dog{
  //any fields

  public Dog(){

  }

  public void eat(Food food){
    bite(food);
    devour(food);
    strength+=food.getValue();
    expel(food);
  }

  private void bite(Food food){
    //dog bites food
  }

  private void devour(Food food){
    //dog devours food
  }

  private void expel(Food food){
    //dog expels food
  }

}

Spójrz na klasę powyżej, tylko metoda eat jest wystawiona na publiczną, reszta jest ukryta dla zwykłego właściciela psa(użytkownika obiektu).

3. Wyodrębnienie interfejsu

Choć powyższy przykład zawiera się również w tym, ponieważ wyodrębniliśmy interfejs klasy – wystawiliśmy tylko do użytku metodę eat – zamiast wszystkich to możemy jeszcze bardziej możemy rozwinąć ten przykład podczas dziedziczenia klas.

Stwórzmy kolejną klasę – SuperDog, który będzie dziedziczyć po klasie Dog, utworzonej w poprzednim przykładzie, jednak zrobimy pewną zmianę – ukryjemy w innych sposób metody w klasie Dog – zmienimy metody z private na protected.

public class Dog{
  //any fields

  public Dog(){

  }

  public void eat(Food food){
    bite(food);
    devour(food);
    strength+=food.getValue();
    expel(food);
  }

  protected void bite(Food food){
    //dog bites food
  }

  protected void devour(Food food){
    //dog devours food
  }

  protected void expel(Food food){
    //dog expels food
  }
}

Teraz stwórzmy klasę SuperDog:

public class SuperDog extends Dog {
    //fields

    public SuperDog(){
        super();
    }


    @Override
    protected void bite(Food food){
      //dog bites food
    }
}

Ze względu, że nasza klasa nadrzędna Dog ma metody protected umożliwia ona przysłonienie metody bazowej w klasie dziedziczącej.

Choć nie jest to wyszukany przykład to i tak możemy zdefiniować jak nasz super pies ma gryźć jedzenie, ponieważ klasa bazowa nam to udostępniła.

Ciekawostką jest jeszcze zadeklarowania metody bite, w klasie bazowej jako final – co powoduje, że metoda nie może być przysłonięta – jednak więcej o tym się rozpisze podczas mówienia o przysłanianiu metod i adnotacji Override.

Trening czyni mistrza

Zachęcam cię do wykonania poniższych zadań, aby mieć pewność, że w 100% zrozumiałeś przedstawione powyżej zagadnienie.

  1. Stwórz klasę Car (w tym samym pakiecie do klasa Main) i stwórz obiekt w main. Spróbuj wyłuskać informację z pola obiektu, używając wszystkich modyfikatorów dostępu w klasie Car.
  2. Stwórz pakiet modeldodaj do niego klasę Human i stwórz obiekt w main. Spróbuj wyłuskać informację z pola obiektu, używając wszystkich modyfikatorów dostępu w klasie Human.