Hermetyzacja

Posiadanie certyfikatu SCJP oznacza ,że znasz każdy aspekt obiektowości w Javie-e. Udowodnia on ,że znasz hierarchie   i potęgę polimorfizmu. Dlatego przez klika dni na blogu w tym kursie będę omawiał zachowanie i zasady obiektowe, które trzeba na egzamin SCJP znać.


Niektórzy programiści nie są  biegli w obiektowości dlatego zacznę od podstaw.

Cel certyfikatu – Objectives 5.1

Book Icon

5.1. Develop code that implements tight encapsulation, loose coupling, and high cohesion in classes, and describe the benefits.

Stwórz kod, który implementuje ściśle hermetyzacje, luźnego powiązania i wysokiej spójności w klasach i opisz ich zalety.

Kod…Kod w prawdziwym życiu nie jest pisany tylko dla ciebie. <dramatyczna muzyka>

Kod…Kod  w rzeczywistości nie jest pisany dla zabawy.

Okej, przejdę do rzeczy pisanie dobrego kodu, który dla innych klas działa zawsze bezbłędnie nie jest takie trudne ,ale często o tym zapominamy. Programy się rozwijają. Jego ulepszanie nie powinno wywoływać drastycznych skutków ubocznych dla istniejących już rozwiązań.

Na przykład wyobraź sobie kod, w którym klasa ma publiczną instancje zmiennej i inni programiści ustawiają wartość tej zmiennej bezpośrednio.

package dPaczka;

class ZloObiektowosci 
{
    public int count;
    public int weight;
    public int lenght;
}
 
public class UzycieZlaObiektowosci
{
     public static void działam(){
         ZloObiektowosci z = new ZloObiektowosci();
         z.count = 10;
         z.weight = 57;
         z.lenght = 17;
     }
}

Teraz istnieje problem. Jak zamierzasz zmienić zachowanie tej klasy w taki sposób, aby nie wywołać problemów dla już istniejących rozwiązań. Wydaje się to proste prawda wystarczy ,że zmienię modyfikatory z publicznych na prywatne ,a potem dodam metody, które pozwolą mi uzyskanie dostępu do tych zmiennych. Jednak jeśli ta klasa ma już istniejące rozwiązania to zmieniając ją w ten sposób zniszczę ich działanie. Czyli jesteśmy w kropce instancje zmiennych były  publiczne i będą publiczne.

Zdolność stworzenie kodu w taki sposób ,aby w przyszłości zarządzanie kodem nie miało wpływu na istniejące rozwiązania to jedna z zalet hermetyzacji.

Chcesz ukryć dokładną implementacje klasy za publicznym programistycznym interfejsem. Mówiąc interfejs mam na myśli zbiór metod akcesyjnych, który sprawiają ,że twój kod jest dostępny dla innego kodu w wyniku wywołania metod. Dzięki ukrywaniu implementacji  możesz bardziej elastycznie i  łatwiej zarządzać kodem klasy.

Jeśli chcesz aby twój kod był zarządzany, elastyczny i rozszerzalny musisz stworzyć klasę w stylu hermetyzacji. Jak to zrobić.

  • Zachowaj instancje zmiennych jako prywatne (w niektórych przypadkach jako protected)
  • Utwórz publiczną akcesyjną metodę i wymuś kod wywołania tych metod zamiast bezpośredniego dostępu do instancji zmiennych,
  • Metody te zgodnie z konwencją JavaBeans powinny się nazywać: set<właściwość> i get<właściwość>.

Ilustracja poniżej obrazuje pomysł hermetyzacji poprzez metody akcesyjne.

public class-09

Metody akcesyjne nazywają się zazwyczaj  getter i setter.

Teraz każdy programista aby użyć instancji zmiennych musi skorzystać z tych metod. Myślę ,że czas na przykład.

package dPaczka;

public class MagicTrashBin 
{ 
    //musimy ochronić instancje zmiennych;  
    //tylko klasa MagicTrashBin może  
    //korzystać z filecount   
    private int filecount; 

