czwartek, 19 grudnia 2019

Rzucanie wyjątków wewnątrz bloku using

Jednym z zaleceń Microsoftu podczas implementacji interfejsu IDisposable jest aby implementowana metoda nie rzucała wyjątku https://docs.microsoft.com/en-us/visualstudio/code-quality/ca1065-do-not-raise-exceptions-in-unexpected-locations?view=vs-2015&redirectedfrom=MSDN

Oczywiście zalecenia zaleceniami, a życie życiem :) Jednym z koronnych przykładów złamania tego zalecenia jest klasa ChannelFactory<TChannel> która pozwala na komunikację z webserwisem WCF. Jeżeli spojrzymy do źródeł tej klasy zobaczymy wiele miejsc, w których metoda Dispose rzuca wyjątkami związanymi np. z połączeniem do serwisu. Dlaczego jest to dla nas takie istotne? 
Aby uświadomić sobie problem z którym mamy tutaj do czynienia spójrzmy na kawałek przykładowego kodu który implementuje interfejs IDisposable

    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                ShowSomethingOnScreen();
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex);
                throw;
            }
        }

        private static void ShowSomethingOnScreen()
        {
            using (var dc = new DisposableClass())
            {
                dc.DoSomething();
                throw new InvalidOperationException("Using block");
            }
        }
    }

    public class DisposableClass : IDisposable
    {
        public void DoSomething()
        {
            Console.WriteLine("I'm doing some work!");
        }

        public void Dispose()
        {
            throw new InvalidOperationException("DisposeMethod");
        }
    }

Powyższy przykład rzuca dwa wyjątki:
  • jeden z wyjątków zdefiniowany jest w metodzie ShowSomethingOnScreen
  • rugi z wyjątków znajduje się w metodzie Dispose klasy DisposableClass
Zadajmy sobie pytanie: po uruchomieniu aplikacji, który z wyjątków zostanie przechwycony w metodzie Main i dlaczego? 
Na pierwszy rzut oka wydawałoby się, że zostanie przechwycony wyjątek z treścią "Using block". Stanie się jednak inaczej - zostanie 



Aby odpowiedź na pytanie dlaczego przechwycimy wyjątek z metody Dispose a nie z wewnątrz bloku using należy zobaczyć w jaki sposób kompilator przetłumaczy nasz kod. Blok using w rzeczywistości jest blokiem try finally:

 .method private hidebysig static 
  void ShowSomethingOnScreen () cil managed 
 {
  // Method begins at RVA 0x2084
  // Code size 37 (0x25)
  .maxstack 1
  .locals init (
   [0] class UsingKeyWord.DisposableClass dc
  )

  // (no C# code)
  IL_0000: nop
  // using (DisposableClass disposableClass = new DisposableClass())
  IL_0001: newobj instance void UsingKeyWord.DisposableClass::.ctor()
  // (no C# code)
  IL_0006: stloc.0
  .try
  {
   IL_0007: nop
   // disposableClass.DoSomething();
   IL_0008: ldloc.0
   IL_0009: callvirt instance void UsingKeyWord.DisposableClass::DoSomething()
   // (no C# code)
   IL_000e: nop
   // throw new InvalidOperationException("Using block");
   IL_000f: ldstr "Using block"
   IL_0014: newobj instance void [System.Runtime]System.InvalidOperationException::.ctor(string)
   // (no C# code)
   IL_0019: throw
  } // end .try
  finally
  {
   IL_001a: ldloc.0
   IL_001b: brfalse.s IL_0024

   IL_001d: ldloc.0
   IL_001e: callvirt instance void [System.Runtime]System.IDisposable::Dispose()
   IL_0023: nop

   IL_0024: endfinally
  } // end handler
 } // end of method Program::ShowSomethingOnScreen


Możemy więc rozpisać w jaki sposób zostanie przetworzony program:
  1. Program startuje od metody Main i wywołuje metodę ShowSomethingOnScreen w bliku try catch
  2. Otwarcie bolku using w metodzie ShowSomethingOnScreen 
  3. Wywołanie metody DoSomething która wypisuje zdanie w konsoli
  4. Rzucenie wyjątku throw new InvalidOperationException("Using block");
  5. Wywołanie bloku finally, który wywołuje metodę Dispose
  6. Metoda Dispose rzuca wyjątek
  7. Wyjątek rzucony w metodzie Dispose przekazywany jest do bloku catch metody Main
