20 lat web developmentu, czyli co naprawdę zmieniało się w pracy programisty

Kiedy zaczynałem tworzyć strony internetowe, największym wyzwaniem było dla mnie zrozumienie, co właściwie dzieje się po wpisaniu adresu w oknie przeglądarki. Dziś w kilka minut można uruchomić kompletny projekt z bazą danych, asynchroniczną kolejką zadań i w pełni funkcjonalnym panelem administracyjnym.

Czy to oznacza, że budowanie aplikacji wreszcie stało się łatwe?

Patrząc wstecz na ponad dwie dekady pracy w web developmencie, widzę zupełnie inną prawidłowość. Trudność wcale nie zniknęła. Ona po prostu nieustannie zmienia miejsce. Za każdym razem, gdy do rąk trafiało narzędzie rozwiązujące jeden uciążliwy problem, zyskiwaliśmy przestrzeń do budowania rzeczy znacznie większych. Jednocześnie natychmiast zderzaliśmy się z kolejnymi barierami, których wcześniej, oczywiście z racji mniejszej skali, nikt z nas nawet nie dostrzegał.

Prehistoria, czyli najpierw zrozumieć, potem uruchomić

Kiedy zaczynałem swoją przygodę z kodowaniem, na rynku zadomowiło się już PHP 4, choć wciąż bez trudu można było trafić na żywe projekty oparte na PHP 3. Standardowy zestaw narzędzi do stawiania czegokolwiek w sieci definiowała klasyczna niemal święta trójca, czyli serwer Apache, interpreter PHP oraz MySQL w wersji 4.

W wielu ówczesnych projektach, tuż obok kodu PHP, w tle działały też skrypty napisane w Perlu albo Bashu. Jeśli ktoś potrafił się nimi sprawnie posługiwać, w oczach otoczenia automatycznie zyskiwał status kogoś w rodzaju prawdziwego hakera. Oczywiście nie brakowało też śmiałków, którzy z powodzeniem uruchamiali całe serwisy dynamiczne wyłącznie w Perlu, a nawet kompilowali strony w C++. Jedno i drugie możliwe było bezpośrednio przy użyciu archaicznego interfejsu CGI. Dla początkującego samouka ten świat wydawał się złożony, wręcz nie do ogarnięcia.

Pierwszy poważny próg wejścia wcale nie dotyczył samej składni języka czy pisania funkcji czy innych pętli. Prawdziwym wyzwaniem było pojęcie, czym w ogóle jest serwer i gdzie dokładnie przebiega granica między tym, co renderuje przeglądarka, a tym, co wykonuje się gdzieś daleko w internecie. Trzeba było ułożyć sobie w głowie, że część plików (HTML, obrazki, skrypty klienckie) pobiera się wprost na komputer użytkownika, kod logiki przetwarza się w procesie serwera, a baza danych to jeszcze zupełnie osobny byt działający gdzieś obok. A najlepsze w tym wszystkim było to, jak zdobywało się wtedy wiedzę. Swoją pierwszą dokumentację do Apache, PHP i MySQL, łącznie z prostymi przykładami, zdobyłem, uwaga, na płycie CD.

Wiadomo jednak, że najlepsza nauka to praca na żywym organizmie. Kolejnym krokiem milowym było więc postawienie własnego środowiska deweloperskiego, czyli serwera WWW, interpretera PHP i relacyjnej bazy danych. Szczególnie pod kontrolą Windowsa samo dojście do momentu, w którym w przeglądarce udało się bezbłędnie wyświetlić banalne “Hello world”, nie było wówczas zadaniem trywialnym. Kiedy wreszcie udało się przebrnąć przez te instalacyjne męki i stworzyć prosty projekt, który bez zgrzytów działał lokalnie, natychmiast pojawiała się kolejna ściana, bo trzeba było tę samą konfigurację precyzyjnie odtworzyć na serwerze produkcyjnym, w zupełnie innym, naturalnym środowisku, czyli pod Linuksem.

Samo wdrożenie, czyli to, co dzisiaj chowamy pod eleganckim parasolem zautomatyzowanych pipeline’ów CI/CD, w tamtych czasach oznaczało po prostu klienta FTP i mozolne kopiowanie zmienionych plików do właściwego katalogu. Ścieżki bezwzględne na serwerze, pliki z logami błędów, poprawne reguły przekierowań. O to wszystko trzeba było dbać ręcznie, pamiętając przy tym o drobiazgach. Czy adres strony ma zawierać przedrostek “www”? Jak generować linki wewnętrzne, żeby działały zarówno lokalnie, jak i po publikacji na serwerze produkcyjnym? Czy Apache na pewno parsuje pliki z rozszerzeniem .php, zamiast bezczelnie serwować ich kod źródłowy użytkownikowi w postaci czystego tekstu? I co do cholery z prawami dostępu do plików?

