Earn More by Sharing What You Love
Do you create content for developers working with .NET, C#, Java, Python, or Node.js? Turn your expertise into extra income!
[[academy-video-youtube({"vid": "QoK7jSZ-viw", "start_time": "0", "title": "Enums aren't evil." Warunkowe wszędzie są", "twórca": "Derek Comartin", "długość": "11m 03s"})]]
W języku C# instrukcje warunkowe, takie jak instrukcja if, instrukcja if else i instrukcja switch, są niezbędnymi narzędziami. Ale co się dzieje, gdy konstrukcje te są używane nadmiernie — zwłaszcza w połączeniu z enumami? Derek Comartin w swoim filmie "Enums Aren't Evil. "Conditionals Everywhere Are", przedstawia szczegółowy proces refaktoryzacji, który zastępuje powszechnie stosowaną logikę warunkową czystszymi i łatwiejszymi w utrzymaniu wzorcami.
W tym artykułe prześledzimy krok po kroku tok rozumowania Dereka, wykorzystując jego znaczniki czasu jako punkty odniesienia. Zbadamy również, w jaki sposób jego pomysły mają zastosowanie do typowych wzorców warunkowych w języku C#, takich jak operator trójargumentowy, instrukcja else oraz struktury switch-case — podkreślając, gdzie mogą one powodować problemy w dużych bazach kodu i jak można je refaktoryzować w celu uzyskania lepszego projektu.
Derek zaczyna od przedstawienia instrukcji if, która sprawdza typ produktu:
if (productType == ProductType.Template || productType == ProductType.Ebook)If productType = ProductType.Template OrElse productType = ProductType.Ebook ThenNa pierwszy rzut oka powyższy warunek if wygląda na prosty. Derek ostrzega jednak, że tego rodzaju instrukcja ocenia dany warunek, a następnie wykonuje blok kodu tylko wtedy, gdy warunek jest prawdziwy — co staje się problematyczne, gdy logika ta jest powtarzana wszędzie.
Ten blok może pojawić się ponownie w innej metodzie lub klasie:
if (offeringType == ProductType.Template || offeringType == ProductType.Ebook)If offeringType = ProductType.Template OrElse offeringType = ProductType.Ebook ThenDerek wyjaśnia, że ten wzorzec szybko rozprzestrzenia się w dużym systemie. Ta sama logika if-else pojawia się w wielu usługach, powodując niespójności i błędy podczas dodawania nowej wartości wyliczeniowej. Na przykład, co się dzieje, gdy wprowadzisz nowy typ produktu, taki jak wideo? Należy pamiętać o aktualizacji każdego bloku, w którym występuje to wyrażenie warunkowe.
W poniższym przykładzie Derek zagłębia się w zagnieżdżone konstrukcje warunkowe. Wewnątrz jednej metody instrukcja if else sprawdza tę samą enum, a wynik jest przekazywany do kolejnej metody, która również zawiera podobną kontrolę.
Instrukcja sprawdza, czy istnieje szablon lub ebook, i zwraca wynik — w przeciwnym razie zwraca wartość null. Derek zauważa, że ta nadmiarowość nie tylko wydłuża kod, ale także stwarza zagrożenia związane z utrzymaniem. Ta sama logika jest powielana w wielu plikach, co prowadzi do chaosu w przepływie sterowania.
Jeśli Twój system wymaga dodawania domyślnego przypadku za każdym razem, gdy modyfikujesz wyliczenie, wiesz, że coś jest nie tak.
Zamiast ciągłego sprawdzania typów za pomocą if else, Derek proponuje zadać lepsze pytanie:
> Czy produkt posiada funkcje umożliwiające pobieranie?
To lepsze wyrażenie intencji. Sprawia to, że kod staje się bardziej czytelny i całkowicie zmniejsza zależność od enum. Zamiast pisać instrukcję if z dwoma warunkami, taką jak:
if (product.Type == ProductType.Template || product.Type == ProductType.Ebook)If product.Type = ProductType.Template OrElse product.Type = ProductType.Ebook ThenMożna po prostu zawrzeć tę logikę w modelu i napisać:
if (product.HasDownloadableResource())If product.HasDownloadableResource() ThenZwraca wartość true tylko wtedy, gdy dostępny jest zasób do pobrania — co ogranicza potrzebę stosowania skomplikowanych wyrażeń warunkowych.
Aby rozwiązać ten kluczowy problem, Derek wprowadza typ DownloadableResource. Ten typ zawiera adres URL do pobrania oraz domyślną nazwę pliku. Staje się to integralną częścią Twojej domeny, zamiast polegać na instrukcjach if w celu jej uzyskania.
Teraz, zamiast powtarzać to:
if (product.Type == ProductType.Template)
{
// Generate file name
}
else if (product.Type == ProductType.Ebook)
{
// Generate file name
}If product.Type = ProductType.Template Then
' Generate file name
ElseIf product.Type = ProductType.Ebook Then
' Generate file name
End IfNapisz to:
var downloadable = product.GetDownloadableResource();
if (downloadable != null)
{
Console.WriteLine(downloadable.FileName);
}Dim downloadable = product.GetDownloadableResource()
If downloadable IsNot Nothing Then
Console.WriteLine(downloadable.FileName)
End IfTo znacznie upraszcza logikę i eliminuje potrzebę stosowania rozgałęzień instrukcji else, a nawet instrukcji switch.
Derek idzie o krok dalej, wyjaśniając istotny wybór projektowy: przeniesienie logiki z etapu kompilacji do etapu wykonywania. Oznacza to sprawdzanie w systemie w czasie wykonywania, czy dla danego produktu istnieje zasób typu DownloadableResource. Jeśli tak jest, podejmij odpowiednie działania. Jeśli nie, pomiń to.
Zauważa on, że to posunięcie zamienia statyczną logikę if-else na zapytania wykonywane w czasie rzeczywistym. Może to wymagać wywołania bazy danych, ale ogranicza zagnieżdżoną logikę if-else i centralizuje zachowanie. Poprawia to łatwość utrzymania na dużą skalę.
Inną ścieżką, którą bada Derek, jest dziedziczenie. Można utworzyć abstrakcyjną klasę bazową Product, a następnie zdefiniować typy pochodne, takie jak Ebook, Template lub OfflineCourse.
Każde z nich nadpisuje metody takie jak:
public virtual string GetDownloadUrl() { ... }Public Overridable Function GetDownloadUrl() As String
' ...
End FunctionTakie podejście pozwala każdemu produktowi obsługiwać własną logikę. Chociaż pozwala to uniknąć instrukcji switch lub wielu instrukcji warunkowych, Derek zwraca uwagę, że jeśli nie będziesz ostrożny, nadal możesz skończyć pisząc wyrażenia warunkowe wewnętrznie.
Jeśli dziedziczenie wydaje się zbyt uciążliwe, Derek sugeruje użycie typów jawnych, takich jak DownloadableProduct, który zawiera własne właściwości i metody — bez powiązania z hierarchią.
W programie może to wyglądać następująco:
var downloader = new DownloadableProduct(product);
Console.WriteLine(downloader.GetDefaultFileName());Dim downloader = New DownloadableProduct(product)
Console.WriteLine(downloader.GetDefaultFileName())Nie ma potrzeby stosowania instrukcji if else lub switch w celu określenia zachowania — każdy obiekt wie, co ma robić.
Jeśli nie jesteś gotowy, aby zrezygnować z enumów, Derek proponuje lekkie rozwiązanie — utwórz metodę rozszerzenia:
public static bool IsDownloadable(this ProductType type)
{
return type == ProductType.Template || type == ProductType.Ebook;
}Public Shared Function IsDownloadable(type As ProductType) As Boolean
Return type = ProductType.Template OrElse type = ProductType.Ebook
End FunctionTeraz zamiast pisać:
if (product.Type == ProductType.Template || product.Type == ProductType.Ebook)If product.Type = ProductType.Template OrElse product.Type = ProductType.Ebook ThenMożna to uprościć do:
if (product.Type.IsDownloadable())If product.Type.IsDownloadable() ThenPozwala to scentralizować logikę i uniknąć ciągłego powtarzania nawiasów klamrowych oraz bloków kodu.
Derek ostrzega również przed nadużywaniem skrótów, takich jak operator trójargumentowy:
string filename = product.Type == ProductType.Template ? "template.pdf" : "default.pdf";Dim filename As String = If(product.Type = ProductType.Template, "template.pdf", "default.pdf")Chociaż jest to poprawna składnia, może być podatna na błędy i trudna do odczytania, gdy logika staje się złożona. Zwłaszcza jeśli warunek zostanie oceniony jako fałszywy, nieprawidłowa wartość może zostać przypisana w subtelny sposób.
Podobnie, instrukcja switch z instrukcją break i przypadkiem domyślnym również wpada w tę pułapkę. Lepiej jest zapytać obiekty o zachowanie niż używać logiki switch-case.
Podsumowując, film Dereka nie jest atakiem na enumy, ale krytyką sposobu, w jaki używamy wokół nich warunkowych struktur if. Rozmieszczając instrukcje if else i switch w całym kodzie, utrudniasz testowanie, utrzymanie i rozwój systemu.
Niezależnie od tego, czy zdecydujesz się na enkapsulację, wyszukiwanie w czasie wykonywania, dziedziczenie czy proste metody rozszerzeń, cel pozostaje ten sam: ograniczyć warunki i przenieść logikę tam, gdzie jej miejsce.
Pamiętaj:
Warunki nie są złe.
Bałagan warunkowy to.
Czysty kod nie opiera się na wielu instrukcjach if else rozrzuconych po różnych klasach.
Oceń kontekst i odpowiednio dostosuj tekst.
Jak mówi Derek: "To zależy od kontekstu". Jedno jest jednak pewne: produkt nie zawsze jest tylko produktem — czasami jest to sygnał, aby przemyśleć swój projekt.
Do you create content for developers working with .NET, C#, Java, Python, or Node.js? Turn your expertise into extra income!
Join our newsletter, you’ll get exclusive access on article updates. We value your privacy