Tym razem artykuł rozpocznę praktycznym przykładem, który zobrazuje moje podejście, od którego zaczynam tworzenia aplikacji. Chcę pokazać jak “proste” i skuteczne jest, to podejście. Następnie, w dalszej części opiszę, co, jak i dlaczego zrobiłem.

Wyobraźmy sobie, że zostałem poproszony o napisanie wersji demonstracyjnej, tzw. Proof of Concept aplikacji bankowej. Od czego zacząć? Nie wpadać w panikę! Opiszę słownie, w dużym uproszczeniu, co według mnie powinna robić aplikacja bankowa.

  1. Opis w formie tekstu, spisanego, np. w notatniku.

Aplikacja bankowa umożliwia klientom zakładanie kont bankowych. Klienci mogą posiadać więcej niż jedno konto w banku. Klient ma możliwość wpłaty i wypłaty pieniędzy w ramach posiadanego konta bankowego oraz możliwość wykonywania przelewu między kontami.

  1. Wybranie rzeczowników i czasowników z opisu.
  • Rzeczowniki: Aplikacja, Bank, Klient, Konto, Przelew,
  • Czasowniki: Zakładanie kont, wpłata, wypłata, wykonywanie przelewu.
  1. Karty CRC – Class Responsibility Collaborators, po prostu tabelki.

Korzystając z kart CRC zastanowię się jakie elementy, atrybuty będą wchodziły w skład klas oraz jak będą ze sobą połączone w jakich relacjach, zależnościach. Karty CRC służą do przedstawienia dla konkretnej klasy (Class) jej odpowiedzialności (Responsibilities) oraz powiązań, relacji (Collaborators) z innymi klasami w ramach tworzonego systemu.

Uwaga: Dla uproszczenia przykładu, karty CRC zastosuję tylko dla rzeczowników: Klient i Konto.

ClassKlient
ResponsibilityCollaborators
Przechowuje (HAS-A) dane klienta:
– Imię, Nazwisko, Adres, Lista Kont.
Wykonuje:
– Zakładanie kont.
Konto (klasa).
Karta CRC dla klasy Klient (rzeczownik ze słownego opisu aplikacji).
ClassKonto
ResponsibilityCollaborators
Przechowuje (HAS-A) dane konta:
– Numer, Środki na koncie, Stan konta.
Wykonuje:
– Wpłata, Wypłata.
Brak.
Karta CRC dla klasy Konto (rzeczownik ze słownego opisu aplikacji).

Uwaga: Dla uproszczenia przykładu pominę ważny krok jakim jest projekt i od razu przedstawię kod klas Klient i Konto w języku Java.

Wykorzystując rzeczowniki ze słownego opisu oraz posiłkując się kartami CRC stworzę nowe klasy Java, natomiast czasowniki posłużą jako metody w tworzonych klasach. Uwaga: metody będą tzw. “metodami wydmuszkami”, czyli nic nie zwracają, nie przyjmują parametrów oraz nie mają ciała, implementacji. Nazwy klas zapiszę w języku angielskim, ponieważ znacząca większość projektów, kod źródłowy pisze się w języku angielskim.

// Klas Klient, rzeczownik ze słownego opisu + karta CRC.
public class Client {
    // Atrybuty klasy
    private String firstName;
    private String lastName;
    private String address;
    private List<Account> accounts;

    // Metody klasy
    void createAccount() {}
}
// Klas Konto, rzeczownik ze słownego opisu + karta CRC.
public class Account {
    // Atrybuty klasy
    private String number;
    private BigDecimal balance;
    private boolean active;

    // Metody klasy
    public void debit() {}
    public void credit() {}
}

Powyżej przedstawione praktyczne podejście, od którego zaczynam tworzenie każdej aplikacji, to analiza, która bardzo dobrze się sprawdza, poprzedza ona wszystkie kolejne kroki. W telegraficznym skrócie analiza pomaga spojrzeć na tworzone oprogramowanie z szerszej perspektywy, co pozwoli nam dokładniej zrozumieć tworzony kod. Przez aplikację rozumiem dowolny program, kod źródłowy tworzony w dowolnym, obiektowym języku programowania. W tym przypadku będzie, to język Java. Tworzoną aplikacją może być aplikacja: konsolowa (CLI), desktop (GUI) lub web (HTML, REST). Jak widać praktyczne podejście, które przedstawiam ma bardzo szerokie zastosowanie.