Takie prozaiczne szczegóły potrafiły pożerać godziny, które człowiek wolałby poświęcić na tworzenie właściwej logiki serwisu. Na tym etapie programista był zmuszony robić wszystko. Projektował i klepał interfejs, pisał kod backendu, stawiał od zera środowisko systemowe i samodzielnie wdrażał projekt na produkcję. Największa trudność leżała w zrozumieniu i uruchomieniu wszystkich klocków tej raczkującej układanki.

Gotowe klocki, czyli mniej pisania od podstaw

Z biegiem lat, patrząc na rozwój samego kodu, zaczęło pojawiać się coraz więcej bibliotek, gotowych systemów CMS i wczesnych frameworków. Mambo (z którego później wyewoluowała Joomla!), rosnący w siłę WordPress, lekki CodeIgniter, a później dojrzałe narzędzia pokroju Symfony czy Laravela zmieniły to, od czego w ogóle zaczynało się codzienną pracę. Przestaliśmy wreszcie za każdym razem na nowo pisać obsługę formularzy, mechanizmy routingu, systemy uwierzytelniania użytkowników czy abstrakcję dostępu do bazy danych.

I to był pierwszy duży krok naprzód. Jeśli projekt wymagał klasycznego zarządzania treścią, wystarczyło wziąć i dostosować jedno z gotowych, powszechnie dostępnych rozwiązań. Kiedy trzeba było napisać dedykowaną aplikację biznesową od zera? Świetnie, dojrzały framework ściągał z barków programisty masę nudnej, powtarzalnej roboty. Walidacja danych wejściowych, bezpieczna autoryzacja, komunikacja z bazą czy warstwa pamięci podręcznej (cache) przestały wymagać pisania kolejnej własnej, nierzadko dziurawej implementacji. Można było sięgnąć po przetestowaną bibliotekę, dzięki czemu coraz większą część energii dało się przekierować na to, co najważniejsze, czyli na konkretną funkcję biznesową, której faktycznie potrzebował użytkownik końcowy.

Ale gotowy klocek również nie był darmowy poznawczo. Trzeba było go najpierw dokładnie poznać i sprawdzić, jak go skonfigurować pod nasz scenariusz, jakie ma ograniczenia, czy nie wymaga dodatkowego rozszerzenia wkompilowanego w PHP i co zrobić, gdy jego komunikaty błędów nagle muszą działać w kilku językach.

Nierzadko zdarzało się, że próba wciśnięcia gotowego rozwiązania na siłę obnażała braki projektowe w samej aplikacji. Pisanie typowych, powtarzalnych elementów stało się dużo szybsze, lecz trudność przesunęła się na trafny dobór, zrozumienie i integrację gotowych elementów. Choć oszczędzało to mnóstwo czasu, samo wdrożenie wciąż bardzo często wiązało się z ręczną konfiguracją na docelowej maszynie. Zupełnie nierozwiązanym problemem pozostawało zautomatyzowane aktualizowanie zewnętrznego kodu czy śledzenie drzewa zależności, a o zachowanie wstecznej kompatybilności programista musiał troszczyć się na własną rękę.

Zależności, czyli “ktoś już to gdzieś robi”

Jeśli w tamtych czasach decydowałeś się na wykorzystanie gotowych rozwiązań, nie miałeś do dyspozycji zbyt wielu usystematyzowanych kanałów ich dystrybucji. Najprostszą i najpopularniejszą metodą było po prostu pożyczanie kodu ze sprawdzonego źródła albo wprost od zaprzyjaźnionego kolegi po fachu. Było to na swój sposób wygodne, bo oszczędzało czas i pozwalało skutecznie wyciszyć sumienie w kwestiach bezpieczeństwa czy odpowiedzialności. Przecież kod podesłany przez dobrego znajomego na pewno musi być porządny i bezpieczny, prawda?

W praktyce wyglądało to tak prosto, jak tylko się dało. Tworzyłeś w projekcie osobny podkatalog, wrzucałeś tam czyjeś pliki albo bezpośrednio rozpakowywałeś pobrane archiwum ZIP i miałeś cichą nadzieję, że wszystko ze sobą zagra. Pozostawało ręczne poprawienie ścieżek w dyrektywach include czy require i voilà, w przeważającej większości przypadków jakoś to działało.

Prawdziwy ból głowy zaczynał się jednak w momencie, gdy wypadało pomyśleć o jakichkolwiek aktualizacjach. Bo skąd, u licha, miałeś wiedzieć, że autor biblioteki właśnie wrzucił nową, poprawioną wersję na swojego prywatnego bloga albo że w kodzie wykryto krytyczną lukę bezpieczeństwa? W tamtej epoce mało kto zaprzątał sobie tym głowę. Jeśli coś nie działało dokładnie tak, jak zakładałeś w projekcie, zazwyczaj bez oporów otwierało się taki zewnętrzny plik w edytorze i nanosiło poprawki bezpośrednio w cudzym kodzie.