Teraz zostaje już tylko odpowiedzieć pytanie - jak prawidłowo obsłużyć oba wyjątki aby nie pominąć żadnego z nich?
Niestety, ale aby było możliwe przechwycenie obu wyjątków musimy porzucić blok using i skorzystać ze zwykłego bolku try catch finally:

    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                ShowSomethingOnScreen();
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex);
                throw;
            }
        }

        private static void ShowSomethingOnScreen()
        {
            DisposableClass dc = null;
            try
            {
                dc = new DisposableClass();
                throw new InvalidOperationException("Using block");
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex);
            }
            finally
            {
                dc?.Dispose();
            }
        }
    }

    public class DisposableClass : IDisposable
    {
        public void DoSomething()
        {
            Console.WriteLine("I'm doing some work!");
        }

        public void Dispose()
        {
            throw new InvalidOperationException("DisposeMethod");
        }
    }


Wydawałoby się, że problem raczej niezbyt popularnych, jednak można z nim się spotkać i warto wiedzieć skąd bierze się jego źródło :)

sobota, 7 grudnia 2019

Dependency Injection - sposoby wstrzykiwania zależności

Dependency injection - po polsku wstrzykiwanie zależności. DI jest wzorcem projektowym opisującym sposoby przekazania zależności do obiektów.
Tradycyjne podejście tworzenia oprogramowania nie zakłada przekazywania zależności za pomocą DI, ale tworzenie obiektów gdy są potrzebne. Najczęściej podczas nauki programowania rozpoczynamy pisanie projektów tworząc obiekty, kiedy chcemy z nich skorzystać. DI to wzorzec, który ma na celu wyeliminowanie tworzenia obiektów ad-hoc do fabryk czy specjalistycznych kontenerów DI.

Ten post ma na celu omówienie sposobów realizacji wstrzykiwania zależności.

Wstrzykiwanie przez konstruktor (constructor injection)

Jest to najczęściej wykorzystywana metoda przekazywania zależności do klasy. Całość opiera się o zastosowanie jednego konstruktora, który przyjmuje wszystkie zależności klasy. Przykład:

    public class Mapper
    {
        private readonly IAddressMapper _addressMapper;
        private readonly IPhoneMapper _phoneMapper;
        private readonly IEmailMapper _emailMapper;

        public Mapper(IAddressMapper addressMapper, IPhoneMapper phoneMapper, IEmailMapper emailMapper)
        {
            _addressMapper = addressMapper ?? throw new ArgumentNullException(nameof(addressMapper));
            _phoneMapper = phoneMapper ?? throw new ArgumentNullException(nameof(phoneMapper));
            _emailMapper = emailMapper ?? throw new ArgumentNullException(nameof(emailMapper));
        }
    }

Kod możemy podzielić na sekcje:
  • prywatne pola, przechowujące referencje do implementacji interfejsów (należy podkreślić, że klasa Mapper nie zna szczegółów implementacji wstrzykiwanych zależności). Pola dodatkowo oznaczone są jako readonly co zapewnia nas, że raz zainicjowane nie zostaną w trakcie wykonywania programu zmienione. 
  • publiczny konstruktor, który przypisuje referencję do zależności w celu ich późniejszego wykorzystania
  • Guard Clause - inaczej znaną także jako null check validation - sprawdzamy, czy wstrzykiwane zależności nie są wartościami null