Moim zdaniem nie należy rozpoczynać pisania kodu źródłowego bez analizy – rekomenduję następującą ścieżkę: analiza, projekt, implementacja (wsparta testami) – szerzej opisałem, to w artykule Stop! Zanim zaczniesz pisać kod zastanów się, co chcesz kodować? Analiza, projekt i implementacja. Programowanie “na kowboja” jeszcze nigdy się nie sprawdziło. Oczywiście współczesne środowiska programistyczne (IDE) wspierają i automatyzują proces refaktoryzacji, zmiany istniejącego kodu źródłowego, ale zawsze wydłuża, to czas pisania aplikacji i powoduje niepotrzebne problemy, komplikacje.

Wyżej wspomniana analiza pozwoli wyodrębnić model dziedziny dla tworzonej aplikacji. Przykładowy model dziedziny dla “aplikacji bankowej”:

Application, Bank, Client, Account, Transfer.

Czym jest model dziedziny? W dużym uproszczeniu to słownik głównych pojęć używanych w aplikacji. Skąd akurat taki, a nie inny model dziedziny? Jak taki model dziedziny został “wybrany” dla “aplikacji bankowej”? Wybrany on zostały w kolejnych krokach zgodnie z artykułem Stop! Zanim zaczniesz pisać kod zastanów się, co chcesz kodować? Analiza, projekt i implementacja.

Na podstawie modelu dziedziny powstaną klasy języka Java. Może się wydawać, że wybrane pięć klas – Application, Bank, Client, Account, Transfer – to mało, ale jak się okazuje, to całkiem spora ilość, która pozwoli na stworzenie głównego modułu w aplikacji bankowej. Pięć klas pozwoli również na “wygenerowanie” dużej ilości kodu.

Czytając powyższe może pojawić się kilka pytań:

  1. Co w przypadku, kiedy muszę wprowadzić tylko drobną zmianę w kodzie?
    • Moim zdaniem, w tym przypadku również warto stosować ścieżkę: analiza, projekt i implementacja. Dodatkowo należy bardzo dobrze znać model dziedziny dla aplikacji, z którą pracujemy.
  2. Czy doświadczony programista również stosuje ścieżkę: analiza, projekt i implementacja?
    • Tak, doświadczony programista również stosuje opisywaną ścieżkę, przeprowadza w głowie proces analizy, projektu, a następnie implementacji. Dla bardziej złożonych aplikacji tworzy niezbędną dokumentację.
  3. Czy zawsze trzeba stosować takie podejście?
    • Rekomenduję stosowanie ścieżki: analiza, projekt i implementacja. Spójrzmy na to z innej perspektywy. Analiza, to: zastanowienie się, przemyślenie zagadnienia czysto teoretycznie; projekt, to: formalizacja, dokumentacja naszych przemyśleń; implementacja, to: praktyczne zastosowanie naszych przemyśleń w oparciu o formalną dokumentację.

Należy zwrócić uwagę na bardzo ważny element, który wyłonił się w fazie analizy, a mianowicie model dziedziny. Wiemy już, że w dużym uproszczeniu to słownik głównych pojęć używanych w aplikacji. To pojęcia, którymi będziemy posługiwać się w ramach zespołu programistów, analityków biznesowych i osób zlecających nam stworzenie aplikacji.


Poniżej pokażę jak może wyglądać kod Java dla naszego głównego modelu dziedziny, czyli klas: Application, Bank, Client, Account, Transfer. Jest to uproszczony model dziedziny bez dodatkowych złożonych relacji, np. bez dodatkowej klasy Address dane przechowuje w polu typu String, czyli private String address; zamiast private Address address;.

Ważnym elementem, który wiąże się z wybranymi klasami są relacje między tymi klasami, relacje możemy podzielić na dwie główne “kategorie”: IS-A oraz HAS-A. W telegraficznym skrócie: Relacja IS-A (jest) – dziedziczenie (inheritance), np.: Car IS-A Vehicle – Samochód jest Pojazdem. Relacja HAS-A (ma) – kompozycja (composition), np.: Car HAS-A Engine – Samochód ma Silnik. Dodatkowe informacje Inheritance (IS-A) vs. Composition (HAS-A) Relationship.

// Model dziedziny dla rzeczownika Application
// Ta klasa będzie "punktem wejścia", będzie zawierać
// kod źródłowy "działającej aplikacji bankowej"
public class Application {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        Bank juniorBank = new Bank();
    }
}
// Model dziedziny dla rzeczownika Bank
public class Bank {
    private String name;
    private String address;
    private List<Client> clients;
}
// Model dziedziny dla rzeczownika Client
public class Client {
    private String firstName;
    private String lastName;
    private String address;
    private List<Account> accounts;
}
// Model dziedziny dla rzeczownika Account
public class Account {
    private String number;
    private BigDecimal balance;
    private boolean active;
}
// Model dziedziny dla rzeczownika Transfer
public class Transfer {
    private Account accountFrom;
    private Account accountTo;
    private BigDecimal amount;
}

Poniżej zaprezentuję powyższe klasy uzupełnione o implementację, czyli kod źródłowy. Uwaga: Dla uproszczenia przykładu pominę ważny krok jakim jest projekt i od razu przedstawię kod klas w języku Java. Będzie, to bardzo uproszczona implementacja, tak, aby nie zaciemnić idei, która przyświeca praktycznemu przykładowi, czyli stworzeniu aplikacji bankowej poprzedzonej procesem analizy. Zdaję sobie sprawę, że w wielu miejscach można było napisać kod lepiej, bardziej zgodnie ze sztuką.

Kod poniżej nie będzie zawierał szczegółowego opisu implementacji, ale będzie zawierał nawiązania do procesu analizy – słowny opis, rzeczowniki i czasowniki oraz karty CRC.

// Model dziedziny dla rzeczownika Bank ze słownego opisu + karta CRC.
public class Bank {
    // Atrybuty klasy
    private String name;
    private String address;
    private List<Client> clients = new ArrayList<>();

    // Metoda klasy
    public Client openClientAccount(Client client) {
        Account openedClientAccount = client.createAccount();
        this.clients.add(client);

        return client;
    }

    // Metoda klasy
    public List<Client> listAllClients() {
        return this.clients;
    }
}
// Model dziedziny dla rzeczownika Client ze słownego opisu + karta CRC.
public class Client {
    // Atrybuty klasy - Responsibility z karty CRC.
    private String firstName;
    private String lastName;
    private String address;
    private List<Account> accounts = new ArrayList<>();

    public Client(String firstName, String lastName, String address) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.address = address;
    }

    // Metoda klasy - Responsibility z karty CRC.
    public Account createAccount() {
        Account account = new Account(BigDecimal.ZERO, true);
        this.accounts.add(account);

        return account;
    }

    // Metoda klasy
    public Account findAccountByNumber(String accountNumber) {
        for (Account account : accounts) {
            if (account.number.equals(accountNumber)) {
                return account;
            }
        }
        return null;
    }
}
// Model dziedziny dla rzeczownika Account ze słownego opisu + karta CRC.
public class Account {
    // Atrybuty klasy - Responsibility z karty CRC.
    public final String number = UUID.randomUUID().toString();
    private BigDecimal balance;
    private boolean active;

    public Account(BigDecimal balance, boolean active) {
        this.balance = balance;
        this.active = active;
    }

    // Metoda klasy - Responsibility z karty CRC.
    public void debit(BigDecimal amount) {
        if (active) {
            if (this.balance.compareTo(amount) > 0) {
                this.balance = this.balance.subtract(amount);
            }
        }
    }

    // Metoda klasy - Responsibility z karty CRC.
    public void credit(BigDecimal amount) {
        if (active) {
            this.balance = this.balance.add(amount);
        }
    }
}
// Model dziedziny dla rzeczownika Transfer ze słownego opisu + karta CRC.
public class Transfer {
    // Atrybuty klasy
    private Account accountFrom;
    private Account accountTo;
    private BigDecimal amount;

    public Transfer(Account accountFrom, Account accountTo, BigDecimal amount) {
        this.accountFrom = accountFrom;
        this.accountTo = accountTo;
        this.amount = amount;
    }

    // Metoda klasy
    public void makeTransfer() {
        accountFrom.debit(amount);
        accountTo.credit(amount);
    }
}

Poniżej klasa zawierająca część “interaktywną” tworzonej aplikacji. W niej zostanie stworzony konkretny Bank, Klienci zarejestrują się w Banku i utworzą w nim swoje Konta, a następnie wpłacą środki na te konta. Na samym końcu zostanie wykonany Przelew między różnymi Kontami Bankowymi.