Z biegiem czasu rolę zbiorczych repozytoriów zaczęły przejmować serwisy w rodzaju Phpclasses.org czy SourceForge, stając się ówczesnymi centrami społeczności, w których dało się wygrzebać użyteczny kod do większości zastosowań. Pierwszą oficjalną, systemową próbą uporządkowania tego chaosu, wzorowaną bezpośrednio na perlowym repozytorium CPAN, był projekt PEAR (PHP Extension and Application Repository). Działało fajnie, ale problem z PEAR-em polegał na tym, że instalowane za jego pośrednictwem pakiety lądowały globalnie w całym systemie operacyjnym (co często wymagało uprawnień root), wymuszały sztywną strukturę katalogów, a same paczki po czasie były porzucone, nieaktualizowane i generowały mnóstwo trudnych do rozwiązania konfliktów wersji.

Kwestiami aktualizacji i bezpieczeństwa wciąż nikt nie zaprzątał sobie głowy, bo kto tak naprawdę na co dzień śledził kanały RSS wyżej wymienionych serwisów albo regularnie czytał branżowe listy mailingowe z biuletynami podatności? W projektach powszechnie panowała niepisana, żelazna zasada “jak działa, to nie ruszaj”. Same aplikacje i serwisy po wdrożeniu praktycznie nigdy nie były aktualizowane, no chyba że administrator hostingu zdecydował o podbiciu wersji samego PHP na całej maszynie (co nierzadko kładło część hostowanych stron) albo gdy okazywało się, że ktoś włamał się na serwer i dopisał złośliwe linki do spamu w stopce witryny.

Temat bezpieczeństwa i planowych aktualizacji był więc w tamtych realiach niemal kompletnie pomijany. Czy to zmniejszało złożoność projektów? Nie, choć nikt tego głośno nie mówił. Z braku odpowiednich narzędzi inżynieryjnych złożoność była po prostu zamiatana pod dywan, co na długie lata skutecznie eliminowało PHP z wielu poważnych obszarów zastosowań korporacyjnych.

Przełom przyniósły dopiero pierwsze skuteczne próby standaryzacji, czyli pojawienie się rekomendacji PSR, natywne wsparcie dla przestrzeni nazw (namespaces) oraz debiut samego Composera. Composer rozwiązał problem automatycznego budowania drzewa zależności, pobierania właściwych wersji bibliotek i zarządzania aktualizacjami na poziomie kodu samej aplikacji. Dziś nikt nie wyobraża sobie codziennej pracy bez Composera, a w środowiskach produkcyjnych stałe dbanie o aktualizacje paczek i kompatybilność z nowymi wersjami stanowi podstawę inżynierskiego rzemiosła.

Composer sprawił, że dołożenie kolejnej zewnętrznej biblioteki i odtworzenie jej wersji w zupełnie innym środowisku przestało być skomplikowanym, osobnym projektem. Trudność techniczna spadła tak bardzo, że zarządzanie zależnościami stało się standardem w praktycznie każdym projekcie.

Ale wraz z tym rozwiązaniem natychmiast ujawnił się nowy problem natury decyzyjnej. Trudniej ocenić, czy w danym przypadku w ogóle warto uzależniać stabilność aplikacji od kolejnego zewnętrznego pakietu. Część repozytoriów po paru miesiącach traci wsparcie twórców, łatwiej niechcący wciągnąć kod z niesprawdzonego źródła, a na zespole spoczywa obowiązek stałego monitorowania całego, często dosyć rozbudowanego, drzewa zależności, pilnując, by drobna zmiana w jednym z podrzędnych pakietów nie rozłożyła na łopatki całego systemu. Zyskaliśmy bezproblemowe aktualizacje, jednolite standardy i mnóstwo zaoszczędzonego czasu, ale w zamian wzięliśmy pełną odpowiedzialność za mnóstwo kodu, którego sami nigdy nie napisaliśmy i który szybko może się zmienić w dług techniczny.

Większe projekty, większe problemy

Skoro dodawanie nowych funkcji stało się tak proste i szybkie, tworzone aplikacje zaczęły szybko rosnąć, a wraz z nimi rosła ich złożoność.

