Nadeszła pora na cykl publikacji: „Historia pewnej refaktoryzacji”. Część 2.
Co należy w pierwszej kolejności zrobić z kodem opublikowanym w pierwszej części? Skoro ma on ulegać zmianom, dobrze by było, aby nie odbywały się one w dotychczasowym pliku przechowującym kod, ale w pliku dedykowanym tylko tym zmianom. Należy zatem przenieść kod, który będzie modyfikowany do innego pliku. Myli się jednak ten, kto myśli, że wykorzystana zostanie do tego możliwość dzielenia klas na części (klauzula partial class) – skoro bowiem kod jest rozdzielany, należy zrobić to w pełni tj. uniezależnić go, jak tylko się to da w obecnej chwili, od kodu, który w starym pliku pozostanie. Najprostszym rozwiązaniem jest zatem wyodrębnienie kodu czterech zdarzeń do oddzielnej klasy jako jej metod. Tym samym powstanie klasa implementująca wzorzec projektowy Obiektu Metody (Method Object). Wzorzec ten w ogólności polega na zamianie rozbudowanej metody w obiekt, dzięki czemu można m.in. ustalić jej parametry za pomocą pól obiektu i uprościć samo wywoływanie metody. W tym przypadku będą to aż cztery metody, zaś polami obiektu staną się referencję do dwóch wykorzystywanych przez nie obiektów (openFileDialog i progressBar) oraz delegat do używanej przez nie metody Store() odpowiedzialnej za utrwalenie zaimportowanych plików. Podstawienie wartości pól będzie się odbywało w konstruktorze klasy – zostaną one przekazane w jego parametrach.
Tutaj wypada uzupełnić, że takie zastosowanie konstruktora realizuje z kolei wzorzec Wstrzykiwania Zależności (Dependency Injection). Klasa Obiektu Metody nie zależy bowiem bezpośrednio od instancji konkretnych obiektów, ale są jej one wstrzykiwane poprzez konstruktor (czyli trafiają do wnętrza klasy, do pól prywatnych, przekazane w parametrach konstruktora i mogą być dowolnymi instancjami – a nie jak dotychczas instancjami istniejącymi wyłącznie w klasie, gdzie metody były umieszczone pierwotnie).
Jak zatem będzie wyglądała klasa implementująca wzorzec Obiektu Metody? Oto jej kod:
public delegate void Store(string xml); public class MethodObject { #region konstruktory public MethodObject(ProgressBar progressBar, OpenFileDialog openFileDialog, Store store) { this.progressBar = progressBar; this.openFileDialog = openFileDialog; this.store = store; } #endregion #region metody public void ImportCSV() { 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)) { string line; string[] values; progressBar.Value = 0; while (!reader.EndOfStream) { line = reader.ReadLine(); 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); } public void ImportTabSeparated() { 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)) { 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); } public void ImportFixed() { 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)) { 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); } public void ImportBinary() { 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); } #endregion #region pola private ProgressBar progressBar; private OpenFileDialog openFileDialog; private Store store; #endregion }
Warto zauważyć, że przy okazji oczyszczono wszystkie metody z dwóch linii niepotrzebnego kodu tj.:
//... string line; int count = 0; // <-- usunięto string[] values; //... line = reader.ReadLine(); count++; // <-- usunięto values = line.Split(','); //...
nie wpływały one bowiem na kod (nie wnosiły nic do niego), więc ich obecność była zbędna, a brak wpływu umożliwiał usunięcie, bez obawy o jakiekolwiek skutki uboczne ich działania. Usunięty kod był najbardziej pospolitym rezultatem kopypasteryzmu – dodanym przypadkowo i niepotrzebnie kodem, który powielił się wskutek kopiowania na wszystkie powstałe kopie.
Jak obecnie będzie wyglądał pierwotny plik, w którym jeszcze niedawno rezydowały przeniesione metody? Przyjmie on następującą postać:
public partial class UI : Form { #region konstruktory public UI() { InitializeComponent(); methodObject = new MethodObject(progressBar, openFileDialog, Store); } #endregion #region zdarzenia private void CsvImport_Click(object sender, EventArgs e) { methodObject.ImportCSV(); } private void TabImport_Click(object sender, EventArgs e) { methodObject.ImportTabSeparated(); } private void FixImport_Click(object sender, EventArgs e) { methodObject.ImportFixed(); } private void BinImport_Click(object sender, EventArgs e) { methodObject.ImportBinary(); } private void Store(string xml) { // na razie jej implementacja jest nieistotna } #endregion #region pola private MethodObject methodObject; #endregion }
Warto zwrócić uwagę na sygnalizowane przy okazji opisywania wzorca Obiektu Metody uproszczenie wywoływania metod zawartych w klasie implementującej ten wzorzec. Niezbędne do ich pracy parametry przekazano przy okazji konstruowania instancji klasy, więc same wywołania już ich nie potrzebują.
Skoro – zgodnie z planem – mamy już odseparowany kod, który będzie podlegał modyfikacji, pora zadbać, aby ta modyfikacja nie sprowadziła go na manowce. Nadszedł zatem czas na przygotowanie odpowiednich zabezpieczeń – czyli testów sprawdzających poprawność kodu. Ale o tym w kolejnym wpisie.