3. Narzędzia i metody automatyzacji inżynierii oprogramowania

W celu usprawnienia pracy zespołów konstruujących systemy oprogramowania powstały różnego rodzaju narzędzia, które nazywamy narzędziami komputerowego wsparcia inżynierii oprogramowania CASE (ang. Computer Aided Software Engineering). Narzędzia te pozwalają usprawnić czynności związane z wieloma aspektami rozwoju produktów systemowych: od zarządzania projektami informatycznymi, przez integrację powstających modułów i zarządzanie zmianami, po wsparcie dla modelowania systemów na różnych poziomach i ułatwianie implementacji.

3.1. Narzędzia automatyzacji analizy i projektowania oprogramowania

Narzędzia wspierające modelowanie obiektowe spełniają bardzo istotną rolę w „warsztacie” twórców oprogramowania. Modelowanie oprogramowania dotyczy praktycznie wszystkich faz rozwoju produktów software’owych. Podstawową funkcjonalnością typowego narzędzia służącego do modelowania obiektowego jest wsparcie dla rysowania diagramów w wybranej notacji, szczególnie – języka UML, ale często również innych, takich jak BPMN czy ERD. Podstawowe wsparcie narzędzia polega na udostępnieniu użytkownikowi możliwości łatwego tworzenia diagramów, modyfikacji elementów diagramów oraz ich przeglądania.

Zaletą narzędzi do modelowania oprogramowania jest to, że „pilnują” poprawności notacji nie pozwalając użytkownikowi na umieszczenie na diagramie danego typu, niewłaściwych elementów czy na nieprawidłowe zapisanie konstrukcji języka. Istotną funkcją narzędzia modelowania jest także umożliwienie zarządzania modelami: odpowiedniego porządkowania ich struktury (np. w formie pakietów), a także wygodnego przechodzenia między poziomami abstrakcji i śledzenia zależności między fragmentami systemów opisanych na różnych poziomach szczegółowości. Cecha ta jest niezmiernie istotna, ponieważ systemy reprezentowane są przez modele na wielu poziomach abstrakcji: poczynając od wymagań, a kończąc na „mapach kodu”, czyli modelach projektowych.

Istotnym elementem funkcjonalności narzędzi modelowania jest inżynieria kodu. Korzystając z funkcji z nią związanych można generować szkielet kodu odzwierciedlający wiedzę zawartą w modelu („inżynieria w przód”), tworzyć modele w celu zrozumienia istniejącego kodu („inżynieria odwrotna”), a także wspierać proces ciągłej synchronizacji kodu z modelem. Ważnym zagadnieniem związanym z tworzeniem modeli jest udostępnianie efektów pracy pozostałym osobom uczestniczącym w projekcie w dogodnej dla nich formie. Większość narzędzi pozwala na generację dokumentacji modeli.

Modelowanie w języku UML dotyczy przede wszystkim logiki systemu i jego kodu. Równie istotnym aspektem jest modelowanie oraz implementacja i utrzymywanie baz danych. Do tego celu służą narzędzia do zarządzania bazami danych. Samo modelowanie struktury bazy danych odbywać się może w sposób podobny do modelowania obiektowego – przy wykorzystaniu notacji ERD lub UML. Narzędzia CASE dedykowane bazom danych dostarczają wielu funkcji ułatwiających budowę systemów bazodanowych. Narzędzia takie pozwalają na wygodne tworzenie kopii zapasowych, importowanie lub eksportowanie danych, kreację raportów dotyczących danych i ich schematów, ustalanie opcji wydajności, a także tzw. „strojenie” (ang. tunning), czyli optymalizację wydajności bazy danych. Niektóre posiadają także funkcje podobne do tych dostarczanych przez narzędzia typu IDE (patrz niżej). Umożliwiają np. uruchamianie poleceń administracyjnych albo związanych z danymi czy odpluskwianie (ang. debugging) przetwarzanych przez silnik bazy skryptów. Często dostępne w tego typu narzędziach są opcje analizy danych pozwalające na wizualne tworzenie zapytań (odpowiadających zapytaniom w języku SQL).