Z jednej strony systemy naturalnie ewoluowały w ociężałe monolity, które starały się obsługiwać wszystko i wszędzie. W odpowiedzi na to zjawisko na rynku jak grzyby po deszczu pojawiały się implementacje coraz to nowszych wzorców projektowych, dobre praktyki, opasłe podręczniki i konferencyjne prelekcje. W rzeczywistości wyglądało to bardzo różnie. Spory odsetek deweloperów, zgodnie z ludzką naturą, okazywał się uodporniony na wszelkie nowinki architektoniczne, trzymając się kurczowo nawyków i schematów wypracowanych przed laty. Mało tego, po latach okazywało się, że część szeroko wówczas promowanych wzorców po czasie zyskiwała oficjalny status antywzorców programowania. Co ciekawe, argumentacja stojąca za ich odrzuceniem okazywała się w pełni logiczna i oczywista, ale w ferworze wcześniejszej mody jakoś nikt nie zawracał sobie tym głowy.

Do tego dochodził kolejny aspekt. Oprócz samego kodu, rozwijanego lepiej lub gorzej, w architekturze pojawiła się cała masa twardych zależności od zewnętrznych usług i demonów systemowych, które były niezbędne do prawidłowego realizowania nowoczesnych funkcji. Kilka przykładów poniżej.

  • Wyszukiwanie pełnotekstowe w oparciu o Apache Lucene, a w późniejszych latach zaawansowane klastry Elasticsearcha,
  • Wszelkiej maści asynchroniczne kolejki zadań do obsługi procesów w tle,
  • Wielopoziomowe systemy cachowania i to nie tylko danych w pamięci (jak Memcached czy Redis), ale też samego zinterpretowanego kodu pośredniego,
  • Coraz bardziej rozbudowane integracje z zewnętrznymi usługami, API i webhookami w przeróżnych formatach danych,
  • Skomplikowane procesy obsługi płatności internetowych,
  • Alternatywne sposoby utrwalania danych, całe podsystemy analityczne i statystyczne oraz wiele innych.

Z czasem nawet bardzo dobrze pomyślany na początku podział kodu w repozytorium przestawał odpowiadać realnym granicom dynamicznie rosnącego systemu.

W takich projektach pojedyncza modyfikacja rzadko kiedy kończyła się na dopisaniu zaledwie kilku linijek. Deweloper musiał z góry dokładnie wiedzieć, które inne moduły zależą od dotykanego fragmentu, w jaki sposób wiarygodnie przetestować zewnętrzne integracje i co właściwie wydarzy się w trakcie wdrożenia produkcyjnego. Odwieczne zapewnienie, że “kiedyś to przebudujemy”, niemal zawsze przegrywało w starciu z twardym biznesowym terminem na dowóz kolejnej funkcji. W ten sposób pierwotnie wykorzystywany, poczciwy grep musiał ustąpić miejsca zaawansowanemu wyszukiwaniu w środowiskach IDE i coraz bardziej wyrafinowanym narzędziom do statycznej analizy kodu.

Tego typu systemy rosły bez opamiętania, pociągając za sobą ogromną złożoność otoczenia uruchomieniowego. To już dawno nie był po prostu zwykły demon Apache spięty z modułem PHP i jedną relacyjną bazą danych. Mówiliśmy o zestawie co najmniej kilku niezależnych usług, które wymagały od każdego programisty w zespole przynajmniej elementarnej wiedzy konfiguracyjnej, by w ogóle dało się odpalić jakąkolwiek działającą instancję deweloperską.

To oczywiście od razu wygenerowało kolejny problem, bo każdy programista pracował na nieco inaczej skonfigurowanej maszynie. Chaos stał się wyjątkowo widoczny w momencie, gdy do projektów zaczęto powszechnie wprowadzać automatyczne testy. Scenariusz “u wszystkich w biurze przechodzi, a u mnie pojawia się błąd” stał się programistycznym chlebem powszednim. Oznaczało to kolejne godziny tracone na debugowanie tylko po to, by na koniec odkryć, że któraś z lokalnych usług miała ustawiony inny limit pamięci, inaczej skonfigurowane InnoDB w MySQL-u, inną domyślną strefę czasową albo minimalnie różniącą się wersję biblioteki. Koszmar.

Pamiętam komercyjne projekty, w których wdrożenie nowo zatrudnionej osoby i samo przygotowanie dla niej działającego środowiska lokalnego potrafiło zająć kilka pełnych dni roboczych. Trudność wcale nie tkwiła już w samym pisaniu kodu. Samo klepanie linijek z czasem stawało się coraz prostsze. To po prostu przestało wystarczać. Aby kod w ogóle mógł zadziałać, trzeba było odtworzyć wokół niego całe technologiczne otoczenie, bez którego aplikacja nie działała, a które z biegiem lat stawało się coraz bardziej skomplikowane.

W ten sposób trudność naszej pracy rozszerzyła się z samego programowania na utrzymanie spójności z całą istniejącą infrastrukturą, a co za tym idzie, na walkę o przewidywalność działania całego systemu jako spójnej całości.

Vagrant i Docker, czyli środowisko zapisane w projekcie

Wtedy do gry wkroczyła wirtualizacja. Początkowo za sprawą maszyn wirtualnych zarządzanych przez Vagranta, a niedługo później dzięki rewolucji, jaką przyniósł sam Docker, udało się wreszcie okiełznać sporą część tego infrastrukturalnego chaosu. Zamiast przekazywać nowej osobie w zespole wielostronicową, najczęściej nieaktualną listę instrukcji w dokumentacji wiki, można było przygotować kompletną konfigurację całego środowiska jeden jedyny raz i przechowywać ją bezpośrednio w repozytorium, ramię w ramię z kodem źródłowym. Przy odrobinie szczęścia i poprawnie przygotowanym pliku docker-compose.yml wystarczyło po prostu sklonować projekt, wpisać jedno polecenie w terminalu i po kilku minutach móc rozpocząć właściwą pracę.

To naprawdę odmieniło naszą codzienność. O ile łatwiej było teraz zagwarantować identyczne wersje baz danych, kolejek czy interpreterów u wszystkich programistów w zespole! Błędy dawały się powtarzać w przewidywalny sposób, a testy automatyczne zaczęły wreszcie dawać miarodajne wyniki. Równocześnie jednak ktoś w zespole musiał te obrazy zbudować, skonfigurować sieci wirtualne łączące kontenery, zadbać o montowanie wolumenów danych, ich spójne aktualizacje i bezpieczny proces wdrożenia na serwery. Docker sam w sobie nie sprawił magicznie, że każdy projekt uruchamiał się w próżni, tylko dał po prostu inżynierom standaryzowane narzędzia do tego, by ten proces dało się wreszcie powtarzać.

Dla wielu deweloperów wejście w nowy projekt stało się prostsze. Trudność przeniosła się jednak na przygotowanie i utrzymanie tego środowiska, zacierając tradycyjną granicę i wymuszając ścisłą współpracę osób piszących aplikację z ludźmi odpowiedzialnymi za infrastrukturę. Mało tego, od tego momentu dało się w pełni wersjonować w systemie kontroli wersji (Git) już nie tylko sam kod aplikacji, ale również deklaratywną konfigurację całego systemu operacyjnego.

Jak można się domyślić, to rozwiązanie pociągnęło za sobą nową falę problemów, windując złożoność i tak już skomplikowanych systemów informatycznych. Podstawowe kwestie, takie jak ulotność kontenerów i trwałość danych, czyli bolesne zderzenie z faktem, że wraz z usunięciem kontenera znikają wszystkie zapisane w nim tabele bazy danych, przeważnie udawało się skutecznie opanować zaraz po pierwszej bolesnej wpadce. Jednak w przypadku nowoczesnych chmur obliczeniowych, które szybko stały się naturalnym środowiskiem do uruchamiania kontenerów na dużą skalę, pojawiły się nowe wyzwania dotyczące samego bezpieczeństwa, zarządzania uprawnieniami czy orkiestracji setek powiązanych ze sobą procesów.

Dla ludzi dopiero wchodzących w świat programowania dosyć dużą barierą stał się próg wejścia związany z koniecznością opanowania nowej klasy narzędzi. Bo wszystko było ok, jeśli działało, gdy jednak coś się wywalało, zdebugowanie najprostszego błędu związanego z lokalną kopią infrastruktury deweloperskiej stanowiło dla “niewtajemniczonych” nie lada problem. Ale bilans zysków i strat wciąż pozostawał bezdyskusyjny, bo wirtualizacja dała nam dużo więcej korzyści niż wszystkie wcześniejsze, chaotyczne próby rozwiązywania tych problemów. Po raz pierwszy w historii pozwoliła też skutecznie rozdzielić tę niewidzialną, wiecznie problematyczną granicę pomiędzy samym rozwojem kodu a infrastrukturą, na której ma on być uruchamiany, zachowując przy tym elegancką izolację pomiędzy środowiskami lokalnymi, stagingiem a produkcją.

Architektura, czyli “my tego nie potrzebujemy”

Wraz ze stałym wzrostem skali systemów i rosnącą liczbą inżynierów zaangażowanych w rozwój pojedynczego produktu przestało nam wystarczać proste pytanie, czy dana funkcja działa poprawnie. Trzeba było pytać, gdzie w strukturze systemu powinna się znaleźć dana logika, od czego może zależeć, a od czego zależeć jej nie wolno, i czy po nagłej zmianie założeń biznesowych da się ją rozszerzyć bez przepisywania dużej części aplikacji.

