W tym artykule przybliżę trzy elementy, które z mojego doświadczenia są bardzo istotne dla osób uczących się języka Java, aby dobrze zrozumieć programowanie obiektowe. Osoby średniozaawansowane oraz zaawansowane też mogą z tego skorzystać, traktując tę wiedzę jako powtórkę. W skład każdego z trzech głównych punktów wchodzą pomniejsze trzy elementy, stąd trzy trójce programistyczne. Poniższe trójce pozwolą stworzyć zestaw narzędzi, którymi będziemy posługiwać się przy programowaniu obiektowym.

W tym artykule umówię poniższe trójce programistyczne:

  1. Trzy elementy składowe klasy:
    • atrybut, konstruktor, metoda.
  2. Trzy paradygmaty programowania obiektowego:
    • hermetyzacja, dziedziczenie, polimorfizm.
  3. Trzy elementy wspierające programowanie obiektowe:
    • zależność, wstrzykiwanie zależności, delegacja.

Powyższe punkty mógłbym opisać w formie oddzielnego artykułu – każdy z tematów jest bardzo obszerny – niemniej moim celem było zgrupowanie wszystkich elementów w jednym miejscu tak, aby można było spojrzeć – z „lotu ptaka”, szerszej perspektywy – na zestaw narzędzi dostępnych dla programowania obiektowego.

Każdy z powyższych punktów jest opisany za pomocą poniższych sekcji:
– wyjaśnienie danego zagadnienia,
– kod źródłowy zagadnienia,
– odpowiedź na pytanie „Co, to oznacza, że…”,
– kod źródłowy z „aplikacją” używającą dane zagadnienie,
– wynik działania „aplikacji”.

Do czego można by wykorzystać taki zestaw narzędzi? Na przykład w codziennej pracy programistycznej, tworząc nowy kod, rozwijając istniejący lub testując go. Kod stworzony w oparciu o reguły programowania obiektowego jest elastyczny na zmiany, rozszerzalny i nadaje się do testowania – nie zawsze tak jest. Można przepisać od nowa istniejący kod, ale wymaga, to ogromnego nakładu pracy i sporego budżetu na taki projekt.

Zacznę od wstępu związanego z programowaniem obiektowym. Do czego używamy programowania obiektowego? W dużym uproszczeniu i bez wnikania w szczegóły techniczne, używamy go do odwzorowania rzeczy, pojęć, obiektów otaczającego nas świata na język „zrozumiały dla komputera”. Na szczęście Java jest językiem programowania wysokiego poziomu w odróżnieniu od np.: Assemblera, który jest językiem programowania niskiego poziomu – tym samym nie musimy programować instrukcji procesora, skupiamy się na „otaczającym nas świecie”.

Przykłady elementów otaczającego nas świata , które możemy odwzorować w języku Java; rzeczy, np.: notatnik, faktura; pojęcia, np.: przesyłka, przelew; obiekty: samochód, dom. W Asemblerze „jedno polecenie odpowiada zasadniczo jednemu rozkazowi procesora”, nie ma tu mowy o odwzorowaniu otaczającego nas świata. Pod koniec artykułu umieszczę porównanie kodu źródłowego języka Java i Assemblera, to porównanie nie stanowi istoty, tego artykułu – po prostu, z doświadczenia wiem, że lepiej pokazać na przykładzie, niż tłumaczyć teorię.

Skoro już wiemy, że język Java pozwala odwzorować otaczające nas rzeczy, pojęcia i obiekty, to czas na omówienie elementów języka, które pozwalają na wykonanie takiego odwzorowania. Podstawową jednostką, pojęciem w programowaniu obiektowym jest klasa, która jest szablonem do tworzenia konkretnych reprezentacji, czyli obiektów, np.: klasa może być szablonem dla różnych notatników, a obiektem może być, np.: notatnik w linię, kratkę, posiadający 60 kartek.

Parafrazując wyżej opisane trójce programistyczne, dodając informację, że opierają się one na klasach języka Java, mogę napisać, że klasa w języku Java:

Elementy klasy pozwalają stworzyć szablon według naszego pomysłu; konwencje, paradygmaty nakładają wspólne reguły na klasy oraz poszczególne elementy składowe; tworzenie relacji pozwala na interakcje pomiędzy klasami. Klasa tworzy nowy typ w języku Java, taki sam jak istniejące w języku Java np.: String, LocalDateTime.