Zalety i wady tego sposobu wstrzykiwania zależności:
+ dokumentacja klasy - widząc jakie zależności są wstrzykiwane, wiemy czego możemy się po danej klasie spodziewać
+ sprawdza się zarówno z kontenerami DI jak i bez nich
+ stan pól (zależności) klasy jest zawsze prawidłowy po utworzeniu obiektu (zależności nie są nullami)
+ podwójna ochrona: kompilator pilnuje aby wszystkie zależności zostały podane podczas tworzenia klasy, Guard Clause sprawdza aby nie były nullami
- jeżeli klasa wymaga zbyt dużej ilości zależności należy rozważyć czy kodu nie należy przeorganizować. Oczywiście nie ma górnego ani dolnego limitu ilości zależności - wszystko zależy od konkretnego przypadku
- jeżeli klasa ma być serializowana, będzie potrzebny domyślny konstruktor
- nie każda metoda wymaga wszystkich zależności - w takim wypadku należy zastanowić się czy aby na pewno architektura klasy jest prawidłowa i czy część kodu nie powinna zostać wydzielona do osobnej klasy/biblioteki

Mimo wszystkich zalet i wad jest to najczęściej polecana technika wstrzykiwania zależności i najłatwiejsza do implementacji za pomocą dostępnych kontenerów. 

Wstrzykiwanie przez właściwość (property injection)

Drugi sposób wstrzykiwania zależności to wstrzykiwanie przez właściwość. Inne nazwy, które można spotkać to: property injection, setter injection. Sposób ten zalecany jest gdy istnieje domyślna implementacja, czyli inaczej mówiąc zależność jest opcjonalna. Jeżeli nie istnieje dobra implementacja domyślna, zalecane jest wykorzystanie wstrzykiwania przez konstruktor. 
Zobaczmy na przykład:

    public class Report
    {
        private IFormat reportFormatter;
        public IFormat ReportFormatter
        {
            get
            { return reportFormatter ?? (ReportFormatter = new TxtFormatter()); }
            set
            {
                if (value == null) throw new ArgumentNullException(nameof(value));
                if (reportFormatter != null) throw new InvalidOperationException();
                reportFormatter = value;
            }
        }

        public void CreateReport()
        {
            //some code that generates report data
            var reportData = new ReportData { Name = "Average salary in company", CreateDate = DateTime.Now, NumberOfEmployee = 1000, AverageSalary = 5000 };
            var report = ReportFormatter.Convert(reportData);
            //rest of code, for example saving to file
        }
    }

    public interface IFormat
    {
        string Convert(ReportData reportData);
    }

    public class TxtFormatter : IFormat
    {
        public string Convert(ReportData reportData)
        {
            var reportTextBuilder = new StringBuilder();
            reportTextBuilder.AppendLine($"Report name: {reportData.Name}");
            reportTextBuilder.AppendLine($"Create date: {reportData.CreateDate}");
            reportTextBuilder.AppendLine($"Number of employees in company: {reportData.NumberOfEmployee}");
            reportTextBuilder.AppendLine($"Average salary in company: {reportData.AverageSalary}");
            return reportTextBuilder.ToString();
        }
    }

    public class ReportData
    {
        public string Name { get; set; }
        public DateTime CreateDate { get; set; }
        public int NumberOfEmployee { get; set; }
        public decimal AverageSalary { get; set; }
    }

Powyżej mamy do czynienia z klasą tworzącą raport. Raport chcemy przed zapisem skonwertować do zadanego formatu. Domyślną implementacją jest zwykły tekst. Property przyjmujące zależność formatującą dane raportu możemy podzielić na następujące części:
  • getter - jeżeli formatter istnieje - zwraca go, w przeciwnym razie podstawia domyślną implementację
  • setter:
    • sprawdzamy czy przesłana wartość nie jest nullem. Podobnie jak w przypadku wstrzykiwania przez konstruktor chcemy uchronić się przed złym stanem obiektu.
    • sprawdzamy czy już wcześniej zależność nie została zainicjowana. Nie chcemy pozwolić aby podczas pracy z klasą w różnych miejscach była inicjowana w różny sposób co mogłoby skutkować niestabilnym czy też niezrozumiałym zachowaniem aplikacji
    • jeżeli powyższe warunki nie mają miejsca zwracana jest zainicjowana zależność

Zalety i wady wstrzykiwania przez właściwości:
+ zależność może zostać zmieniona podczas działania programu
+ elastyczność
- obiekt może mieć nieprawidłowy stan
- większy narzut kodu aby kod był odporny na błędy programistyczne 
- mniej intuicyjne