W tego typu strategicznych analizach z pomocą przyszły podejścia i praktyki inżynieryjne, które w dużej mierze powstały jako uniwersalne koncepcje jeszcze w epoce przedinternetowej. Domain-Driven Design (DDD), Clean Architecture, architektura heksagonalna czy Event Sourcing okazały się znakomitym orężem do walki ze skomplikowanymi problemami domenowymi. Do tego doszły techniki modelowania procesów na żywo, poparte sesjami Event Stormingu z udziałem biznesu.

Oczywiście żadna z tych technik nie była traktowana jako obowiązkowy etap rozwoju każdej witryny. Prosty system czy klasyczna strona wizytówkowa nie potrzebują żadnego z tych rozbudowanych podejść, a ich bezrefleksyjne wdrożenie na wyrost potrafi sztucznie pompować złożoność do granic absurdu. Jeżeli jednak cykl życia aplikacji trwa wystarczająco długo, projekt stale rośnie, a rozwija go spory zespół ludzi, brak jakiegokolwiek planowania architektonicznego nieuchronnie prowadzi do paraliżującego chaosu. Wtedy, prędzej czy później, wyżej wymienione techniki okazują się po prostu niezbędne, by projekt w ogóle dało się dalej utrzymywać.

W większych organizacjach siłą rzeczy wykształciły się mocno wyspecjalizowane role i zespoły. Jedni programiści skupiają się wyłącznie na warstwie interfejsu (frontend), inni drążą architekturę backendową, jeszcze inni dbają o platformę chmurową i infrastrukturę, automatyzację testów jakościowych (QA) czy kierunek rozwoju produktu. Choć granice między tymi rolami bywają elastyczne i płynne, sprawiają, że część decyzji podejmuje architekt, a część cały zespół w toku dyskusji.

To wcale nie zwalnia pojedynczego programisty z myślenia o architekturze, wręcz przeciwnie. Kluczowe jest to, by wszyscy w projekcie patrzyli na nią w dokładnie ten sam, spójny sposób. Ma to duże znaczenie w codziennej pracy. Operując w zdefiniowanych, sztywnych ramach, deweloper każdego dnia podejmuje dziesiątki mikrodecyzji o tym, jak logicznie wpiąć nowo pisany kod w gąszcz już działających, krytycznych elementów istniejącego ekosystemu.

Zyskaliśmy w ten sposób realną możliwość budowania i stabilnego utrzymywania znacznie większych, bardziej ambitnych produktów cyfrowych. W zamian jednak musieliśmy opanować trudną sztukę wspólnego podejmowania decyzji, wyznaczania granic odpowiedzialności oraz ciągłej komunikacji między ludźmi i rozproszonymi zespołami, i to w sposób w pełni transparentny oraz zrozumiały dla każdego członka organizacji.

AI, czyli kolejny raz zmienia się miejsce trudności

Nie ma większego znaczenia, czy spojrzymy na frontend, backend, czy konfigurację serwerów, gdziekolwiek się one znajdują. Od dwudziestu lat jako branża robiliśmy wszystko, by przyspieszyć naszą codzienną pracę lub wręcz zautomatyzować każdy możliwy jej element. Widać to w wersjonowaniu zmian w kodzie, automatycznych wdrożeniach na produkcję bez przestojów (zero-downtime deployment), automatycznym pobieraniu aktualizacji czy narzucaniu ścisłych reguł architektonicznych, które wprowadzały logiczny porządek i dyscyplinowały procesy wytwórcze.

Od lat dążyliśmy do tego, żeby było szybciej, stabilniej, powtarzalniej i taniej. Chodziło o to, by samo prowadzenie skomplikowanych projektów było jak najbardziej przewidywalne, a nagła rotacja poszczególnych deweloperów nie wywracała do góry nogami całego procesu produkcji oprogramowania.

Patrząc ogólnie, dziś wszystko stało się przecież bez porównania łatwiejsze. Czyż nie taki był pierwotny cel całego tego technologicznego wyścigu? Na co dzień czerpiemy garściami ze wszystkich dobrodziejstw wypracowanych w ostatnich latach, czyli z uniwersalnych standardów kodu i rekomendacji PSR, pełnej historii decyzji bezpiecznie zapisanej w repozytoriach, dojrzałych wzorców architektonicznych, gotowych, dobrze udokumentowanych frameworków, automatycznie rozwiązywanych drzew zależności w menedżerach pakietów, skalowalnych systemów rozproszonych i środowisk o wysokiej dostępności (High Availability) czy nawet deklaratywnego wersjonowania konfiguracji maszyn oraz całej infrastruktury (IaC).

Czy przez to współczesne systemy stały się prostsze? Nie. Po prostu przestaliśmy zaprzątać sobie głowy rzeczami, których rozwiązanie dwie dekady temu wymagało niemal oddzielnych, pionierskich projektów inżynieryjnych. Równocześnie jednak ogólny poziom skomplikowania systemów i całościowy próg wejścia w zaawansowaną inżynierię oprogramowania wzrosły o całe rzędy wielkości.

