?Object reference not set to an instance of an object? ? taki, pojawiający się w najmniej oczekiwanym momencie komunikat to zmora każdego programisty
„Object reference not set to an instance of an object” — taki, pojawiający się w najmniej oczekiwanym momencie komunikat to zmora każdego programisty C#. Przyczyna jest zawsze taka sama, a jest nią wyjątek NullReferenceException.

Wspomniany wyjątek i komunikat występują w sytuacji, gdy próbujemy wykonać jakąś operację na obiekcie, który jest nullem. Innymi słowy, oczekiwaliśmy, że jakiś fragment naszej aplikacji zwróci nam gotowy obiekt, ale tego nie zrobił.   Standardowym rozwiązaniem, które przychodzi do głowy większości programistów, jest instrukcja IF na zwróconym wyniku. Jeśli jest to null, po prostu nie wykonamy określonej operacji. W przeciwnym wypadku pójdziemy standardową ścieżką. Czy rzeczywiście to najlepsza opcja?   Problem We wstępie przedstawiłem proste rozwiązanie, które przetwarza określony kod warunkowo. Zasadniczo ma ono kilka istotnych problemów, które zostały wyliczone poniżej: - jest brzydkie i nienaturalne, zwłaszcza jeśli takich miejsc w kodzie jest więcej; - utrudnia przetwarzanie i komplikuje kod — każda dodatkowa instrukcja IF to nowy scenariusz testowy; - problem nulla może ciągnąć się przez całą aplikację — zwłaszcza gdy kod go generujący znajduje się bardzo głęboko; - łamie zasady SOLID, a konkretniej SRP — klasa, która powinna operować na tym obiekcie, musi się w tej chwili troszczyć o stan klasy zależnej, a nie operować na wynikowym obiekcie.   Null object pattern eliminuje większość tych problemów — i to w naprawdę prosty sposób.   Przykład praktyczny  W przykładzie praktycznym wykorzystam kod generatora raportów, który powstał przy okazji wpisów na temat mnemonika SOLID (zobacz wpisy). Poniżej całość dotychczasowego kodu: [sourcecode language="csharp"] public interface IFileWriter { void Open(); void WriteData(IEnumerable<string> data); void Close(); } 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"); } } 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"); } } public interface IDatabaseManager { IEnumerable<string> GetReportData(DateTime date); } 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 ReportGenerator { public void GenerateReport(IFileWriter fileWriter, DateTime date) { IDatabaseManager databaseManager = new MyDatabaseManager(); var reportData = databaseManager.GetReportData(date); fileWriter.Open(); fileWriter.WriteData(reportData); fileWriter.Close(); } } public class Program { public static void Main(string[] args) { IFileWriter csvFileWriter = new CsvFileWriter("myreport.csv"); ReportGenerator reportGenerator = new ReportGenerator(); DateTime dt = new DateTime(2016, 05, 9); reportGenerator.GenerateReport(csvFileWriter, dt); } } [/sourcecode] Nasze działania skupimy w obszarze klasy Program, gdzie dodamy prostą metodę wytwórczą, która w zależności od przekazanego rozszerzenia pliku będzie zwracać odpowiedni generator. Dodamy również NullFileWriter, który będzie symulował domyślne zachowanie w sytuacji, gdy podane rozszerzenie nie będzie obsługiwane przez system. Zaczniemy od nowej klasy: [sourcecode language="csharp"] public class NullFileWriter : IFileWriter { public NullFileWriter() { } public void Open() { } public void WriteData(IEnumerable<string> data) { } public void Close() { } } [/sourcecode]   Jak widać, klasa nie robi nic — po prostu jest. Ponieważ implementuje zadeklarowany interfejs w poprawny sposób, możemy z niej skorzystać zawsze. Zrezygnowałem w tym przypadku nawet z przekazywania ścieżki do pliku w konstruktorze. Skoro klasa nie robi nic, to byłby on tu zbędnym przerostem formy nad treścią. Oczywiście, gdyby w produkcyjnym rozwiązaniu zdecydować się na skorzystanie z klasy Activator do tworzenia obiektów, to zastosowanie tego parametru mogłoby się okazać niezbędne. Poniżej modyfikacja klasy Program: [sourcecode language="csharp"] public class Program { public static void Main(string[] args) { IFileWriter fileWriter = GetFileWriter("txt", "File.txt"); ReportGenerator reportGenerator = new ReportGenerator(); DateTime dt = new DateTime(2016, 05, 9); reportGenerator.GenerateReport(fileWriter, dt); } private static IFileWriter GetFileWriter(string extension, string filePath) { IFileWriter fileWriter; switch(extension) { case "csv": fileWriter = new CsvFileWriter(filePath); break; case "pdf": fileWriter = new PdfFileWriter(filePath); break; default: fileWriter = new NullFileWriter(); break; } return fileWriter; } } [/sourcecode] W klasie Program dodałem metodę wytwórczą, która w zależności od przekazanego rozszerzenia generuje odpowiednią klasę Writer. Konstrukcja default tworzy obiekt klasy NullFileWriter, dzięki czemu mamy pewność, że wspomniana metoda nigdy nie zwróci null. Oczywiście dla porządku moglibyśmy wyrzucić dubel rozszerzenia z parametru filePath i ustawiać go na bazie parametru extension.   Null object w ogólności zwiększa przejrzystość tworzonego kodu, zgodność z zasadą SRP mnemonika SOLID, a także zabezpiecza nas przed wyjątkiem NullReferenceException. Warto również zwrócić uwagę na opisane w Wikipedii  jego powiązania z innymi wzorcami.  

Jerzy Piechowiak

Altcontroldelete.pl