“Aplikacja bankowa” będzie działać w trybie konsoli, tzw. “czarnym okienku”, bez graficznego interfejsu użytkownika. Nie zmienia, to faktu, że możemy dla powyższego kodu stworzyć aplikację desktop z GUI lub web na www.

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

        System.out.println("Witamy w Junior Bank!");
        System.out.println("Zarejestruj się i otwórz konto.\n");

        System.out.println("Podaj swoje imię:");
        String firstName = scanner.nextLine();
        System.out.println("Podaj swoje nazwisko:");
        String lastName = scanner.nextLine();
        System.out.println("Podaj swój adres:");
        String address = scanner.nextLine();

        // pierwszy klient jest tworzony na podstawie
        // danych wprowadzonych z konsoli
        Client firstClient = new Client(firstName, lastName, address);
        // dla uproszczenia drugi klient jest tworzony
        // bez wprowadzania danych z konsoli
        Client secondClient = new Client("Jan", "Kowalski", "Ul. Krótka 2");

        Bank juniorBank = new Bank();
        juniorBank.openClientAccount(firstClient);
        juniorBank.openClientAccount(secondClient);

        System.out.println("\nRejestracja zakończona, konto zostało utworzone.");
        System.out.println("Szczegółowe dane klient i konta:\n" + firstClient);

        System.out.println("\n__________________________________________________");
        System.out.println("Wszyscy klienci w JuniorBank:\n");
        List<Client> allClients = juniorBank.listAllClients();
        allClients.forEach(System.out::println);

        System.out.println("\n__________________________________________________");
        System.out.println("Przed wykonaniem przelewu wpłać środki na konto.\n");
        System.out.println("Wyszukaj konto nadawcy po numerze:");
        String senderAccNumber = scanner.nextLine();
        System.out.println("Wyszukaj konto nadawcy po numerze:");
        String receiverAccNumber = scanner.nextLine();

        Account senderAccount = firstClient.findAccountByNumber(senderAccNumber);
        System.out.println("Konto nadawcy: " + senderAccount);
        Account receiverAccount = secondClient.findAccountByNumber(receiverAccNumber);
        System.out.println("Konto odbiorcy: " + receiverAccount);

        // zasilenie kont, przelew na własne konto
        senderAccount.credit(BigDecimal.valueOf(60));
        receiverAccount.credit(BigDecimal.valueOf(40));

        System.out.println("\n__________________________________________________");
        System.out.println("Wykonaj przelew między kontami.\n");
        System.out.println("Konto nadawcy (przed przelewem): " + senderAccount);
        System.out.println("Konto odbiorcy (przed przelewem): " + receiverAccount);

        Transfer giftTransfer =
                new Transfer(senderAccount, receiverAccount, BigDecimal.TEN);
        giftTransfer.makeTransfer();
        System.out.println("Konto nadawcy (po przelewie): " + senderAccount);
        System.out.println("Konto odbiorcy (po przelewie): " + receiverAccount);
    }
}

Poniżej wynik działania części “interaktywnej” stworzonej aplikacji.

Witamy w Junior Bank!
Zarejestruj się i otwórz konto.

Podaj swoje imię:
Jan
Podaj swoje nazwisko:
Kowalski
Podaj swój adres:
Ul. Długa 1

Rejestracja zakończona, konto zostało utworzone.
Szczegółowe dane klient i konta:
Client{firstName='Jan', lastName='Kowalski', address='Ul. Długa 1', 
accounts=[Account{number='9db92b6e-05fc-4918-901e-4e3eb86cffe3', balance=0, active=true}]}


__________________________________________________
Wszyscy klienci w JuniorBank:

Client{firstName='Jan', lastName='Kowalski', address='Ul. Długa 1', 
accounts=[Account{number='9db92b6e-05fc-4918-901e-4e3eb86cffe3', balance=0, active=true}]}

Client{firstName='Jan', lastName='Nowak', address='Ul. Krótka 2', 
accounts=[Account{number='1520230c-178f-4be2-962d-61e5efdf19b3', balance=0, active=true}]}


__________________________________________________
Przed wykonaniem przelewu wpłać środki na konto.

Wyszukaj konto nadawcy po numerze:
9db92b6e-05fc-4918-901e-4e3eb86cffe3
Wyszukaj konto nadawcy po numerze:
1520230c-178f-4be2-962d-61e5efdf19b3
Konto nadawcy: Account{number='9db92b6e-05fc-4918-901e-4e3eb86cffe3', balance=0, active=true}
Konto odbiorcy: Account{number='1520230c-178f-4be2-962d-61e5efdf19b3', balance=0, active=true}