I właśnie w ten gęsty ekosystem wkroczyło generatywne AI.

Sztuczna inteligencja potrafi w kilka sekund przygotować działający szkic nowej funkcji, wygenerować cały moduł, a nawet zarys kompletnej aplikacji. Bez problemu zaproponuje zestaw testów jednostkowych, wyjaśni logikę cudzego, zawiłego fragmentu kodu, z którym stykamy się po raz pierwszy, albo w trymiga pomoże przyswoić składnię zupełnie nowego narzędzia. To duży zastrzyk produktywności, który przyspiesza pracę na każdym etapie.

Tylko czy to oznacza, że od teraz automatycznie, jak za dotknięciem czarodziejskiej różdżki, wydajne, skalowalne i bezpieczne aplikacje dowolnej skali zaczną się budować same?

W małych, hobbystycznych projektach pisanych do szuflady albo prostych narzędziach wewnętrznych taki wygenerowany kod będzie rozwiązaniem często więcej niż wystarczającym. Jednak w przypadku poważnych, komercyjnych produktów ktoś za ich stabilne i bezbłędne działanie musi wziąć pełną odpowiedzialność, i nie mam tu na myśli tylko odpowiedzialności czysto technicznej, ale również biznesowej i finansowej. Na to nakłada się przecież masa przepisów, norm bezpieczeństwa danych i prawnych regulacji rynkowych, które każdy komercyjny system musi spełniać, by w ogóle móc legalnie funkcjonować na rynku.

Wygenerowany przez asystenta kod nadal trzeba umieć kompetentnie ocenić. Trzeba sprawdzić, czy odpowiada na realny problem biznesowy i czy pasuje do standardów i architektury projektu. Trzeba wiedzieć, jak zachowa się przy błędzie, pod dużym obciążeniem albo gdy zewnętrzna usługa nagle zmieni format odpowiedzi. I wreszcie, czy da się go bezpiecznie utrzymywać i zmieniać przez kolejne lata.

I ta ostatnia kwestia wydaje mi się najbardziej krytyczna, gdyż tworzenie produktu tak naprawdę to tylko pierwszy etap cyklu życia aplikacji. Tak naprawdę nie ma to większego znaczenia, czy ktoś go zbuduje w najlepszym software house’ie, wykorzysta freelancerów z drugiego końca świata czy też może wykorzysta do tego AI. Czyż nie? A tam będzie ukryta główna część wszystkich kosztów.

Im łatwiej i szybciej przychodzi nam generowanie kolejnych klocków aplikacji, tym ważniejsze staje się rozumienie architektury, do której te klocki dokładamy.

Dla modelu językowego nie ma najmniejszego znaczenia, czy ma dołożyć do Twojego projektu skomplikowany klaster działającego Elasticsearcha. Mając odpowiednie uprawnienia, bez wahania go skonfiguruje i chętnie wdroży nawet na środowisku produkcyjnym. W parę godzin otrzymasz superskalowalny i w teorii całkowicie bezpieczny (bo w odpowiedzi zapewni Cię, że to “production-ready best practice”) sposób przeszukiwania danych. Pytanie tylko, czy Twoja aplikacja w ogóle potrzebuje takiego rozwiązania, czy nie jest to strzelanie z armaty do wróbla przy Twojej skali danych i, co najważniejsze, kto na koniec miesiąca pokryje koszty utrzymania kilku kolejnych maszyn w AWS-ie?

AI obniża próg wejścia w samo programowanie, a osoba stawiająca pierwsze kroki znacznie szybciej zobaczy na ekranie działający, satysfakcjonujący rezultat. Jednak samo uruchomienie działającego prototypu a zrozumienie jego długofalowych celów, architektonicznych ograniczeń i podatności to wciąż dwa zupełnie różne światy. To dokładnie ten sam schemat, który jako branża przerabialiśmy już przy okazji narodzin gotowych frameworków, globalnych repozytoriów bibliotek czy kontenerów. Różnica polega na tym, że dziś wszystko dzieje się znacznie szybciej.

Co więc naprawdę stało się łatwiejsze?

Dzisiejsze aplikacje w ogóle nie przypominają tych, które z zapałem stawialiśmy w sieci kilkanaście czy dwadzieścia lat temu. Choć w codziennym slangu wciąż używamy tych samych pojęć (baza danych, serwer, żądanie HTTP, formularz), to sam poziom skomplikowania, gęstość wzajemnych zależności i skala współczesnego oprogramowania wzrosły wykładniczo. Bo kto dwadzieścia parę lat temu zajmował się np. shardingiem? I do niedawna cały ten skok technologiczny był możliwy wyłącznie dzięki skoordynowanej, mrówczej pracy ludzkich zespołów.

