Tworząc oprogramowanie, bardzo często stoimy przed dylematem: czy pisać kod szybko, czy porządnie. Oczywiście, o ile tylko mamy możliwość, w

  Tworząc oprogramowanie, bardzo często stoimy przed dylematem: czy pisać kod szybko, czy porządnie. Oczywiście, o ile tylko mamy możliwość, wybierajmy tę drugą opcję, jednak taka decyzja generuje nowy problem, ponieważ na wczesnym etapie procesu wytwarzania oprogramowania trudno jest określić dokładny kierunek rozwoju. Dlatego też projektując oprogramowanie, nie musimy od razu rozplanowywać wszystkiego, ale powinniśmy zadbać o to, by tworzony przez nas kod był łatwo rozszerzalny zarówno dziś, jak i w przyszłości. Innymi słowy, warto sięgnąć po znane praktyki i dobre wzorce specjalistów, którzy przed podobnymi wyzwaniami stanęli już wcześniej.   W poprzednim poście zrobiłem krótki wstęp do mnemonika Solid oraz szerzej opisałem pierwszą z jego reguł, czyli Single Responsibility Principle. Dziś chcę zająć się zasadą Open/Closed, która w pewnym sensie narzuca kolejną technikę, którą powinniśmy uwzględnić we wspomnianym wcześniej procesie planowania.   Open/Closed Principle Zasada Open/Closed mówi o tym, że powinniśmy projektować oprogramowanie w taki sposób, by w razie konieczności jego rozwoju nie było potrzeby modyfikacji istniejącego kodu. Taki problem można rozwiązać na wiele sposobów, m.in. wykorzystując wzorzec projektowy Dekorator, jednak jest to tylko pewien środek zaradczy po wystąpieniu problemu. W praktyce powinniśmy działać od samego początku, tworząc oprogramowanie, którego kod wykorzystuje interfejsy we wszystkich (lub w zdecydowanej większości) klasach funkcjonalnych.   Być może zastanawialiście się czasem, czy interfejsy, które pozostawiamy w kodzie, nie są nadmiarowe, ponieważ w wielu przypadkach i tak tworzymy dla nich jedną implementację. Takie wątpliwości pojawiają się szczególnie w pierwszych latach programistycznej tułaczki, dlatego warto sobie uzmysłowić, że nasz kod ulega ciągłej zmianie. Jeśli zawczasu zadbamy o odpowiednie interfejsy przy wszelakich klasach funkcyjnych, to istnieje spora szansa, że w przyszłości unikniemy refaktoryzacji działającego i przetestowanego kodu — i przy okazji napiszemy kod zgodny z regułami sztuki :-) Poniżej mały przykład, który wykorzystuje kod powstały w ostatnim tekście.   Przykład praktyczny W poprzednim wpisie [link] przygotowaliśmy mały generator raportów, który wykorzystywał pliki PDF. Wyobraźmy sobie teraz, że przyszło nowe wymaganie do systemu, które zakłada konieczność generowania plików CSV. Jeśli spojrzymy na kod z poprzedniego przykładu, to szybko okaże się, że spełnia on zarówno zasadę SRP, jak i częściowo OCP. Kluczem do sukcesu jest w tym przypadku interfejs IFileWriter:   [sourcecode language="csharp"] public interface IFileWriter { void Open(); void WriteData(IEnumerable<string> data); void Close(); } [/sourcecode]   który nie jest ukierunkowany na żaden konkretny format plików. Bardzo łatwo możemy więc teraz dodać klasę CsvFileWriter, która będzie zgodna z Open/Closed Principle. Nowa klasa rozszerzy możliwości systemu i jednocześnie nie wpłynie na istniejące funkcje generatora PDF-ów. Poniżej kod źródłowy:   [sourcecode language="csharp"] public class CsvFileWriter : IFileWriter { public CsvFileWriter(string filePath) { Console.WriteLine($"CSV file with path: {filePath}"); } public void Open() { Console.WriteLine("CSV file open"); } public void WriteData(IEnumerable<string> data) { Console.WriteLine($"Write {data.Count()} records to CSV file"); } public void Close() { Console.WriteLine("CSV file close"); } } [/sourcecode]   Niestety, jest jedna rzecz, która w wyjściowym kodzie mogłaby być zrobiona lepiej. Jest nią fakt, że w kodzie klasy ReportGenerator zapięliśmy się na klasę PdfFileWriter. Oczywiście, tak jak pisałem w poprzednim tekście, można było ten problem rozwiązać za pomocą IoC na poziomie klasy, ale można też wprowadzić stosunkowo małą modyfikację na poziomie metody. Poniżej zmodyfikowany kod:   [sourcecode language="csharp"] public class ReportGenerator { public void GenerateReport(IFileWriter fileWriter, DateTime date) { IDatabaseManager databaseManager = new MyDatabaseManager(); var reportData = databaseManager.GetReportData(date); fileWriter.Open(); fileWriter.WriteData(reportData); fileWriter.Close(); } } [/sourcecode]   Na dobrą sprawę nie dokonaliśmy tutaj żadnej rewolucji. Nasza klasa wciąż generuje raport, jedynie zmieniliśmy miejsce tworzenia generatora plików. Zmiany w tym miejscu wymuszają również modyfikację klasy Program. Poniżej jej nowy kod, który uwzględnia generowanie raportów dla dwóch formatów plików:   [sourcecode language="csharp"] public class Program { public static void Main(string[] args) { IFileWriter pdfFileWriter = new PdfFileWriter("myreport.pdf"); IFileWriter csvFileWriter = new CsvFileWriter("myreport.csv"); ReportGenerator reportGenerator = new ReportGenerator(); DateTime dt = new DateTime(2016, 05, 9); reportGenerator.GenerateReport(pdfFileWriter, dt); reportGenerator.GenerateReport(csvFileWriter, dt); } } [/sourcecode]   W prosty sposób spełniliśmy założenia Open/Closed Principle i dodaliśmy nową funkcję, która zasadniczo nie wpłynęła na istniejący kod. Oczywiście, nie zawsze uda się w pełni zachować zasady OCP, czy też dowolnej innej reguły Solid, ale mimo wszystko warto o to powalczyć, tworząc oprogramowanie.

Jerzy Piechowiak

Altcontroldelete.pl

   

 Szukasz książki do C#? Kliknij TU lub zerknij poniżej: