Nadeszła pora na cykl publikacji: „Historia pewnej refaktoryzacji”. Część 1.
Dawno, dawno temu, kiedy graficzne środowisko Windows zaczęło pretendować do miana systemu, powstały także środowiska dla programistów realizujące ideę RAD, czyli Rapid Application Development, co w tłumaczeniu na polski oznaczało szybkie tworzenie aplikacji. W Windows – początkowo – istniały dwa takie środowiska: Visual Basic i Delphi. Po jakimś czasie pojawiły się kolejne. Ale nie to było istotne, a fakt, że nieświadomie narodził się także nowy styl programowania, który nigdy oficjalnie nie zyskał nazwy, ale który można było określić jako programowanie „raptowne”. Rzesze programistów dostawszy do ręki RAD zaczęły – jak w amoku – wrzucać na formy elementy interfejsu i programować ich zdarzenia. Po pewnych czasie część z nich wyrwała się z tego owczego pędu i zaczęła dostrzegać, że nie tędy droga, pozostali jednak nie potrafili. Płynęły lata, zmieniały się środowiska RAD, metodyki, a oni dalej turkotali w koleinach swoich przyzwyczajeń, zostawiając po sobie kod. Kod który w pewnym momencie nie był już do ogarnięcia, a jednak ogarnięty być musiał. Oto historia takiego kodu i próba jego ogarnięcia.
Przyjmijmy, że w pewnej firmie, pewien decydent zlecił pewnemu programiście wykonanie pewnego importu (miał być to import ze wskazanego przez użytkownika pliku CSV, z prezentacją postępu importu). To czego jeszcze można być pewnym jest to, że programista dodał pozycję do menu programu, tam gdzie wydawało się to właściwe, a następnie dwuklikiem przeniósł się do kodu zdarzenia, jakie owa pozycja menu wywoływała. I napisał taki oto kod:
private void CsvImport_Click(object sender, EventArgs e) { openFileDialog.Filter = "Wartości separowane przecinkiem (*.csv)|*.csv"; if (openFileDialog.ShowDialog() != DialogResult.OK) return; FileInfo fileInfo = new FileInfo(openFileDialog.FileName); progressBar.Maximum = (int)fileInfo.Length; string xml = ""; using (StreamReader reader = new StreamReader(openFileDialog.FileName, Encoding.Default)) { string line; int count = 0; string[] values; progressBar.Value = 0; while (!reader.EndOfStream) { line = reader.ReadLine(); count++; values = line.Split(','); if (values.Length == 2) xml += "<r s=\"" + values[0] + "\" q=\"" + values[1] + "\"/>"; progressBar.Value = progressBar.Value + line.Length + 2/*znak powrotu do początku linii + znak przejścia do nowej linii*/; } } if (xml.Length == 0) { MessageBox.Show("Nie udało się przetworzyć pliku!"); return; } xml = "<data>" + xml + "</data>"; Store(xml); }
Aplikacja z takim importem trafiła do klienta, który był niezmiernie zadowolony z tej funkcjonalności. Dzięki zakupionej aplikacji mógł on wydajniej zarządzać swoim biznesem, więc i biznes zaczął się rozrastać. I tu okazało się, że posiadany import jest dla klienta niewystarczający, albowiem jego potencjalni, nowi kontrahenci używają innego formatu i aplikacja musi się do niego dostosować (były to pliki rozdzielane tabulacją). Zgłosił się więc do firmy, która program napisała i zlecił jej taki sam import, ale w innym formacie. Firma zlecenie przyjęła, a ponieważ miała w produkcji zupełnie inną aplikację, zapadła decyzja, aby rzeczony import wykonać jak najmniejszym nakładem. Zwrócono się z tym do programisty, który był autorem poprzedniego importu, informując o potrzebie pośpiechu. Programista zrobił to od ręki korzystając ze sprawdzonej metody tzw. kopypasteryzmu, tj. skopiował istniejący kod, wkleił go do zdarzenia nowej pozycji menu i dokonał stosownych modyfikacji.
private void TabImport_Click(object sender, EventArgs e) { openFileDialog.Filter = "Wartości separowane tabulacją (*.txt)|*.txt"; if (openFileDialog.ShowDialog() != DialogResult.OK) return; FileInfo fileInfo = new FileInfo(openFileDialog.FileName); progressBar.Maximum = (int)fileInfo.Length; string xml = ""; using (StreamReader reader = new StreamReader(openFileDialog.FileName, Encoding.Default)) { string line; string[] values; int count = 0; progressBar.Value = 0; while (!reader.EndOfStream) { line = reader.ReadLine(); count++; values = line.Split('\t'); if (values.Length == 2) xml += "<r s=\"" + values[0] + "\" q=\"" + values[1] + "\"/>"; progressBar.Value = progressBar.Value + line.Length + 2/*znak powrotu do początku linii + znak przejścia do nowej linii*/; } } if (xml.Length == 0) { MessageBox.Show("Nie udało się przetworzyć pliku!"); return; } xml = "<data>" + xml + "</data>"; Store(xml); }
Odbiorca aplikacji był wniebowzięty, jego biznes rozwijał się nad wyraz zadowalająco. Jak można się domyślić, po pewnym czasie pojawiła się potrzeba kolejnego importu (dla danych bez separatora, ale o ustalonej długości). I tym razem firma programistyczna nie robiła przeszkód i przyjęła zlecenie, choć autor poprzednich importów już u nich nie pracował (jego sława szybkiego programisty pozwoliła mu zmienić pracę na bardziej intratną). Zadanie wykonania nowego importu zlecono nowo zatrudnionemu programiście – to była jego szansa, aby się wykazał. Młody i ambitny człowiek dokonał analizy dotychczasowych modyfikacji programu w zakresie importu i ponieważ nie był w ciemię bity, w lot pojął na czym polega siła metody kopypasteryzmu (z tego zresztą powodu jest to tak niezwykle spopularyzowana metoda programowania). W niezmiernie krótkim czasie pojawił się więc kod obsługi kolejnego zdarzenia, a nowy programista okrył się blaskiem zasłużonej chwały
private void FixImport_Click(object sender, EventArgs e) { openFileDialog.Filter = "Wartości o stałej długości (*.fix)|*.fix"; if (openFileDialog.ShowDialog() != DialogResult.OK) return; FileInfo fileInfo = new FileInfo(openFileDialog.FileName); progressBar.Maximum = (int)fileInfo.Length; string xml = ""; int count = 0; count++; using (StreamReader reader = new StreamReader(openFileDialog.FileName, Encoding.Default)) { string line; progressBar.Value = 0; while (!reader.EndOfStream) { line = reader.ReadLine(); if (line.Length > 0) xml += "<r s=\"" + line.Substring(0, 20).Trim() + "\" q=\"" + line.Substring(20, 6).Trim() + "\"/>"; progressBar.Value = progressBar.Value + line.Length + 2/*znak powrotu do początku linii + znak przejścia do nowej linii*/; } } if (xml.Length == 0) { MessageBox.Show("Nie udało się przetworzyć pliku!"); return; } xml = "<data>" + xml + "</data>"; Store(xml); }
Także tym razem odbiorca aplikacji był zadowolony z dobrze wydanych pieniędzy. Wkrótce potem zlecił jeszcze jeden import w zupełnie nowym formacie (był to zapis binarny). Zajął się tym trzeci już programista, albowiem jego poprzednik zajmował się czymś innym. Po konsultacji pomiędzy programistami następca stał się kolejnym adeptem sztuki kopypasteryzmu, a kod trzech importów, został uzupełniony przez kolejną, czwartą obsługę zdarzenia.
private void BinImport_Click(object sender, EventArgs e) { openFileDialog.Filter = "Wartości z obrazu pamięci (*.dmp)|*.dmp"; if (openFileDialog.ShowDialog() != DialogResult.OK) return; Encoding encoding = Encoding.Default; string xml = ""; int count = 0; count++; using (FileStream reader = new FileStream(openFileDialog.FileName, FileMode.Open, FileAccess.Read)) { int v; progressBar.Value = 0; byte[] buffer = new byte[20 + 4]; progressBar.Maximum = (int)reader.Length; while (reader.Read(buffer, 0, buffer.Length) > 0) { v = BitConverter.ToInt32(buffer, 20); xml += "<r s=\"" + encoding.GetString(buffer, 0, 20).Trim() + "\" q=\"" + v.ToString() + "\"/>"; progressBar.Value = progressBar.Value + buffer.Length; } } if (xml.Length == 0) { MessageBox.Show("Nie udało się przetworzyć pliku!"); return; } xml = "<data>" + xml + "</data>"; Store(xml); }
Odbiorca aplikacji zadowolony ze współpracy z firmą programistyczną, widząc jej wielkie zrozumienie dla jego potrzeb postanowił zlecić jej kolejne zadanie. Chodziło o modyfikację importu w celu dostosowania do nowych potrzeb prowadzonego biznesu. Zmianie ulegał zakres importowanych wartości, klient dostarczył specyfikację zakresu i jako przykład dwa pliki w formacie CSV. Programista dostosował kod zdarzenia ImportCSV do nowego formatu i nowa wersja trafiła do odbiorcy. Tym razem jednak odbiorca nie był do końca zadowolony, albowiem nowy import działał tylko dla plików CSV. W firmie programistycznej okazało się nagle, że zmiany oczekiwane przez klienta są niezmiernie kosztowne, albowiem trzeba modyfikować aż cztery różne fragmenty kodu. Co więcej, zlecenie tego kilku programistom prowadziło do innej implementacji i częstokroć innych błędów. Odbiorca aplikacji nie mógł wyjść ze zdumienia, co się stało z jego ulubioną firmą programistyczną, która do tej pory tak wspaniale realizowała jego potrzeby. Także firma programistyczna pytała swoich programistów skąd nagle takie olbrzymie nakłady pracy, a efekt niezadowalający. Szybko okazało się, że użyte podczas rozwijania oprogramowania „metody” (tak, cudzysłów to właściwe oznaczenie dla tego pojęcia), choć zaważyły na jego początkowym sukcesie, obecnie stają się przyczyną porażki.
Czy z kodem, który do tej chwili powstał można cokolwiek zrobić? Czy może raczej napisać go od nowa? Ta decyzja zależy od kierownictwa wspomnianej firmy, ja pokażę, jak można ten kod zmodyfikować (lub – by być trendy 😉 – zrefaktoryzować), tak aby zachował on swoją funkcjonalność, a jednocześnie ustrzegł się błędów, jakie taka modyfikacja może wprowadzić i wyzbył się wszystkich wad, jakie kopypasteryzm na niego wywarł.
Ale to w kolejnej odsłonie tego cyklu :).