    //tworzenie publicznych getter i setter 
    public int getFileCount()
    {
        return filecount;
    } 

    public void setFileCount(int newCount)
    {
        filecount = newCount;
    }
}

Kod ten nie wykonuje żadnej skomplikowanej operacji. Jakie są zalety dodania getter i setter bez dodania żadnej inne funkcjonalności. Możesz jednak to zmienić  później bez martwienia się  o  istniejące już implementacje korzystające z twojej klasy. Dobrego kodu nie można stworzyć od początku do końca. Na szczęście wystarczy tylko  przestrzegać pewnych zasad ,aby twoja klasa mogła bez problemów się rozwijać w przyszłości, bez gniewu kaszubskich programistów z innego wymiaru.

Dziedziczenie Is-A, Has-A

Przejdźmy do następnego celu egzaminu.

Cel certyfikatu – Objectives 5.5

Alternate Text

5.1. Develop code that implements “is-a” and/or “has-a” relationships.

Stwórz kod, który implementuje związki “is-a” i/albo “has-a”.

Dziedziczenie jest wszędzie <dramatyczna muzyka>. Dziedziczenie jest to podstawowym mechanizmem języków obiektowych w tym Java. Jest prawie niemożliwe napisanie programu bez użycia dziedziczenia. Użyjemy operatora “instanceof” jednak  szerzej omówię go później. Na razie musisz wiedzieć ,że ten operator zwraca prawdę, gdy typ obiektu jest zgodny z podanym typem.

public class Test 
{
    public static void main(String[] args) 
    {
        Test t1 = new Test();
        Test t2 = new Test();
        
        if (!t1.equals(t2))
            System.out.println("nie są równe");

        if (t1 instanceof Object)
            System.out.println("t1 jest obiektem");
    }
}

Wynik kodu:

nie są równe
t1 jest obiektem

Skąd wzięła się metoda“equals. Zmienna referencyjna jest typu Test ,ale ta klasa nie posiada metody equals. Drugi if pyta czy t1 jest instancją klasy Object i ponieważ nim jest w pewnym sensie ten warunek się spełnił.

Jak t1 może być instancją  typu Object skoro jest typu Test. Zapewne już to wiesz. Każda klasa w Javie dziedziczy po klasie Object (poza samą klasą Object). Innymi słowy, każda klasa jakąkolwiek użyjesz bądź napiszesz automatycznie dziedziczy po klasie Object. Każda klasa posiada więc takie metody jak: clone, notify, wait, equals i inne.

Dlaczego tak jest? Twórcy Java założyli ,że porównywanie obiektów będzie wykonywane dość często. Gdyby klasa Object nie miała tej metody musiałbyś napisać ją sam jak  i każdy innych programista Java. Poza tym metoda equals jest nie tylko dziedziczona przez inne klasy ,ale też nadpisywana (overridden) o czym później.

Do egzaminu musisz wiedzieć jak stworzyć związki dziedziczenia za pomocą słowa extend w klasach. Istotną sprawą jest też zrozumienie, dlaczego dziedziczenie jest w ogóle stosowane. Oto dwa najważniejsze powody.

  • Ponowne użycie kodu.
  • Użycie polimorfizmu.

Ponowne użycie kodu najłatwiej wykazać dlatego zacznę od tego. Zwykle w architekturze programowania  najpierw tworzy się dość ogólną wersje klasy, która później służy jako rozszerzenie (extend) dla klasy pochodnej. Oto przykład:

class Shape
{
    public void drawShape()
    {
        System.out.println("wyświetlę kształty");
    }
}

class Square extends Shape
{
    private int a;

    public void field()
    {
        System.out.println(a*a);
    }
    
    public Square(int a) 
    {
        this.a = a;
    }
}

public class TestShape 
{
    public static void main(String[] args) 
    {
        Square square = new Square(5);
        square.drawShape();
        square.field();
    }
}

Wynik kodu:

wyświetlę kształty
25

Zauważ ,że klasa Square odziedziczyła metodę “drawShape” z mniej określonej klasy Shape. W podobny sposób mogę stworzyć klasę prostokąt i trójkąt i tak dalej bez powielania tego samego kodu dzięki klasie (Kształty)Shape.  Nie chciałbyś przepisywać tego samego kodu do klasy prostokąt i trójkąt.

Zapewne już o tym wiedziałeś. Duplikacja kodu zawsze w programistach wywołuje nieprzyjemne uczucie, zwłaszcza że da się tego łatwo uniknąć.

Drugą główną zaletą dziedziczenia jest dostęp do klas polimorficznie. Powiedzmy ,że masz prosty program do rysowania i chcesz ,aby program w jednej  pętli narysował różne klasy jak trójkąt czy prostokąt.  Do tego nie wiesz, czy w przyszłości nie pojawią się inne kształty, które trzeba będzie też narysować w tej pętli. Nie chcesz później pisać ponownie kodu, tylko dlatego ,że twój program musi jeszcze rysować gwiazdy.

Piękną rzeczą w polimorfizmie (wiele form) jest fakt ,że każda klasa pochodna może być traktowana jak typ klasy bazowej. Co znaczy ,że klasa Square może być traktowana jako klasa Shape.

Dzięki temu możesz napisać metodę, która będzie mówiła“Nie wiem jakim dokładnym obiektem jesteś ale dopóki dziedziczysz po klasie Shape wiem ,że mogę u ciebie wywołać metodę “DrawShape()”, wiec ją wywołam “

Oto przykład tego użycia.

class Shape
{
    public void drawShape(){
        System.out.println("wyświetlę kształty");
    }

}

class Square extends Shape{}
class Circle extends Shape{}
class Triangle extends Shape{}
class Star extends Shape{}
class Arc extends Shape{}

public class TestShape 
{
    public static void main(String[] args) {
        Square square = new Square();
        Circle circle = new Circle();
        Triangle triangle = new Triangle();
        Star star = new Star();
        Arc arc = new Arc();
        
        drawThis(arc);
        drawThis(star);
        drawThis(triangle);
        drawThis(circle);
        drawThis(square);
    }
    
    public static void drawThis(Shape shape)
    {
        shape.drawShape();
    }
}


Wynik kodu:

wyświetlę kształty
wyświetlę kształty
wyświetlę kształty
wyświetlę kształty
wyświetlę kształty

Jak widzisz metoda zadeklarowana w klasie TestShape przyjmuje klasę Shape jako parametr. Argumentem może być nie tylko klasa Shape ,ale też wszystkie klasy dziedziczącej po niej. Oczywiście są pewne ograniczenia , w końcu metoda ta będzie traktować wszystkie obiekty jako Shape, więc nie mogę wewnątrz tej metody wywołać czegoś, co istnieje tylko dla klasy Square ,nawet jeśli ten obiekt zostanie umieszczony jako argument.

Nie martw się ta tematyka będzie kontynuowana  w interfejsach.

Związki IS-A

Na egzaminie będziesz musiał też wykazywać czy dany kod jest w związku IS-A, czy HAS-A. Zasady są proste dlatego powinieneś się raczej cieszyć ,że trafiłeś na takie pytanie.

W języku zorientowanym obiektowo koncepcja IS-A bazuje na podstawowym dziedziczeniu klasy czy implementacji interfejsu. IS-A mówi ,że“ta rzecz X jest typu tej rzeczy Y”.

Przykładowo tygrys jest też kotem po angielsku ma to nawet większy sens “(Tiger IS-A CAT)”

W javie aby wyrazić związek IS-A stosuje się słowo “extends” dla klas i słowo “implements” dla interfejsów.

public class Cat {}

class Tiger extends Cat{}

class Manchurian extends Tiger{}
//Manchurian to nazwa tygrysa syberyjskiego

W terminologii obiektowości ten kod mówi:

Klasa Cat jest super klasą dla Tiger.
Tiger jest klasą pochodną dla Cat.
Tiger jest super klasą dla  Manchurian
Manchurian jest klasą pochodną dla Cat.
Tiger dziedziczy po Cat.
Manchurian dziedziczy po Cat i Tiger.
Manchurian jest uzyskana z Tiger.
Tiger jest uzyskana z Cat.
Manchurian jest uzyskany z Cat.
Manchurian jest podtypem obu klas Cat i Tiger.


Nie wyszło to dobrze angielski niezbyt dobrze się kombinuje z językiem polskim. Wracając do związku IS-A poniższe wyrażenia wykazują prawdę, co do kotów:

Tiger rozszerza klasę Cat, czyli Tiger IS-A Cat

Manchurian rozszerza klasę Tiger, czyli Manchurian IS-A Tiger

Możemy też powiedzieć ,że “Manchurian IS-A CAT”, ponieważ klasa ta też jest typu kot, jak i innych rzeczy znajdujących się powyżej w drzewie dziedziczenia jak np. Ssak.

Używając operatora “instanceof” łatwo to wykazać. Jeśli wyrażenie ( x instanceof y) jest prawdziwe oznacza to ,że klasa “x” IS-A “y”. Klasa “x” nie musi bezpośrednio rozszerzać się od klasy "y" by to wyrażenie byłoby prawdziwe.

Poniżej znajduje się ilustracja opisująca przykład z kotami i tygrysami.

public class-10

Związek HAS-A

Związek HAS-A bazuje bardziej na użytkowaniu niż na dziedziczeniu.  Wygląda to tak klasa X HAS-A Y, jeżeli kod w klasie X ma referencje do instancji klasy Y. Oto przykład:

A Door HAS-A lock.

Czyli drzwi mają zamek.

 


public class Door 
{
    private Lock myLock;
}

class Lock
{
    void lockIt()
    {
        System.out.println("Closed");
    }
}

W tym kodzie klasa Door posiada instancje zmiennej o typie Lock więc mogę powiedzieć ,że “Door HAS-A Lock” . Czyli Door posiada referencje do Lock. Door może wywołać metody zawarte w klasie Lock i skorzystać z jego zachowań, mimo iż po nim nie dziedziczy.

Ilustracja poniżej pokazuje związek pomiędzy Door ,a Lock.

public class-11 
Wywołanie metody  “lockIt” jest możliwe w instancji zmiennej Lock, która znajduje się w obiekcie Door.

Związek HAS-A pozwala w konstrukcji klas na wykonanie dobrego kodu poprzez nie posiadanie jednej dużej klasy, która robiłaby wszystko. Klasy i stworzone na ich wzór obiekty powinny specjalizować się w jednej rzeczy. Jest to nawet zgodne z zasadami S.O.L.I.D. Im bardziej obiekty są wyspecjalizowane, tym większa szansa ,że będą użyte ponownie w innym programie.  Przykładowo w tym kodzie klasa Lock może być użyta nie tylko do drzwi ,ale też do samochodu czy okna.

Użytkownicy klasy Door będą myśleć ,że ta klasa ma zachowanie klasy Lock ,ale tak nie jest. Użytkownicy jednak nie muszą tego wiedzieć. Klasa Door może mieć metodę lockIt() , która będzie delegatą do metody z klasy Lock.lockIt(). Wyglądałoby to tak.

public class Door 
{
    private Lock myLock;
    
    void lockIt()
    {
        myLock.lockIt();
    }
}

class Lock
{
    void lockIt()
    {
        System.out.println("Closed");
    }
}

W obiektowości nie chcemy aby wywoływacze metod przejmowały się tym, która klasa czy obiekt aktualnie wykonuje robotę. Aby to było możliwe wystarczy ukryć implementacje klasy Door przed użytkownikami i stworzyć tylko metody do wykonywania odpowiednich poleceń. Użytkownicy klasy Door nie muszą nawet wiedzieć ,że istnieje w nim instancja klasy Lock.

Co dalej:

Polimorfizm.

Spis treści: