Podręcznik
Implementacja systemu informatycznego oznacza realizację przez programistów założeń projektowych zgodnych w wymaganiami zamawiającego. Celem implementacji jest zapisanie struktury i funkcjonalności systemu w wybranym języku (lub językach) programowania przy wykorzystaniu wybranego środowiska implementacyjnego. Tworzony kod powinien wypełniać ramy określone przez modele komponentów, dostarczając ich odpowiedniej funkcjonalności.
1. Podstawy implementacji oprogramowania
1.1 Kodowanie systemu na podstawie projektu
Implementacja systemu informatycznego oznacza realizację przez programistów założeń projektowych zgodnych w wymaganiami zamawiającego. Celem implementacji jest zapisanie struktury i funkcjonalności systemu w wybranym języku (lub językach) programowania przy wykorzystaniu wybranego środowiska implementacyjnego. Tworzony kod powinien wypełniać ramy określone przez modele komponentów, dostarczając ich odpowiedniej funkcjonalności.
Przeniesienie projektu systemu opisanego modelami projektowymi do kodu nazywane jest inżynierią w przód (ang. forward engineering). Najbardziej typowym zadaniem jest tutaj przekształcenie modelu klas zapisanego w języku UML w kod zapisany w wybranym języku programowania. W niektórych sytuacjach konieczna jest jednak decyzja projektanta lub programisty. Dotyczy to na przykład decyzji odnośnie rodzaju struktur danych stosowanych do przechowania referencji. Inżynierię w przód omówimy na przykładzie modelu pokazanego na rysunku 1.1. Widzimy tu fragment modelu klas stanowiącego projekt realizacji jednego z interfejsów komponentu warstwy logiki dziedzinowej.
Rysunek 1.1: Przykładowy projekt struktury komponentu
Po zastosowaniu odpowiednich reguł translacji, otrzymamy kod pokazany poniżej. Kod został wygenerowany automatycznie w narzędziu CASE i jest zapisany w języku C#. Dla zwięzłości, pominięto kod dwóch operacji klasy „MZamowienia”.
///////////////////////////////////////////////////////////
// Implementation of the Interface IZamowienia
///////////////////////////////////////////////////////////
public interface IZamowienia {
XListaZamowien PobierzListeZamowien(XFiltrZamowien filtr);
XListaZamowien PobierzPelnaListeZamowien();
short DodajZamowienie(XZamowienie zam);
}//end IZamowienia
///////////////////////////////////////////////////////////
// Implementation of the Class MZamowienia
///////////////////////////////////////////////////////////
public class MZamowienia : IZamowienia {
private List<MZamowienie> zam;
private MZamowieniaDao dao;
public MZamowienia(){
}
public XListaZamowien PobierzListeZamowien(XFiltrZamowien filtr){
return null; }
public XListaZamowien PobierzPelnaListeZamowien(){
return null; }
// (...) pozostałe metody pominięte
}//end MZamowienia
///////////////////////////////////////////////////////////
// Implementation of the Class MZamowienie
///////////////////////////////////////////////////////////
public class MZamowienie : MDokument {
public MZamowienie(){
}
public short SprawdzPoprawnosc(){
return 0; }
}//end MZamowienie
Inżynieria w przód dotyczy najczęściej modeli statycznych języka UML. Oznacza to, że kod wygenerowanych metod jest pusty – zawiera jedynie standardowe instrukcje powrotu („return”). Jest to oczywiste, gdyż źródłowy model klas nie zawiera informacji, które pozwoliłyby taki kod wygenerować. Dynamika działania kodu (treść metod) jest opisywana innymi modelami. Informacje zawarte w modelach dynamiki programista może wykorzystać do napisania istotnych części kodu metod. Przykładem jest model sekwencji. Na rysunku 1.2 widzimy diagram zawierający kilka podstawowych konstrukcji tego modelu.
Rysunek 1.2: Przykładowy projekt działania operacji interfejsu
Na podstawie tego diagramu, programista może stworzyć kod metody „SprawdzWszystkieZamowienia” w klasie „MZamowienia”, który został pokazany poniżej.
public short SprawdzWszystkieZamowienia(DateTime data)
{
short
wynik
= 0;
lista = dao.Pobierz(wszystkie);
Przepisz(lista, zam);
if (null != lista)
{
foreach
(MZamowienie zamowienie in zam)
{
wynik = zamowienie.SprawdzPoprawnosc();
}
}
return
wynik;
}
Dobrą praktyką implementacji jest utrzymywanie jej zgodności z modelami projektowymi. W ten sposób, programiści mają stale aktualną „mapę kodu”, co zwiększa efektywność pracy programistów. Często jednak programiści wprowadzają zmiany projektowe bezpośrednio w kodzie. Aby zachować zgodność, można zastosować tzw. inżynierię odwrotną (ang. reverse engineering). Polega ona na odtworzeniu modelu (najczęściej – modelu klas) na podstawie kodu. Oczywiście, mechanizmy inżynierii odwrotnej, podobnie jak inżynierii w przód, są wbudowane w odpowiednie narzędzia CASE i są wykonywane automatycznie. Warto również zauważyć, że wykorzystanie inżynierii odwrotnej jest jeszcze szersze. Dzięki niej możemy stworzyć „wizualną mapę” dla kodu, który nie posiada dokumentacji w postaci modelu klas.
1.2. Dobre praktyki w zakresie kodowania
Kod, tak jak inne produktu procesu wytwarzania oprogramowania, jest najczęściej tworzony w zespołach. Jest on zatem czytany przez osoby o różnych kwalifikacjach, doświadczeniu i nawykach. Dobre praktyki programowania zostały wypracowane w wyniku doświadczenia wielu lat pracy różnych zespołów programistów. Praktyki te w obiektywny sposób opisują pewne pożądane cechy kodu – niezależnie od języka programowania, rozwiązań technicznych, charakteru organizacji informatycznej czy dziedziny projektu, w obrębie którego kod powstawał.
Podstawową przesłanką, którą każdy programista („koder”) powinien uwzględniać podczas pracy z kodem jest fakt, że nie pisze on kodu dla siebie. Troska kodera o przyszłego czytelnika objawia się w szeroko rozumianej łatwości przyswajania znaczenia kodu. Niestety, wielu programistów tworzy kod nadmiernie skomplikowany, stosując różnego rodzaju „sztuczki programistyczne”, które bardzo utrudniają współpracę w zespole. Co więcej, często takie „sztuczki” obracają się przeciwko ich autorom, którzy po kilku miesiącach wracają do swojego kodu i mają problem z jego zrozumieniem.
Dobre praktyki kodowania można podzielić na praktyki redaktorskie, merytoryczne oraz związane z nawykami codziennej pracy. Praktyki redaktorskie dotyczą pracy z kodem traktowanych jako tekst. Obejmują one kwestie dotyczące formatowania tekstu, jego komentowania oraz konwencji nazewniczych.
· Formatowanie tekstu. Kod powinien zawierać wcięcia i odstępy organizujące go w logiczne bloki. Linie nie powinny być zbyt długie, aby można je było łatwo objąć wzrokiem.
· Komentowanie kodu. Komentowanie jest ważne, gdyż często kod nie jest wystarczająco przejrzysty. Może na przykład zawierać pozornie niejednoznaczne odwołania, czy też mieć nieewidentne motywacje.
· Konwencje nazewnicze. Konwencje mają na celu ujednolicenie nazw stosowanych w kodzie, przy zachowaniu ich czytelności i podkreśleniu roli nazw nawet dla najmniej istotnych elementów .
Praktyki merytoryczne dotyczące pisania kodu w dosyć dużym stopniu pokrywają się z dobrymi praktykami w zakresie projektowania. Praktyki merytoryczne obejmują zasady strukturalizacji kodu, definiowania zmiennych i stałych, zasad przetwarzania danych oraz testowalności.
- Projektowanie podczas kodowania. Projekt oddawany w ręce programistów nie jest „martwy”. Pamiętajmy, że wprowadzając zmiany w kodzie, powinniśmy jednocześnie nanosić je w modelu projektowym, stosując zasady inżynierii odwrotnej.
- Zapewnienie parametryzowalności kodu. Wszystkie używane w kodzie stałe (napisy, liczby, symbole itp.) powinny być definiowane poza kodem, który się do nich odwołuje.
- Unikanie stosowania „magicznych wartości” (ang. magic numbers). Zagadnienie to związane jest z omówioną wyżej parametryzowalnością kodu. Należy unikać formuł przekazywanych bezpośrednio w postaci liczb, a nie określonych symbolicznie.
- Posługiwanie się wzorcami projektowymi. Programista, podobnie do projektanta, często ma możliwość zastosowania ogólnie przyjętych i sprawdzonych rozwiązań.
- Uwzględnianie kodu dla testów. Dzięki temu, poważne błędy mogą być wykryte na tyle wcześnie, aby móc uniknąć pracochłonnego ich poszukiwania oraz znacznej przebudowy kodu.
- Przestrzeganie walidacji parametrów wejściowych. Tworzony kod powinien „pilnować” kontraktów określonych dla implementowanych przez siebie modułów.
- Przestrzeganie zasad obsługi raportowania wykonania kodu (logowania). Należy zdefiniować politykę reakcji na błędy, unikać niepotrzebnego filtrowania wyjątków, a z drugiej strony – zasypywania nimi klas wywołujących.
- Dbałość o dane. Oprogramowanie często przetwarza dane istniejące od wielu lat. Kod powinien zatem szanować takie „odziedziczone” dane (ang. legacy data). W szczególności, zbiory danych (np. bazy danych) nie powinny być „zaśmiecane” danymi specyficznymi dla konkretnego systemu.
- Dbałość o wykorzystanie zasobów maszyn. Należy pamiętać, że kod będzie pracował w określonym środowisku wykonawczym o ograniczonych zasobach (pamięci, procesory, sieci).
- Branie pod uwagę przenośności. Należy pamiętać, że kod może być uruchamiany na różnych architekturach sprzętowych, pod kontrolą różnych (wersji) systemów operacyjnych itd.
- Branie pod uwagę skalowalności. Skalowalność oznacza możliwość reakcji kodu na różne poziomy obciążenia obliczeniami. Kod, który wymaga skalowalności powinien być pisany z myślą, że może on być on wywoływany wielokrotnie w krótkich odstępach czasu.
- Przestrzeganie zasad paradygmatu obiektowego. Częstym błędem jest pisanie kodu klas w stylu wywodzącym się z języków proceduralnych.
Trzecią grupą dobrych praktyk są praktyki związane z pewnymi nawykami w pracy z kodem. Dotyczą one przede wszystkim pracy zespołowej i przestrzegania zasad wersjonowania kodu.
- Zapewnienie „czystości” własnego kodu. Bardzo istotne jest, aby kod podlegający kontroli wersji był „czysty”, czyli pozbawiony błędów kompilacji, ostrzeżeń oraz innych wad utrudniających (lub wręcz uniemożliwiających) innym pracę i testowanie.
- Praca na aktualnym kodzie. Przed rozpoczęciem pracy z nowym fragmentem kodu powinniśmy się upewnić, że jest on aktualny.
1.3. Zarządzanie wersjami
Zarządzanie wersjami (ang. version control) oznacza zapisywanie kolejnych zmian w pliku lub zestawie plików. Każda zapisana zmiana tworzy wersję, do której można wrócić w dowolnym momencie. Zarządzanie zmianami w projektach konstrukcji oprogramowania najczęściej dotyczy plików z kodem źródłowym. Do zarządzania wersjami stosujemy specjalne narzędzia, które nazywamy systemami zarządzania wersjami (ang. Version Control System).
Istnieją dwa główne modele zarządzania wersjami: scentralizowane (klient-serwer) oraz rozproszone. Model scentralizowany polega na ustanowieniu centralnego serwera, na którym przechowywane są wszystkie wersjonowane pliki i dokonywane są wszystkie zapisy zmian w tych plikach. Współcześnie dużą popularność zyskał model zdecentralizowany. W tym systemie nadal mamy serwer, który umożliwia przechowywanie oraz wersjonowanie plików. Różnica polega na tym, że maszyny klienckie posiadają możliwość lokalnego wersjonowania plików i przechowują lokalnie kopie całej historii wersji. W ten sposób, każda maszyna lokalna ma możliwość odtworzenia całego zestawu wersjonowanych plików, nawet w razie awarii serwera. Najpopularniejszym obecnie systemem realizującym model zdecentralizowany jest system Git.
Istnieją dwie podstawowe strategie zarządzania równoległymi zmianami w plikach. Pierwsza polega na tworzeniu blokad (ang. lock), a druga na wykonywaniu scaleń (ang. merge). Pierwsza strategia zakłada możliwość zablokowania przez użytkownika wersjonowanego zasobu, by uniemożliwić innym zapis (stworzenie nowej wersji). Jednocześnie, odczyt jest cały czas dopuszczalny. Strategia polegająca na scaleniach zakłada, że istnieje mechanizm tworzenia spójnego i użytecznego pliku w oparciu o dwie różne jego wersje. Mechanizm scalania ilustruje rysunek 1.3. Przedstawia on sytuację, gdy osoby X i Y pracują jednocześnie na tej samej wersji pliku. Osoba X zapisuje swoje zmiany jako pierwsza. Następnie, zapisać zmiany próbuje osoba Y. Otrzymuje ona wtedy informację o nieaktualności swojego pliku. System sprawdza różnice i umożliwia scalenie zmian i zapis w postaci kolejnej wersji.
Rysunek 1.3: Scalanie zmian w systemie kontroli wersji
Od strony technicznej każda kolejna rewizja przechowywana jest najczęściej jako tzw. delta (zmiana) w stosunku do rewizji ją poprzedzającej.
Praca nad kolejnymi wersjami zasobu (pliku lub zestawu plików) nie musi być liniowa. Różne osoby mogą tworzyć różne warianty składające się z wielu wersji. Takie warianty nazywamy gałęziami (ang. branch). Gałęzie mogą być rozwijane całkowicie niezależnie, jednak w pewnym momencie powinny być ze sobą synchronizowane. Podstawą synchronizacji jest gałąź główna, zwana pniem (ang. trunk). Jeśli twórca uzna, że jego wariant (gałąź) jest już stabilna, może ją scalić z pniem. Rysunek 11.4 ilustruje tworzenie kilku gałęzi na bazie gałęzi głównej, czyli pnia.
Rysunek 11.4: Schemat wersjonowania dla zasobu o dwóch gałęziach
Rysunek 11.5: Znaczniki dla plików w systemie kontroli wersji
Podczas pracy w systemie zarządzania wersjami możemy wykonywać różne operacje umożliwiające równoległą pracę nad różnymi plikami. Pierwszą czynnością jest dokonanie operacji check-out, czyli stworzenie lokalnej kopii roboczej, zawierającej aktualną wersję zasobów, nad którymi będziemy pracować. Po dokonaniu zmian, wykonujemy operację check-out. W wyniku tej operacji tworzona jest kolejna wersja w aktualnej gałęzi. Ważne jest, aby przez utworzeniem nowej gałęzi dokonać aktualizacji (ang. update), czyli pobrać aktualną wersję zasobów z gałęzi głównej. Synchronizacja gałęzi z pniem odbywa się w wyniku operacji scalania (ang. merge). Istotnym elementem tej operacji jest pokazanie przez system różnicy między wersjami oraz pomoc w wybraniu zmian, które zostaną włączone do pnia.