Brzmi skomplikowanie, ale nie musi tak być, dlatego napisałem ten artykuł, żeby ułatwić osobom początkującym zrozumienie powyższych elementów.

Ważne! Wszystkie elementy kodu powinny być samo opisujące się.

Poniższy kod źródłowy prezentuje tworzenie komentarzy w języku Java.

// tak tworzy się komentarz dla jednej linii w języku Java

Poniższy kod źródłowy prezentuje w najprostszej postaci klasę języka Java – Notebook.

// klasa w języku Java
public class Notebook {
    // ciało klasy
}

Klasa jest zwykłym plikiem tekstowym, który możemy znaleźć za pomocą eksploratora plików i wyświetlić jego zawartość za pomocą dowolnego edytora tekstu. Programiści i programistki do tego celu używają zintegrowanego środowiska programistycznego (IDE), np.: IntelliJ IDEA Community. Więcej na temat klas i obiektów w języku Java można przeczytać w oficjalnej dokumentacji na stronie The Java™ Tutorials – Lesson: Classes and Objects lub The Java™ Tutorials – Declaring Classes.

W wielu miejscach dla maksymalnego uproszczenia przykładów pomijam pewne elementy w klasach, np.: metody getters/setters i odwołuję się bezpośrednio do atrybutów i/lub metod klasy, nie zmniejsza, to jakości przekazywanych informacji, ale moim zdaniem ułatwia zrozumienie konkretnego zagadnienia. Nie tworzę też sztucznie klas typu POJO, żeby nie zaciemniać obrazu i skupić się na konkretach.

Poniższy kod źródłowy prezentuje różnicę pomiędzy klasą 'Notebook’, a jej obiektem 'simpleNotebook’.

// utworzenie obiektu 'simpleNotebook' klasy 'Notebook'
Notebook simpleNotebook = new Notebook();

Trzy elementy składowe klasy

  1. Atrybut / Pole / Zmiennapozwalają na przechowywanie stanu, cech, danych dla obiektu. Dla ułatwienia będę posługiwał się tylko jednym określeniem atrybut. Pierwszy element klasy jak na złość można określać, nazywać za pomocą trzech pojęć, które wskazują na to samo. Warto o tym pamiętać, pomoże, to w wyszukiwaniu informacji w internecie. Klasa może posiadać zero lub więcej atrybutów różnego typu. Więcej na ten temat można przeczytać w oficjalnej dokumentacji języka Java na stronie The Java™ Tutorials – Declaring Member Variables.

Poniższy kod źródłowy prezentuje klasę języka Java posiadającą jedną z trzech składowych – atrybut.

public class Notebook {
    // atrybuty klasy
    int pages;
    int currentPage;
    Map<Integer, String> contents = new HashMap<>();
}

Co, to oznacza, że atrybuty przechowują stanu, cech dla obiektu? Na przykładzie notatnika (klasa Notebook), kupując notatnik w sklepie posiada on cechy, takie jak rozmiar, ilość stron (int pages). Używając notatnik możemy wypełnić zawartością każdą ze stron (Map<Integer, String> contents) i zaznaczyć „bieżącą/ostatnią” stronę (int currentPage). Obrazuje, to poniższy kod źródłowy.

public class ApplicationMain {
    public static void main(String[] args) {
        Notebook historyNotebook = new Notebook();
        historyNotebook.pages = 60;
        historyNotebook.contents.put(33, "Historia powszechna.");
        historyNotebook.currentPage = 33;
        System.out.println("Notatnik do historii: "
                + historyNotebook);
    }
}

Poniżej wynik programu prezentującego wykorzystanie atrybutów.

Notatnik do historii: Notebook{pages=60, currentPage=33, contents={33=Historia powszechna.}}

Process finished with exit code 0
  1. Konstruktorumożliwia tworzenie obiektów danej klasy według wyznaczonego wzoru, schematu. Klasa może posiadać zero lub więcej konstruktorów, każdy z nich dla innego wzoru, schematu. Więcej na ten temat można przeczytać w oficjalnej dokumentacji języka Java na stronie The Java™ Tutorials – Providing Constructors for Your Classes.

Poniższy kod źródłowy prezentuje klasę języka Java posiadającą jedną z trzech składowych – konstruktor.

public class Notebook {
    // pomijam atrybuty klasy z punktu 1.

    // konstruktor klasy
    public Notebook() {
    }

    // konstruktor klasy
    public Notebook(int pages) {
        // ciało konstruktora, instrukcje do wykonania
        this.pages = pages;
    }
}

Co, to oznacza, że umożliwia tworzenie obiektów danej klasy według wyznaczonego wzoru, schematu? Podobnie jak dla atrybutów, na przykładzie notatnika (klasa Notebook), kupując notatnik w sklepie posiada on cechy, takie jak rozmiar, ilość stron (int pages). Z tą różnicą, że konstruktory „wymuszają” podanie konkretnych danych w momencie tworzenia obiektu. Pozwala, to uniknąć sytuacji, w której wyprodukowany notatnik nie ma określonej liczby stron i musimy samemu policzyć sobie ile faktycznie jest stron. Obrazuje, to poniższy kod źródłowy.

public class ApplicationMain {
    public static void main(String[] args) {
        Notebook mathNotebook = new Notebook(90);
        System.out.println("Ilość stron: " + mathNotebook.pages);
    }
}

Poniżej wynik programu prezentującego działanie konstruktora.

Ilość stron: 90

Process finished with exit code 0
  1. Metodaumożliwia wykonywanie czynności, interakcję z klasą, obiektem. Klasa może posiadać zero lub więcej metod. Więcej na ten temat można przeczytać w oficjalnej dokumentacji języka Java na stronie The Java™ Tutorials – Defining Methods.

Poniższy kod źródłowy prezentuje klasę języka Java posiadającą jedną z trzech składowych – metoda.

public class Notebook {
    // pomijam atrybuty klasy z punktu 1.
    // pomijam konstruktory klasy z punktu 2.

    // metoda klasy
    void writeNoteContent(String noteContent) {
        // ciało metody, instrukcje do wykonania
        contents.merge(currentPage, noteContent,
                (oldContent, newContent) 
                        -> oldContent + " " + newContent);
    }

    // metoda klasy
    void goToPage(int page) {
        // ciało metody, instrukcje do wykonania
        currentPage = page;
    }
}

Co, to oznacza, że metody umożliwiają wykonywanie czynności, interakcję z klasą, obiektem? Na przykładzie notatnika (klasa Notebook), skoro wiemy, że każdy notatnik ma stan, cechy opisujące liczbę stron (int pages), to dzięki metodzie możemy „otworzyć/pójść” do wybranej przez nas strony – void goToPage(int page) – następnie wpisać treść – void writeNoteContent(String noteContent) – która ma być na wybranej stronie. Obrazuje, to poniższy kod źródłowy.

public class ApplicationMain {
    public static void main(String[] args) {
        Notebook smallNotebook = new Notebook(60);
        smallNotebook.goToPage(22);
        smallNotebook.writeNoteContent("Pierwsza notatka.");
        System.out.println("Treść notatki na bieżącej stronie: "
                + smallNotebook);
    }
}

Poniżej wynik programu prezentującego działanie metody.


Notebook - append note content to the current page: Pierwsza notatka.
Treść notatki na bieżącej stronie: Notebook{pages=60, currentPage=22, contents={22=Pierwsza notatka.}}

Poniższy kod źródłowy prezentuje klasę języka Java posiadającą wszystkie trzy składowe klasy – atrybuty, konstruktory i metody.

public class Notebook {
    // atrybuty klasy
    int pages;
    int currentPage;
    Map<Integer, String> contents = new HashMap<>();

    // konstruktor klasy
    public Notebook() {
    }

    // konstruktor klasy
    public Notebook(int pages) {
        // ciało konstruktora, instrukcje do wykonania
        this.pages = pages;
    }

    // metoda klasy
    void writeNoteContent(String noteContent) {
        // ciało metody, instrukcje do wykonania
        contents.merge(currentPage, noteContent,
                (oldContent, newContent) 
                        -> oldContent + " " + newContent);
    }

    // metoda klasy
    void goToPage(int page) {
        // ciało metody, instrukcje do wykonania
        currentPage = page;
    }
}

