Nadeszła pora na cykl publikacji: „Historia pewnej refaktoryzacji”. Część 7.
Zgodnie z zapowiedzią z poprzedniej części, pora przystąpić do definiowania stosownych interfejsów. Zaczniemy od interfejsu wyboru pliku – zgodnie z planem, który ukształtował się w trakcie dotychczasowych rozważań. Jak powinien wyglądać taki interfejs? Jak wiadomo interfejsy definiują pewien zakres funkcjonalności, specyfikują: potrafię robić takie, a nie inne rzeczy – to są moje umiejętności. Jakie powinny być zatem umiejętności interfejsu wyboru pliku? Przyjrzyjmy się dowolnej z czterech metod i zobaczmy, jak używany jest obiekt klasy OpenFileDialog:
openFileDialog.Filter = "Wartości separowane tabulacją (*.txt)|*.txt"; if (openFileDialog.ShowDialog() != DialogResult.OK) return; FileInfo fileInfo = new FileInfo(openFileDialog.FileName);
Mamy tu kolejno następujące umiejętności:
- uwzględnianie filtru plików
- wybór pliku
- informacja o dokonaniu wyboru (kod lekko przeredagowano, aby przystawał do umiejętności)
- udostępnienie nazwy pliku
openFileDialog.Filter = "Wartości separowane tabulacją (*.txt)|*.txt";
openFileDialog.ShowDialog()
openFileDialog.ShowDialog() == DialogResult.OK
openFileDialog.FileName
Na podstawie tej listy możemy teraz zdefiniować interfejs:
public interface IFileSelector { void Select(); // 2 bool Selected { get; } // 3 string FileName { get; } // 4 string Filter { get; set; } // 1 }
W komentarzach podano numery funkcjonalności z powyższej listy, jakim odpowiada dany składnik interfejsu.
Czy ten interfejs jest wystarczający – właściwie tak, wystarczy on bowiem do zastąpienia klasy OpenFileDialog. Czy jest jednak zadowalający? Wg mnie nie. Niestety jest skażony pierwotną funkcjonalnością, na podstawie której powstał. Powiela rozwiązanie rodem z OpenFileDialog, gdzie treść filtra definiowała zarówno jego opis, jak i wyrażenie filtrujące. To przeczy zasadzie pojedynczej odpowiedzialności. Lepiej gdyby było tak:
public interface IFileSelector { void Select(); // 2 bool Selected { get; } // 3 string FileName { get; } // 4 string Filter { get; set; } // 1 - wyrażenie filtrujące string FilterDescription { get; set; } // 1 - opis filtra }
No dobrze, ale przecież filtr w OpenFileDialog mógł zawierać nie tylko jedną parę: filtr-opis, ale kilka takich par. Co z nimi teraz? Hm, czy potrzebna jest nam aż taka funkcjonalność? Nie – metody korzystają z jednego filtra. Czy może w przyszłości pojawić się taki import, który będzie potrzebował więcej niż jednego filtra? Całkiem prawdopodobne. Czy zatem nie powinniśmy – wiedząc to – dokonać kolejnej modyfikacji interfejsu, tak aby Filter i FilterDescription były tablicami? Nie – możemy zrobić coś o wiele lepszego – wykorzystać kolejną z reguł SOLID – regułę Otwarte-Zamknięte (Open-closed principle). Mówi ona:
Składniki oprogramowania powinny być otwarte na rozbudowę, ale zamknięte dla modyfikacji.
Skoro zdajemy sobie sprawę, że interfejs może ulec zmianie musimy doprowadzić go do takiej postaci, aby nie była potrzebna jego modyfikacja, ale był możliwy rozwój. Jak to zrobić? Trzeba odpowiedzieć na pytanie co się może zmienić? Zmienić może się sposób definiowana filtra. Nic nie wskazuje jednak na to, by zmienić miał się sposób wyboru, pobierania pliku i stwierdzania, że wyboru dokonano. Zatem rozdzielmy te dwa obszary, oddzielmy to co zmienne, od tego co stałe:
public interface IFileSelector { void Select(); bool Selected { get; } string FileName { get; } } public interface IFilteredFileSelector : IFileSelector { string Filter { get; set; } string FilterDescription { get; set; } }
W ten sposób powstały dwa interfejsy, z których drugi jest rozszerzeniem pierwszego. Czemu nie rozdzieliliśmy ich całkowicie, a uzależniliśmy zmienny od niezmiennego. Bo ich przeznaczeniem jest współpracować oraz dlatego, że obecnie nie ma uzasadnienia, by były rozdzielone. Bez wątpienia mamy jednak kod zamknięty na modyfikację (nikt już nie zmieni tych interfejsów z powodu, który przyszedł nam do głowy) i otwarty na rozbudowę, bo wystarczy w przyszłości zdefiniować chociażby następujący interfejs, aby obsłużyć wiele filtrów:
public interface IMultiFilteredFileSelector : IFileSelector { string[] Filter { get; set; } string[] FilterDescription { get; set; } }
Z takiego rozdzielenia interfesju płyną też dodatkowe korzyści. Do metod korzystających z wyboru pliku wystarczy przekazać jedynie prosty IFileSelector. Nie potrzebują one nic więcej, nie interesuje ich czy wybór pliku oparto na filtrach czy na średniej opadów w bieżącym roku. Dostają po prostu obiekt odpowiednio skonfigurowany, implementujący prosty interfejs i pozostaje im jedynie go użyć. Taki prosty interfejs łatwiej będzie też testować.
Skoro uporaliśmy się z interfejsem wyboru plików (muszę przyznać, że wyjątkowo mi się podoba :)), pora zdefiniować także drugi interfejs – prezentujący postęp importu. Czy to będzie trudne zadanie. Właściwie nie – w pewnym sensie już go zdefiniowaliśmy przy okazji tworzenia na potrzeby testów atrapy klasy ProgressBar. Dla przypomnienia wyglądała tak:
public class ProgressBar { public int Value { get; set; } public int Maximum { get; set; } }
Interfejs właściwie mógłby wyglądać dokładnie tak samo, mimo to nazwiemy i jego, i jego składowe inaczej:
public interface IProgressNotifier { int Expected { get; set; } int Completed { set; } }
Pierwsza właściwość pozwala ustalić, jaka jest oczekiwana wartość końcowa postępu, druga określa aktualną wartość tegoż postępu. Jest to wystarczające, aby informować o postępie – a skoro informować, to właśnie taka, a nie inna nazwa interfejsu.
Zrealizowaliśmy trzy punkty z naszej listy, ale pominęliśmy jeden – odczyt pliku. Będzie to zatem zadanie, które zrealizujemy w kolejnej część cyklu.