Wstrzykiwanie przez metodę

Kiedy zależność zmienia się dla każdego wywołania metody, możemy ją przesłać jako argument wywołania metody. 
W poprzednich typach wstrzykiwania zależności (przez konstruktor, przez właściwość) była ona dostarczana podczas budowania drzewa zależności. Wstrzykiwanie przez metodę pozwala dostarczyć zależność do już istniejącego obiektu.
Technika ta szczególnie może być przydatna podczas implementacji encji (Entity) w podejściu DDD (Domain-driven design). 
Spójrzmy na przykład:

    public class Engine
    {
        public int Accelerate(int fuelValue, ITurboEngine turboEngine)
        {
            if (turboEngine == null)
            {
                throw new ArgumentNullException(nameof(turboEngine));
            }

            return turboEngine.IsTurboEngine() ? (int) (value * 1.2) : value;
        }
    }

Klasa Engine posiada metodę Accelerate która na podstawie wartości wtryskiwanego paliwa zwraca o ile samochód przyśpieszy (oczywiście w realnym świecie obliczenie wartości przyspieszenia jest znacznie bardziej skomplikowane, tutaj tylko prosty przykład :)). Interfejs ITurboEngine posiada metodę determinującą czy silnik posiada turbodoładowanie. Jeżeli mamy do czynienia z silnikiem turbodoładowanym osiągnięte przyśpieszenie będzie większe. 
Powyższy przykład pokazuje w jaki sposób możemy wstrzyknąć zależność poprzez metodę. Najczęściej będziemy mieli do czynienia z konstrukcją w której przekażemy konkretną wartość dla metody oraz serwis - czyli kontekst wykonania (zależność). 

Zalety i wady tej techniki:
+ pozwala wstrzyknąć zależność w odniesieniu do kontekstu wywołania metody
+ zależności można wstrzykiwać poza Composition Root
- małe spektrum zastosowań
- zależność staje się publiczną częścią API klasy

piątek, 22 listopada 2019

SOLID - D jak Dependency Inversion principle

Ostatnia z zasad SOLID to Dependency Inversion principle - zasada ta mówi, że wysokopoziomowe moduły nie powinny zależeć od modułów niskiego poziomu. Zależności pomiędzy tymi modułami powinny być realizowane za pomocą abstrakcji. Abstrakcja nie powinna zależeć od implementacji, a implementacja powinna zależeć od abstrakcji.

Tradycyjne podejście komponenty wysokopoziomowe zależą od niskopoziomowych. Przykładowy kod:

    public class AutomaticWashingMachine
    {
        public void StartWashingCycle()
        {
            var engine = new Engine();
            var heater = new Heater();
            //all other required objects
            //code to process washing cycle
        }
    }

    public class Heater
    {
        private TemperatureSensor temperatureSensor;

        public Heater()
        {
            temperatureSensor = new TemperatureSensor();
        }

        public void HeatWater(double requiredTemperature)
        {
            while(Math.Abs(requiredTemperature - temperatureSensor.GetCurrentTemperatur()) < 0.1)
            {
                Console.WriteLine("Rising water temperature...");
            }
        }
    }

    public class TemperatureSensor
    {
        private double currentWaterTemperature = 10;

        public double GetCurrentTemperatur()
        {
            return currentWaterTemperature += 1;
        }
    }

    public class Engine
    {
        public void On()
        {
            Console.WriteLine("I turn the drum...");
        }

        public void Off()
        {
            Console.WriteLine("Stoping engine...");
        }
    }

Powyżej przykład kodu oprogramowującego działanie pralki automatycznej. Powyższy kod nie jest zgodny z zasadą DIP. Oprócz tego posiada następujące wady:

  • trudno napisać do niego testy jednostkowe. Wynika to z faktu, że aby przetestować przycisk włączający pralkę należy stworzyć cały system - nie możemy zaślepić chwilowo grzałki i silnika (Mocki).
  • stałe zależności w kodzie, trudno będzie wymienić czujnik temperatury na inny, gdyż obecny jest mocno powiązany z grzałką.