Często używaną grupą narzędzi są narzędzia do modelowania interfejsu użytkownika. Narzędzia takie pozwalają na tworzenie szkiców interfejsu użytkownika, nazywanych interfejsami szkieletowymi (ang. wireframe). W podstawowej swej funkcjonalności zastępują one kartkę papieru i ołówek – pozwalają przede wszystkim na zobrazowanie rozmieszczenia elementów interfejsu użytkownika. Interfejs szkieletowy nie oddaje ostatecznego wyglądu produktu, lecz jedynie odzwierciedla jego kontury. Główną zaletą takiego podejścia do modelowania interfejsu użytkownika jest możliwość koncentracji na meritum, czyli właściwej treści prezentowanej użytkownikom. Pozwala to zidentyfikować ograniczenia struktury interfejsu użytkownika i odpowiednio rozplanować wyświetlanie różnych typów informacji.

Na dalszych etapach modelowania interfejsu użytkownika możemy użyć narzędzia do tworzenia makiet (ang. mockup). Narzędzia takie mogą być zintegrowane z narzędziami do tworzenia interfejsów szkieletowych. Wystarczy wtedy po prostu przełączyć tryb modelowania. Od rzeczywistego interfejsu użytkownika makieta różni się tym, że jest statyczna – nie posiada funkcjonalności, a wyświetlane elementy są tylko przykładowe. Niektóre narzędzia do makietowania pozwalają na symulację nawigacji między poszczególnymi ekranami.

3.2. Narzędzia wsparcia implementacji i testowania oprogramowania

Klasą narzędzi CASE używaną w praktycznie wszystkich projektach są narzędzia  wspierające tworzenie kodu. Podstawowym zadaniem takich narzędzi jest zwiększenie produktywności przez zintegrowanie wszystkich funkcjonalności używanych w typowym cyklu życia kodu w jedną spójną całość. Narzędzia takie nazywamy zintegrowanymi środowiskami programistycznymi (ang. IDE – Integrated Development Environment).

Podstawowym elementem IDE jest edytor kodu. Współczesne edytory tego typu posiadają wiele funkcji pomagających w pisaniu kodu poprawnego składniowo. Typowo pozwalają one kolorować kod (rozróżniać różne elementy składni kolorami), dokonywać automatycznego formatowania (np. wcinanie tekstu), wykonywać autouzupełnianie (np. podpowiadanie nazw zmiennych) oraz oferują inne podobne funkcje edytorskie. Część środowisk zintegrowanych pozwala traktować kod nie tylko jako tekst, ale pewną strukturę. Wiele spośród IDE posiada opcję wstawiania często wykorzystywanych fragmentów programów (ang. snippets), które można parametryzować. Takie fragmenty zawierają konstrukcje programistyczne, takie jak pętle, polecenia sterujące czy konstrukcje językowe odpowiedzialne za obsługę wyjątków. Do kategorii automatycznego przetwarzania struktury kodu należą też opcje refektoryzacji kodu (ang. code refactoring), pozwalające na zmianę jego organizacji w celu poprawy czytelności i łatwiejszej późniejszej pielęgnacji. Dotyczy to takich operacji jak przemianowanie klas (odniesienia do refaktoryzowanej klasy są automatycznie zmieniane w całym zawierającym je kodzie), czy też aktualizacja struktury pakietów lub hierarchii klas. Inną użyteczną operacją dostępną w popularnych IDE jest generowanie szkieletów kodu np. dla klas implementujących określone interfejsy.

