Nie SOLID-nie #05: Dependency Inversion Principle

N

Seria zainspirowana bardzo dobrym kursem SOLID od Jarka Stadnickiego, dostępnym na platformie Udemy – SOLID praktyczny kurs

Nie jest to reklama, ani żadna afiliacja. Wyrażam swoje zdanie 🙂 . Polecam zerknąć. Jarek za pomocą obrazowych, trafnych porównań tłumaczy poszczególne zasady.

 


Spis postów z serii Nie SOLID-nie:

  1. Nie SOLID-nie #01: Single Responsibility Principle
  2. Nie SOLID-nie #02: Open Close Principle
  3. Nie SOLID-nie #03: Liskov Substitution Principle
  4. Nie SOLID-nie #04: Interface Segregation Principle
  5. Nie SOLID-nie #05: Dependency Inversion Principle

 

Wstęp

Robert C. Martin (Uncle Bob) w swoim artykule nt. Dependency Inversion Principle, skondensowanej wersji rozdziału DIP: The Dependency-Inversion Principleze swojej książki pt. Agile Software Development, Principles, Patterns and Practices, opisuje „zły” design aplikacji i wprowadza pojęcie „kruchości” aplikacji.

Mianowicie, kruchość aplikacji to m.in. tendencja aplikacji do psucia się w wielu miejscach, w momencie jednej małej zmiany w miejscu zupełnie nie powiązanym do miejsca gdzie zaszła zmiana. Tego typu „kruchość”, często jest powodem obniżenia wiarygodności w niezawodność zmian i poprawek. Użytkownicy i managerowie nie są w stanie potwierdzić jakości wykonania produktu. Projektanci natomiast, nie są w stanie przewidzieć re-użycia komponentów aplikacji w innych miejsach, ponieważ nie mają pewności jak one się zachowają.

Zazwyczaj w tego typu aplikacji, napisanie jej na nowo, wydaje się lepszym pomysłem niż jej poprawa. Głównie ze względu na koszty.

 

Reguły Dependency Inversion Principle

Uncle Bob ukuł, m.in. dwie zasady którymi warto się kierować podczas planowania, czy programowania.

 

Moduły wysokiego poziomu nie powinny być zależne od modułów niskiego poziomu. Oba powinny być zależne od abstrakcji.

 

Abstrakcje nie powinny być zależne od detali. Detale powinny być zależne od abstrakcji.

 

Tradycyjny model pisania oprogramowania wygląda tak, że wysokopoziomowe warstwy aplikacji są zależne od niskopoziomowych, a te z kolei są zależne od detali implementacyjnych. Robert C. Martin namawia aby z tym skończyć. Wyobraź sobie, że wysokopoziomowy moduł z logiką biznesową jest zależny od implementacji modułu dostępu do danych, lub jakiegoś zapisu na dysku, które przecież powinny być wymienne! Wystarczy przecież jakiś rodzaj abstrakcji między tymi modułami, a sztywne połączenie znika i oba stają się wymienne i re-używalne!

Ba! Idąc dalej! Co powiecie na abstrakcję która ma zależność z inną abstrakcją (interfejs z interfejsem)? Wtedy mamy pełną dowolność w wymianie relacji typów które implementują tę abstrakcje. Dosłownie „każdy z każdym„.

 

Simple as that!

 

 

Anty-przykład

Poniżej przykład kodu który „modelowo” nadaje się na refaktoryzację i zastosowanie Dependency Inversion Principle.

Najpierw klasa ExternalHttpHandler która korzysta (zawiera) z obiektu klasy ExternalHttpClient. Jednak wyraźnie widać, że obiekt ten jest tworzony „na miejscu”. Jeśli się przyjżymy klasie ExternalHttpClient od razu zauważymy, że nie implementuje ona żadnego interfejsu, nie może się „podzielić z innymi swoją odpowiedzialnością za pomocą jakiejś abstrakcji”.

Zobaczcie sami jak to wygląda…

public class ExternalHttpHandler
{
    private readonly ExternalHttpClient _externalHttpClient;
    private readonly ConfigurationSettings _configurationSettings;

    public ExternalHttpHandler(IConfigurationProvider configurationProvider)
    {
        _configurationSettings =
            configurationProvider.GetSection<ConfigurationSettings>(ConfigurationSettings.SectionName);
        _externalHttpClient = new ExternalHttpClient("http://localhost:5000", "http://localhost:5001");
    }