Powyższy kod można w bardzo łatwy sposób przerobić do kodu opartego o abstrakcję (interfejsy). Zobaczmy jak może to wyglądać:

    public class AutomaticWashingMachine
    {
        private readonly IEngine engine;
        private readonly IHeater heater;

        public AutomaticWashingMachine(IEngine engine, IHeater heater)
        {
            this.engine = engine;
            this.heater = heater;
        }

        public void StartWashingCycle()
        {
            //all other required objects
            //code to process washing cycle
        }
    }

    public interface IHeater
    {
        void HeatWater(double requiredTemperature);
    }


    public class Heater : IHeater
    {
        private readonly ITemperatureSensor temperatureSensor;

        public Heater(ITemperatureSensor temperatureSensor)
        {
            this.temperatureSensor = temperatureSensor;
        }

        public void HeatWater(double requiredTemperature)
        {
            while (Math.Abs(requiredTemperature - temperatureSensor.GetCurrentTemperatur()) < 0.1)
            {
                Console.WriteLine("Rising water temperature...");
            }
        }
    }

    public interface ITemperatureSensor
    {
        double GetCurrentTemperatur();
    }

    public class TemperatureSensor : ITemperatureSensor
    {
        private double currentWaterTemperature = 10;

        public double GetCurrentTemperatur()
        {
            return currentWaterTemperature += 1;
        }
    }

    public interface IEngine
    {
        void On();
        void Off();
    }

    public class Engine : IEngine
    {
        public void On()
        {
            Console.WriteLine("I turn the drum...");
        }

        public void Off()
        {
            Console.WriteLine("Stoping engine...");
        }
    }

Powyższy kod zawiera sporo nowych interfejsów: IEngine, IHeater, ITemperatureSensor. Dzięki zastosowaniu interfejsów nie wiążemy poszczególnych modułów/klas z pralką automatyczną. Każdy komponent jest wymienny tak długo jak implementuje zadany interfejs. Takie podejście ułatwia implementowanie testów jednostkowych. W przypadku gdy chcemy wymienić jeden z komponentów na inny, także możemy to w prosty sposób zrobić bez konieczności przerabiania tony kodu.

czwartek, 21 listopada 2019

SOLID - I jak Interface segregation principle

Czwarta zasada - interface segregation principle porusza temat tworzenia interfejsów i zarządzania nimi.
Zgodnie z definicją encyklopedyczną ISP mówi o tym, że klient nie powinien być zmuszony do implementowania metod, których nie potrzebuje. Łatwiej tę zasadę zrozumieć na przykładzie.

    public interface IMapper
    {
        CustomerDetails MapCustomerDetails(CustomerDetailsDto customerDetailsDto);
        Address MapAddress(AddressDto addressDto);
        Email MapEmail(EmailDto emailDto);
        Phone MapPhone(PhoneDto phoneDto);
    }

Interfejs IMapper zawiera kilka metod mapujących dane klienta (adres, email, telefon, dane osobowe). Załóżmy, że potrzebujemy mapować nowe dane np. informacje o pracy:

    public interface IMapper
    {
        CustomerDetails MapCustomerDetails(CustomerDetailsDto customerDetailsDto);
        Address MapAddress(AddressDto addressDto);
        Email MapEmail(EmailDto emailDto);
        Phone MapPhone(PhoneDto phoneDto);
        IJob MapJob(JobDto jobDto);
    }

Proste! Kolejna metoda i po kłopocie :) Wydawałoby się, że wszystko jest w porządku. Niestety prawdopodobnie złamaliśmy zasadę ISP z przynajmniej dwóch powodów. Pierwszym problemem jest zmuszenie klienta do implementacji nowej metody. Jeżeli klas implementujących jest więcej będziemy musieli zaimplementować nową metodę w każdej z nich. Drugą kwestią jest re-używalność interfejsu. Niestety ale często możemy spotkać się z następującym zjawiskiem:

    public class Mapper : IMapper
    {
        public CustomerDetails MapCustomerDetails(CustomerDetailsDto customerDetailsDto)
        {
            //code
        }

        public Address MapAddress(AddressDto addressDto)
        {
            //code
        }

        public Email MapEmail(EmailDto emailDto)
        {
            throw new System.NotImplementedException();
        }

        public Phone MapPhone(PhoneDto phoneDto)
        {
            throw new System.NotImplementedException();
        }
    }

