Część 1
Używanie C# i .NET
W pierwszej części tej książki odbędziemy krótką wycieczkę po języku C# i porozmawiamy o niektórych jego cechach. Rozdział 1 wyjaśnia, czym są C# oraz .NET i dlaczego mielibyśmy ich używać (albo nie) w swoich projektach. Rozdział 2 wchodzi głębiej w rozmaite iteracje .NET oraz omawia proces kompilacji metod języka C#, zatrzymując się przy każdym istotnym kroku.
Choć część ta jest w istocie wprowadzeniem do książki, nadal zapewnia nieocenione informacje również dla kogoś, kto dobrze zna C#. Część wiedzy przedstawianej w tych pierwszych rozdziałach jest tym, co trzeba poznać, zanim przejdziemy do bardziej zaawansowanych tematów.
Przedstawiamy C# i .NET
W tym rozdziale
Czym są C# i .NET
Dlaczego powinniśmy używać C# w swoich projektach (i dlaczego nie powinniśmy)
Przełączanie się na C# i jak rozpocząć
O nie, kolejna książka o C#? Tak, jeszcze jedna. Mnóstwo książek powstało wokół tematyki C# i .NET, ale tę cechuje jedna fundamentalna różnica: napisałem ją, aby pomóc w tworzeniu czystego, idiomatycznego kodu C# w swojej codziennej pracy. Nie jest to podręcznik źródłowy, ale raczej praktyczny przewodnik. Nie wyjaśniam w niej takich rzeczy, jak napisać instrukcję if, czym jest sygnatura metody ani czym są obiekty. Nie zajmujemy się tu składnią - zakładam, że ten poziom opanowania języka Czytelnik ma już za sobą - ale skupiamy się na koncepcjach i ideach. Istnieje znacząca różnica pomiędzy znajomością składni języka a zdolnością do pisania przejrzystego, idiomatycznego kodu. Po przejściu całej książki To właśnie powinien wynieść Czytelnik z lektury tej książki. Bez względu, jakie Czytelnik ma przygotowanie i jakie zna języki programowania, o ile rozumie koncepcje programowania obiektowego, książka ta pozwoli mu skutecznie przejść do ekosystemu C# i .NET, co pokazuje rysunek 1.1.
Rysunek 1.1 Każdy rozdział zawiera diagram postępów, który pozwala szybko ustalić miejsce, w którym się znajdujemy.
Co wspólnego ze sobą mają takie organizacje, jak Microsoft, Google i rząd Stanów Zjednoczonych? Wszystkie one używają C# - i mają do tego dobry powód. Ale dlaczego? C# to przecież tylko "kolejny język" programowania. Wykazuje podobieństwo do Javy i C++, pozwalając zarówno na programowanie obiektowe, jak i funkcjonalne, a także korzysta z wielkiego wsparcia ogromnej społeczności open source. Wspaniale. Ale dlaczego mielibyśmy się tym przejmować? W tym rozdziale szczegółowo zajmę się tym zagadnieniem, ale na początek kilka spoilerów: C# jest doskonały, jeśli chodzi o tworzenie skalowalnego oprogramowania. Wszystko, czego potrzebujemy, aby zacząć pisać kod C#, to dowolnie wybrane .NET SDK (więcej na ten temat w rozdziale 2) i - zapewne - jakieś IDE. Zarówno język, jak i runtime (środowisko wykonawcze) należą do domeny open source.
Za każdym razem, gdy szukamy online jakiś informacji o C#, niemal na pewno natrafimy na termin .NET Framework. O .NET Framework można myśleć jak o ciepłym kocyku, trzaskającym ogniu w kominku i kubku gorącej czekolady w zimowy dzień. Dostarcza wszystkiego, czego potrzebujemy: bibliotek opakowujących niskopoziomowe API Windows, eksponuje powszechnie używane struktury danych i zapewnia wrappery dla złożonych algorytmów. Codzienne tworzenie oprogramowania w C# niemal na pewno będzie wiązało się z używaniem .NET Framework, .NET Core albo .NET 5, zatem zbadamy te frameworki i to, gdzie są odpowiednie.
Rysunek 1.2 pokazuje, jak tematyka tej książki wpasowuje się w ogólną architekturę aplikacji webowej .NET. Przedstawia również architekturę, której użyjemy do całkowitego przekonstruowania (przepisania) istniejącej aplikacji, co rozpoczniemy w rozdziale 5 (zielone/przerywane strzałki wskazują tę ścieżkę).
Rysunek 1.2 Przykład architektury typowej usługi webowej opartej na stosie Microsoft. W książce tej będziemy realizować podejście pokazane zielonymi/przerywanymi strzałkami. Omówimy warstwy prezentacji, logiki biznesowej oraz dostępu do danych.
Dla tych Czytelników, którzy mają już wcześniejsze doświadczenia w C#: książkę tę można spozycjonować pomiędzy poziomem początkowym i zaawansowanym. Umiejętności, których można się z niej nauczyć, wypełniają lukę w wiedzy i zapewniają podstawy dla zaawansowanych zadań. Pierwsze dwa rozdziały mogą się wydawać powtórką ze znanych już zagadnień, ale zachęcam, aby ich nie pomijać. Zawsze warto odświeżyć sobie posiadaną wiedzę.
1.1 Dlaczego warto pracować w C#?
Ktoś, kto już zna jakiś język programowania inny niż C# i lubi się nim posługiwać, może zapytać, dlaczego miałby w ogóle zacząć używać C#? Dobrym powodem może być zatrudnienie się w firmie, która używa tylko C#. A być może ktoś chciałby po prostu dowiedzieć się, o co w tym wszystkim chodzi.
Obiecuję, że nie będę w kółko powtarzać, że C# jest "silnie typowanym obiektowym językiem programowania, który umożliwia międzyplatformowe wytwarzanie skalowalnego oprogramowania klasy przedsiębiorstwa". Czytelnik zapewne już jest tego świadomy i analizowanie tego stwierdzenia zapewne nie jest najbardziej ekscytującym zadaniem. W tym podrozdziale omówię wszystkie modne słowa zawarte w tej definicji tylko raz - i nie będziemy już do tego wracać. Podejmę ryzyko, że zabrzmi to tak, jakbym był pracownikiem działu marketingu Microsoftu, w pozostałej części tego podrozdziału skupię się na następujących wyróżniających cechach i przypadkach użycia języka C#:
C# (oraz ekosystem .NET) umożliwia wytwarzanie oprogramowania w sposób ekonomiczny. Efektywne rozwiązania są ważne, gdyż wytwarzanie na potrzeby przedsiębiorstw jest chlebem powszednim pracy w C#.
C# pozwala ulepszyć stabilność i utrzymywanie kodu dzięki wsparciu dla samo-dokumentowanego kodu, bezpiecznym bibliotekom i łatwości użycia.
C# jest przyjazny dla dewelopera i łatwy w użyciu. Nie ma nic gorszego, niż odkrycie, że język programowania, którego chcemy użyć, nie zapewnia dobrego wsparcia dla rzeczy, które naprawdę lubimy (takich jak stabilny menedżer pakietów, dobre wsparcie dla testów jednostkowych i międzyplatformowe środowisko wykonawcze).
Oczywiście pisanie skalowalnego, dającego się utrzymywać i przyjaznego dla deweloperów "czystego kodu" można zrealizować w większości (jeśli nie w każdym) języków programowania. Różnica tkwi w komforcie pracy dewelopera. Niektóre języki są naprawdę dobre w pomaganiu tworzenia czystego kodu, podczas gdy inne takie nie są. C# nie jest doskonały, ale naprawdę stara się pomóc nam w tym względzie.
1.1.1 Powód 1: C# jest ekonomiczny
C# jest darmowy - zarówno w tworzeniu, jak i w stosowaniu. Język i platforma w całości należą do świata open source, cała dokumentacja jest bezpłatna, a większość narzędzi oferuje darmowe opcje. Dla przykładu, typowa konfiguracja C# obejmuje instalację C# 8, .NET 5 oraz Visual Studio Community. Wszystkie te komponenty są bezpłatne i będziemy używać ich w tej książce. Środowisko wykonawcze nie wymaga żadnych opłat licencyjnych, a produkt końcowy można wdrożyć w dowolnie wybranym miejscu.
1.1.2 Powód 2: C# jest łatwy w utrzymaniu
Ilekroć w tej książce piszę o utrzymywaniu kodu, mam na myśli zdolność do poprawiania błędów, zmian funkcjonalności i rozwiązywania innych problemów bez niezamierzonych efektów ubocznych. Może się to wydawać oczywistym wymaganiem dla dowolnego języka programowania, ale w rzeczywistości jest bardzo trudne do zaimplementowania. C# zawiera funkcjonalności, które poprawiają utrzymywanie (a dzięki temu bezpieczne rozszerzanie) obszernych baz kodu. Dla przykładu rozważmy typy ogólne oraz Language-Integrated Query (LINQ). Obie te rzeczy omówimy szczegółowo w dalszej części książki, ale są to przykłady funkcjonalności platformy, które pomagają w pisaniu lepszego kodu.
Pozornie, z punktu widzenia firmy zdolność utrzymywania kodu nie musi być priorytetem numer jeden, ale jeśli tworzymy kod, który jest utrzymywalny (przez co rozumiem czysty kod, który jest łatwo rozszerzalny i oparty na testach), koszty wytwarzania znacząco maleją. Spadek kosztów wytwarzania dzięki pisaniu utrzymywalnego kodu może się początkowo wydawać czymś sprzecznym z intuicją: zaprojektowanie i napisanie takiego kodu wymaga więcej czasu, co zwiększa początkowe koszty wytwarzania. Wystarczy jednak wyobrazić sobie, co nastąpi nieco później, gdy użytkownicy odkryją błąd albo zapragną dodatkowej funkcjonalności. Jeśli napisany przez nas kod jest utrzymywalny, możemy szybko i łatwo znaleźć ten błąd (i go naprawić). Dodanie nowej funkcji jest prostsze, gdyż baza kodu jest rozszerzalna. Jeśli możemy łatwo rozszerzać i poprawiać bazę kodu, koszty wytwarzania zmaleją.
Zasada otwarte/zamknięte (Open/Closed Principle)
W roku 1988 francuski informatyk Bertrand Meyer (twórca języka programowania Eiffel) opublikował książkę zatytułowaną Object-Oriented Software Construction (Prentice Hall, 1988). Wydanie tej książki było punktem zwrotnym w historii obiektowego programowania i projektowania, gdyż wprowadza ona zasadę otwarte/zamknięte (Open/Closed Principle - OCP). Zasada OCP skupia się na usprawnianiu utrzymywalności i elastyczności projektów programistycznych. W ujęciu Meyera OCP oznacza, że "byty programistyczne (klasy, moduły, funkcje itp.) powinny być otwarte na rozszerzanie, ale zamknięte na modyfikacje".
Co jednak OCP oznacza w praktyce? Aby to zbadać, zastosujmy OCP do klasy: postrzegamy klasę jako "otwartą" na rozszerzenie i "zamkniętą" na modyfikację, jeśli możemy dodać do niej jakąś funkcjonalność bez zmieniania już istniejącej (a dzięki temu bez psucia innych części naszego kodu). Jeśli będziemy trzymać się tej reguły, ryzyko wprowadzenia zakłóceń (albo nowego błędu) w istniejącym kodzie jest znacznie mniejsze, niż gdybyśmy próbowali wymusić poprawkę albo nową funkcję bez zwracania uwagi na utrzymywanie i rozszerzalność kodu. Gdy pracujemy z kodem, ktory jest bardziej złożony (i sprzężony; omówimy to w rozdziale 8), rośnie prawdopodobieństwo wprowadzania nowych błędów z powodu niezrozumienia efektów ubocznych naszych zmian. Jest to coś, czego wolelibyśmy uniknąć za wszelką cenę.
1.1.3 Powód 3: C# jest przyjazny dla dewelopera i łatwy w użyciu
Najczęstsze zastosowania C# to wytwarzanie oprogramowania na potrzeby przedsiębiorstw i zarazem miejsce, w których C# i .NET szczególnie błyszczą. Jak powinna wyglądać idealna baza kodu w środowisku przedsiębiorstwa? Zapewne chcielibyśmy mieć bazę kodu, po której można się łatwo poruszać, opartą na solidnym menedżerze pakietów i wspomaganą testami (jednostkowymi, integracyjnymi i dymnymi). Dodajmy do tego jeszcze doskonałą dokumentację oraz wieloplatformowość.
Definicja Poprzez samodokumentujący się kod (self-documenting code) rozumiemy kod, który jest napisany wystarczająco jasno, że nie potrzeba komentarzy objaśniających logikę. Kod dokumentuje się sam. Dla przykładu, jeśli mamy metodę o nazwie DownloadDocument, inni będą mieli oczywiste skojarzenie, co ona robi. Nie ma potrzeby dodawania komentarza wyjaśniającego, że logika w tej metodzie pobiera (download) dokument.
Na domiar złego, zapewne będziemy chcieli mieć dobrą integrację z usługą chmurową, aby zapewnić ciągłą integrację i dostarczanie (continuous integration and delivery - CI/CD). Pragmatyczne podejście mówi nam, że prawdopodobieństwo posiadania takiej bazy kodu nie jest najwyższe. Naturalnie ta lista życzeń jest nierealistyczna w większości scenariuszy. Jednak jeśli chcemy osiągnąć przynajmniej niektóre z tych rzeczy (albo wszystkie, jeśli ktoś lubi przygody), C# przynajmniej nie będzie nam w tym przeszkadzać. Zapewnia istniejące już przepływy pracy, funkcjonalności i natywne biblioteki pozwalające pokonać 99% drogi do wymarzonego celu.
Deweloperzy przechodzący z takich języków, jak Java, zauważą pewne podobieństwa w strukturze projektu. Choć istnieją także pewne różnice, nie są one zbyt wielkie. Strukturę projektu C# omówimy szczegółowo w całej książce.
.NET zapewnia również wsparcie dla wielu popularnych frameworków testowych. Microsoft udostępnia Visual Studio Unit Testing Framework, który zawiera MSTest (a poprzez uogólnianie niekiedy jest tak nazywany). MSTest jest po prostu środowiskiem wiersza polecenia dla Visual Studio Unit Testing Framework. Inne typowo używane frameworki testowe to xUnit i NUnit. Można również znaleźć wsparcie dla frameworków imitacyjnych (mocking), takich jak Moq (Moq jest analogiem środowiska Mockito w Javie albo GoMock w Go; więcej informacji na temat stosowania Moq w testach jednostkowych zawiera podrozdział 10.3.3.), SpecFlow (wytwarzanie sterowane zachowaniami, podobne do Cucumber), NFluent (biblioteka płynnych asercji), FitNesse i wielu innych.
Na koniec można uruchamiać kod C# na najrozmaitszych platformach, choć z pewnymi ograniczeniami (niektóre starsze platformy mogą korzystać jedynie ze starszych wersji C# i .NET Framework). Przy stosowaniu .NET 5 możemy uruchamiać ten sam kod w Windows 10, Linuksie i macOS. Ta funkcjonalność miała swój początek jako .NET Core, odgałęzienie .NET Framework, które następnie zostało scalone z .NET Framework (i innymi frameworkami), tworząc .NET 5. Kod C# można nawet uruchamiać w systemach iOS i Android za pośrednictwem narzędzia Xamarin, a także na platformach PlayStation, Xbox oraz Nintendo Switch za pomocą Mono.
1.2 Kiedy lepiej nie pracować w C#?
C# nie jest najlepszym wyborem dla każdego i każdych okolicznościach. Oczywiste jest, że powinniśmy zawsze wybierać najlepsze narzędzie dla konkretnej roboty. C# działa świetnie w szerokim zakresie zastosowań. Jednak trzeba mieć na uwadze, że istnieje kilka przypadków użycia, w których nie będziemy chcieli (ani nawet rozważali) użycia C# oraz .NET, a mianowicie:
Tworzenie systemu operacyjnego
Kod dla systemów czasu rzeczywistego (oprogramowanie wbudowane)
Przetwarzanie numeryczne
Przyjrzyjmy się w skrócie, dlaczego C# nie jest dobrym wyborem dla tych przypadków użycia.
1.2.1 Tworzenie systemu operacyjnego
Budowanie systemów operacyjnych (OS) jest niezwykle ważnym elementem inżynierii oprogramowania (wręcz kamieniem węgielnym), jednak tylko nieliczni programiści faktycznie tworzą systemy. Wytworzenie OS zajmuje mnóstwo czasu i starań, a odpowiednie bazy kodu rutynowo obejmują miliony wierszy kodu, tworzonego i utrzymywanego przez wiele lat, a czasami dziesięcioleci.
Główny powód, dla którego C# nie nadaje się do tworzenia OS, sprowadza się do niezgodności pomiędzy ręcznym zarządzaniem pamięcią (kod niezarządzany) a procesem kompilacji C#. Wprawdzie C# pozwala na używanie wskaźników przy korzystaniu z trybu "unsafe", nie może równać się z takim językiem programowania, jak C, jeśli chodzi o łatwość ręcznego zarządzania pamięcią.
Inny problem związany z próbą użycia C# do tworzenia OS jest jego częściowa zależność od kompilatora just-in-time (JIT) (więcej informacji na ten temat zawiera rozdział 2). Wyobraźmy sobie, że musimy uruchomić nasz system operacyjny za pomocą maszyny wirtualnej. Natychmiast ujawni się problem wydajnościowy, gdyż maszyna wirtualna będzie musiała ciągle czekać i nadganiać, aby wykonać kod skompilowany na bieżąco, analogicznie do tego, co się dzieje, gdy kod .NET działa w naszym komputerze. To spostrzeżenie oznacza, że w pełni statycznie kompilowany język jest lepiej dopasowany do potrzeb tworzenia OS.
Tym niemniej istnieją systemy operacyjne napisane w językach wyższego poziomu. Przykładem takim jest Pilot-OS (utworzony przez Xerox PARC w roku 1977), napisany w języku Mesa1, poprzedniku Javy.
Jeśli ktoś chciałby dowiedzieć się więcej na temat tworzenia systemów operacyjnych, doskonałym źródłem może być społeczność osdev.org (wiki.osdev.org). Można tam znaleźć wskazówki, tutoriale i sugerowane lektury. Ze źródeł dotyczących nauki języka C warto wymienić książę Jensa Gustedta Modern C (Manning, 2019) oraz klasyczne dzieło The C Programming Language Briana Kernighana i Dennisa Ritchie'go (Prentice Hall, 1988)2.
1.2.2 Tworzenie wbudowanych systemów czasu rzeczywistego w C#
Analogicznie jak w przypadku budowania systemu operacyjnego (podrozdział 1.2.1), kod dla systemów czasu rzeczywistego (real-time operating system - RTOS), który najczęściej spotykamy w tzw. systemach wbudowanych, napotyka znaczące problemy wydajnościowe, gdy jest wykonywany za pośrednictwem maszyny wirtualnej. RTOS skanuje kod liniowo, w czasie rzeczywistym i wykonuje instrukcje w konfigurowalnych interwałach, zmieniających się od jednej operacji na sekundę do wielu działań na mikrosekundę, zależnie od życzeń dewelopera i możliwości mikrokontrolera lub sterownika programowalnego (programmable logic controller - PLC), w którym działa ten kod. Maszyna wirtualna w pewnym stopniu wyklucza prawdziwe działanie w czasie rzeczywistym ze względu na opóźnienia i nadmiarowość wprowadzaną w czasie działania.
Osoby zainteresowane kodem dla RTOS i wytwarzaniem oprogramowania wbudowanego mogą zainteresować się wieloma cenionymi książkami, takimi jak An Embedded Software Primer Davida E. Simona (Addison-Wesley Professional, 1999) albo Making Embedded Systems: Design Patterns for Great Software autorstwa Elecii White (O'Reilly Media, 2011).
1.2.3 Przetwarzanie numeryczne a C#
Przetwarzanie numeryczne (nazywane też analizą numeryczną) dotyczy badania, rozwijania i analizowania algorytmów. Osoby pracujące przy przetwarzaniu numerycznym (zazwyczaj informatycy lub matematycy) używają przybliżeń numerycznych do rozwiązywania problemów w niemal każdej dziedzinie nauki i inżynierii. Z perspektywy języka programowania stwarza to unikatowe wyzwania i uwarunkowania. Każdy język programowania może obliczać matematyczne wzory i formuły, jednak niektóre zostały zbudowane specjalnie do tego celu.
Rozważmy tworzenie wykresów. C# naturalnie jest w stanie obsłużyć kreślenie, ale jaką wydajność i wygodę może zaoferować, gdy porównamy go z czymś takim, jak MATLAB? (MATLAB jest zarówno środowiskiem obliczeniowym, jak i językiem programowania stworzonym przez MathWorks). Krótka odpowiedź brzmi - to w ogóle nie jest porównywalne. Programowanie graficzne w C# oznacza pracę w czymś podobnym do WPF (który wykorzystuje Direct3D), OpenGL, DirectX albo jeszcze innej bibliotece graficznej innego dostawcy (zazwyczaj nakierowanej na gry wideo). W przypadku MATLAB mamy język, który idealnie pasuje do środowiska zbudowanego w celu renderowania złożonych wykresów dwu- i trójwymiarowych. Musimy jedynie wywołać plot(x, y), a MATLAB samodzielnie sporządzi potrzebny wykres.
Tak więc, C# potrafi wykonywać obliczenia numeryczne, ale nie oferuje takiej prostoty użycia, jak język wyposażony w biblioteki wysokiego poziomu oraz abstrakcje zajmujące się kreśleniem wykresów, jak MATLAB. Jeśli ktoś chciałby dowiedzieć się więcej na temat MATLAB lub przetwarzania numerycznego, wśród licznych dostępnych źródeł warto wymienić Numerical Methods for Scientists and Engineers Richarda Hamminga (Dover Publications, 1987), MATLAB: An Introduction with Applications Amosa Gilata (Wiley, 2016) oraz tutorial Cody dla samego MATLAB-a (https://www.mathworks.com/matlabcentral/cody).
1.3 Przełączanie się na C#
Ze względu na podobieństwo pomiędzy tymi językami deweloperzy z dobrą znajomością języków opartych na składni maszyny wirtualnej Java (JVM) (najważniejsze z nich to naturalnie Java, a ponadto Scala i Kotlin) albo C++ mogą mieć łatwiej podczas lektury tej książki, niż ktoś, kto przychodzi od języka o stylu innym niż C, nie opartego na maszynie wirtualnej lub języków skoncentrowanych na działaniach chmurowych, takich jak Dart, Ruby lub Go. Doświadczenie w językach "nie-C" nie oznacza jednak, że C# będzie niemożliwy do zrozumienia. Być może okaże się, że niektóre fragmenty trzeba będzie przeczytać dwukrotnie, ale ostatecznie wszystko powinno się udać.
Czytelnicy, którzy przychodzą ze środowiska języka interpretowanego, takiego jak Python, mogą początkowo postrzegać proces kompilacji .NET za coś dziwacznego. Języki leżące w granicach .NET używają dwustopniowego procesu kompilacji. Najpierw kod jest kompilowany statycznie do języka niższego poziomu, nazywanego Common Intermediate Language (wspólny język pośredni, w skrócie CIL, IL albo MSIL, MS oznacza tu Microsoft - programiści Javy zauważą, że jest to coś analogicznego do kodu bajtowego Javy), który następnie jest kompilowany na żądanie (just-in-time - JIT) na natywny kod maszynowy, gdy środowisko wykonawcze .NET wykonuje kod na docelowym hoście. Może się to wydawać zbyt dużo do strawienia na raz, ale po kilku kolejnych rozdziałach Czytelnicy wszystko zrozumieją.
Dla osób przychodzących z języka skryptowego, takiego jak JavaScript, statyczne typowanie może się wydawać ograniczeniem i powodować frustrację. Jednak gdy już przyzwyczają się do pamiętania cały czas, jaki typ jest używany, zapewne docenią to i polubią.
Natomiast tych, którzy dotychczas posługiwali się takim językiem, jak Go lub Dart, dla których niekiedy trudno jest znaleźć natywne biblioteki, .NET 5 może zaskoczyć bogactwem dostępnych bibliotek. Zapewniając funkcje dla większości rzeczy, o których można pomyśleć, biblioteki .NET są podstawowym źródłem funkcjonalności. Wiele aplikacji napisanych w .NET nigdy nie używa żadnych bibliotek innych firm.
Dla zachowania porządku zajmijmy się teraz narzędziami. W tym rozdziale nie będziemy zajmować się instalowaniem IDE ani .NET SDK. Jeśli ktoś jeszcze nie wykonał tych instalacji i potrzebuje nieco pomocy, dodatek C zawiera kilka szybkich poradników. Aby móc podążać za przykładami przedstawionymi w książce, konieczne jest zainstalowanie najnowszych wersji .NET Framework oraz .NET 5. W tej książce zaczynamy od starej bazy kodu wykorzystującej .NET Framework. Z tego względu będziemy używać .NET Framework do uruchamiania tego starego kodu w miarę migrowania go do .NET 5.
Jak wspomniałem wcześniej, C# jest językiem open source i jest utrzymywany przez społeczność - przy znaczącej pomocy ze strony Microsoftu. Nie trzeba płacić za licencje na środowisko wykonawcze, SDK ani IDE. Jeśli chodzi o IDE, Visual Studio (to środowisko edycyjne jest używane w przykładach przedstawionych w książce) oferuje bezpłatną edycję Community, której można używać do tworzenia osobistych projektów i oprogramowania open source. Jeśli ktoś preferuje aktualnie używane IDE, są spore szanse, że można znaleźć do niego wtyczkę obsługującą C#. Można również posługiwać się trybem wiersza polecenia do kompilowania, uruchamiania i testowania projektów C#, choć osobiście zdecydowanie zachęcam, aby dać szansę narzędziom dedykowanym dla C# (Visual Studio), gdyż zapewnia to najwygodniejszą pracę i jest najprostszą drogą do pisania idiomatycznego kodu C#.
Wiele koncepcji i technik, które znamy z innych miejsc, można przenieść do C#, ale nie wszystkie. C# osiągnął większą dojrzałość w mechanizmach backendu, niż frontendu, gdyż tradycyjnie używany jest głównie w takich celach. Historyczna koncentracja na tworzeniu backendów w C# nie oznacza, że możliwości dotyczące frontendu robią mniejsze wrażenie. Można napisać kompletną aplikację jedynie w C#, bez konieczności dotykania JavaScript. Wprawdzie ta książka również skupia się na tworzeniu backendu, wiele pokazanych tu koncepcji może być pomocnych również przy projektowaniu frontendu.
Wyobraźmy sobie teraz metodę-potwora, z pięcioma zagnieżdżonymi pętlami for, mnóstwem zafiksowanych liczb ("magiczne cyfry") i większą liczbą komentarzy, niż kodu. Wyobraźmy sobie teraz, że jesteśmy nowym członkiem zespołu, który właśnie do niego dołączył. Jak się poczujemy, gdy uruchomimy IDE, ściągniemy kod źródłowy i zobaczymy taką metodę? Rozpacz to za mało powiedziane. A teraz wyobraźmy sobie, że przenieśliśmy wszystkie indywidualne działania tego potwora w ich własnych, mniejszych metodach (zapewne krótszych niż 5 do 10 wierszy kodu). Jak teraz wygląda nasz potwór? Zamiast być gąszczem trudnych do prześledzenia zależności i przypisań, bez widocznego sensu, o ile nie dysponujemy bardzo specjalistyczną wiedzą dziedzinową, kod daje się czytać, jak zwykła proza. Jeśli do tego odpowiednio ponazywamy nasze metody, metoda główna powinna teraz wyglądać jak przepis na potrawę, którą potrafi ugotować nawet najgorszy kucharz.
Kiedy piszę o "czystym kodzie", mam na myśli praktyki kodowania zalecane przez Roberta C. Martina w jego tutorialach wideo (https://cleancoders.com/videos) oraz książkach Clean Code (Prentice Hall, 2008), Clean Architecture (Prentice Hall, 2017) oraz, wspólnie z Micah Martin, Agile Principles, Patterns, and Practices in C# (Pearson, 2006). Nie wolno też zapominać o ukutych przez niego zasadach "SOLID": Single Responsibility Principle (zasada jednej odpowiedzialność), Open/Closed Principle (zasada otwarte/zamknięte), Liskov Substitution Principle (zasada podstawienia Liskov), Interface Segregation Principle (zasada segregacji interfejsów) oraz Dependency Inversion Principle (zasada odwrócenia zależności). Zasady czystego kodu wyjaśnię dogłębnie, gdy przyjdzie na to czas, wraz z praktycznymi informacjami, jak ich faktycznie używać.
Dlaczego jednak w ogóle mamy trudzić się pisaniem czystego kodu? Czysty kod działa jak pralka wobec błędów i niewłaściwej funkcjonalności. Jeśli włożymy naszą bazę kodu do pralki czystego kodu, jak na rysunku 1.3, zobaczymy, że gdy zrefaktoryzujemy coś, aby było "czyste", błędy wychodzą na wierzch, a niewłaściwe funkcjonalności tkwią schwytane w światła reflektorów, bez miejsca do ukrycia. Ostatecznie "wszystko wychodzi w praniu". Oczywiście, refaktoryzacja kodu produkcyjnego wiąże się także z ryzykiem; często pojawiają się niezamierzone efekty uboczne. Sprawia to, że zarządowi trudniej jest zaaprobować wielkie refaktoryzacje bez dodawania funkcjonalności. Jednak dysponując odpowiednimi narzędziami (niektóre zostaną omówione w tej książce) można zminimalizować ryzyko negatywnych efektów ubocznych i poprawić jakość bazy kodu.
Rysunek 1.3 Metodologia czystego kodu jest jak pralka dla naszego kodu. Przyjmuje brudną bieliznę (nasz kod), dodaje mydło i wodę (zasady czystego kodu) i oddziela brud od ubrań (odseparowuje błędy od kodu). To co z niej wychodzi, to ubrania (kod) z mniejszą ilością brudu (błędów), niż było na początku.
Książka zawiera szereg ramek zawierających informacje o zagadnieniach czystego kodu. Jeśli ramka dotyczy czystego kodu, oznakowuję ją jako taką i wyjaśniam zarówno koncepcje, jak i sposoby ich stosowania w rzeczywistym świecie. Dodatek B zawiera listę kontrolną czystego kodu. Można ją wykorzystać w celu ustalenia, czy potrzebujemy zrefaktoryzować istniejący kod. Lista ta służy też jako ściągawka dla niektórych z częściej zapominanych (choć nadal ważnych) koncepcji.
1.4 Czego można się nauczyć z tej książki
Książka ta ma na celu nauczenie pisania idiomatycznego i czystego kodu C#. Nie uczy języka C#, .NET 5 ani programowania od podstaw. Będziemy stosować się do praktycznego podejścia: realistycznego scenariusza biznesowego, w którym wykonamy refaktoryzację starego API, aby było czystsze i bezpieczniejsze. Po drodze nauczymy się wielu rzeczy. Oto niektóre z nich:
Refaktoryzacja starej (istniejącej) bazy kodu pod kątem zabezpieczeń, wydajności i przejrzystości.
Pisanie samo-dokumentowanego kodu, który może przejść dowolny przegląd.
Używanie wytwarzania sterowanego testami w celu pisania testów równolegle z kodem implementacji.
Bezpieczne łączenie się z chmurową bazą danych za pomocą Entity Framework Core.
Wprowadzenie zasad czystego kodu do istniejącej bazy kodu.
Czytanie Common Intermediate Language (języka pośredniego) i wyjaśnienie procesu kompilacji C#.
Co zatem musimy wiedzieć, aby najbardziej skorzystać z tej książki? Oczekuję, że Czytelnik zna i rozumie podstawowe zasady programowania obiektowego (dziedziczenie, enkapsulacja, abstrakcja i polimorfizm) i dobrze zna przynajmniej jeden inny język programowania, który pozwala tworzyć kod za pomocą podejścia obiektowego (na przykład C++, Go, Python lub Java).
Po przeczytaniu tej książki Czytelnik będzie pisać czysty, bezpieczny, testowalny kod C#, zgodny z dobrymi praktykami i zasadami projektowania obiektowego. Dodatkowo będzie gotowy na dalsze pogłębianie swojej znajomości C# poprzez zaawansowane źródła. Niektóre sugerowane lektury obejmują: Jon Skeet, C# in Depth, 4 wydanie (Manning, 2019), Jeffrey Richter, CLR via C#, 4 wydanie (Microsoft Press, 2012), Bill Wagner Effective C#, 2 wydanie (Microsoft Press, 2016), Dustin Metzgar, .NET Core in Action (Manning, 2018), John Smith, Entity Framework Core in Action, 2 wydanie (Manning, 2021) oraz Andrew Lock, ASP.NET Core in Action, 2 wydanie (Manning, 2021).
1.5 Czego nie nauczymy się z tej książki
Książka ta ma na celu wypełnienie luki pomiędzy źródłami dla początkujących i dla zaawansowanych. Cel ten pociąga za sobą pewne konsekwencje co do założeń, które poczyniłem na temat znajomości języka C# i programowania w ogólności. Jak wyjaśniłem wcześniej, oczekuję, że Czytelnik ma pewne profesjonalne doświadczenia programistyczne i że czuje się swobodnie w zakresie podstaw języka C# lub innego obiektowego języka programowania.
Co przez to rozumiem? Aby jak najwięcej skorzystać z tej książki, Czytelnik powinien rozumieć zasady programowania obiektowego i być w stanie tworzyć podstawowe aplikacje lub API w swoim ulubionym języku programowania. W rezultacie książka ta nie próbuje uczyć żadnego z poniższych zagadnień, które zazwyczaj są zawarte w podręcznikach programowania dla początkujących:
Samego języka C#. To nie jest książka w rodzaju C# od podstaw albo Elementarz C#. Zamiast tego zamierzam pokazać, jak podnieść już posiadaną wiedzę o języku C# lub programowaniu obiektowym na wyższy poziom.
Składni instrukcji warunkowych i sterujących, które nie są specyficzne dla C# (if, for, foreach, while, do-while, itp.).
Czym są polimorfizm, enkapsulacja i dziedziczenie (choć pojęcia te będą regularnie używane w całej ksiażce).
Czym jest klasa i jak wykorzystujemy klasy do modelowania obiektów świata rzeczywistego.
Czym są zmienne ani jak przypisywać do nich wartości.
Jeśli ktoś dopiero rozpoczyna przygodę z programowaniem, zdecydowanie zalecam przeczytanie najpierw innej książki, takiej jak Head First C# autorstwa Jennifer Greene i Andrew Stellmana, 4 wydanie (O'Reilly, 2020) albo Structure and Interpretation of Computer Programs Harolda Abelsona, Geralda Jaya Sussmana i Julie Sussman, 2 wydanie (The MIT Press, 1996)3.
Książka ta nie obejmuje również poniższych, bardziej specjalistycznych sposobów posługiwania się językiem C#:
Architektura mikrousług. W tej książce nie zajmuję się tym, czym są mikrousługi i jak ich używać. Architektura mikrousług jest obecnie wiodącym trendem i jest użyteczna w bardzo wielu sytuacjach, ale nie jest powiązana z C# ani z profesjonalnym tworzeniem kodu. Trzy wspaniałe źródła, z których można dowiedzieć się więcej na temat mikrousług, to książki Chrisa Richardsona Microservices Patterns (Manning, 2018), Prabatha Siriwardena oraz Nuwan Diasa Microservices Security in Action (Manning, 2019) oraz Christiana Horsdala Gammelgaarda Microservices in .NET Core (Manning, 2020).
Wykorzystywanie C# w środowiskach skonteneryzowanych, takich jak Kubernetes i/lub Docker. Choć jest to bardzo praktyczne i stosowane w wielu środowiskach deweloperskich, umiejętność korzystania z Kubernetes lub Dockera nie gwarantuje, że będziemy umieli "kodować jak profesjonalista" w C#. Więcej informacji na temat tych technologii można znaleźć np. w książkach Marko Lukša Kubernetes in Action, 2 wydanie (Manning, 2021), Eltona Stonemana Learn Docker in a Month of Lunches (Manning, 2020) oraz Ashley'a Davisa Bootstrapping Microservices with Docker, Kubernetes, and Terraform (Manning, 2021).
Współbieżność w C#, poza zagadnieniami wielowątkowości i blokad (omówionymi w rozdziale 6). Często napotykamy na to zagadnienie w wysokowątkowych, wydajnościowo istotnych scenariuszach. Większość deweloperów jednak nie ma do czynienia z kodem tego rodzaju. Jeśli jednak ktoś znalazł się w takiej sytuacji, doskonałym źródłem nauczenia się więcej o programowaniu współbieżnym w C# jest książka Joe Duffy'ego Concurrent Programming on Windows (Addison-Wesley, 2008).
Głębokie wewnętrzne szczegóły tak CLR, jak i samego .NET Framework. Choć CLR i .NET 5 są interesującymi tematami, znajomość ich wszystkich drobnych detali nie ma praktycznego zastosowania dla większości deweloperów. Książka ta omawia CLR oraz .NET Framework do pewnego poziomu szczegółów, ale zatrzymuje się, gdy dalsze detale stają się nieprzydatne lub nieporęczne. "Biblią" dla CLR i .NET Framework jest książka Jeffrey'a Richtera CLR via C#, 4 wydanie (Microsoft Press, 2012).
Czytelnik ma do wyboru dwa sposoby czytania tej książki. Sposób zalecany to przeczytanie jej w całości, od początku do końca i w tej kolejności. Jeśli jednak ktoś jest zainteresowany jedynie refaktoryzacją i najlepszymi praktykami, może przeczytać jedynie części od 3 do 6.
Podsumowanie
Książka ta nie omawia "programowania od podstaw". Zakłada (dość zaawansowaną) znajomość programowania obiektowego. Pozwala to skupić się na praktycznych koncepcjach.
C# i .NET 5 szczególnie wyróżniają się w tworzeniu skalowalnego oprogramowania klasy przedsiębiorstwa, ze skupieniem uwagi na stabilność i łatwość utrzymania. Sprawia to, że C# i .NET są doskonałym wyborem platformy zarówno dla firm, jak i indywidualnych deweloperów.
C# i .NET 5 nie sprawdzają się w takich zagadnieniach, jak tworzenie systemów operacyjnych, oprogramowania wbudowanego czasu rzeczywistego czy przetwarzaniu numerycznym (albo analizach). Do takich zadań lepiej pasują C lub MATLAB.
1 Nazwa "Mesa" (jedno ze znaczeń tego wyrazu to "góra stołowa") jest żartem, odnoszącym się do tego, że język programowania jest tak wysokiego poziomu, że wygląda jak izolowane, wyniosłe wzgórze z płaskim szczytem.
2 Pełne zestawienie polecanych lektur znajduje się w dodatku E. Tamże zostały podane polskie wydania wymienionych książek, o ile istnieją (przyp. tłum.).
3 Książka jest dostępna za darmo w witrynie MIT Press pod adresem https://mitpress.mit.edu/sites/default/files/sicp/index.html.