    public bool ProcessGrantPoints(string phoneNumber)
    {
        try
        {
            var response = _externalHttpClient.GrantPoints(_configurationSettings.UserName,
                phoneNumber, _configurationSettings.ConfigurationType);

            return !string.IsNullOrEmpty(response);
        }
        catch (Exception)
        {
            return false;
        }
    }

    public string ProcessPostToCreateContact(UserContact userContact)
    {
        try
        {
            var campaign = CreateContactCampaign(userContact);
            var response = _externalHttpClient.CreateContact(campaign);

            if (!string.IsNullOrEmpty(response))
            {
                return response;
            }

            return null;
        }
        catch (Exception e)
        {
            throw e;
        }
    }

    private CreateContactCampaign CreateContactCampaign(UserContact userContact)
    {
        var campaign = new CreateContactCampaign
        {
            Header = new CreateContactHeader
            {
                UserName = _createContactSettings.UserName,
                Password = _createContactSettings.ApiPassword,
                Keyword = _createContactSettings.Keyword,
                Shortcode = _createContactSettings.Shortcode
            }
        };

        campaign.Header.Mobile = userContact.Mobile;
        campaign.Header.FirstName = userContact.FirstName;
        campaign.Header.LastName = userContact.LastName;
        campaign.Header.Email = userContact.Email;
        return campaign;
    }
}

 

I klasa ExternalHttpClient…

public class ExternalHttpClient
{
    private readonly string _grantPointsUrl;
    private readonly string _createContactUrl;

    private HttpWebRequest _webRequest;

    public ExternalHttpClient(string grantPointsUrl, string createContactUrl)
    {
        _grantPointsUrl = grantPointsUrl;
        _createContactUrl = createContactUrl;
    }

    public string GrantPoints(string userName, string phoneNumber, string submissionType)
    {
        var uriBuilder = new UriBuilder(_grantPointsUrl);
        var paramValues = HttpUtility.ParseQueryString(uriBuilder.Query);
        paramValues.Add("user_name", userName);
        paramValues.Add("mobile", phoneNumber);
        paramValues.Add("submission_type", submissionType);
        uriBuilder.Query = paramValues.ToString();

        _webRequest = (HttpWebRequest) WebRequest.Create(uriBuilder.Uri);
        _webRequest.Method = "GET";
        _webRequest.PreAuthenticate = true;
        _webRequest.ContentType = "text/xml";

        return GetResponse(_webRequest.GetResponse());
    }

    public string CreateContact(CreateContactCampaign campaignRoot)
    {
        var uriBuilder = new UriBuilder(_createContactUrl);

        _webRequest = (HttpWebRequest) WebRequest.Create(uriBuilder.Uri);
        _webRequest.Method = "POST";
        _webRequest.PreAuthenticate = false;
        _webRequest.ContentType = "text/xml;";

        var campaignDoc = campaignRoot.ToXmlDocument();
        var offer = campaignDoc.DocumentElement.OuterXml;

        using (var postStream = _webRequest.GetRequestStream())
        {
            // Convert the string into a byte array. 
            byte[] byteArray = Encoding.UTF8.GetBytes(offer);
            postStream.Write(byteArray, 0, offer.Length);
            postStream.Close();
        }

        return GetResponse(_webRequest.GetResponse());
    }

    private string GetResponse(WebResponse response)
    {
        using (var streamResponse = response.GetResponseStream())
        {
            if (streamResponse == null)
                return null;

            using (var streamRead = new StreamReader(streamResponse))
            {
                var responseString = streamRead.ReadToEnd();

                streamResponse.Close();
                streamRead.Close();

                response.Close();

                return responseString;
            }
        }
    }
}

 

Nie dość, że nie korzystamy w tym miejscu z „enkapsulacji za pomocą kontraktu” (tak to nazwijmy) to jeszcze na dodatek, jeśli będziemy chcieli użyć innej implementacji klasy ExternalHttpClient, przykładowo ExternalWebSocketsClient, będziemy musieli robić zmiany w klasie ExternalHttpHandler. 

Po prostu elementy naszej układanki nie są wymienne. Jakby to powiedział Uncle Bob, nasz kod jest kruchy.

 

Kruchy jak ciasteczko.

 