Widząc throw new System.NotImplementationException(); powinna się zapalić się czerwona lampka. Jeżeli interfejs posiada 4 metody, a nasza klasa implementuje tylko dwie z nich, oznacza to, że nasz interfejs nie jest re-używalny. W takim przypadku najlepszym wyjściem jest podzielenie tego interfejsu.
Małe interfejsy, posiadające małą ilość składowych jest dużo łatwiej implementować. Skomplikowany interfejs prowadzi do sytuacji gdzie większość metod jest pomijana.

Podsumowując, ISP mówi o:

  • tworzeniu małych, zwinnych interfejsów
  • eliminacji zależności, które komplikują kod

Można się w tym momencie zastanowić czy wzorzec Fasady łamie założenie ISP? Moim zdaniem, jak i wielu innych publikacji nie. Trzeba mieć na uwadze, że pomimo iż do fasady dodajemy metody, to większość z tych metod jest delegowana do zależności.

poniedziałek, 11 listopada 2019

SOLID - L jak Liskov substitution principle

Kolejną, już trzecią zasadą SOLID jest Liskov subsitution principle.
Zasada ta mówi, że używając wskaźnika klasy bazowej do potomnej, powinniśmy być w stanie używać tej referencji bez znajomości implementacji klasy potomnej.

Powyższe stwierdzenie na pierwszy rzut oka może wydać się niejasne. Najłatwiej jednak jest je zobrazować przykładem, który pokazuje kiedy ta zasada jest złamana:

    public class LiskovPrinciple
    {
        public void LiskovViolation(Report report)
        {
            if (report is EmployeeReport)
            {
                ((EmployeeReport)report).GenerateEmployeeReport();
            }
            if (report is FinancialReport)
            {
                ((FinancialReport)report).GenerateFinancialReport();
            }
        }
    }

    public class Report
    {
    }

    public class EmployeeReport : Report
    {
        public void GenerateEmployeeReport()
        {
            System.Console.WriteLine("Employee report");
        }
    }

    public class FinancialReport : Report
    {
        public void GenerateFinancialReport()
        {
            System.Console.WriteLine("Financial report");
        }
    }


W przypadku powyżej funckja LiskovViolation przyjmuje jako argument obiekt typu Report. Niestety, nawet mając informację, że jest to obiekt typu Report musi sprawdzić na co dokładnie wskazuje referencja. Jeżeli w kodzie widzimy coś podobnego możemy wnioskować, że jest to jeden z symptomów złamania zasady Liskov. Często widzi się w kodzie iterowanie po kolekcji obiektów i sprawdzanie czy dany obiekt jest typu X - to także symptom łamania tej zasady.


Innym przykładem złamania tej zasady jest brak implementacji w klasie dziedziczącej. Przykładowo dla metody jest rzucany wyjątek, gdyż w naszym przypadku ta metoda może nie mieć sensu istnienia. Zobaczmy na przykład z naszego rodzimego podwórka:

        public void SecondLiskovPrincipleViolation()
        {
            ICollection<int> collection = new int[10];
            collection.Add(10);
        }

Tworzymy tablicę typu int oraz referencję typu ICollection<int>. Wszystko do tego miejsca jest jak najbardziej poprawne. W kolejnej linijce próbujemy dodać do kolekcji nowy element, bum:


Z punktu programistycznego jest to prawidłowe zachowanie - tablica ma stałą długość. Jeżeli chcemy dodać nowy element musimy stworzyć nową o większej długości i przepisać wartości ze starej tablicy do nowej. Do takich operacji zostały stworzone inne kolekcje danych jak np. lista. W klasie Array metoda Add została zaimplementowana w następujący sposób:

        int IList.Add(Object value)
        {
            throw new NotSupportedException(Environment.GetResourceString("NotSupported_FixedSizeCollection"));
        }