Większość klas tworzonych przez programistów, programistki wygląda podobnie jak prezentowany wyżej kod. Obiekt klasy Notebook będzie: posiadał, przechowywał swój stan; tworzył się według określonego schematu; wykonywał czynności, wchodził w interakcję.

Poniższy kod źródłowy prezentuje działanie wszystkich trzech składowych klasy – atrybuty, konstruktory i metody.

public class ApplicationMain {
    public static void main(String[] args) {
        // utworzenie obiektu 'simpleNotebook' klasy 'Notebook'
        Notebook simpleNotebook = new Notebook(9);

        System.out.println("Bieżąca strona notatnika: "
                + simpleNotebook.currentPage);
        simpleNotebook.goToPage(3);
        System.out.println("Bieżąca strona notatnika," +
                " po zmianie strony: " 
                + simpleNotebook.currentPage);

        System.out.println("Bieżąca zawartość notatnika: "
                + simpleNotebook.contents);
        simpleNotebook.writeNoteContent("Wpis w notatniku");
        System.out.println("Bieżąca zawartość notatnika," +
                " po dodaniu treści: " + simpleNotebook.contents);
    }
}

Poniżej wynik programu prezentującego atrybuty, konstruktory i metody.

Bieżąca strona notatnika: 0
Bieżąca strona notatnika, po zmianie strony: 3
Bieżąca zawartość notatnika: {}
Notebook - append note content to the current page: Wpis w notatniku
Bieżąca zawartość notatnika, po dodaniu treści: {3=Wpis w notatniku}

Process finished with exit code 0

Trzy paradygmaty programowania obiektowego

  1. Hermetyzacjapozwala „ukryć” elementy klasy przed „niechcianym” dostępem z poziomu innej klasy – używamy do tego modyfikatory dostępu, np.: public, protected, private. Hermetyzacja czasem nazywa się enkapsulacją. Więcej na ten temat można przeczytać w oficjalnej dokumentacji języka Java na stronie The Java™ Tutorials – Controlling Access to Members of a Class.

Poniższy kod źródłowy prezentuje jedną z konwencji, paradygmatów programowania obiektowego – hermetyzacja.

public class Notebook {
    // atrybuty klasy
    private int pages;
    private int currentPage;
    private Map<Integer, String> contents = new HashMap<>();

    // metoda klasy
    void goToPage(int page) {
        // ciało metody, instrukcje do wykonania
        if (page < pages) {
            currentPage = page;
        }
    }
}

Co, to oznacza, że hermetyzujemy elementy klasy? Hermetyzacja, enkapsulacja chroni atrybuty, konstruktory i metody przed niewłaściwym użyciem, mówiąc inaczej, to twórcy danej klasy decydują o tym jak ona będzie się zachowywała. Na przykładzie notatnika (klasa Notebook) atrybuty są „ukryte”, mają oznaczenie private, wybór/przejście do bieżącej strony (int currentPage) możliwy jest tylko za pomocą metody void goToPage(int page). Metoda goToPage sprawdza czy numer podanej strona jest mniejszy niż ilość wszystkich stron if (page < pages). Bez tego warunku „zabezpieczającego” program „na ślepo” wykonał by przejście do nieistniejącej strony, co zakończyłoby się zgłoszeniem błędu i/lub zakończeniem programu. Obrazuje, to poniższy kod źródłowy.

public class ApplicationMain {
    public static void main(String[] args) {
        Notebook hermeticNotebook = new Notebook();
        // nie można nadać wartości dla poniższych atrybutów

        // 'pages' has private access in 'Notebook'
        // hermeticNotebook.pages = 30;
        // 'currentPage' has private access in 'Notebook'
        // hermeticNotebook.currentPage = 3;
    }
}
  1. Dziedziczeniepozwala wydzielić wspólne elementy klasy – stan, cechy, dane – do innej klasy tzw. nad klasy, która będzie rodzicem dla innych klas – używamy do tego słowa kluczowego extends. Więcej na ten temat można przeczytać w oficjalnej dokumentacji języka Java na stronie The Java™ Tutorials – Inheritance.

Poniższy kod źródłowy prezentuje jedną z konwencji, paradygmatów programowania obiektowego – dziedziczenie.

