Bardzo lubię powiedzenie „lepiej nie wymyślać koła od nowa”, ponieważ pasuje do wielu obszarów naszego codziennego życia, w tym programowania. Pisząc kod, bardzo często rozwiązujemy problemy, nad którymi ktoś już wcześniej pracował.

W takich sytuacjach lepiej jest re-użyć pewnych istniejących rozwiązań, niż tworzyć coś niesprawdzonego od nowa. W dzisiejszych czasach mamy tysiące bibliotek i frameworków — w większości przypadków wystarczy tylko dobrze poszukać. Sam w obszarze programowania nie ograniczałbym się jedynie do re-używania istniejącego kodu, starałbym się również re-używać tych najlepszych praktyk, wymyślonych przez specjalistów. W pewnym sensie do takich praktyk można zaliczyć słynny mnemonik Solid autorstwa Roberta C. Martina. Z pewnością wielu z Was o nim słyszało — mniej lub więcej — wydaje mi się jednak, że to temat godny pogłębienia, ponieważ jeśli dobrze poznamy pięć zawartych tu reguł, to kod po prostu obroni się sam. Niniejszy tekst rozpoczyna serię pięciu postów, z których każdy będzie opisywał kolejną regułę Solid. Rozpoczynamy oczywiście od literki S, która reprezentuje wyrażenie Single Responsibility Principle.   Single Responsibility Principle Zasada jednej odpowiedzialności (bo tak właśnie tłumaczymy ten angielski termin) mówi, że każda z klas powinna mieć tylko jedną odpowiedzialność. Można też powiedzieć inaczej — w razie zmiany uwarunkowań powinien istnieć tylko jeden powód do modyfikacji zawartości wskazanej klasy. SRP mówi również o implementacji komunikacji za pomocą publicznych interfejsów. Spójrzmy na pierwszy z brzegu przykład. Tworząc oprogramowanie, bardzo często spotykamy się z raportami. Te mogą być generowane na podstawie zgromadzonych w bazie danych, które później zostają zrzucone do pliku wyjściowego typu PDF. Ignorując SRP, moglibyśmy stworzyć klasę RaportGenerator, która wewnętrznie: - pobiera informacje z bazy danych; - tworzy plik PDF. Mamy tu, jak widać, dwie różne odpowiedzialności — nie mówiąc o samym fakcie sklejania tego wszystkiego w całość. I choć kompletny kod mógłby się zmieścić prawdopodobnie w kilkuset liniach, co na pozór „nie wygląda źle”, już w tym momencie łamie on podstawowe zasady SRP. W kolejnych akapitach przedstawię prostą dekompozycję, która pozwoli na rozwiązanie tego problemu i dopasowanie się do reguły SRP.   Przykład praktyczny Naszym studium przypadku będą klasa RaportGenerator oraz dwie klasy od niej zależne. Na potrzeby przykładu nie będziemy skupiać się na logice związanej z dostępem do bazy danych czy generowaniem PDF-ów, ale na samych interfejsach i abstrakcji. Chodzi mi przede wszystkim o przygotowanie działającego przykładu. Zaczniemy od zdefiniowania interfejsów dla dwóch klas zależnych: [code language="csharp"] public interface IDatabaseManager { IEnumerable<string> GetReportData(DateTime date); } public interface IFileWriter { void Open(); void WriteData(IEnumerable<string> data); void Close(); } [/code] W kolejnym kroku musimy wygenerować przykładowe implementacje tych dwóch klas:   [code language="csharp"] public class MyDatabaseManager : IDatabaseManager { public IEnumerable<string> GetReportData(DateTime date) { string str = date.ToString(); for(int i = 0; i < 10; ++i) { yield return str + "_" + i; } } } public class PdfFileWriter : IFileWriter { public PdfFileWriter(string filePath) { Console.WriteLine($"Pdf file with path: {filePath}"); } public void Open() { Console.WriteLine("PDF file open"); } public void WriteData(IEnumerable<string> data) { Console.WriteLine($"Write {data.Count()} records to PDF file"); } public void Close() { Console.WriteLine("PDF file close"); } } [/code] Poszedłem w tym przypadku po linii najmniejszego oporu, ponieważ, tak jak wcześniej pisałem, nie chodzi tutaj o obsługę bazy danych ani PDF-y ;-) Powyższe klasy zadaniowe implementują stworzone wcześniej interfejsy i realizują wydzielone dla nich zadania. Spoiwem dla powyższego kodu jest klasa ReportGenerator, która wykorzystuje wszystko, co napisaliśmy wyżej: [code language="csharp"] public class ReportGenerator { public void GenerateReport(string filePath, DateTime date) { IDatabaseManager databaseManager = new MyDatabaseManager(); var reportData = databaseManager.GetReportData(date); IFileWriter pdfWriter = new PdfFileWriter(filePath); pdfWriter.Open(); pdfWriter.WriteData(reportData); pdfWriter.Close(); } } [/code] W moim przykładzie obiekty są generowane na żądanie w kodzie metody, ale oczywiście nic nie stoi na przeszkodzie, by w produkcyjnym rozwiązaniu użyć IoC, którego jednak nie chciałem tutaj wtłaczać, by nie zaciemniać przykładu. Efekt naszej pracy można sprawdzić w aplikacji konsolowej: [code language="csharp"] public class Program { public static void Main(string[] args) { ReportGenerator reportGenerator = new ReportGenerator(); reportGenerator.GenerateReport("myreport.pdf", new DateTime(2016, 05, 9)); } } [/code] Jak widać, nawet na prostych klasach łatwo jest pokazać ideę SRP. Kod, który podzieliliśmy właściwie na trzy klasy, często nie wyglądałby tragicznie, będąc w jednej. Można by powiedzieć: ot „prosta” klasa na kilkaset linii. Co dostaliśmy w zamian? Łatwość rozwoju, jawny podział na odpowiedzialności oraz publiczne interfejsy. Wiemy również, kto rozmawia z bazą danych, kto operuje na plikach, a kto jest w stanie złączyć to wszystko w jedną, logiczną całość. Same korzyści :) I o to chodziło!  

Jerzy Piechowiak

Altcontroldelete.pl

   

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