poniedziałek, 20 lipca 2020

Tuple

Temat stosowania tupli ma tylu samo przeciwników ilu zwolenników. Wraz z wejściem C# 7.0 Tuple zyskał nową składnię, która ułatwia pracę z nimi. Na początek zobaczmy co się zmieniło a później zastanówmy się kiedy ich używać.

Najprostsza postać Tupla to ta znana przed C# 7.0:

            var person = new Tuple<string, string, int>("Jacek", "Kowalski", 10);
            Console.WriteLine($"My name is {person.Item1} {person.Item2}. I'm {person.Item3} years old.");

Składnia ta nie pozwala na nazwanie poszczególnych składowych. Do poszczególnych składowych odwołujemy się poprzez składowe ItemX gdzie X to numer pola.
Zastosowanie pól numerowanych nie jest wygodnym rozwiązaniem i z pewnością nie motywowało do częstego używania Tupli. Wszystko zmieniło się wraz z C# 7.0 gdzie wprowadzono nową składnię:

            (string FirstName, string LastName, int Age) person = ("Jacek", "Kowalski", 10);
            Console.WriteLine($"My name is {person.FirstName} {person.LastName}. I'm {person.Age} years old.");

Zapis możemy skrócić korzystając z słówka var:

            var person = (FirstName: "Jacek", LastName: "Kowalski", Age: 10);
            Console.WriteLine($"My name is {person.FirstName} {person.LastName}. I'm {person.Age} years old.");

Zapis możemy jeszcze bardziej skrócić, jednak wtedy wracamy do dawnej konwencji nazywania składowych Tupla Item:

            var person = ("Jacek", "Kowalski", 10);
            Console.WriteLine($"My name is {person.Item1} {person.Item2}. I'm {person.Item3} years old.");

Warto wspomnieć jeszcze o dekonstrukcji Tupla na jego poszczególne składowe. Oczywiście do poszczególnych składowych można odnieść się po nazwie pola / ItemX. Warto jednak wiedzieć, że istnieje alternatywa:

            var (firstName, lastName, age) = person;
            Console.WriteLine($"My name is {firstName} {lastName}. I'm {age} years old.");

Teraz zastanówmy się nad tym kiedy używać Tupli a kiedy nie. Zacznijmy od kilku ich właściwości, które mogą pomóc zdecydować czy je używać czy nie:
  1. Zdefiniowane nazwy składowych Tupla nie są stałe - podczas działania programu znikają.
  2. Nowy Tupl to instancja struktury ValueTuple<T> - typ wartościowy, więc przekazywany jest przez wartość. Problem częstego kopiowania można zaadresować przez zastosowanie ref local / ref return
  3. Tuple pozwalają na porównywanie za pomocą == czy !=

Kiedy więc warto skorzystać z Tupli? Z pewności w metodach prywatnych biblioteki czy klasy. Pozwala to uniknąć tworzenia kolejnych definicji obiektów. W przypadku publicznych API nie jest zalecane używanie Tupli.

sobota, 14 marca 2020

Hierarchia plików konfiguracyjnych w ASP.NET

Obecnie większość z Was pewnie tworzy aplikacje w .NET Core. Konfiguracja w .NET Core znacząco różni się od tej znanej z ASP.NET. Niemniej jednak ostatnio musiałem prześledzić zgłoszenie klienta w jednej ze starszych aplikacji. Ponieważ nie do końca wiedziałem dlaczego jedno z ustawień pobieranych z Web.configa ma taką a nie inną wartość, musiałem prześledzić hierarchię ustawień.
Konfiguracja w ASP.NET ma strukturę hierachiczną:


Na samej górze hierarchii znajduje się Machine.config - jest to globalny plik ustawień dla każdej aplikacji .NET. Kolejnym miejscem gdzie możemy wprowadzić ustawienia jest globalny Web.config. Plik ten nadpisuje część ustawień machine.config jak i dodaje nowe.
Kolejne poziomy to:

  • katalog witryn
  • pojedyncza aplikacja webowa
  • folder w aplikacji webowej
Ostatni poziom hierarchii jest o tyle ciekawy, że w aplikacji webowej dla każdego podfolderu możemy zdefiniować osobny plik ustawień.

Ustawienia są oczywiście cachowane, przez co nie są za każdym razem odczytywane z dysku twardego co przyspiesza aplikację.  

poniedziałek, 9 marca 2020

Rola klienta i serwera

Często podczas rozmowy kwalifikacyjnej pada pytanie o rolę serwera / klienta aplikacji internetowej. Postaram się w kilku słowach streścić prawidłową odpowiedź na to pytanie.
Zacznijmy od tego czym jest klient i serwer:

  • klient - w przypadku aplikacji internetowej jest to przeglądarka, pozwala wyświetlać interfejs aplikacji i wysyłać komendy do serwera w celu ich przetworzenia
  • serwer - zwraca żądane przez klienta strony; odpowiedź z serwera zawiera kod HTML który determinuje sposób wyświetlenia żądania klienta
Zarówno klient jak i serwer komunikują się ze sobą za pomocą protokołu HTTP. Protokół ten używa portu 80, lub jeżeli serwer używa certyfikatu SSL - HTTPS na porcie TCP 443.

Proces uzyskiwania połączenia:
  1. Użytkownik wprowadza w przeglądarce adres żądanej strony
  2. Przeglądarka za pomocą protokołu HTTP wysyła żądanie GET do serwera
  3. Serwer WWW otrzymuje zapytanie i rozpoczyna je przetwarzać
  4. Jeżeli zapytanie jest poprawne, serwer zwraca odpowiedź wraz z kodem statusu 200. W przeciwnym przypadku, np. jeżeli adres strony jest błędny, zwraca status błędu 404.
  5. Przeglądarka internetowa użytkownika otrzymuje odpowiedź wraz z kodem statusu. Jeżeli kod jest równy 200 wyświetla zwróconą stronę. Jeżeli kod błędu jest inny, np. 404 wyświetla błąd.
Rola serwera:
  1. Walidacja przychodzącego żądania (requestu)
  2. Uwierzytelnienie - jeżeli serwer używa certyfikatu SSL
  3. Uwierzytelnienie użytkownika jeżeli aplikacja wymaga podania użytkownika i hasła
  4. Autoryzacja użytkownika - sprawdzenie czy ma dostęp do żądanych treści
  5. Przetworzenie żądania
  6. Obsługa błędów
  7. Wynik odpowiedzi może zostać zapamiętany w pamięci (cache) co pozwoli przetworzyć kolejne, takie samo zapytanie znacznie szybciej
  8. Kompresja wyniku w celu zredukowania ilości przesyłanych danych
  9. Logowanie danych zapytania, statystyk
Rola klienta:
  1. Przesyłanie żądań do serwera
  2. Uwierzytelnianie serwera gdy ten dysponuje certyfikatem SSL
  3. Przetworzenie odpowiedzi z serwera i wyświetlenie odpowiedzi
  4. Uruchamianie skryptów po stronie przeglądarki (JavaScript)
Niektóre komendy HTTP:
  • GET - pobieranie danych
  • POST - wysyłanie danych do serwera (np. pola formularza)
  • PUT - pozwala tworzyć dane bezpośrednio na serwerze (o ile użytkownik ma do tego prawo)
  • DELETE - usuwa dane po stronie serwera (o ile użytkownik ma do tego prawo)
  • HEAD - zwraca metadane strony bez jej pobierania; jeżeli strona została zapisane w pamięci (cache) po stornie przeglądarki, można wykorzystać HEAD w celu weryfikacji czy dane po stronie serwera się zmieniły
  • OPTIONS - zwraca listę obsługiwanych komend przez serwer
  • TRACE - diagnostyka i śledzenie wysłanego żądania
  • CONNECT - tworzy tunel, np. gdy chcemy użyć serwera proxy
Kody statusów podzielone są na grupy:

  • 1xx - informacje - żądanie zostało dostarczone do serwera i wciąż jest przetwarzane
  • 2xx - żądanie zostało poprawnie przetworzone
  • 3xx - żądanie przekierowania - klient musi użyć innego adresu aby otrzymać żądane dane
  • 4xx - błąd klienta - w żądaniu (requescie) albo serwer nie wie jak przetworzyć zapytanie
  • 5xx - błędy serwera; serwer nie jest w stanie przetworzyć zapytania
Pełną listę kodów odpowiedzi można sprawdzić w Wikipedii. 

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.