__________________________________________________
Wykonaj przelew między kontami.

Konto nadawcy (przed przelewem): Account{number='9db92b6e-05fc-4918-901e-4e3eb86cffe3', balance=60, active=true}
Konto odbiorcy (przed przelewem): Account{number='1520230c-178f-4be2-962d-61e5efdf19b3', balance=40, active=true}

Konto nadawcy (po przelewie): Account{number='9db92b6e-05fc-4918-901e-4e3eb86cffe3', balance=50, active=true}
Konto odbiorcy (po przelewie): Account{number='1520230c-178f-4be2-962d-61e5efdf19b3', balance=50, active=true}

Process finished with exit code 0

Patrząc na powyższy kod, śmiało można stwierdzić, że “niewiele zmieniło się” od etapu analizy. Słowny opis pozwolił wyłonić rzeczowniki i czasowniki, które przekształciły się w model dziedziny. Na tej podstawie powstały karty CRC, które następnie zamieniły się w klasy języka Java.

ClassKlient
ResponsibilityCollaborators
Przechowuje (HAS-A) dane klienta:
– Imię, Nazwisko, Adres, Lista Kont.
Wykonuje:
– Zakładanie kont.
Konto (klasa).
Karta CRC dla klasy Klient (rzeczownik ze słownego opisu aplikacji).
// Model dziedziny dla rzeczownika Client ze słownego opisu + karta CRC.
public class Client {
    // Atrybuty klasy - Responsibility z karty CRC.
    private String firstName;
    private String lastName;
    private String address;
    private List<Account> accounts = new ArrayList<>();

    public Client(String firstName, String lastName, String address) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.address = address;
    }

    // Metoda klasy - Responsibility z karty CRC.
    public Account createAccount() {
        Account account = new Account(BigDecimal.ZERO, true);
        this.accounts.add(account);

        return account;
    }

    // Metoda klasy
    public Account findAccountByNumber(String accountNumber) {
        for (Account account : accounts) {
            if (account.number.equals(accountNumber)) {
                return account;
            }
        }
        return null;
    }
}

Poniżej elementy, które zostały dodane w trakcie implementacji, a nie były uwzględnione w analizie.

  1. Konstruktory dla klas Client, Account oraz Transfer:
    • public Client(String firstName, String lastName, String address) {},
    • public Account(BigDecimal balance, boolean active) {},
    • public Transfer(Account accountFrom, Account accountTo, BigDecimal amount) {}.
  2. Dwie metody “pomocnicze” w klasie Bank oraz Client:
    • public List<Client> listAllClients() {},
    • public Account findAccountByNumber(String accountNumber) {}.

Poniżej elementy, które wymagają głębszego zastanowienie i modyfikacji.

  1. Generowanie unikalnych numerów kont bankowych – na pewno zmieniłbym obecną “implementację” – public final String number = UUID.randomUUID().toString();.
  2. Klasa Transfer oraz jej metoda public void makeTransfer() {} :
    • Metoda mogłaby przyjmować parametry – public void makeTransfer(Account accountFrom, Account accountTo, BigDecimal amount) {},
    • W klasie Transfer zrezygnowałbym z konstruktora i nie przechowywał bym stanu, danych w atrybutach klasy. Dzięki temu klasa Transfer stała by się klasą narzędziową.

Podsumowując, przedstawiona aplikacja bankowa składa się z pięciu klas, w których w sumie jest około 100 linii kodu, “dodatkowe” 60 linii kodu stanowi sama część “interaktywna” aplikacji. Na podstawie analizy powstało 160 linii kodu, w fazie implementacji dodałem cztery elementy – dwie metody i dwa konstruktory – zaproponowałem trzy modyfikacje. Dzięki analizie uniknąłem niepotrzebnej refaktoryzacji, zmiany kodu źródłowego. Ktoś mógłby powiedzieć, że doświadczony programista tak by zrobił, ale nie młodszy programista, programistka. Z biegiem czasu i po wykonaniu kilku takich “aplikacji” zgodnie ze ścieżką – analiza, projekt i implementacja – mniej doświadczone osoby nabrałby wprawy. Zachęcam do porzucenia programowania “na kowboja” na rzecz ścieżki opisanej w tym artykule.

Zdjęcie autorstwa Suzy Hazelwood z Pexels.