You are probably wondering how to use the theoretical knowledge acquired either during an online course or at a Bootcamp. In this article, I will try to explain how to practically use the knowledge of Java that you have acquired while learning. I will also show you how to avoid the trap that we set for ourselves, which is creating the entire code in one class with the main() method.

O tym, że sam język Java nie wystarczy już pisałem – Czy sam język Java wystarczy?. Teraz pokażę na przykładzie jak przekuć zdobytą wiedzę na działający program w języku Java. Na początek użyję analogii, tak samo jak w przypadku matematyki sama znajomość wzorów i twierdzeń matematycznych nie czyni z nas matematyków-praktyków, którzy wiedzą jak zastosować np. twierdzenie Pitagorasa do wyznaczania odległości między dwoma punktami na osi współrzędnych. To samo tyczy się programowania.

Cztery kluczowe elementy, z których należy zdać sobie sprawę

  1. Nie umieszczamy całego kodu w metodzie main().
  2. Jedna klasa z metodą main(), to za mało.
  3. Wypiszmy w konsoli działanie aplikacji.
  4. Dobry program, aplikacja powinna mieć wiele klas i pakietów.

Na przykładzie prostej implementacji kalkulatora pokaże proces przejścia z „całego kodu w metodzie main()” do kilku klas, które są łatwiejsze w czytaniu i utrzymaniu. Każdy z kroków omówię szczegółowo poniżej.

Kroki umożliwiające przejście z all-in-one do S.O.L.I.D.nego kodu aplikacji

  1. Przeniesienie logiki, obliczeń, algorytmów do oddzielnej klasy – CalculatorPhaseX.
  2. Utworzenie klasy przechowującej, przenoszącej dane – CalculatorInputData.
  3. Stworzenie klasy przechowującej zwracane wyniki – CalculatorResultData.
import java.util.Scanner;

public class MainCalculator {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        System.out.println("Podaj pierwszą liczbę:");
        int a = scanner.nextInt();
        System.out.println("Podaj drugą liczbę:");
        int b = scanner.nextInt();

        System.out.println("Jakie działanie wykonać?");
        System.out.println("1. Dodawanie");
        int operation = scanner.nextInt();

        int result = 0;
        if (operation == 1) {
            result = a + b;
        }
        System.out.println("Wynik działania: " + result);
    }
}

Powyższy kod pokazuje program, aplikację, która działa jak kalkulator. Użytkownik jest proszony o podanie dwóch liczb scanner.nextInt() oraz działania scanner.nextInt(), które chce wykonać na wprowadzonych liczbach. Dla uproszczenia prezentowany kalkulator wykonuje tylko dodawanie result = a + b;, analogicznie można napisać metody dla innych działań matematycznych np. odejmowanie, mnożenie itp.

Wynik działania CalculatorMain - pułapka metody main()
Wynik dzialania CalculatorMain – pulpka metody main()

Krok 1. Przeniesienie logiki, obliczeń, algorytmów do oddzielnej klasy

CalculatorPhaseOne

Z racji tego, że metoda main() zaczyna „rosnąć” trzeba zastanowić się nad wydzieleniem kodu do innej klasy. Można przyjąć, że metoda powinna mieć maksymalnie 5-6 linijek – według Uncle Bob Robert C. Martin (Czysty kod. Podręcznik dobrego programisty, Clean Code).

W takim razie zabieramy się za stworzenie nowej klasy z dedykowaną metodą dla operacji kalkulatora.

public class CalculatorPhaseOne {
    public static int sum(int a, int b) {
        System.out.println("sum(" + a + ", " + b + ")");
        int sum = a + b;
        System.out.println("sum=" + sum);
        return sum;
    }
}

Powyższy kod klasy CalculatorPhaseOne zawiera statyczną metodę sum(int a, int b). Dlaczego metoda statyczna? Po to, aby nie trzeba było tworzyć obiektu, a następnie wywoływać metody CalculatorPhaseOne calculator = new CalculatorPhaseOne(); calculator.sum(1, 2);. Dzięki metodzie statycznej możemy od razu wywołać metodę na rzecz klasy CalculatorPhaseOne.sum(1, 2);.

result = a + b;
result = CalculatorPhaseOne.sum(a, b);
Wynik działania CalculatorMain - przeniesienie logiki, obliczeń, algorytmów do oddzielnej klasy
Wynik dzialania CalculatorMain – przeniesienie logiki, obliczen, algorytmow do oddzielnej klasy

Krok 2. Utworzenie klasy przechowującej, przenoszącej dane

CalculatorInputData

