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.

poniedziałek, 4 listopada 2019

Single, SingleOrDefault, First, FirstOrDefault

Tytuł posta to często spotykane pytanie rekrutacyjne, które w pełnej formie brzmi najczęściej:
Omów metody Single, SingleOrDefault, First, FirstOrDefault. Podaj różnice między nimi.
Pytanie, być może dla jednych proste, dla innych już niekoniecznie. O ile FirstOrDefault jest bardzo często wykorzystywane w kodzie o tyle z Single-* może być już różnie.

Zobaczmy na początek na metody Single or SingleOrDefault. Metoda ta oczekuje, że kolekcja będzie zawierała tylko jeden element.

Do rozważenie mamy 4 przypadki:
- kolekcja jest nullem

            List<string> collection = null;

            var singleOrDefault = collection.SingleOrDefault();
            var single = collection.Single();

W obu przypadkach zostanie rzucony wyjątek System.ArgumentNullException


- kolekcja zawiera 1 element

            var collection = new List<string> {"Value1"};

            var singleOrDefault = collection.SingleOrDefault();
            var single = collection.Single();

W obu przypadkach zostanie zwrócony pojedynczą wartość Value1


- kolekcja zawiera więcej niż 1 element

            var collection = new List<string> {"Value1", "Value2"};

            var singleOrDefault = collection.SingleOrDefault();
            var single = collection.Single();

W obu przypadkach otrzymamy błąd System.InvalidOperationException: 'Sequence contains more than one element'. 


- kolekcja jest pusta

            var collection = new List<string>();

            var singleOrDefault = collection.SingleOrDefault();
            var single = collection.Single();

Tu już robi się ciekawiej. W przypadku kolekcji pustej metoda Single zwróci wyjątek: System.InvalidOperationException: 'Sequence contains no elements' a metoda SingleOrDefault() - wartość domyślną:




Drugą grupę metod stanowi First i FirstOrDefault. Tu już sprawa wydaje się znacznie łatwiejsza, gdyż są to metody znacznie częściej goszczące w naszym kodzie.
Jak poprzednio rozpatrzmy 4 przypadki:

- kolekcja jest nullem

            List<string> collection = null;

            var firstOrDefault = collection.FirstOrDefault();
            var first = collection.First();

W obu przypadkach zostanie rzucony wyjątek System.ArgumentNullException


- kolekcja zawiera 1 element

            var collection = new List<string> { "Value1" };

            var firstOrDefault = collection.FirstOrDefault();
            var first = collection.First();

W obu przypadkach zostanie zwrócony pojedynczą wartość Value1


- kolekcja zawiera więcej niż 1 element

            var collection = new List<string> { "Value1", "Value2" };

            var firstOrDefault = collection.FirstOrDefault();
            var first = collection.First();

W obu przypadkach zostanie zwrócony pojedynczą wartość Value1 - chcemy pobrać pierwszy element, jeżeli kolekcja zawiera więcej i tak zawsze otrzymamy pierwszy.


- kolekcja jest pusta

            var collection = new List<string>();

            var firstOrDefault = collection.FirstOrDefault();
            var first = collection.First();


W tym przypadku dla metody FirstOrDefault zostanie zwrócony null, gdyż to wartość domyślna jeżeli kolekcja jest pusta. Metoda First rzuci wyjątkiem System.InvalidOperationException: 'Sequence contains no elements'



niedziela, 3 listopada 2019

cURL - łatwe zapytania HTTP

cURL to biblioteka sieciowa, która w łatwy sposób pozwala wysłać dowolne zapytanie HTTP. Lista obsługiwanych protokołów jest bardzo długa - bardziej niż wystarczająca dla codziennych zastosowań.

cURL to biblioteka napisana w języku C - zapewne większość użytkowników Linuxa słyszała bądź też używała tej biblioteki. Istnieje także wersja konsolowa dla Windows, którą można pobrać z oficjalnej strony https://curl.haxx.se/windows/

Do czego może przydać się cURL na co dzień podczas pracy nad projektem? Konsolowa aplikacja występuje w wersji portable. Umożliwia nam to łatwy sposób wrzucenia archiwum na dowolny serwer testowy/produkcyjny i szybką weryfikację czy z danej maszyny jest dostępny serwis, który chcemy odpytywać. Pozwala to zaoszczędzić sporo czasu, który pewnie przeznaczylibyśmy na debugowanie naszego kodu w poszukiwaniu problemu z połączeniem, np. szukaniem problemu z headerami czy proxy. 

Zobaczmy na najprostszy przykład cURLa:


Zapytanie typu curl adres_serwisu wyświetla odpowiedź z serwisu. Nie mamy tu zbyt wielu informacji. Bardzo przydatną flagą będzie -v, która wyświetli bardzo dużo przydatny informacji:



Inną przydatną flagą może być -I, flaga ta wyświetla tylko zwracane nagłówki:



To tylko podstawowe informacje jak używać cURL w środowisku Windows. Po pełną listę możliwości warto zajrzeć na oficjalną stronę https://curl.haxx.se/docs/manual.html. Możliwości biblioteki są praktycznie nieograniczone. 

Używając cURL do tworzenia zapytań może przydać się także dla użytkowników innych programów. Popularny klient ułatwiający tworzenie i wysyłanie zapytań SOAP/REST Postman także pozwala na importowanie zapytań cURL: