Najgorszy projekt w karierze?
Poprzednio pisałem, że trafiłem do nowego projektu, który „dostała” nasza firma. Już wtedy sygnalizowałem, że nie jest najlepiej. Teraz po pierwszych zrobionych zadaniach i pracy z naszego biura mogę powiedzieć Wam dlaczego nie tylko nie jest najlepiej. Jest to najgorszy projekt jaki w życiu widziałem.
To co widzieliśmy będąc u klienta okazało się tylko wierzchołkiem góry tragedii jaką okazuje się ten projekt. Całości dopełniają dwa zdania powtarzane przez programistów po stronie klienta – „robimy tak jak umiemy” i „a co w tym złego?”.
Nie będę się za bardzo zagłębiał w szczegóły, zwłaszcza, że mimo wszystko nie chcę, żeby dało się jednoznacznie zidentyfikować projekt. Jednak jestem przekonany, że uświadomicie sobie skalę tragedii. Kwiatków jest tak dużo, że można by napisać książkę.
Techniczny shake
Zacznijmy spokojnie czyli od nie najświeższych technologii.
Wieloma językami
Jest to projekt w technologii .NET. Ale nie myślcie sobie, że to oznacza jedyny słuszny język czyli C#, ewentualnie przez niektórych jeszcze czasami lubiany Visual Basic. O nie nie panie i panowie.
Zacznijmy od backendu. Ten powstaje w języku C++/CLI. Jest to .NETowa odmiana C++ z kilkoma dodatkami. C++ nie brzmi tak strasznie, prawda? A co powiecie na to, że jakieś 90% kodu w tym C++ jest całkowicie proceduralna, w dodatku w dużej części ze zmiennymi globalnymi? Dołóżcie do tego SQL, który generuje całkiem znaczącą część tego kodu C++owego. A to jest dopiero początek.
Desktopowy klient napisany jest w Visual Basicu z użyciem WinForms (chociaż WinFormsów w pierwszech chwili trzeba się mocno naszukać). Jest jeszcze klient webowy. On natomiast napisany jest w C# (możliwe, że fragmenty w VB też się trafią) i WebForms.
Oprócz tego jest kilka skryptów SQL i tona, naprawdę tona skryptów windowsowych, bez których mało co da się zrobić.
Tutaj warto tylko dodać, że część podprojektów jest w .NET 3.5, a część w .NET 2.0.
Biblioteki? Zrobimy lepsze.
Wymieniłem wcześniej dwa frameworki – WinForms i WebForms. Jeśli w tym momencie spodziewacie się jakiegoś standardowego stacka technologicznego dla tych frameworków to jesteście w błędzie.
Najpierw delikatnie – własna biblioteka do unit testów. Tak naprawdę jest to obudowane rzucanie i łapanie wyjątków w przypadku kiedy wartości się nie zgadzają. Nie da się uruchomić pojedynczych testów, a wyniki są po prostu wyświetlane w konsoli. Testy da się uruchomić tylko przez specjalnie przygotowany skrypt. Ale to nie problem bo po prostu testów się nie pisze. Każdy test wymaga postawienia całej bazy i systemu. W konsekwencji testów jest coś koło 200. Mniej niż formularzy w aplikacji desktopowej.
Drugą kwestią jest autorski system do wiadomości. Pomiędzy serwerem i klientem dane są przesyłane przez wiadomości jednak sposób w jaki jest to zrobione wymaga po pierwsze pamiętania kolejności wywoływania funkcji, a po drugie wywoływania wielu z tych funkcji nawet dla prostej komendy ( właściwie minimum to otwarcie połączenia, przekazanie bufora/buforów, przekazanie co chce się zrobić, wysłanie wiadomości, odebranie bufora).
I pora na wisienkę w kwestii bibliotek – autorski ORM. A właściwie „ORM”. Ten kawał kodu opiera się w przytłaczającej większości na dwóch rzeczach – intach opakowanych za pomocą #define w różne nazwy i masie kodu wygenerowanego przy buildzie za pomocą SQLa. Generowany kod to właśnie to mapowanie na obiekty. Po prostu z bazy są SQLem pobierane wszystkie tabele i kolumny i dla każdej z nich jest generowany kod klas C++. Każda zmiana na bazie wymaga praktycznie przebudowania większości projektu. Niech dobrym wyznacznikiem jakości tego kodu będzie fakt, że przy zapisie wartość primary key dla encji jest nadawana poprzez … GetRandomInt64. Mówiąc wprost id jest po prostu losowane. Jak po kilkunastu próbach nie uda się trafić na wolne to trudno. Nie będzie encji.
Brakuje za to jakiejś biblioteki do logowania, a przez to praktycznie żaden wyjątek czy zdarzenie nie są zapisywane. Brak logów w połączeniu z trudnym debugowaniem sprawia, że znalezienie przyczyny błędu sprowadza się do wróżenia z fusów.
Wzorce? Dobre praktyki? A na co to komu?
Kiedy technologicznie projekt to misz-masz to może chociaż budowa kodu czy architektura jakoś się trzymają? Hehe. Nie.
Copy-paste programming
Bez przesady można powiedzieć, że minimum 1/3 kodu to copy-paste. Przestało mnie dziwić, że widzę dokładnie taki sam kod po raz 5 i każą mi dodać go 6 raz. Właśnie – każą. Jeśli spróbujemy wyciągnąć coś do funkcji czy klasy, którą można ponownie użyć to odpowiedź jest tylko jedna – mamy to wycofać i zrobić tak jak jest wszędzie czyli skopiować po raz kolejny.
Właściwie większość zadań wykonuje się poprzez znalezienie jakiegoś fragmentu kodu i skopiowaniu go. Programowania za bardzo w tym projekcie nie ma.
Ale kopiowanie jest tutaj obecne też na wyższym poziomie. Mianowicie po dodaniu jakiejś kontrolki na widoku trzeba przebudować projekt dzięki czemu zostanie wygenerowany plik z eventami dla tej kontrolki. Plik ten należy skopiować z jednego folderu do drugiego, który ma dokładnie taką samą strukturę jak pierwszy. Cała zabawa z kopiowaniem polega na tym, że mamy min. 3 warstwy kodu – kod wygenerowany, kod zmodyfikowany wspólny dla wszystkich klientów i kod specyficzny dla danego klienta. Wszystkie 3 warstwy mają takie same pliki. Różnią się one dodatkowymi metodami albo treścią metod. W trakcie kompilacji kod dla klienta jest nakładany na kod wspólny i na koniec na kod wygenerowany. Wszystko działa za pomocą replace i dyrektyw w kodzie. Dzięki temu numery linii w błędach kompilacji mają mało wspólnego z numerami linii w edytorze.
Krótki kod jest dla słabych
Pliki po kilka tysięcy linii kodu to tutaj norma. Co więcej wiele pojedynczych funkcji potrafi mieć po kilka tysięcy linii. Takim wielotysięcznikiem jest chociażby metoda zwracająca listę wartości dla dropdownów. Dlaczego jest ona taka długa? Bo wszystko opiera się o gigantyczny switch. Każdy dropdown ma swój numer nadawany ręcznie w edytorze. Dodając kolejną listę rozwijalną trzeba sprawdzić jaki numer miała ostatnia dodana i nadać naszej większy. Potem pisze się metodę ustawiającą listę wartości i dopisuje kolejny case w switchu obsługującym wszystkie dropdowny. W tym momencie zawiera on ponad 1000 caseów.
Jest też kilka plików z samymi definami. Otwierasz i widzisz kilka tysięcy #define. Ale wszystkie są oddzielone od reszty kodu i siedzą w swoim pliku, więc co w tym złego? Chociażby to, że bez działającego Ctrl+F lepiej od razu zrezygnować z szukania właściwej linijki.
Największy plik jaki znalazłem to chyba wygenerowany plik z klasami dla „ORMa”, który ma ok. 80 tys. linii. Ale to jest generowany plik więc go pomińmy. „Zwykłe” pliki są małe – więcej niż 10 tys. linii nie widziałem.
Rozdzielenie warstw? Nie chcemy podziałów
Wystarczyłoby w tym miejscu powiedzieć, że istnieje coś takiego jak logika biznesowa klienta i serwera. Przypominam – mamy aplikację desktopową i webową. W ogóle kod klienta w jednym pliku jest odpowiedzialny za część logiki biznesowej, obsługę widoczności kontrolek, zapis/odczyt danych, obsługę zdarzeń z kontrolek.
Ale ale, logikę biznesową mimo wszystko lepiej trzymać w jednym miejscu! Dlatego są też miejsca w kodzie gdzie jeden plik zawiera dwie wersje kodu – dla serwera i klienta. To, który zostanie skompilowany zależy od tego czy jest kompilowany w kontekście serwera czy klienta i jest wybierany za pomocą dyrektyw.
W aplikacji webowej nie jest lepiej. Cały kod znajduje się w dwóch miejscach – w gigantycznym pliku z toną constów i metod statycznych albo w kilku plikach połączonych z widokami (w WebForms jest plik plik.aspx i powiązany z nim plik.aspx.cs z kodem źródłowym).
Twórczość abstrakcyjna w nazwach
Na pewno znacie to stwierdzenie, że nie ważne jaką konwencję przyjmiecie w projekcie ważne żeby była taka sama w całym projekcie. Jednak w tym projekcie to by było zbyt mainstreamowe. Dlatego mamy zarówno nazwy_z_podkresleniem, NazwyBezPodkreslenia albo czasami w ogóle lepiejniewnikac.
Wracając chociażby do wspomnianego wcześniej switcha. Korzysta on z metod wypełniających wartości dropdownów. Myślicie, że wszystkie metody są nazywane w ten sam sposób? Nic bardziej mylnego. Część jest pisana CamelCasem, a część z_podkreśleniem. Podobno w ten sposób jakoś rozróżniają stare od nowych metod, albo specyficzne dla klienta i wspólne dla wszystkich. Ciężko powiedzieć, zapamiętać jeszcze trudniej.
Nazwy plików też nie ułatwiają sprawy. Jak już wspomniałem pliki dla kontrolek zawierają trochę logiki, trochę eventów, trochę zmiany widoczności kontrolek. Nazywają się tak jak np. formularz w aplikacji desktopowej. Ale pliki z taką samą nazwą są dla serwera i klienta. Plików w folderze jest kilkaset więc lista nie mieści się na ekranie. Wiele razy po żmudnym szukaniu jakiegoś pliku nie znając jego dokładnej nazwy trafiałem na nie ten co trzeba bo nazywał się tak samo, ale był w nie tym folderze co trzeba.
Domyśl się
Na koniec kwestia wersjonowania zmian na bazie i w aplikacji desktopowej.
Migrowanie bazy to piękna sprawa. Istnieje jedynie plik tworzący całą bazę od zera. Robiąc zmianę modyfikujemy go i commitujemy. Reszta zespołu musi się domyślić, że trzeba przegenerować bazę lokalną.
Jeszcze lepiej to działa w przypadku wgrywania nowej wersji na produkcji. Tutaj jest kosmos. Otóż kiedy jest nowa wersja wrzucana na live to jest osoba, która ręcznie za pomocą odpowiedniego narzędzia porównuje schematy starej i nowej bazy. I ręcznie migruje tabele i dane. RĘCZNIE ROBI UPDATE TABEL NA PRODUKCJI!
Podobnie sprawa wygląda w przypadku modyfikowania widoków w aplikacji desktopowej. Otóż wszystkie widoki i menu są trzymane w bazie danych. I na podstawie tej bazy działa aplikacja. Jednak jakoś trzeba wrzucić zmiany do repo, prawda? Robi się to bardzo prosto – wystarczy uruchomić odpowiedni skrypt, który zapisze zmiany do pliku XML i ten plik jest commitowany. Potem jedynie reszta zespołu musi pamiętać żeby inną komendą zaaplikować te zmiany u siebie po pobraniu ich. Jak pracowali na tych samych formularzach to najwyżej coś im się usunie. Trudno.
Słowo na koniec
To co tutaj opisałem to naprawdę tylko mały fragment tego co się dzieje w projekcie. Można by wymieniać dalej, chociażby wspomnieć o rzucaniu treści błędów technicznych użytkownikowi końcowemu, czy niemożliwości wyjścia z formularza jak jakieś pole nie przechodzi walidacji. Niestety sporo ciekawych historii wymaga jednak kawałków kodu, a tego po pierwsze nie mam w domu, a po drugie nie mogę go od tak pokazać.
W tym momencie próbujemy zaatakować problem i przepchnąć rewolucje w kodzie. Mamy pomysły, mamy sugestie, nawet chęć edukowania ludzi klienta by się znalazła. Jednak to wszystko będzie wymagało minimum chęci zmian z ich strony.
Na pewno będę starał się opisywać co ciekawsze rozwiązania i historie związane z tym programistycznym wyzwaniem i tragedią.
Witam,
Jeszcze jeden fanatyk pięknego kodu i absurdalnych „norm” czystego kodu.
Jako przykład mogę podać pierwszy na świecie program do strukturalnej analizy
o nazwie NASTRAN napisany w 1971 r. w Fortranie 66 i liczący około 1M linii kodu.
Ów program jest totalnym zaprzeczeniem tej plagi i zarazy jaką jest tzw.
czysty kod co nie przeszkodziło NASTRAN’owi odnieść ogromny sukces i być
masowo używanym do analizy strukturalnej mostów poprzez przemysł
motoryzacyjny i zakończywszy na przemyśle aero-kosmicznym (NASA)