Nadeszła pora na cykl publikacji: „Historia pewnej refaktoryzacji”. Część 14.
W poprzedniej części uczyniliśmy spostrzeżenie, że z dotychczasowych klas można wyodrębnić niezależną funkcjonalność – interpretację odczytywanych danych. Obecnie zajmiemy się jej implementacją. Nie będzie ona specjalnie trudna, ponieważ większość kodu już istnieje – zawierają go metody Extract() klas potomnych klasy FileOfValuesReader. Przypomnijmy je sobie wszystkie.
Dla klasy FileOfSeparatedValuesReader:
protected override void Extract() { Values = Line.Split(separator); }
Dla klasy FileOfFixedValuesReader:
protected override void Extract() { List<string> v = new List<string>(); if (Line.Length > 19) v.Add(Line.Substring(0, 20).Trim()); if (Line.Length > 19 + 6) v.Add(Line.Substring(20, 6).Trim()); Values = v.ToArray(); }
Dla klasy BinaryFileOfValuesReader:
protected override void Extract() { List<string> v = new List<string>(); v.Add(encoding.GetString(buffer, 0, 20).Trim()); int x = BitConverter.ToInt32(buffer, 20); v.Add(v.ToString()); Values = v.ToArray(); }
Zaskoczenie
W chwili gdy umieszczałem kod powyższych metod w treści wpisu, zdziwiło mnie, jak to może działać:
v.Add(v.ToString());
jest to przecież kompletnie bez sensu, dodaje do listy nazwę tej listy, prawidłowo powinno być tak:
v.Add(x.ToString());
Jak to w ogóle przeszło testy? Pierwszą rzeczą którą zrobiłem było oczywiście ich wykonanie – moje zaskoczenie przerodziło się w bezbrzeżne zdumienie – testy nie przeszły. Przez pół godziny starałem się dojść, co takiego uczyniłem, że testy poprzednio przeszły pozytywnie. Jedyne co przyszło mi do głowy, to użycie w teście zupełnie innej klasy niż BinaryFileOfValuesReader. Ale to jest jeszcze bardziej niewiarygodne niż to, że testy przeszły (a to że przeszły jest pewne). Cóż pozostanie to zagadką.
Należy jednak wyciągnąć z tego zdarzenia wniosek – nigdy nie używaj jednoliterowych nazw zmiennych, nawet w tak – wydawałoby się – krótkiej metodzie, może to prowadzić do błędów, które gdybym użył nazw opisowych nie miałyby prawa wystąpić – niezaprzeczalnie kod tej metody nie jest samokomentujący się. Trzeba to będzie bezwzględnie poprawić.
Powrót do refaktoryzacji
Czy pomimo tego, że namieszałem, możemy uzyskać jakąś korzyść z tego niepoprawnego kodu? Owszem. W przypadku dwóch pierwszych (poprawnych) metod widać, że otrzymujemy tekst i tekst przekazujemy. Ale w przypadku odczytu z pliku binarnego otrzymujemy tekst i liczbę całkowitą, a przekazujemy je obydwie jako tekst. Zdecydowanie lepiej byłoby gdyby dane przekazywać w zgodzie z ich typem, zamiast konwertować je do tekstu. Ale jak to zrealizować? Nie bardzo mam w tej chwili pomysł, ale to nie jest problem. Mam potencjalne źródło zmienności – trzeba będzie kiedyś odpowiednio zaimplementować listę przekazywanych wartości. Co mogę zrobić obecnie? Skoro mam zmienność, to ją zhermetyzuję. Przyjmę, że uzyskane wartości będą przekazywane jako obiekty dedykowanej do tego celu klasy. Obecnie zaimplementuję ją tak, że będzie przechowywać – tak jak było do tej pory – przekazaną wartość (jakiego typu by nie była) jako tekst.
Skoro wiem, że obecnie mam do czynienia jedynie z tekstami i liczbami całkowitymi, oto jak będzie wyglądała ta klasa:
public class AnyValue { #region konstruktory public AnyValue(string value) { this.value = value; } public AnyValue(int value) { this.value = value.ToString(); } #endregion #region metody public override string ToString() { return value; } #endregion #region pola private string value; #endregion }
Przygotowałem dwa konstruktory: jeden do wartości tekstowych, a drugi dla liczbowych. Dzięki temu będę mógł przekazywać wartości w ich natywnej postaci i dopiero powyższa klasa dokona ich transformacji. W przyszłości mogę zrezygnować z owej transformacji na rzecz dowolnego innego rozwiązania. Zaimplementowałem też standardową metodę konwersji do tekstu, aby nadal możliwe było uzyskanie wartości tekstowych przez metodę odpowiedzialną za transfer.
Wyposażeni w klasę do przechowywania pojedynczej wartości możemy przystąpić do zdefiniowania interfejsu, jaki będzie wykorzystywała klasa odczytująca plik, aby otrzymać listę wartości.
public interface IValuesExtractor { void Process(); AnyValue[] Values { get; } }
Dziwić może, że interfejs realizuje jedynie przetworzenie danych i udostępnienie wartości, nie ma jednak żadnego mechanizmu przekazania porcji danych. Wynika to z tej prostej przyczyny, że owa porcja danych nie jest zunifikowana – zależy od rodzaju pliku (w tekstowym to linia tekstu, w binarnym – tablica bajtów). Sposób przekazania danych będzie indywidualną kwestią danej klasy wyodrębniającej wartości.
Przygotujemy teraz – na podstawie powyższego interfejsu – abstrakcyjną klasę, która będzie bazową klasą dla wszystkich klas wyodrębniającej wartości.
public abstract class ValuesExtractor : IValuesExtractor { #region właściwości public AnyValue[] Values { get { return values; } } #endregion #region metody public abstract void Process(); #endregion #region pola protected AnyValue[] values; #endregion }
W stosunku do interfejsu, doszło tylko pole z listą wartości. Jest to pole protected, zatem klasy potomne będą mogły używać go do umieszczania wyodrębnionych wartości, które następnie – za pomocą interfejsu – klasa odczytująca plik przekaże dalej.
Co dalej? Jak wspomniałem odrobinę wcześniej, interpretowane będą dane tekstowe i binarne. Zatem na pewno potrzebne są dwie odpowiadające tym formatom klasy. Dodatkowo – ze względu na sposób interpretacji danych tekstowych (separator lub ustalona długość) będziemy mieli dwie dedykowane tym mechanizmom klasy. Czy będą one miały jakąś wspólną cechę? Tak – obie będą korzystać z porcji tekstu. Warto więc zdefiniować dla nich wspólną klasę bazową, o bardzo prostej strukturze:
public abstract class TextValuesExtractor : ValuesExtractor { #region właściwości public string Content { protected get; set; } #endregion }
Teraz możemy wykorzystać ją do zdefiniowania konkretnych klas wyodrębniających wartości.
Dla wartości oddzielanych dowolnym separatorem:
public class SeparatedTextValuesExtractor : TextValuesExtractor { #region konstruktory public SeparatedTextValuesExtractor(char separator) { this.separator = separator; } #endregion #region metody public override void Process() { List<AnyValue> items = new List<AnyValue>(); string[] values = Content.Split(separator); foreach (string value in values) items.Add(new AnyValue(value)); this.values = items.ToArray(); } #endregion #region pola private char separator; #endregion }
Dla wartości o ustalonej długości:
public class FixedTextValuesExtractor : TextValuesExtractor { #region konstruktory public FixedTextValuesExtractor(params int[] lenghts) { this.lenghts = lenghts; } #endregion #region metody public override void Process() { List<AnyValue> items = new List<AnyValue>(); string content = Content; int offset = 0, left = content.Length; foreach (int lenght in lenghts) { left -= lenght; if (left > 0) items.Add(new AnyValue(content.Substring(offset, Math.Min(lenght, left)).Trim())); offset += lenght; } values = items.ToArray(); } #endregion #region pola private int[] lenghts; #endregion
Zauważmy, że obie klasy zyskały przy okazji większą uniwersalność. Są w stanie interpretować więcej niż dwie wartości w tekście. Klasa SeparatedTextValuesExtractor rozdziela wartości uwzględniając wszystkie separatory w tekście, zaś klasa FixedTextValuesExtractor pobiera w konstruktorze listę długości kolejnych wartości w tekście i na jej podstawie je rozdziela.
Pozostało zaimplementować jeszcze klasę interpretującą binaria:
public abstract class BinaryValuesExtractor : ValuesExtractor { #region właściwości public byte[] Content { protected get; set; } #endregion } public class String20AndIntValuesExtractor : BinaryValuesExtractor { #region metody public override void Process() { List<AnyValue> items = new List<AnyValue>(); items.Add(new AnyValue(encoding.GetString(Content, 0, 20).Trim())); int value = BitConverter.ToInt32(Content, 20); items.Add(new AnyValue(value.ToString())); values = items.ToArray(); } #endregion #region pola Encoding encoding = Encoding.Default; #endregion }
Ta klasa (String20AndIntValuesExtractor) nie jest uniwersalna – jest dedykowana wyłącznie takiemu, a nie innemu formatowi pliku binarnego. Ale ona także posiada swoją klasę bazową (BinaryValuesExtractor), na wypadek gdyby pojawiły się inne formaty binarne. Wówczas – tak jak w przypadku danych tekstowych – wspólna dla tych klas będzie binarna zawartość.
W kolejnej części cyklu połączymy powyższe klasy z tymi z poprzedniej części, tak by utworzyły pełen mechanizm importu.