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.
- 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.
- Wybranie rzeczowników i czasowników z opisu.
- Rzeczowniki: Aplikacja, Bank, Klient, Konto, Przelew,
- Czasowniki: Zakładanie kont, wpłata, wypłata, wykonywanie przelewu.
- 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.
| Class | Klient |
|---|---|
| Responsibility | Collaborators |
| Przechowuje (HAS-A) dane klienta: – Imię, Nazwisko, Adres, Lista Kont. Wykonuje: – Zakładanie kont. | Konto (klasa). |
| Class | Konto |
|---|---|
| Responsibility | Collaborators |
| Przechowuje (HAS-A) dane konta: – Numer, Środki na koncie, Stan konta. Wykonuje: – Wpłata, Wypłata. | Brak. |
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ń:
- 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.
- 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ę.
- 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.
| Class | Klient |
|---|---|
| Responsibility | Collaborators |
| Przechowuje (HAS-A) dane klienta: – Imię, Nazwisko, Adres, Lista Kont. Wykonuje: – Zakładanie kont. | Konto (klasa). |
// 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.
- Konstruktory dla klas
Client,AccountorazTransfer:public Client(String firstName, String lastName, String address) {},public Account(BigDecimal balance, boolean active) {},public Transfer(Account accountFrom, Account accountTo, BigDecimal amount) {}.
- Dwie metody „pomocnicze” w klasie
BankorazClient:public List<Client> listAllClients() {},public Account findAccountByNumber(String accountNumber) {}.
Poniżej elementy, które wymagają głębszego zastanowienie i modyfikacji.
- Generowanie unikalnych numerów kont bankowych – na pewno zmieniłbym obecną „implementację” –
public final String number = UUID.randomUUID().toString();. - Klasa
Transferoraz jej metodapublic void makeTransfer() {}:- Metoda mogłaby przyjmować parametry –
public void makeTransfer(Account accountFrom, Account accountTo, BigDecimal amount) {},
- W klasie
Transferzrezygnowałbym z konstruktora i nie przechowywał bym stanu, danych w atrybutach klasy. Dzięki temu klasa Transfer stała by się klasą narzędziową.
- Metoda mogłaby przyjmować parametry –
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.
Serdecznie dziękuję, przymierzam się do napisania pierwszego programu i Twój artykuł wspaniale pokazał mi drogę „jak zacząć”.