W naszym przykładnie mamy możliwość zaimplementowania architektury warstwowej, wedle idei pana Grandy’iego Boocha o której Uncle Bob wspomina, w taki sposób, że klasa ExternalHttpHandler jest na wyższym poziomie abstrakcji i powinna kożystać z klasy niższego poziomu abstrakcji ExternalHttpClient, ale poprzez interfejs. Dzięki czemu klasa ExternalHttpHandler staje się niezależna od niższego poziomu obstrakcji, dodatkowo ExternalHttpClient staje się wymienna, a zestaw swoich „usług” dostarcza własnie przez interfejs.

 

 

Masz jakieś uwagi? Chcesz coś skomentować?

A zatem do dzieła! Napisz coś od siebie w komentarzach poniżej!

Czołem!

 

…acha, jeszcze jedno!

 

KONKURS!

Konkurs na najważniejszą zasadę SOLID!

Wraz z zakończeniem serii postów nt. zasad SOLID na moim blogu, bo ten jest ostatni, postanowiłem zorganizować dla Was konkurs z nagrodami.

ZADANIE: Napisz która, Twoim zdaniem, z zasad SOLID jest najważniejsza i dlaczego. Odpowiedzi proszę umieszczać w komentarzach pod tym postem, który właśnie czytasz.

CZAS TRWANIA: Konkurs zaczyna się dzisiaj (02.04.2019), a kończy wraz z upłynięciem dwóch tygodni, tzn. niedziela (14.04.2019) jest ostatnim dniem.

NAGRODY:

To trzy kupony, na kurs „SOLID – praktyczny kurs” od Jarka Stadnickiego, współtwórcy podcastu „Ostra piła”.

A także trzy kupony, na kurs „Od chaosu do SOLIDa” od Pawła Klimczyka, jednego z naszych MVP (można go dorwać na jego blogu).

Kupony zostaną rozdane wśród autorów najciekawszych odpowiedzi.

 

Pamiętajcie!

Będą brane pod uwagę tylko wpisy w komentarzach do tego posta i tylko wpisy z adresem email (inaczej Was nie zlokalizuję).

Przemyślcie sprawę i piszcie komentarz! WARTO!

 



About the author

5 komentarzy

  • Dla mnie najważniejsza jest literka S . A dlaczego ? Bo jest prosta i dosyć łatwo ja zacząć stosować w praktyce a korzyści sa bardzo duże.
    Gdyby każdy początkujący programista już od startu jej przestrzegał, może dałoby się uniknąć kilku ośmiotysięczników;) ps. Kurs Jarka już widziałem 😛

  • jak dla mnie oczywiście SRP, bo:
    – kod staję się czytelny, prostszy do zrozumienia
    – łatwiej go przetestować, zmniejsza ryzyko powstania błędów
    – wymusza trzymania się jednej warstwy abstrakcji.

  • Ja lubię dwa:
    * SRP: czystość, krótki kod
    * IOC: 2 powody: a) mam wszystko gdzieś, jak przyjdą materiały potrzebne do roboty to będę robić, jak nie przyjdą to nie będę robić; b) gdy mam klasę, która ma duże zależności, to wiem, że ona ma nic nie robić, tylko rozdzielać zadania po niższych od siebie, nawet nie muszę jej nazywać manager, bo wystarczy spojrzenie na listę zależności które przyjmuje

  • Ja niedawno zacząłem zgłębiać dobre praktyki programowania, tak też trafiłem na ten wpis. Długo myślałem, jaka zasada jest najważniejsza i raczej cięzko wybrać jedną – wszystkie wydają się ważne i niejako, ze sobą połączone – często trzymanie się jednej implikuje wprowadzenie pozostałych. Myślę, że warto mieć z tyłu głowy wszystkie zasady jednak to co mi pomaga to skupienie się na zasadzie Open Closed – powoduje to, że program od razu projektuje w inny sposób, często wymusza to stosowanie wzorców projektowych oraz pozostałych zasad, których wprowadzenie nie wymaga już większej modyfikacji w kodzie.

    niezgodzkibartosz@gmail.com

  • Osobiście uważam, że najważniejszą z zasad SOLID jest Dependency Inversion Principle. Poza oczywistą zaletą z jej stosowania, którą jest łączenie modułów projektowanej aplikacji za pomocą interfejsów, pozwala na tworzenie łatwo testowanego kodu, gdyż wszystkie zależności danego modułu, w miarę potrzeby mogą zostać zastąpione odpowiednio skonfigurowanymi mockami.

By Patryk

Autor serwisu

Patryk

Społecznościowe

Instagram

Newsletter



Historycznie

Tagi