Nadeszła pora na cykl publikacji: „Historia pewnej refaktoryzacji”. Część 11.
W poprzedniej części cyklu utworzyliśmy klasę realizującą wzorzec metody szablonowej. Obecnie będziemy tworzyć klasy potomne, implementujące konkretne typy importu. Warto zauważyć, że w toku dotychczasowych rozważań – czego właściwie nie podkreśliłem – zarysował się ciekawy schemat. Wszystkie pliki tekstowe (o wartościach rozdzielonych separatorem czy o ustalonej długości) są obsługiwane tak samo – inaczej są jedynie interpretowane ich zawartości. Można skorzystać z tej obserwacji i wykorzystać ją do utworzenia klasy unifikującej obsługę plików tekstowych. Jedynym zadaniem, którego nie będzie ona realizować, to interpretacja zawartości takich plików, co pozostanie w gestii klas pochodnych. Oto jak będzie wyglądać taka klasa:
abstract public class TextFileOfValuesReader : FileOfValuesReader { #region konstruktory public TextFileOfValuesReader(string fileName, Transfer transfer, IProgressNotifier progressNotifier = null) : base(fileName, transfer, progressNotifier) { } #endregion #region właściwości protected string Line { get; set; } protected override int Size { get { return size; } } protected override int Completed { get { return completed; } } protected override bool EndOf { get { return reader.EndOfStream; } } protected override string[] Values { get; set; } #endregion #region metody protected override void Open() { FileInfo fileInfo = new StreamReader(fileName, Encoding.Default); size = (int)fileInfo.Length; reader = new StreamReader(fileName); completed = 0; } protected override void Read() { Line = reader.ReadLine(); completed += Line.Length + EndOfLineSize; } protected override void Close() { reader.Close(); } #endregion #region pola private StreamReader reader; private int completed, size; #endregion #region stałe private const int EndOfLineSize = 2; #endregion }
Klasa ta oczywiście dziedziczy po utworzonej w poprzedniej części klasie bazowej wzorca metody szablonowej. Mając powyższą klasę można zaimplementować klasy pochodne odpowiedzialne jedynie za interpretację danych. Zauważcie, jak ładnie klasy zaczynają zyskiwać odpowiedzialność jedynie za jedno zadanie – tym samym realizując zasadę pojedynczej odpowiedzialności.
Jako pierwszą zaimplementujemy klasę interpretującą wartości oddzielanie separatorem:
public class FileOfSeparatedValuesReader : TextFileOfValuesReader { #region konstruktory public FileOfSeparatedValuesReader(string fileName, char separator, Transfer transfer, IProgressNotifier progressNotifier = null) : base(fileName, transfer, progressNotifier) { this.separator = separator; } #endregion #region metody protected override void Extract() { Values = Line.Split(separator); } #endregion #region pola private char separator; #endregion }
Jak widać klasa jest bardzo prosta i czytelna.
Pora na klasę interpretującą wartości o ustalonej długości:
public class FileOfFixedValuesReader : TextFileOfValuesReader { #region konstruktory public FileOfFixedValuesReader(string fileName, Transfer transfer, IProgressNotifier progressNotifier = null) : base(fileName, transfer, progressNotifier) { } #endregion #region metody 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(); } #endregion }
I w tym przypadku klasa jest prosta i czytelna. Wszystkie powyższe klasy realizują import plików tekstowych. Pora zatem na klasę realizującą import pliku binarnego. W tym przypadku nie będziemy rozdzielać jej na klasy odpowiedzialne za odczyt i interpretację, albowiem odczytywany jest obecnie jeden format pliku binarnego.
public class BinaryFileOfValuesReader : FileOfValuesReader { #region konstruktory public BinaryFileOfValuesReader(string fileName, Transfer transfer, IProgressNotifier progressNotifier = null) : base(fileName, transfer, progressNotifier) { } #endregion #region właściwości protected override int Size { get { return size; } } protected override int Completed { get { return completed; } } protected override bool EndOf { get { return endOf; } } protected override string[] Values { get; set; } #endregion #region metody protected override void Open() { reader = new FileStream(fileName, FileMode.Open, FileAccess.Read); size = (int)reader.Length; completed = 0; } protected override void Read() { buffer = new byte[20 + 4]; endOf = (reader.Read(buffer, 0, buffer.Length) == 0); completed += buffer.Length; } 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(); } protected override void Close() { reader.Close(); } #endregion #region pola private FileStream reader; private int completed, size; private byte[] buffer; bool endOf; Encoding encoding = Encoding.Default; #endregion }
Analizując powstałe klasy można zauważyć, że właściwość Values jest używana przez wszystkie z nich. Nie powinna być zatem abstrakcyjną na poziomie klasy FileOfValuesReader, tylko konkretną. Usuńmy zatem słowo kluczowe abstract z jej deklaracji i wszystkie jej deklaracje z klas potomnych. Na tym przykładzie widać, że trudno od razu właściwie zdefiniować klasę bazową i że ulega ona zmianom. Należy być na to przygotowanym i traktować to jako naturalny bieg rzeczy. Nie powinno się w żadnym przypadku obstawać przy postaci pierwotnej, jeżeli nie ma ona sensu.
Jak zwykle po napisaniu nowego kodu należy go przetestować. To zadanie na kolejną część cyklu. Chciałbym też na koniec tego wpisu zachęcić czytelników do przeanalizowania, dlaczego powyższe klasy nie są jeszcze idealnym rozwiązaniem problemu importu plików i dlaczego. Swoje wnioski można umieścić w komentarzach, pamiętając o tym, że komentarze z nieistniejącym adresem e-poczty są odrzucane (odrzucane są też takie adresy, które należą do „udawanych” serwerów pocztowych – tj. pozwalających wysyłać e-korespondencję z ulotnych e-adresów).