Zależności w projektach: podstawy

Posted by Przemysław Owsianik on 2021-10-02
<p>Gdy kiełkuje pomysł o projekcie, wymagania leżą tuż obok, a w Visualu wybiera się <i>New-&gt;Project</i>, to fajnie byłoby mieć w głowie koncepcję dotyczącą budowy aplikacji. Jednym z problemów do rozwiązania jest właściwe zarządzanie projektem. Ogólnie znana prawda, mówi, że gdy piszemy program a on, daj Boże, działa, to znaczy że jest napisany „właściwie” (przynajmniej na tym najbardziej zewnętrznym poziomie). Czy tak jest faktycznie? Myślę, że podlega to indywidualnej ocenie.</p> <h2>Prosta klasyfikacja zależności</h2> <p><b>Zależności </b>możemy umownie podzielić na <b>pierwszoplanowe </b>– czyli takie, które opisują relację <b>pomiędzy zestawami i przestrzeniami</b> nazw naszego własnego kodu, oraz <b>drugoplanowe – pochodzące z zewnętrznych bibliotek</b>. Problem z zależnościami polega na tym, że nie przykładając uwagi do właściwego nimi zarządzania, ryzykujemy, że jakieś części programu, będą dostępne w miejscach gdzie być ich w ogóle nie powinno, lub po prostu nie są wykorzystywane, ale czasu zmarnowanego na ich kompilację nikt kompilatorowi nie zwróci :). Oprócz tego, dobrym argumentem na to by jak najszybciej ustalić sposób zarządzania zależnościami jest to, że odkładanie tej decyzji na później może skutkować po prostu spaghetti, w którym zacierają się odpowiedzialności poszczególnych modułów.</p> <h2>Kiedy mówimy o zależności?</h2> <pre class="language-aspnet" tabindex="0"><code class="language-aspnet">public LoggerViewModel(ILoggerRepo repo) { this._repo = repo; this.OpenProfileManagerCmd = new RelayCommand(this.OpenProfileManager); this.LogOnCmd= new RelayCommand(this.LogOns); } </code></pre> <p>W powyższym przykładzie widzimy, że klasa której konstruktor oglądamy jest zależna od interfejsu ILoggerRepo (jest to luźne powiązanie), a więc projekt zawierający klasę LoggerViewModel jest zależny od projektu w którym znajduje się interfejs ILoggerRepo. Oprócz tego widzimy, że LoggerViewModel wykorzystuje klasę RelayCommand, tym samym jest od niej uzależniony (od implementacji, więc jest to powiązanie ścisłe)</p> <pre class="language-aspnet" tabindex="0"><code class="language-aspnet">public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { bool isVisible = (bool) value; if (isVisible) { return Visibility.Visible; } else { return Visibility.Hidden; } }</code></pre> <p>&nbsp; Tutaj widzimy jedną z metod klasy pozwalającej na konwersję z wartości Boolean do enum’a Visiblity, który jest częścią zestawu System.Windows. Tym samym metoda Convert, jest uzależniona od System.Windows.Visiblity. Jeżeli z jakiegoś powodu dostęp do tego typu przestanie być możliwy, to CLR nie będzie w stanie skompilować naszego kodu. W projekcie mamy więc do czynienia z zależnościami na różnym poziomie: klas, metod, projektów…&nbsp;&nbsp;</p> <h2>&nbsp; „Unikanie problemów”</h2> <p>&nbsp; Podstawą podstaw jest&nbsp;<b>uzależnianie kodu od abstrakcji, a nie od implementacji</b>. W wielu językach zorientowanych obiektowo mamy konstrukcję zwaną interfejsem, a w tych w których jej nie mamy zazwyczaj możemy ją dosyć łatwo „emulować”.Oczywiście bezrefleksyjne tworzenie interfejsów, też nie prowadzi do cyfrowej krainy szczęśliwości, ale to temat na inną okazję. Faktem jest, że gdy abstrakcja jest dobra to zazwyczaj może korzystać z interfejsu, klasy abstrakcyjnej itd., i że gdy w dobrze prowadzonym projekcie nawet nie jest tak na początku, to w przyszłości (to znaczy gdy zaistnieje racjonalna przesłanka do utworzenia interfejsu) będzie.&nbsp;&nbsp;</p> <p>&nbsp; Zakładając, że projekt jest w większym lub mniejszym stopniu uniezależniony od implementacji należy zadbać o odpowiednią integrację jego elementów. Jednym z często spotykanych sposobów, zwłaszcza w kodzie zastanym, ale nie tylko – wystarczy poprzeglądać github’a – jest, mniej lub bardziej bezrefleksyjne dodawanie referencji do projektów, i równie bezrefleksyjne korzystanie z dyrektyw typu include, import, czy using. Często też nasze IDE, czy inne Resharper’y „ułatwiają” takie podejście.&nbsp;</p> <img src="/static/img/blog/DepndencyWithResharper.png" alt="" class=" postImage" /> <p>To powyżej to przykład fałszywego pojmowania uniezależniania kodu od implementacji. Mamy interfejs, ale tuż obok tworzymy obiekt, korzystając jeszcze ze słowa kluczowego ‚new’. Tak naprawdę stworzona tu została po prostu silna zależność, a programista pewnie chętnie naciśnie [alt] + [enter]. Dużo lepszym sposobem byłoby po prostu takie napisanie kodu aby korzystał z zalet dependency injection, lub w ostateczności napisanie jakiejś fabryki, ukrywającej szczegóły implementacji.</p> <pre class="language-aspnet" tabindex="0"><code class="language-aspnet">public CategoriesController(ICategoriesRepo repo) { this.repo = repo; }</code></pre> <p>Kolejną kwestią jest samo rozmieszczenie kodu w projektach. W miarę możliwości dbajmy o to aby interfejsy znajdowały się w oddzielnych zestawach niż implementacja. Jest to postulat stawiany przez wzorzec zarządzania zależnościami&nbsp;The Stairway&nbsp;(spotkalem się z nim przy okazji czytania&nbsp;<a href="http://www.amazon.com/Adaptive-Code-via-principles-Developer/dp/0735683204" target="_blank">Adaptive Code via C#</a>&nbsp;– polecam). Dzięki temu istnieje duża szansa, że jedynym miejscem w kodzie, które będzie znało wszystkie implementacje, będzie konfiguracja naszego&nbsp;kontenera IoC, a tym samym elastyczność projektu wzrośnie.</p> <p>Ten post, nie aspiruje do bycia tutorialem, a raczej notką skłaniającą do ponownego spojrzenia na nasz projekt, na każdej płaszczyźnie, od poziomu architektury do poszczególnych metod, pod kątem zależności. Zanim jednak zaczniemy dokonywać jakichkolwiek zmian, zastanówmy się w szerszym kontekście nad ich opłacalnością w stosunku do planowanej przyszłości projektu, kosztami jakie będziemy musieli ponieść i korzyściami jakie odniesiemy. 🙂</p>