Prawdziwa siła wsparcia, jakie IDE udzielają programiście leży jednak w tym, w jaki sposób łączą się one z narzędziami zewnętrznymi. Ścisła integracja z kompilatorami pozwala na natychmiastowe wskazywanie błędów w kodzie, a często nawet automatyczne ich poprawianie. Popularne narzędzia do modelowania (omówione w poprzedniej sekcji) umożliwiają współpracę z IDE, czy to przez udostępnienie funkcjonalności związanych z modelowaniem w samych IDE, czy przez możliwość edycji kodu modelowanych elementów w środowisku modelowania. Inną, standardową już dzisiaj funkcjonalnością, jest wygodna współpraca z różnymi repozytoriami kodu i systemami wersjonowania. Środowiska programistyczne ułatwiają także tworzenie środowisk testowych przez automatyczną generację szkieletu skryptów weryfikujących aplikację oraz możliwość analizy i kontroli uruchomienia już zaimplementowanych testów.

Ważnym narzędziem w obrębie IDE jest tzw. odpluskwiacz (ang. debugger) pozwalający na analizę programów podczas ich wykonywania. Uruchomiony w ramach odpluskwiacza proces może być zatrzymywany w czasie wykonywania wybranych przez programistę instrukcji czy też wykonywany krokowo. Debugger pozwala na obejrzenie stanu pamięci przypisanej do tworzonego programu. Sam proces odpluskwiania pozwala na rozwiązanie problemów, których przezwyciężenie tradycyjnymi metodami (np. poprzez analizę kodu) jest bardzo pracochłonne.

Praca z kodem skutkuje powstaniem odpowiednim modułów wykonywalnych, które wspólnie tworzą gotowy system. Istotnym problemem, szczególnie w większych projektach konstrukcji oprogramowania jest integracja, czyli procesu kombinacji systemu ze stale aktualizowanych modułów. Aby wyeliminować problemy z integracją lokalną wprowadzono zcentralizowane systemy ciągłej integracji (ang. continuous integration). Są one najczęściej ściśle powiązane z systemami ciągłego wdrażania (ang. constant deployment). Centrum takich systemów są odpowiednie narzędzia, które potrafią automatycznie – na bazie przygotowanych wcześniej modułów - stworzyć działające wersje rozwijanego produktu. Takie działające wersje (wydania) systemu mogą być następnie w sposób automatyczny zainstalowane (wdrożone) w środowisku wykonawczym (testowym lub produkcyjnym). Przed instalacją przeprowadzane są – również automatycznie – zestawy testów mających na celu sprawdzenie, czy system działa prawidłowo.

Warto zauważyć, że proces ciągłej integracji i wdrażania powinien być realizowany z poziomu środowisk IDE. Oznacza to konieczność integracji IDE ze środowiskiem wykonawczym (np. serwery aplikacyjne), na jakim uruchamiany jest tworzony produkt. Współpraca IDE z docelowymi bądź testowymi środowiskami uruchomieniowymi polega na wsparciu dla łatwej kontroli serwera aplikacji z poziomu narzędzi programistycznych.

Jedną z podstawowych funkcjonalności narzędzi wspierających testowanie jest możliwość testowania interfejsu użytkownika (ang. UI testing). Tego typu testowanie pozwala na zbadanie reakcji interfejsu użytkownika, poprawności jego struktury (zachowanie pozycji elementów na ekranie), a także przejść między stanami aplikacji (np. kolejność pokazywanych ekranów). Podstawą jest tutaj zapisywanie (nagrywanie) typowych przebiegów interakcji użytkowników z badanym systemem. Proces rejestracji dialogu użytkownik-aplikacja polega na zapisie decyzji użytkownika (naciśniętych przez niego przycisków, wybranych opcji menu, wprowadzonych danych itd.). Taki zapis (akcje użytkownika oraz ich parametry) może być później modyfikowany.

Bardzo popularną grupą narzędzi wspomagających testowanie są narzędzia dla testów jednostkowych. Pozwalają one na generację szkieletów testów, kontrolowane uruchamianie skryptów testowych, analizę wyników działania oraz raportowanie. Testy jednostkowe uruchamiane poprzez odpowiednie oprogramowanie pracują w odpowiednio spreparowanym środowisku. Możliwe jest np. określanie danych testowych oddzielnie dla każdego uruchomienia, a także reprezentowane innych modułów przy pomocy zaślepek. Uruchomienie testów jest kontrolowane – kroki przypadków testowych mogą być realizowane w określonej kolejności. Wyniki testów jednostkowych pozwalają na stwierdzenie, który z warunków zapisanych w testach nie został spełniony oraz jaki był rezultat takiego sztucznie wytworzonego błędu. Dla przeprowadzonego zestawu testów narzędzia generują raporty zawierające statystykę znalezionych błędów, ich rodzaj, zakres kodu jaki został objęty testami itd. Narzędzia wspierające testy jednostkowe łatwo integrują się z popularnymi zintegrowanymi środowiskami programistycznymi (IDE).

O ile powyżej opisane narzędzia działają na zasadzie testów metodą „czarnej skrzynki”, to istnieje także wsparcie narzędziowe dla testów przezroczystej skrzynki. Służą do tego różnego rodzaju analizatory kodu. Narzędzia tego rodzaju pozwalają na weryfikację zgodności efektów pracy programistów z przyjętymi w ramach danej organizacji dobrymi praktykami tworzenia kodu. Badanie kodu pozwala także na zlokalizowanie możliwych przeoczeń programistów (np. puste lub nieosiągalne bloki programów, nieużywane zmienne, duplikowany kod). Narzędzia tej kategorii pozwalają na przejrzysty zapis reguł opisujących poprawny kod, a następnie analizę wybranych plików i tworzenie raportów opisujących wyniki analizy. Większość środowisk IDE posiada wbudowane w edytory kodu elementarne mechanizmy analizy kodu. Na przykład, pozwalają one kontrolować styl identyfikatorów, czy sprawdzać ich poprawność ortograficzną.

3.3. Metody automatyzacji wytwarzania i eksploatacji

Wszystkie omówione narzędzia można połączyć w cykl pełnej automatyzacji wytwarzania oraz eksploatacji oprogramowania. Na cykl ten składają się dwie podstawowe metody: wytwarzanie oprogramowania sterowane modelami oraz tzw. DevOps.

Wytwarzanie oprogramowania sterowane modelami (WOSM, ang. Model-Driven Software Development) dotyczy przede wszystkim tych dyscyplin inżynierii oprogramowania, które opierają się na wykonywaniu modeli: dyscyplinie wymagań, a także analizie i projektowaniu. Podstawą WOSM jest automatyzacja procesu przejścia między wymaganiami, projektem i kodem poprzez definiowanie i wykonywanie automatycznych transformacji między modelami. Elementem WOSM jest również automatyczna generacja kodu z modeli, która przenosi nas do dyscypliny implementacji systemu.

WOSM dostarcza technik w dwóch obszarach: definiowania języków modelowania w sposób formalny oraz definiowania przekształceń między modelami zdefiniowanymi w tych językach modelowania. Najczęstszym sposobem formalnego zdefiniowania graficznego języka modelowania jest tzw. metamodel. Metamodel to „model modelu”, który wyrażamy również w języku graficznym. Najczęściej jest to wariant modelu klas języka UML. Każda meta-klasa w metamodelu wyraża jeden element składni języka modelowania. Przykład bardzo prostego metamodelu widzimy na rysunku 3.1. Po lewej stronie rysunku widzimy diagram klas, który definiuje elementarny język modelowania składający się z kropek, kółek i strzałek. Jak wynika z metamodelu, język dopuszcza tworzenie modeli, w których kółka są połączone strzałkami. Z każdego kółka może wychodzić oraz wychodzić co najwyżej jedna strzałka. W kółkach mogą być zawarte kropki, przy czym może ich być maksymalnie dwie. Metamodel nie definiuje wyglądu elementów języka modelowania, a jedynie ich wzajemne relacje. Mówimy, że metamodel definiuje składnię abstrakcyjną języka. Po prawej stronie rysunku 13.1 widzimy przykład składni konkretnej, czyli składni widocznej dla użytkowników języka. Jak można się domyślić, na rysunku widzimy dwa kółka połączone dwoma strzałkami. W kółkach znajdują się odpowiednio – jedna oraz dwie kropki.

