Nadeszła pora na cykl publikacji: „Historia pewnej refaktoryzacji”. Część 10.
Do tej pory udało nam się zrefaktoryzować dwie metody sprowadzając je do jednej i koniec końców uczynić składową dedykowanej klasy. Pozostały jeszcze dwie: odczytująca pliki tekstowe, w których wartości mają ściśle określone położenie w linii oraz odczytująca pliki binarne o dedykowanym formacie.
Przyjrzyjmy się jeszcze raz metodzie unifikującej odczyt z plików rozdzielanych dowolnym separatorem:
public void Read() { string line; string[] values; int completed = 0; progressNotifier.Completed = completed; using (StreamReader reader = new StreamReader(fileName, Encoding.Default)) { while (!reader.EndOfStream) { line = reader.ReadLine(); values = line.Split(separator); transfer(values); completed += line.Length + EndOfLineSize; progressNotifier.Completed = completed; } } }
Choć realizuje oczekiwane od niej zadanie, to nie jest specjalnie czytelna. Trudno na pierwszy rzut oka stwierdzić, jakie czynności wykonuje. A skoro tak, to trudno będzie ją porównywać z pozostałymi dwiema, które również – przyznajmy – nie są czytelne (są mniej czytelne od powyższej). Nie ulega wątpliwości – dobrze byłoby jasno sprecyzować, co tak naprawdę, po kolei, robi metoda Read(). Zróbmy więc to – tak samo jak zrobiliśmy poprzednim razem, kiedy utworzyliśmy plan działania metody ImportCSV(). Metoda Read() wykonuje kolejno następujące czynności:
- deklaracja zmiennych
- wystartowanie prezentacji postępu
- otwarcie pliku
- pętla przetwarzająca, w ramach której następuje:
- odczytanie linii
- jej interpretacja
- zaktualizowanie postępu
Po utworzeniu planu spójrzmy jeszcze raz na kod i porównajmy go z planem, aby upewnić się, że niczego nie pominęliśmy. Wygląda na to, że nie, ale coś w przestawionym kodzie wzbudza niepokój. No tak, jeśli chodzi o wystartowanie prezentacji postępu, to owszem postęp jest ustalany na zero, ale nie wiadomo, jaka jest jego oczekiwana wartość końcowa. Przed linią:
progressNotifier.Completed = completed;
powinno bowiem znajdować się ustalenie oczekiwanej wartości końcowej postępu:
FileInfo fileInfo = new FileInfo(fileName); progressNotifier.Expected = (int)fileInfo.Length;
Czy ten błąd byłby propagowany do aplikacji przekazywanej użytkownikom? Nie, jeżeli dla poprawności prezentacji postępu, także zostałby utworzony test. Do tej pory testowaliśmy jedynie poprawność importu pliku i nie uwzględnialiśmy innych, związanych z tym procesem, czynności. Trzeba to będzie naprawić podczas kolejnego pisania testów.
Wróćmy jednak do meritum, jak można powyższy plan zapisać w kodzie? Na przykład tak:
public void Read() { Open(); InitializeProgress(); while (!EndOf) { InternalRead(); Interpret(); Transfer(); RefreshProgress(); } }
Teraz wygląda to już o wiele lepiej, wszystko widać jak na dłoni, wreszcie wiadomo co metoda robi. Od razu też widać, że nazwa metody nie bardzo pasuje do zakresu powierzonych jej zadań. Ona nie czyta jedynie pliku, ona go czyta między innymi. Oprócz tego jeszcze: interpretuje i przekazuje to, co zinterpretowała. Czyli ona tak naprawdę plik przetwarza. Trzeba więc poprawić nazwę metody, aby nie wprowadzała w błąd. Tu od razu zwrócę uwagę, że to co teraz robimy, to też refaktoryzacja, która prowadzi do uzyskania samo opisującego się kodu.
public void Process() { Open(); InitializeProgress(); while (!EndOf) { Read(); Interpret(); Transfer(); RefreshProgress(); } }
Po tych poprawkach spójrzmy, czy w metodzie ImportFixed() można wyodrębnić fragmenty zgodne z powyższym scenariuszem.
// Open() using (StreamReader reader = new StreamReader(openFileDialog.FileName, Encoding.Default)) // InitializeProgress(); progressBar.Maximum = (int)fileInfo.Length; progressBar.Value = 0; // while (!EndOf) while (!reader.EndOfStream) // Read(); line = reader.ReadLine(); // Interpret(); line.Substring(0, 20).Trim() line.Substring(20, 6).Trim() // Transfer(); xml += ... // RefreshProgress(); progressBar.Value = progressBar.Value + line.Length + 2
Czy analogicznie uda się wyodrębnić fragmenty w metodzie ImportBinary()? Zobaczmy:
// Open() using (FileStream reader = new FileStream(openFileDialog.FileName, FileMode.Open, FileAccess.Read)) // InitializeProgress(); progressBar.Maximum = (int)reader.Length; progressBar.Value = 0; // while (!EndOf) while (reader.Read(buffer, 0, buffer.Length) > 0) // Read(); reader.Read(buffer, 0, buffer.Length) // Interpret(); encoding.GetString(buffer, 0, 20).Trim() v.ToString() // Transfer(); xml += ... // RefreshProgress(); progressBar.Value = progressBar.Value + buffer.Length;
Jak widać, powstały plan daje się zastosować do wszystkich metod. Co to znaczy? Nie mniej ni więcej to, że wszystkie metody działają wg pewnego planu – szablonu. Istnieje zaś wzorzec projektowy, który pasuje jak ulał do obsługi szablonów – jest nim wzorzec metody szablonowej. Funkcjonuje on tak, że w klasie bazowej tworzy się metodę realizującą ustalony z góry plan (szablon). Elementy tego planu rozbija się na metody, które w dużej mierze są metodami abstrakcyjnymi. W klasach potomnych te metody uzyskują konkretną implementację, niemniej sam proces realizowany przez wzorzec przebiega zawsze w ten sam sposób, wg z góry ustalonego schematu. Jedynie jego poszczególne kroki mogą się różnić implementacją.
Stwórzmy zatem klasę, która będzie realizować wzorzec metody szablonowej. Będzie ona skonstruowana analogicznie do klasy SeparatedFileReader, ale z uwzględnieniem refaktoryzacji jej metody Read() do metody Process(). Wprowadzimy też dodatkową kontrolę parametrów konstruktora – w tym celu potrzebna nam będzie klasa wyjątku FileOfValuesReaderException . Oto jak będzie wyglądać kompletny kod:
public delegate void Transfer(params string[] values); public class FileOfValuesReaderException : Exception { public FileOfValuesReaderException(string info) : base(info) { } } abstract public class FileOfValuesReader { #region konstruktory public FileOfValuesReader(string fileName, Transfer transfer, IProgressNotifier progressNotifier) { this.fileName = fileName; if (progressNotifier == null) throw new FileOfValuesReaderException("Nie sprecyzowano klasy powiadamiającej o postępie przetwarzania"); else this.progressNotifier = progressNotifier; if (transfer == null) throw new FileOfValuesReaderException("Nie sprecyzowano metody przekazującej dane"); else this.transfer = transfer; } #endregion #region właściwości abstract protected int Size { get; } abstract protected int Completed { get; } abstract protected bool EndOf { get; } abstract protected string[] Values { get; set; } #endregion #region metody abstract protected void Open(); abstract protected void Read(); abstract protected void Extract(); abstract protected void Close(); public void Process() { Open(); progressNotifier.Expected = Size; progressNotifier.Completed = 0; while (!EndOf) { Read(); Extract(); Transfer(); progressNotifier.Completed = Completed; } } private void Transfer() { transfer(Values); } #endregion #region pola protected string fileName; private IProgressNotifier progressNotifier; private Transfer transfer; #endregion }
Klasa zyskała nową nazwę, korespondującą z jej zakresem odpowiedzialności. W kolejnej części postaramy się zaimplementować wszystkie abstrakcyjne elementy tej klasy, tak aby importować za jej pomocą wszystkie wymagane formaty plików.