Widząc taki kod w swoim projekcie powinniśmy się zastanowić, czy dziedziczenie czy też implementacja interfejsu X ma rzeczywiście sens. Można rzecz że zasada Liskov uczy nas prawidłowego korzystania z polimorfizmu oraz tworzenia łatwiejszego w zarządzaniu kodu.

piątek, 8 listopada 2019

SOLID - O jak Open-closed principle

Druga z zasad SOLIDa Open-closed principle mówi o tym, że klasy, moduły czy funkcje powinny być otwarte na rozszerzanie a zamknięte na modyfikacje.
Ktoś może zadać pytanie czy oznacza to, że jeżeli kod zostanie raz napisany to nie może już być zmieniony np. podczas refaktoringu? Oczywiście, że nie!
Zasadę tę lepiej odnieść do możliwości, które udostępnia nam język C#. Budując bibliotekę, dla przykładu do zapisu plików na dysku twardym, chcemy udostępnić końcowemu użytkownikowi informację w jaki sposób powinien jej używać. Najlepszym kontraktem jest interfejs, np:

    public interface IFile
    {
        void SaveFile(object content, string path);
    }


Klienci w swoich aplikacjach implementują następnie metody korzystające z metody SaveFile z naszej biblioteki. Może to być klasa zapisująca pliki w formacie XML, JSON czy jakimkolwiek innym.
Co stanie się jeżeli stwierdzimy, że nasza biblioteka podczas tworzenia pliku powinna dokonywać logowania?

    public interface IFile
    {
        void SaveFile(object content, string path, ILog logger);
    }

Każda aplikacja, korzystająca z naszej biblioteki nie będzie mogła się skompilować bez podania argumentu logger. Łamiemy tym samym zasadę Open-closed principle. Dwa powodujemy duże niezadowolenie naszych klientów, którzy muszą poświęcić czas na poprawki w kodzie.

Strategy Pattern

Wzorzec strategi pozwala w łatwy sposób zrozumieć znaczenie używania interfejsów. Mówiąc skrótowo wzorzec ten opisuje sposób jak opakować algorytmy w klasy i wymieniać ich implementację podczas działania programu. Spójrzmy na poniższy przykład:

    public class ReportProcessor
    {
        public void DoReport(string format)
        {
            var reportFormatters = new Dictionary<string, IReportFormatter>
            {
                ["xml"] = new XmlFormatter(),
                ["json"] = new JsonFormatter()
            };

            var report = new Report(reportFormatters[format]);
            report.FormatReport(someReport);
            //Rest of code
        }
    }

    public class Report
    {
        private readonly IReportFormatter _reportFormatter;

        public Report(IReportFormatter reportFormatter)
        {
            _reportFormatter = reportFormatter ?? throw new ArgumentNullException(nameof(reportFormatter));
        }

        public string FormatReport(object content)
        {
            return $"Weekly report:{Environment.NewLine}{_reportFormatter.Format(content)}";
        }
    }

    public interface IReportFormatter
    {
        string Format(object content);
    }

    public class XmlFormatter : IReportFormatter
    {
        public string Format(object content)
        {
            return "Xml format";
        }
    }

    public class JsonFormatter : IReportFormatter
    {
        public string Format(object content)
        {
            return "Json format";
        }
    }


Template Method

Drugi z wzorców projektowych, który ułatwia tworzenie kodu zgodnego z Open-close principle. Wzorzec ten definiuje szkielet algorytmu. Konkretne implementacje algorytmu (bądź jego części) zdefiniowane są w klasach dziedziczących. 
Wzorzec ten opiera się na dziedziczeniu. Można powiedzieć, że większość programistów obiektowych języków używa go na co dzień, być może nawet sobie z tego nie zdając sprawy :)
Powyższy przykład z raportem w wydaniu Template Method mógłby wyglądać jak poniżej:

    public class TemplatePattern
    {
        public void DoReport(string format)
        {
            Report xmlReport = new XmlFormatter();
            xmlReport.FormatReport(someReport);

            Report jsonReport = new JsonFormatter();
            jsonReport.FormatReport(someReport);
        }
    }

    public abstract class Report
    {
        public string FormatReport(object content)
        {
            return $"Weekly report:{FormatData(content)}";
        }

        public abstract string FormatData(object content);
    }

    public class XmlFormatter : Report
    {
        public override string FormatData(object content)
        {
            return "Xml format";
        }
    }

    public class JsonFormatter : Report
    {
        public override string FormatData(object content)
        {
            return "Json format";
        }
    }