Rysunek 3.1: Przykład metamodelu prostego języka graficznego

Definicję swojej składni wyrażoną w postaci metamodelu posiadają praktycznie wszystkie języki modelowania, takie jak UML, BPMN czy ERD. Pozwala to na automatyczne przetwarzanie modeli. Każdy model jest przechowywany w odpowiednim repozytorium zgodnym z metamodelem. Taki model możemy odczytać przy pomocy odpowiedniego programu, a następnie dokonać automatycznej transformacji w inny model. Przykładem praktycznego zastosowania takiej automatyzacji jest automatyczna generacja struktury relacyjnej bazy danych (język ERD) z modelu dziedzinowego wyrażonego w języku UML.

Aby móc wykonywać transformacje modeli, należy napisać odpowiedni program transformujący. Programy takie możemy pisać w standardowych językach programowania, takich jak Java czy C#. Często jednak stosuje się języki dedykowane dla transformacji modeli. Wynika to z tego, że języki te posiadają specjalne konstrukcje, które ułatwiają deklarowanie przetwarzania składni języka wyrażonej za pomocą metamodelu. Języki te pozwalają na formułowanie „zapytań” do modeli, na podstawie których można znajdować fragmenty odpowiadające zadanym wzorcom (grafom wzorcowym). Takie fragmenty następnie podlegają przekształceniu poprzez np. transformacje tekstowe (konkatenacje słów, zmiana wielkości znaków itp.), dodawanie lub usuwanie elementów, czy też zmianę relacji między elementami.

Drugą metodą w cyklu automatyzacji inżynierii oprogramowania jest metoda nazywana DevOps (ang. Development and Operations), czyli – Rozwój i Obsługa. O ile metoda WOSM dotyczyła dyscyplin związanych z modelowaniem, o tyle DevOps dotyczy przede wszystkim dyscyplin związanych z kodowaniem (implementacja, testowanie, wdrożenie i utrzymanie). Podstawową zasadą metody DevOps jest stworzenie spójnego zestawu narzędzi, które w jak największym stopniu zautomatyzują powtarzalne i rutynowe czynności w cyklu życia kodu. Co jest szczególnie istotne – nacisk jest położony na zintegrowanie czynności związanych z wytworzeniem oprogramowania (rozwój) z czynnościami związanych z eksploatacją (obsługa). Zależność ta jest zilustrowana na rysunku 3.2.

Rysunek 3.2: Schemat cyklu automatyzacji metodą DevOps

Cykl DevOps rozpoczyna się od etapu planowania. Etap ten jest wspierany przez narzędzia do zarządzania projektami dedykowane projektom konstrukcji oprogramowania. W szczególności, narzędzia te wspierają planowanie oraz kontrolę realizacji czynności w procesie implementacji systemu. Istotna jest również integracja tych narzędzi z narzędziami do śledzenia błędów i problemów. Każde zgłoszenie problemu może być dzięki temu w łatwy sposób zamienione na odpowiednie cechy systemu (np. historie użytkownika lub przypadki użycia) w planie projektu oraz czynności w planie odpowiednich iteracji.

Następny etap to kodowanie. Na pierwszy plan wchodzą tutaj oczywiście narzędzia IDE wraz z centralnymi repozytoriami kodu oraz narzędziami do kontroli wersji. Jest to typowe środowisko pracy programistów. Bardzo ważna jest jednak integracja tego środowiska z innymi narzędziami. W pierwszej kolejności, integracja dotyczy narzędzi z etapu planowania. Dzięki integracji, programista wie na bieżąco, jakie czynności są konieczne do wykonania oraz bardzo szybko może zgłosić podjęcie czynności i jej wykonanie. W niektórych rozwiązaniach dzieje się to bezpośrednio podczas wykonywania typowych czynności związanych z zarządzaniem kodem i jego wersjonowaniem. Nie wymaga zatem dodatkowej pracy, a znacznie usprawnia komunikację w zespole.

Drugi obszar integracji narzędzi do kodowania jest związany z etapem budowy. Na tym etapie wykorzystywane są narzędzia do ciągłej integracji systemu. Integracja polega głównie na bezpośrednim uruchamianiu narzędzi integrujących z poziomu środowiska programistycznego IDE. Programista jednym przyciskiem jest w stanie skompilować i zintegrować cały system uzyskując bardzo szybko i automatycznie – gotowe do zainstalowania wydanie sytemu.

Kolejny obszar integracji dotyczy etapu testowania. Wykorzystując odpowiednie narzędzia możemy uruchamiać testy jednostkowe, automatycznie wykonywane podczas integracji systemu. Taka integracja jest szczególnie istotna dla testów regresyjnych. Programista ma możliwość szybkiego i automatycznego zweryfikowania, czy nie pojawiły się błędy dotyczące kodu już wcześniej sprawdzonego. Podobnie, możliwa jest automatyzacja wykonania testów akceptacyjnych. Po zintegrowaniu systemu uruchamiane są wtedy odpowiednie skrypty testowe sprawdzające działanie systemu z punktu widzenia jego użytkownika końcowego.

Na etapie testowania kończy się obszar rozwoju (Dev) i przechodzimy do obszaru obsługi (Ops). Ważne jest to, że przejście to jest automatyczne i ściśle zintegrowane. Obsługa rozpoczyna się od etapów konfiguracji oraz wdrożenia. Wykorzystywane są tu narzędzia do zarządzania konfiguracją oraz do ciągłego wdrażania. Wykorzystują one produkty narzędzi do ciągłej integracji. Często wykorzystywane są tutaj środowiska do konteneryzacji. Konfiguracja polega wtedy na stworzeniu odpowiedniego opisu kontenerów, które zawierają wszystkie moduły niezbędne do ich uruchomienia (kod wykonywalny, system bazy danych, system kolejkowania itd.). Kontenery te są budowane na etapie integracji. Na etapie konfiguracji z odpowiednich wersji kontenerów tworzony jest pakiet instalacyjny dla całego systemu. Następnie, odpowiednie wersje kontenerów są instalowane przy pomocy narzędzi do ciągłej instalacji. Instalacja odbywa się poprzez umieszczenie kontenerów w odpowiednich środowiskach kontenerowych.

Po zainstalowaniu systemu w środowisku wykonywalnym przechodzimy do etapu obsługi. Obsługa polega na uruchamianiu zainstalowanego systemu oraz utrzymywaniu uruchomionego systemu w ruchu. Tak jak już wspomnieliśmy, najlepszymi środowiskami wspierającymi te działania – w kontekście metody DevOps – są środowiska kontenerowe. Środowiska te pozwalają na ciągłą kontrolę działania uruchomionych kontenerów. Jednocześnie, bardzo łatwe jest dokonywanie serwisowania systemu poprzez zatrzymywanie kontenerów oraz szybkie uruchamianie kolejnych ich wersji. W łatwy sposób można również zarządzać obciążeniem systemu poprzez automatyzację uruchamiania wielu instancji kontenerów.

Ostatni etap cyklu DevOps to monitorowanie. Na tym etapie zbierane są różne informacje na temat działającego systemu. Informacje te są następnie przekazywane do zespołu deweloperskiego (Dev) w celu podjęcia dalszych działań rozwojowych. Zbierane informacje mogą dotyczyć zauważonych niedogodności w działaniu systemu, niewystarczającej wydajności czy też postulowanych rozszerzeń lub zmian funkcjonalności. Informacje te zbierane są przez odpowiednie narzędzia analityczne i przekazywane do systemów planowania projektu. W ten sposób zamyka się cykl DevOps.