Nadeszła pora na cykl publikacji: „Historia pewnej refaktoryzacji”. Część 16.
W tym wpisie zaprezentuję dwie klasy dziedziczące po FileOfValuesReader i realizujące odczyt z plików tekstowych oraz binarnych. Obie klasy będą współpracować z odpowiadającymi im klasami wyodrębniającymi wartości z odczytanej zawartości. A ponieważ dopełnią one całości mechanizmu importu, to przygotuję także odpowiednie testy, które pozwolą upewnić się, że mechanizm importu nadal działa tak samo.
Jako pierwszą zaimplementuję klasę importującą dane tekstowe – TextFileOfValuesReader. Jak będzie wyglądał jej konstruktor? Bardzo podobnie, jak w klasie bazowej, z tym jednakże wyjątkiem, że obiekt wyodrębniający dane (parametr extractor) nie będzie przekazywany jako interfejs, ale jako obiekt konkretnej klasy. Jest to niezbędne ze względu na fakt, że klasa TextFileOfValuesReader musi przekazywać odczytaną porcję danych do interpretera, a interfejs go reprezentujący tego nie umożliwia. Tyle tytułem wstępu. Przedstawię teraz kod klasy, a następnie wyjaśnię pozostałe kwestie z nią związane.
public class TextFileOfValuesReader : FileOfValuesReader { #region konstruktory public TextFileOfValuesReader(string fileName, TextValuesExtractor extractor, Transfer transfer, IProgressNotifier progressNotifier) : base(fileName, extractor, transfer, progressNotifier) { this.extractor = extractor; } #endregion #region właściwości protected override int Size { get { return size; } } protected override int Readed { get { return readed; } } #endregion #region metody protected override void Open() { FileInfo fileInfo = new FileInfo(fileName); size = (int)fileInfo.Length; reader = new StreamReader(fileName, Encoding.Default); } protected override void Read() { readed = 0; if (reader.EndOfStream) return; string content = reader.ReadLine(); readed = content.Length + EndOfLineSize; extractor.Content = content; } protected override void Close() { reader.Close(); } #endregion #region pola private TextValuesExtractor extractor; private StreamReader reader; private int readed, size; #endregion #region stałe private const int EndOfLineSize = 2; #endregion }
Jak widać konstruktor oczekuje, że do wyodrębnienia danych użyty zostanie obiekt konkretnej klasy tj. TextValuesExtractor. Jest to klasa bazowa dla klas wyodrębniających wartości z danych typu tekstowego. Dzięki temu będzie można interpretować zarówno wartości rozdzielane separatorem, jak i te o ustalonej długości. Skoro do klasy przekazywany jest konkretny obiekt, to musi być zapamiętany w takiej samej postaci – stąd taki a nie inny typ pola extractor.
Metoda Open() otwiera – zgodnie z oczekiwaniem – plik. Przy okazji ustala jego rozmiar, ponieważ jest ku temu najlepszy moment i zapamiętuje go na potrzeby zwracania przez właściwość Size.
Metoda Read() odczytuje porcję danych i przekazuje ją do obiektu wyodrębniającego dane. Musi też zapamiętać jej rozmiar, więc zanim dokona odczytu, zakłada, że jest on zerowy, aby w przypadku osiągnięcia końca właśnie taki rozmiar raportować. Jeśli dane można odczytać, to jako rozmiar podawana jest ich długość + standardowy rozmiar znacznika końca linii.
Metoda Close() po prostu zamyka strumień używany do odczytu pliku.
Jak widać jedyna interakcja pomiędzy klasą odczytującą a wyodrębniającą wartości, to przekazanie porcji danych. Reszta działań (wyodrębnienie wartości i ich pobranie) – jak mieliśmy już okazję sięwcześniej przekonać – odbywa się w klasie FileOfValuesReader.
Implementacja pierwszej klasy za nami. Teraz czas na drugą – tutaj także konstruktor będzie spełniał te same założenia, jak w przypadku poprzednim. Klasa będzie wyglądać tak:
public class BinaryFileOfValuesReader : FileOfValuesReader { #region konstruktory public BinaryFileOfValuesReader(string fileName, BinaryValuesExtractor extractor, Transfer transfer, IProgressNotifier progressNotifier = null) : base(fileName, extractor, transfer, progressNotifier) { this.extractor = extractor; } #endregion #region właściwości protected override int Size { get { return size; } } protected override int Readed { get { return readed; } } #endregion #region metody protected override void Open() { reader = new FileStream(fileName, FileMode.Open, FileAccess.Read); size = (int)reader.Length; } protected override void Read() { byte[] content = new byte[20 + 4]; readed = reader.Read(content, 0, content.Length); extractor.Content = content; } protected override void Close() { reader.Close(); } #endregion #region pola private BinaryValuesExtractor extractor; private FileStream reader; private int readed, size; Encoding encoding = Encoding.Default; #endregion }
Jak widać, także tutaj konstruktor oczekuje, że do wyodrębnienia danych użyty zostanie obiekt konkretnej klasy tj. BinaryValuesExtractor. I tak jak poprzednio jest to klasa bazowa dla klas wyodrębniających wartości z danych typu binarnego. Analogicznie zdefiniowane są pozostałe elementy klasy. Co prawda implementacja metody Read() jest odrobinę inna, ale wynika to akurat ze specyfiki strumienia dla danych binarnych.
Być może część czytelników zastanawia się dlaczego nie zwracam ilości odczytanych danych jako wyniku metody Read() – analogicznie do metody strumienia. Wynika to ze stosowania się do reguły oddzielania poleceń od zapytań.
Skoro mamy już komplet klas, pora przystąpić do testów – najwyższa na nie pora – trzeba się upewnić, że wszystko zostało prawidłowo zaprojektowane i zaimplementowane. I tym razem da się wykorzystać fragmenty kodu z poprzednich testów nieznacznie je zmieniając i dodając nowe. Oto jak będzie wyglądać klasa testująca:
[TestClass] public class ValuesReaderTests { #region metody testowe #region testy importów [TestMethod] public void test_Comma_Separated_File_Import() { AnyImport( new TextFileOfValuesReader( Location("csv.csv"), new SeparatedTextValuesExtractor(','), StoreContent, new ProgressMock() ) ); } [TestMethod] public void test_Tab_Separated_File_Import() { AnyImport( new TextFileOfValuesReader( Location("txt.txt"), new SeparatedTextValuesExtractor('\t'), StoreContent, new ProgressMock() ) ); } [TestMethod] public void test_Semicolon_Separated_File_Import() { AnyImport( new TextFileOfValuesReader( Location("ssv.ssv"), new SeparatedTextValuesExtractor(';'), StoreContent, new ProgressMock() ) ); } [TestMethod] public void test_Fixed_File_Import() { AnyImport( new TextFileOfValuesReader( Location("fix.fix"), new FixedTextValuesExtractor(20, 6), StoreContent, new ProgressMock() ) ); } [TestMethod] public void test_Binary_File_Import() { AnyImport( new BinaryFileOfValuesReader( Location("dmp.dmp"), new String20AndIntValuesExtractor(), StoreContent, new ProgressMock() ) ); } #endregion // tu będą jeszcze inne testy #endregion #region metody private void AnyImport(FileOfValuesReader reader) { actual = ""; reader.Process(); Assert.AreEqual(expected, actual); } public void StoreContent(AnyValue[] values) { actual += values[0].ToString() + values[1].ToString(); } private string Location(string fileName) { return Folder + fileName; } #endregion #region pola private string actual; #endregion #region stałe const string expected = "jeden1dwa2trzy3cztery4pięć5"; const string Folder = @"..\..\..\"; #endregion }
Zasada testowania jest nadal ta sama. Należy pobrać plik i sprawdzić czy wartości sklejone w jeden ciąg będą zgodne z oczekiwanymi. W następnej części dodamy do testów jeszcze kilka – zbliżając się dzięki nim do realnych testów jednostkowych.