Przyjęło się, że metody, konstruktory, które mają więcej niż pięć parametrów są oznaką złego kodu. Dla takich metod i konstruktorów tworzy się parametry, które są własnymi klasami przechowującymi dane. Warto zastanowić się nad stosowaniem tej reguły dla dwóch i więcej parametrów. Poniższa klasa CalculatorInputData zawiera dane, które wcześniej były oddzielnie przesyłane do metody public static int sum(int a, int b).

Co daje utworzenie oddzielnej klasy jako typu parametru metody? Po pierwsze mamy porządek w parametrach metody, po drugie bez zmieniania parametrów metody możemy w łatwy sposób dodać lub usunąć dowolny element do klasy, czyli nasza metoda jest bardziej „elastyczna”, a tym samym odporna na zmiany.

public class CalculatorInputData {
    private int a;
    private int b;

    public int getA() {
        return a;
    }

    public void setA(int a) {
        this.a = a;
    }

    public int getB() {
        return b;
    }

    public void setB(int b) {
        this.b = b;
    }

    @Override
    public String toString() {
        return "CalculatorInputData{" +
                "a=" + a +
                ", b=" + b +
                '}';
    }
}
public class CalculatorPhaseTwo {
    public static int sum(CalculatorInputData inputData) {
        System.out.println("sum(" + inputData + ")");
        int sum = inputData.getA() + inputData.getB();
        System.out.println("sum=" + sum);
        return sum;
    }
}
CalculatorInputData inputData = new CalculatorInputData();
inputData.setA(a);
inputData.setB(b);

result = CalculatorPhaseTwo.sum(inputData);
Wynik działania CalculatorMain - utworzenie klasy przechowującej, przenoszącej dane
Wynik dzialania CalculatorMain – utworzenie klasy przechowujacej, przenoszacej dane

Krok 3. Stworzenie klasy przechowującej zwracane wyniki

CalculatorResultData

Skoro metody jako typ parametrów wykorzystują własne klasy, to czemu nie stworzyć własnego typu dla wartości zwracanej z metody?

Co daje utworzenie oddzielnej klasy dla wartości zwracanej z metody? Po pierwsze mamy porządek w metodzie, po drugie bez zmieniania typu zwracanego metody możemy w łatwy sposób dodać lub usunąć dowolny pola w klasie zwracanej, czyli nasza metoda jest bardziej „elastyczna”, a tym samym odporna na zmiany.

public class CalculatorResultData {
    private int result;

    public int getResult() {
        return result;
    }

    public void setResult(int result) {
        this.result = result;
    }

    @Override
    public String toString() {
        return "CalculatorResultData{" +
                "result=" + result +
                '}';
    }
}
result = CalculatorPhaseTwo.sum(inputData);
CalculatorResultData resultData = new CalculatorResultData();
resultData.setResult(result);
Wynik działania CalculatorMain - stworzenie klasy przechowującej zwracane wyniki
Wynik dzialania CalculatorMain – stworzenie klasy przechowujacej zwracane wyniki

Dodatkowo na każdym, etapie nasza aplikacja powinna „informować nas, co robi”, czyli wypisywać, logować na konsoli i/lub do pliku swoje działanie. Zalecam wypisywanie, logowanie wejścia i wyjścia dla każdej metody publicznej w klasach. Dla uproszczenia logowanie odbywa się na konsolę za pomocą System.out.println(„sum(” + a + „, ” + b + „)”);, docelowo System.out trzeba będzie zastąpić wyspecjalizowaną klasą Logger.

    public static CalculatorResultData sum(CalculatorInputData inputData) {
        System.out.println("sum(" + inputData + ")");
        int sum = inputData.getA() + inputData.getB();
        CalculatorResultData resultData = new CalculatorResultData();
        resultData.setResult(sum);
        System.out.println("resultData=" + resultData);
        return resultData;
    }
Wynik działania CalculatorPhaseThree - wypisywanie, logowanie wejścia i wyjścia dla metody publicznej
Wynik dzialania CalculatorPhaseThree – wypisywanie, logowanie wejscia i wyjscia dla metody publicznej

Summary

Powyższy przykład pokazuje jak w prosty sposób wyjść z pułapki metody main(). Oczywiście powyższy schemat można zastosować do większych aplikacji pisanych w Java. Dla przypomnienia najważniejsze, to stworzyć więcej niż jedną klasę w aplikacji i działać zgodnie z SOLID.

  1. Przeniesienie logiki, obliczeń, algorytmów do oddzielnej klasy.
  2. Utworzenie klasy przechowującej, przenoszącej dane.
  3. Stworzenie klasy przechowującej zwracane wyniki.

Całość kodu można znaleźć na: https://github.com/juniorjavadeveloper-pl/java-what-where-in-the-code