public class Notebook {
    // atrybuty klasy
    protected int pages;
    protected int currentPage;
    protected Map<Integer, String> contents = new HashMap<>();
}
// dziedziczenie - słowo kluczowe 'extends'
public class SpiralNotebook extends Notebook {
    // atrybuty klasy
    protected String spiralMaterial;
    protected boolean perforated;

    // metoda klasy
    void removePage(int pageNumber) {
        if (perforated) {
            pages = pages - 1;
        }
    }
}

Co, to oznacza, że dziedziczymy elementy nad klasy? Na przykładzie notatników (klasa Notebook oraz SpiralNotebook), posiadają one wspólne cechy takie jak ilość stron (int pages), ale mogą być notatniki, które różnią się pewnymi cechami. Kołonotatnik (klasa SpiralNotebook) posiada spiralę łączącą kartki, która może być z różnego materiału (String spiralMaterial), jego kartki mogą mieć lub nie mieć perforacji (boolean perforated), która umożliwia wyrwanie strony (void removePage(int pageNumber). To samo dotyczy metod i konstruktorów w klasach. Dziedziczenie pozwala zmniejszyć ilość powtarzalnego kodu oraz ujednolicić, zgrupować wiele klas w jeden typ. Obrazuje, to poniższy kod źródłowy.

public class ApplicationMain {
    public static void main(String[] args) {
        SpiralNotebook spiralNotebook = new SpiralNotebook();
        spiralNotebook.pages = 30;
        System.out.println("Ilość stron: "
                + spiralNotebook.pages);

        spiralNotebook.spiralMaterial = "Metal";
        System.out.println("Materiał spirali: "
                + spiralNotebook.spiralMaterial);

        spiralNotebook.perforated = true;
        spiralNotebook.removePage(2);
        System.out.println("Ilość stron: "
                + spiralNotebook.pages);
    }
}

Poniżej wynik programu prezentującego dziedziczenie.

Ilość stron: 30
Materiał spirali: Metal
Ilość stron: 29

Process finished with exit code 0

Praktyczna uwaga: należy ostrożnie używać dziedziczenia, jeżeli to możliwe należy je zastąpić zależnością, kompozycją. Zamiast relacji IS-A (jest czymś) warto używać relacji HAS-A (ma coś). Więcej o tym pisałem w artykule Stop! Zanim zaczniesz pisać kod zastanów się, co chcesz kodować? Analiza, projekt i implementacja w sekcji „Analiza – Karty CRC – Class Responsibility Collaborators”.

  1. Polimorfizmpozwala tej samej metodzie zachowywać się, wykonywać kod programu w różny sposób w ramach dziedziczenia klas i/lub implementacji interfejsów – polimorfizm można wskazać za pomocą adnotacji @Override. Polimorfizm w wolnym tłumaczeniu, to wielopostaciowość. Więcej na ten temat można przeczytać w oficjalnej dokumentacji języka Java na stronie The Java™ Tutorials – Polymorphism.

Poniższy kod źródłowy prezentuje jedną z konwencji, paradygmatów programowania obiektowego – polimorfizm.

public class Notebook {
    // pomijam atrybuty klasy
    // pomijam konstruktory klasy
    // pomijam metody klasy

    // metoda klasy
    void writeNoteContent(String noteContent) {
        // ciało metody, instrukcje do wykonania
        contents.merge(currentPage, noteContent,
                (oldContent, newContent)
                        -> oldContent + " " + newContent);

        System.out.println("Notebook - append note content" +
                " to the current page: " + noteContent);
    }
}
public class SpiralNotebook extends Notebook {
    private String spiralMaterial;
    private boolean perforated;

    void removePage(int pageNumber) {
        if (perforated) {
            pages = pages - 1;
        }
    }

    // metoda klasy używająca polimorfizmu
    // nadpisująca metodę z nad klasy
    @Override
    void writeNoteContent(String noteContent) {
        // ciało metody, instrukcje do wykonania
        contents.put(currentPage, noteContent);
        removePage(currentPage);

        System.out.println("SpiralNotebook - write note content" +
                " and remove page: " + noteContent);
    }
}

Co, to oznacza, że metody klasy mają wiele form, postaci – polimorfizm? Daje, to możliwość dostosowania działania metody dla klas, które dziedziczą wspólne elementy (Map<Integer, String> contents), ale chciałby, aby ich zachowanie było inne@Override void writeNoteContent(String noteContent) – niż klasy nadrzędnej, rodzica. Na przykład w kołonotatniku po zapisaniu strony – contents.put(currentPage, noteContent) – chcielibyśmy ją wyrwać – removePage(currentPage). Obrazuje, to poniższy kod źródłowy.

public class ApplicationMain {
    public static void main(String[] args) {

        // utworzenie obiektu 'janNotebook' klasy 'Notebook'
        Notebook janNotebook = new Notebook();

        // utworzenie obiektu 'alaNotebook' klasy 'SpiralNotebook'
        Notebook alaNotebook = new SpiralNotebook();

        janNotebook.writeNoteContent("Jan ma psa");
        alaNotebook.writeNoteContent("Ala ma kota");
    }
}

Poniżej wynik programu prezentującego działanie polimorfizmu.

Notebook - append note content to the current page: Jan ma psa
SpiralNotebook - write note content and remove page: Ala ma kota

Process finished with exit code 0

Trzy elementy wspierające programowanie obiektowe

  1. Zależność – inaczej powiązanie, pozwala używać jednej klasy w drugiej, zazwyczaj zależność reprezentowana jest jako atrybut klasy. Zależności tworzą relacje pomiędzy klasami.

Poniższy kod źródłowy prezentuje jeden ze sposobów współdziała z innymi klasami poprzez tworzenie relacji – zależność.

public class Notebook {
    // pomijam atrybuty klasy
    // pomijam konstruktory klasy
    // pomijam metody klasy
}
public class Student {
    // zależność do klasy Notebook
    // zależność jest jednocześnie atrybutem klasy Student
    private Notebook notebook;
}

Co, to oznacza, że tworzymy zależności, powiązania do innych obiektów? Dzięki zależnościom możemy wykorzystać istniejący kod klasy i użyć go w naszych własnych klasach, co pozwala zredukować powtarzanie kodu, zaoszczędzić czas na „wymyślanie koła” od nowa. Pozwala na tworzenie tzw. luźnych powiązań między klasami – jest to zgodne z dobrymi praktykami programowania obiektowego.

  1. Wstrzykiwanie zależności – sposób przekazywania konkretnej zależności do klasy, która jej używa.

Poniższy kod źródłowy prezentuje jeden ze sposobów współdziała z innymi klasami poprzez tworzenie relacji – wstrzykiwanie zależności.

public class Student {
    private Notebook notebook;

    public Student(Notebook notebook) {
        this.notebook = notebook;
    }
}

Co, to oznacza, że wstrzykujemy zależności do innych obiektów? W momencie tworzenia obiektu decydujemy z jakiej klasy zależnej będzie korzystał tworzony obiekt. Nie „zaszywamy na stałe” w kodzie, nie uzależniamy się od jednej konkretnej implementacji naszego kodu. Na przykładzie klas Student i Notebook, dopiero w momencie tworzenia obiektu klasy student podajemy z jakiego notatnika będzie on korzystał, zwykły notatnik – new Student(new Notebook()) – czy kołonotatnik – new Student(new SpiralNotebook()). Pisanie notatek przez studenta – startNote(…) – będzie miało inny przebieg w zależności od wybranego notatnika. Obrazuje, to poniższy kod źródłowy.

public class ApplicationMain {
    public static void main(String[] args) {

        // utworzenie obiektu 'janNotebook' klasy 'Notebook'
        Notebook tomNotebook = new Notebook(30);
        // utworzenie obiektu 'tomStudent' klasy 'Student'
        // wstrzyknięcie zależności 'tomNotebook' do klasy Student
        Student tomStudent = new Student(tomNotebook);
        tomStudent.startNote("Pewnego dnia.", 2);

        // utworzenie obiektu 'alaNotebook' klasy 'SpiralNotebook'
        Notebook eveNotebook = new SpiralNotebook(60);
        // utworzenie obiektu 'eveStudent' klasy 'Student'
        // wstrzyknięcie zależności 'eveNotebook' do klasy Student
        Student eveStudent = new Student(eveNotebook);
        eveStudent.startNote("Dawno temu.", 4);
    }
}

Poniżej wynik programu prezentującego wstrzykiwanie zależności.

Notebook - append note content to the current page: Pewnego dnia.
SpiralNotebook - write note content and remove page: Dawno temu.

Process finished with exit code 0
  1. Delegacja – odwołanie się do klasy zależnej w celu przekazania, przekierowania do niej działania.

Poniższy kod źródłowy prezentuje jeden ze sposobów współdziała z innymi klasami poprzez tworzenie relacji – delegacja.

public class Student {
    private Notebook notebook;

    void writeNote(String noteContent, int page) {
        notebook.goToPage(page);
        notebook.writeNoteContent(noteContent);
    }
}

Co, to oznacza, że delegujemy działanie do innych obiektów? Na przykładzie powyższej klasy Student i metody void writeNote(String noteContent, int page) – kiedy wywołujemy dla studenta metodę writeNote(…) – ona deleguje swoje działanie do klasy Notebook, która, wie, że musi przejść do strony – notebook.goToPage(page) – na której będzie mogła zapisać – notebook.writeNoteContent(noteContent) – treść notatki. Tym samym wspieramy dobrą praktykę programistyczną jaką jest Single Responsibility Principle z S.O.L.I.D..


Poniższy kod źródłowy prezentuje wszystkie sposoby współdziała z innymi klasami poprzez tworzenie relacji – zależność, wstrzykiwanie zależności oraz delegacja.

public class ApplicationMain {
    public static void main(String[] args) {

        // utworzenie obiektu 'janNotebook' klasy 'Notebook'
        Notebook janNotebook = new Notebook();
        // utworzenie obiektu 'janStudent' klasy 'Student'
        Student janStudent = new Student(janNotebook);
        janStudent.startNote("Jan ma psa", 1);

        // utworzenie obiektu 'alaNotebook' klasy 'SpiralNotebook'
        Notebook alaNotebook = new SpiralNotebook();
        // utworzenie obiektu 'alaStudent' klasy 'Student'
        Student alaStudent = new Student(alaNotebook);
        alaStudent.startNote("Ala ma kota", 3);
    }
}
Notebook - append note content to the current page: Jan ma psa
SpiralNotebook - write note content and remove page: Ala ma kota

Process finished with exit code 0

Assembler vs Java

Na zakończenie jako ciekawostkę prezentuję porównanie kodu źródłowego języka Java i Assemblera, które moim zdaniem pokazuje jakie ułatwienie mają programiści, programistki języka Java w porównaniu z osobami piszącymi kod w Assemblerze.

section	.text
   global_start   ;must be declared for linker (ld)
  
_start:	          ;tells linker entry point
   mov	edx,len   ;message length
   mov	ecx,msg   ;message to write
   mov	ebx,1     ;file descriptor (stdout)
   mov	eax,4     ;system call number (sys_write)
   int	0x80      ;call kernel
  
   mov	eax,1     ;system call number (sys_exit)
   int	0x80      ;call kernel

section	.data
msg db 'Hello, world!', 0xa  ;string to be printed
len equ $ - msg     ;length of the string

Powyżej kod źródłowy Assemblera.

public class MainProgram {
    public static void main(String[] args) {
        System.out.println("Hello World!");
    }
}

Powyżej kod źródłowy języka Java.


Podsumowując, moim zdaniem niezbędna jest znajomość wyżej opisanego zestawu narzędzi w postaci:
Trzech elementów składowych klasy,
Trzech paradygmatów programowania obiektowego,
Trzech elementów wspierających programowanie obiektowe.
Tak samo jak kierowca samochodu jadący cały czas na pierwszym biegu nie rozwinie pełnego potencjału samochodu – może go również popsuć – tak samo programista, programistka nie przechodząc na wyższy poziom wiedzy nie rozwinie w pełni możliwości języka Java – może „popsuć kod” – pisząc kod, którego późniejsze utrzymanie będzie bardzo trudne lub wręcz niemożliwe. Kolejnym krokiem po opanowaniu materiału z tego artykułu może być zapoznanie się z moim innym artykułem Dlaczego kod w języku Java powinien być SOLID’ny oraz suchy, DRY?.

Zdjęcie autorstwa Maddy Freddie z Pexels.