Jak rozpoznać naruszenie tej zasady?

Odnalezienie naruszenia tej zasady jest bardzo proste. Jeżeli widzimy, że w naszej metodzie jest sporo konstrukcji if then i przy każdej edycji musimy dodać kolejną, jest to bardzo dobry kandydat do refaktoringu. Może warto wydzielić konkretny kod do klas "algorytmów" a w miejscu pierwotnym zawrzeć tylko konfigurację wyboru odpowiedniej implementacji?

wtorek, 5 listopada 2019

SOLID - S jak Single responsibility principle

Tym postem chciałbym rozpocząć, krótką powtórkę dobrych praktyk programistycznych. SOLID z pewnością można uznać za jedną z nich - każdy chce być SOLIDny :). Ten post będzie poświęcony pierwszej literze - S.

Single responsibility principle - klasa, funkcja czy moduł powinny mieć tylko jeden powód zmiany. Stosując tę zasadę w praktyce tworzymy kod, który jest:

  • prosty do zrozumienia
  • łatwy do debugowania
  • łatwy do zmiany - zmieniając pojedyncze zachowanie nie wpływamy na inne
  • łatwy do testowania - np. za pomocą unit testów
  • kod jest re-używalny - proste funkcje / klasy / moduły łatwo wdrożyć w nowych projektach
  • można podciągnąć pod to zasadę: lepiej robić jedną rzecz a dobrze niż 10 źle :)

Jak rozpoznać ów powód do zmiany? Jednym aspektem będzie tu pewna praktyka, drugim pomocnikiem będzie zadawanie sobie pytania "Co ta klasa / funkcja / moduł właściwie robi?". Jeżeli znajdziemy kilka odpowiedzi na to pytanie - łamiemy zasadę pojedynczej odpowiedzialności.

Zobaczmy na poniższy przykład:

    public class Report
    {
        private readonly string _connectionString;

        public Report(string connectionString)
        {
            _connectionString = connectionString;
        }

        public void CreateReport(int startId, string reportPath)
        {
            using var sqlConnection = new SqlConnection(_connectionString);
            sqlConnection.Open();
            var sqlCommand = sqlConnection.CreateCommand();
            sqlCommand.CommandText = "SELECT firstName, lastName, city, postalCode FROM person WHERE id > 200;";
            using var dataReader = sqlCommand.ExecuteReader();
            var personList = new List<Person>();
            while (dataReader.NextResult())
            {
                var person = new Person
                {
                    LastName = (string) dataReader["lastName"],
                    FirstName = (string) dataReader["firstName"],
                    City = (string) dataReader["city"],
                    PostalCode = (string) dataReader["postalCode"]
                };
                personList.Add(person);
            }

            var report = new StringBuilder();
            var xmlSerializer = new XmlSerializer(typeof(List<Person>));
            using var stringWriter = new StringWriter();
            xmlSerializer.Serialize(stringWriter, report);
            File.WriteAllText(reportPath, stringWriter.ToString());
        }
    }

    public class Person
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string City { get; set; }
        public string PostalCode { get; set; }
    }


Odpowiedzialnością metody CreateReport jest:

  • pobranie danych z bazy
  • stworzenie z danych XMLa (serializacja do XML)
  • zapis danych do pliku
Jak widać mamy tutaj jak na tacy podane 3 powody do zmiany tej metody:
  • źródło danych może zostać zmienione - zamiast bazy może to być plik, inny serwis itp
  • format danych które zapisujemy - obecnie jest to XML, w przyszłości być może JSON lub CSV
  • Raport zapisywany jest do pliku na dysku twardym, być może przyjdzie potrzeba zapisu danych do S3 na AWS
Powyższą metodę, a właściwie klasę wypadałoby podzielić na 3 mniejsze, dzięki czemu kod będzie prostszy w każdej z nich, testowalny, a w przypadku jego zmiany będzie to znacznie prostsze.