Po ponad dwudziestu latach w branży, dzięki generatywnemu AI możemy zbudować w zaledwie jeden dzień coś, na co jeszcze całkiem niedawno potrzebowalibyśmy tygodni pracy. Co prawda oprócz odrobiny czasu potrzeba do tego paru dodatkowych dolarów na opłacenie tokenów i subskrypcji modeli. W zamian jednak nie musimy już samodzielnie pisać wielu podstawowych, powtarzalnych mechanizmów ani za każdym razem ręcznie odtwarzać konfiguracji serwera. Ba, w zasadzie to momentami można odnieść wrażenie, że praktycznie niczego nie musimy już programować ani konfigurować od zera w tradycyjnym tego słowa znaczeniu. Większość błędów, wąskich gardeł i problemów integracyjnych na każdym etapie prac da się szybko zdiagnozować, a często wręcz automatycznie poprawić kilkoma trafnymi zapytaniami.

Programista w tym nowym układzie sił staje się swego rodzaju “inżynierem produktu”, co przesuwa punkt ciężkości z wytwarzania kodu na modelowanie funkcji, projektowanie logiki biznesowej i definiowanie sposobu działania systemu.

Czy to w jakikolwiek sposób uprości same aplikacje? Śmiem twierdzić, że wręcz przeciwnie. Jeśli jako ludzie byliśmy w stanie osiągnąć taki postęp techniczny bez pomocy sztucznej inteligencji, to dzisiaj możemy sobie jedynie wyobrażać, jak złożone systemy będziemy budować i utrzymywać za kilka lat.

Wbrew hucznym zapowiedziom wizjonerów i efektownym pokazom najnowszych modeli, w codziennej pracy wciąż z łatwością da się dostrzec ich twarde ograniczenia. Nowe narzędzia są niczym genialni superdeweloperzy, w dodatku napędzani sterydami, którzy mogą bez zmęczenia pracować dla nas w trybie 24/7. Ich możliwości poznawcze nie są jednak (a przynajmniej nie są dzisiaj) nieskończone.

Widać to jak na dłoni na przykładzie pracy z większymi modułami, szczególnie przy skomplikowanym, wieloetapowym budowaniu nowej funkcjonalności czy refaktorowaniu. Modele potrafią w pewnym momencie zwyczajnie gubić wątek, zupełnie zapominać wyniki twardych ustaleń architektonicznych sprzed paru godzin, a przy braku wystarczającej uwagi i krytycyzmu ze strony programisty, z zapałem rozpocząć implementację obsługi edge case’ów, które w rzeczywistości w danym systemie nigdy nie mają prawa wystąpić.

Oczywiście dokładnie tak, jak każdy żywy deweloper dołączający do zespołu, AI musi najpierw uzyskać dostęp do wszystkich kluczowych artefaktów i ustaleń projektowych. A to rodzi kolejne trudności, bo trzeba rozstrzygnąć, co z credentialami, jak czytelnie dokumentować logikę biznesową i założenia architektury, jak dzielić się kontekstem w zespole, w którym maszyny biorą udział w każdym etapie pracy, jak planować testy i szybkie pętle informacji zwrotnej oraz jak wyłapywać subtelne błędy, które maszyna wprowadza w logikę modułów i w połączenia między nimi.

Dodatkowo sztuczna inteligencja wciąż nie eliminuje żadnego z tych narzędzi, które z takim mozołem wypracowaliśmy i wdrożyliśmy do naszej pracy na długo przed jej powstaniem. Nowoczesny tooling, statyczna analiza, systemy CI/CD, konteneryzacja, metryki biznesowe działają dokładnie tak samo na korzyść modeli, jak na korzyść żywych programistów. Narzędzia te ułatwiają pracę i zwiększają pewność, że finalnie zobaczymy poprawny, stabilny wynik znacznie szybciej, a do tego taniej (jak na razie). Świadomy dobór architektury systemowej oraz sprawdzonych narzędzi inżynieryjnych, które wykuwaliśmy przez ostatnie dekady, ma więc dzisiaj jeszcze większe znaczenie niż kiedykolwiek wcześniej.

Praca programisty bardzo się zmienia i nabiera niespotykanego dotąd rozpędu. Daje nam możliwość tworzenia jeszcze lepszych, bardziej zaawansowanych i ciekawszych rzeczy. Dziś, niczym podczas elastycznego modelowania w cyfrowej glinie, jesteśmy w stanie bardzo szybko materializować pomysły i tworzyć użyteczne produkty cyfrowe.

Otwarte pozostaje jedno pytanie. Skoro spędzamy dziś tak niewiele czasu na faktycznym programowaniu, czy wciąż jeszcze jesteśmy programistami?

Wszystkie wpisy