Opanowanie zasady DRY: Zastosowanie wzorców projektowych w C# dla czystszego kodu
Wzorce projektowe w C# są niezbędnymi narzędziami do pisania efektywnego, wielokrotnego użycia i łatwego w utrzymaniu kodu. Te wzorce dostarczają standardowe rozwiązania dla powszechnych problemów projektowych w oprogramowaniu, promując najlepsze praktyki i pomagając deweloperom unikać redundantnego kodu. Jedną z głównych zasad przy stosowaniu wzorców projektowych jest zasada DRY (Don't Repeat Yourself), która podkreśla minimalizowanie powtórzeń w kodzie w celu poprawy czytelności i utrzymywalności.
Ten artykuł jest inspirowany wnikliwym wideo Tima Coreya, "Design Patterns: Don't Repeat Yourself in C#", które zgłębia zasadę DRY oraz jej praktyczne zastosowanie w tworzeniu czystszego, bardziej zorganizowanego kodu. Poprzez eksplorację kluczowych koncepcji i strategii omawianych w wideo Tima, ten artykuł ma na celu zapewnienie kompleksowego przewodnika do skutecznego wdrażania zasady wzorca projektowego DRY w Twoich projektach C#.
Introduction to the DRY Principle in C
We wprowadzeniu Tim Corey wyjaśnia zasadę DRY, która oznacza "Don't Repeat Yourself" (Nie powtarzaj się). Ta zasada jest fundamentalną koncepcją w programowaniu, podkreślającą unikanie redundancji poprzez zapewnienie, że każda cząstka wiedzy lub logiki jest reprezentowana tylko w jednym miejscu w kodzie. Tim ilustruje zasadę przy użyciu prostego przykładu aplikacji WinForms z formą pulpitu. Forma zawiera pola do wprowadzania imienia i nazwiska oraz przycisk do generowania identyfikatora pracownika na podstawie tych pól.
Identyfikacja i przewidywanie powtórzeń w kodzie
O godzinie (0:53) Tim przechodzi do identyfikowania i przewidywania powtórzeń w kodzie. Używa przykładu aplikacji WinForms, aby pokazać, jak mogą wystąpić powtórzenia, nawet gdy metody są wywoływane tylko raz. W aplikacji logika generowania identyfikatora pracownika obejmuje wyodrębnienie podciągów z pól tekstowych dla imion i nazwisk oraz dołączenie trzycyfrowego kodu na końcu.

Na powyższym zrzucie ekranu o godzinie (1:31) Tim demonstruje funkcjonalność aplikacji, pokazując, jak generuje ona identyfikator pracownika, łącząc cztery pierwsze litery imienia i nazwiska z trzycyfrowym kodem. Podkreśla, że choć kod zdaje się przestrzegać zasady DRY, ponieważ nie powtarza tej samej logiki jawnie, istnieją ukryte problemy z wzorem powtórzeń, które należy rozwiązać.
O godzinie (1:51) wskazuje, że chociaż kod wydaje się prosty, nie całkowicie przestrzega zasady DRY, ponieważ logika generowania identyfikatora pracownika jest ściśle powiązana z zdarzeniem kliknięcia przycisku. Oznacza to, że jeśli ta logika byłaby potrzebna gdzie indziej w kodzie klienta, takim jak przetwarzanie listy nowych pracowników (3:58), kod musiałby być powtórzony lub dostosowany, co prowadzi do redundancji.
Tworzenie niezależnych, wielokrotnego używania metod
W tym segmencie Tim Corey demonstruje, jak stworzyć niezależną, wielokrotnego używania metodę, aby przestrzegać zasady DRY. Zaczyna od wyodrębnienia logiki generowania identyfikatora pracownika z obsługi zdarzenia do osobnej metody. To refaktoryzowanie obejmuje stworzenie metody prywatnej o nazwie GenerateEmployeeID i przeniesienie istniejącego kodu do tej metody (5:15). Zmieniony kod w obsłudze zdarzenia następnie po prostu wywołuje tę metodę.
Kroki i przykład:
-
Kod początkowy: Logika generowania identyfikatora pracownika była bezpośrednio w obsłudze zdarzenia kliknięcia przycisku.

-
Kod refaktoryzowany: Tim poprawia metodę, czyniąc ją bardziej elastyczną. Zamiast polegać na konkretnych elementach UI, metoda teraz akceptuje
firstNameilastNamejako parametry i zwraca wygenerowany identyfikator. Ta zmiana pozwala na użycie metody w różnych kontekstach i elementach UI:private string GenerateEmployeeID(string firstName, string lastName) { string employeeID = firstName.Substring(0, 4) + lastName.Substring(0, 4) + DateTime.Now.Millisecond.ToString(); return employeeID; }private string GenerateEmployeeID(string firstName, string lastName) { string employeeID = firstName.Substring(0, 4) + lastName.Substring(0, 4) + DateTime.Now.Millisecond.ToString(); return employeeID; }Tim następnie demonstruje, jak ta metoda jest wywoływana z obsługi zdarzenia kliknięcia:
employeeIdText.Text = GenerateEmployeeID(firstNameText.Text, lastNameText.Text);employeeIdText.Text = GenerateEmployeeID(firstNameText.Text, lastNameText.Text);Zauważa również, że ta metoda może teraz być używana w innych częściach aplikacji, takich jak przetwarzanie plików CSV z wieloma rekordami pracowników, bez powtarzania kodu.
Tworzenie i używanie biblioteki klas
Tim Corey następnie eksploruje koncepcję biblioteki klas, aby jeszcze bardziej zwiększyć ponowne użycie kodu i utrzymywalność. Pokazuje, jak zapakować metodę GenerateEmployeeID do obiektu biblioteki klas, który może być używany w wielu projektach.
O godzinie (8:00) Tim wyjaśnia, że projekt nadal się zmienia na podstawie wymagań użytkownika lub polityki firmy, aby uczynić go bardziej interaktywnym z grafiką i animacjami. Dlatego wprowadza projekt WPF w ramach rozwiązania z dokładnymi polami i przyciskiem do Generowania Identyfikatora Pracownika.
Tim o godzinie (9:15) przedstawia mocne argumenty za używaniem biblioteki klas, mówiąc, że jeśli chcemy unikać powtarzania się, to kod zostałby kopiowany w nowym projekcie WPF. Zatem aby utrzymać zasady DRY, musimy stworzyć klasy w bibliotece klas.
Kroki i przykład:
-
Tworzenie biblioteki klas:
-
Tim o godzinie (9:47) tworzy nowy projekt biblioteki klas w .NET Framework, nazywając go DRYDemoLibrary.
-
W tej bibliotece definiuje klasę publiczną
EmployeeProcessori przenosi metodęGenerateEmployeeIDdo tej klasy:public class EmployeeProcessor { public string GenerateEmployeeID(string firstName, string lastName) { string employeeID = firstName.Substring(0, 4) + lastName.Substring(0, 4) + DateTime.Now.Millisecond.ToString(); return employeeID; } }public class EmployeeProcessor { public string GenerateEmployeeID(string firstName, string lastName) { string employeeID = firstName.Substring(0, 4) + lastName.Substring(0, 4) + DateTime.Now.Millisecond.ToString(); return employeeID; } }
-
-
Używanie biblioteki klas w projektach:
-
W swoich projektach WinForms (13:18) i WPF (14:00) Tim dodaje odnośnik do biblioteki klas DRYDemoLibrary.
-
Następnie zastępuje stary kod wywołaniami metody
GenerateEmployeeIDz biblioteki klas:EmployeeProcessor processor = new EmployeeProcessor(); employeeIDText.Text = processor.GenerateEmployeeID(firstNameText.Text, lastNameText.Text);EmployeeProcessor processor = new EmployeeProcessor(); employeeIDText.Text = processor.GenerateEmployeeID(firstNameText.Text, lastNameText.Text); - To podejście eliminuje powtarzanie, ponieważ metoda jest teraz utrzymywana w jednym miejscu. Tim demonstruje, że ta sama biblioteka klas może być używana w różnych frameworkach UI (WinForms i WPF) bez powtarzania kodu.
-
-
Zalety:
-
Spójność: Centralizując logikę w bibliotece klas, Tim zapewnia, że zmiany w logice (np. poprawki błędów) są stosowane równomiernie we wszystkich projektach.
- Zmniejszone utrzymanie: Zmiany w metodzie trzeba wprowadzać tylko w bibliotece klas, unikając niespójności i zmniejszając koszty utrzymania.
-
Integracja biblioteki klas z wieloma projektami
Tim Corey dalej bada, jak używać biblioteki klas DRYDemoLibrary w różnych typach projektów, koncentrując się na integracji biblioteki z nową aplikacją konsolową. Pokazuje to, jak funkcjonalność biblioteki można ponownie wykorzystywać w różnych aplikacjach, nie tylko jednej instancji, czy tych w ramach tego samego rozwiązania.
Kroki i przykład:
-
Tworzenie nowego rozwiązania i projektu:
-
Tim o godzinie (17:29) zaczyna od stworzenia nowego rozwiązania dla aplikacji konsolowej, symulując scenariusz, w którym możesz potrzebować użyć DRYDemoLibrary w innym typie projektu, jak usługa Windows lub aplikacja konsolowa.
-
Nazywa nowy projekt ConsoleUI i pokazuje, jak uruchomić podstawową aplikację konsolową.
class Program { static void Main(string[] args) { Console.ReadLine(); } }class Program { static void Main(string[] args) { Console.ReadLine(); } }
-
-
Dodawanie odnośnika do biblioteki klas:
-
Tim wyjaśnia, jak dodać odnośnik do DLL DRYDemoLibrary w nowym projekcie. To wymaga przeglądania do pliku DLL w folderze bin projektu biblioteki klas i dodania go do aplikacji konsolowej.
using DRYDemoLibrary;using DRYDemoLibrary; -
Po dodaniu odnośnika Tim (19:24) używa klasy EmployeeProcessor z biblioteki do generowania identyfikatora pracownika na podstawie danych wejściowych użytkownika.
Console.WriteLine("What is your first name?"); string firstName = Console.ReadLine(); Console.WriteLine("What is your last name?"); string lastName = Console.ReadLine(); EmployeeProcessor processor = new EmployeeProcessor(); string employeeID = processor.GenerateEmployeeID(firstName, lastName); Console.WriteLine($"Your employee ID is {employeeID}");Console.WriteLine("What is your first name?"); string firstName = Console.ReadLine(); Console.WriteLine("What is your last name?"); string lastName = Console.ReadLine(); EmployeeProcessor processor = new EmployeeProcessor(); string employeeID = processor.GenerateEmployeeID(firstName, lastName); Console.WriteLine($"Your employee ID is {employeeID}");
-
-
Uruchamianie aplikacji konsolowej:
-
Tim demonstruje uruchamianie aplikacji konsolowej, aby pokazać, że pomyślnie generuje identyfikator pracownika używając biblioteki. To potwierdza, że ten sam kod z biblioteki klas można ponownie wykorzystywać w różnych projektach.

-
-
Aktualizacja DLL:
- Tim krótko wspomina, że jeśli DLL się zmieni, można je zaktualizować w projektach, które jej używają. Zauważa, że chociaż to wideo tego nie omawia w szczegółach, używanie paczek NuGet jest zalecanym podejściem do zarządzania i aktualizowania DLL w wielu projektach.
Aktualizacja DLL i zarządzanie paczkami NuGet
Tim Corey krótko wprowadza koncepcję użycia paczek NuGet do zarządzania i aktualizowania bibliotek klas. To podejście oferuje bardziej skalowalne rozwiązanie dla obsługi zależności i aktualizacji, szczególnie w większych projektach lub organizacjach.
Kluczowe punkty:
-
Tworzenie paczki NuGet:
- Zamiast ręcznego zarządzania plikami DLL Tim sugeruje stworzenie paczki NuGet dla biblioteki klas. To obejmuje zapakowanie DLL do paczki NuGet i przesłanie jej na serwer NuGet (prywatny lub publiczny).
-
Aktualizacja paczek:
- Używając paczki NuGet, można zaktualizować bibliotekę we wszystkich projektach, które jej używają, po prostu aktualizując wersję paczki. To zapewnia spójność i zmniejsza ryzyko niezgodności wersji lub brakujących aktualizacji.
-
Zalety:
-
Zarządzanie centralne: Paczki NuGet zapewniają centralizowany sposób zarządzania wersjami bibliotek i zależnościami.
-
Łatwość aktualizacji: Aktualizacja biblioteki w wielu projektach staje się łatwiejsza i bardziej niezawodna.
- Integracja: NuGet integruje się z różnymi narzędziami i środowiskami deweloperskimi, upraszczając proces zarządzania zależnościami bibliotek.
-
Implementacja DRY w testach jednostkowych: szybki kurs
W tym segmencie Tim Corey demonstruje, jak zastosowanie zasady DRY (Don't Repeat Yourself) może poprawić testy jednostkowe. Pokazuje, jak implementować zasady DRY w pracy deweloperskiej, szczególnie skupiając się na testach jednostkowych.
Początkowe ustawienie testu
Tim zaczyna od uruchamiania testu jednostkowego, który obecnie nie przechodzi z powodu błędu w DLL. Podkreśla znaczenie testów jednostkowych w identyfikacji problemów, nawet gdy kod jest poza głównym rozwiązaniem. Kod oczekiwał wprowadzenia 4-literowego, ale zamiast tego Tim wprowadził 3-literowe imię, które powoduje awarię w pliku DLL, nawet jeśli nie jest bezpośrednio włączony do rozwiązania.

Refaktoryzacja kodu w celu rozwiązania błędów
Aby rozwiązać problem z obsługą imion, Tim refaktoruje kod. Wyjaśnia, jak DRY można zastosować w rozwoju, tworząc nowy projekt biblioteki klas (23:50). To podejście zapewnia, że zmiany wielu obiektów można dokonać raz i efektywnie testować bez powtarzania poprawek.

Dodawanie testów jednostkowych
Tim wprowadza nową klasę testową jako EmployeeProcessorTest w projekcie biblioteki klas i tworzy testy jednostkowe przy użyciu XUnit. Pokazuje, jak stworzyć metodę testową do generowania identyfikatorów pracowników i omawia znaczenie zakładania zależności zamiast polegania na rzeczywistych wartościach.

Pisanie metody testowej
Tim pisze metodę testu jednostkowego pod nazwą GenerateEmployeeID_ShouldCalculate. Ustawia teorię z danymi inline do testowania różnych scenariuszy, zapewniając, że metoda zwraca oczekiwane wyniki. Wyjaśnia również, jak używać Assert.Equal, aby zweryfikować wynik.
public class EmployeeProcessorTest
{
[Theory]
[InlineData("Timothy", "Corey", "TimoCore")]
public void GenerateEmployeeID_ShouldCalculate(string firstName, string lastName, string expectedStart)
{
// Arrange
var processor = new EmployeeProcessor();
// Act
var actualStart = processor.GenerateEmployeeID(firstName, lastName).Substring(0, 8);
// Assert
Assert.Equal(expectedStart, actualStart);
}
}
public class EmployeeProcessorTest
{
[Theory]
[InlineData("Timothy", "Corey", "TimoCore")]
public void GenerateEmployeeID_ShouldCalculate(string firstName, string lastName, string expectedStart)
{
// Arrange
var processor = new EmployeeProcessor();
// Act
var actualStart = processor.GenerateEmployeeID(firstName, lastName).Substring(0, 8);
// Assert
Assert.Equal(expectedStart, actualStart);
}
}
Uruchamianie testu jednostkowego
Tim podkreśla znaczenie zakładania dynamicznych danych, takich jak wartości daty-czasu, aby kontrolować warunki i wyniki testów. Omówił wyzwanie pracy z dynamicznymi ciągami i testowanie różnych scenariuszy, używając kontrolowanych wartości. Następnie uruchomia test jednostkowy, ale zanim to, dodaje dwie paczki NuGet, które są niezbędne do przeprowadzenia testów: xunit.runner.console i xunit.runner.visualstudio.

Po pomyślnym uruchomieniu wszystkich testów dla jednego inline data wynik jest pokazywany w następujący sposób:

Teraz o godzinie (31:30) Tim dodał inne dane inline i zmienił drugi parametr funkcji podłożeń na expectedStart.Length:
public class EmployeeProcessorTest
{
[Theory]
[InlineData("Timothy", "Corey", "TimoCore")]
[InlineData("Tim", "Corey", "TimCore")]
public void GenerateEmployeeID_ShouldCalculate(string firstName, string lastName, string expectedStart)
{
var processor = new EmployeeProcessor();
var actualStart = processor.GenerateEmployeeID(firstName, lastName).Substring(0, expectedStart.Length);
Assert.Equal(expectedStart, actualStart);
}
}
public class EmployeeProcessorTest
{
[Theory]
[InlineData("Timothy", "Corey", "TimoCore")]
[InlineData("Tim", "Corey", "TimCore")]
public void GenerateEmployeeID_ShouldCalculate(string firstName, string lastName, string expectedStart)
{
var processor = new EmployeeProcessor();
var actualStart = processor.GenerateEmployeeID(firstName, lastName).Substring(0, expectedStart.Length);
Assert.Equal(expectedStart, actualStart);
}
}
Po ponownym uruchomieniu testu jednostkowego o godzinie (32:05) z drugą teorią, test się załamał:

Polepszanie kodu za pomocą metod prywatnych
Aby przestrzegać zasady DRY, Tim dalej refaktoruje kod, tworząc prywatną metodę GetPartOfName w rzeczywistej klasie EmployeeProcessor w ramach DRYDemoLibrary. Ta metoda obsługuje wyodrębnianie części imion, poprawiając ponowne użycie i czytelność kodu. Tim dokonał następujących zmian:
public string GenerateEmployeeID(string firstName, string lastName)
{
string employeeID = $@"{GetPartOfName(firstName, 4)}{GetPartOfName(lastName, 4)}{DateTime.Now.Millisecond.ToString()}";
return employeeID;
}
private string GetPartOfName(string name, int numberOfCharacters)
{
string output = name;
if (name.Length > numberOfCharacters)
{
output = name.Substring(0, numberOfCharacters);
}
return output;
}
public string GenerateEmployeeID(string firstName, string lastName)
{
string employeeID = $@"{GetPartOfName(firstName, 4)}{GetPartOfName(lastName, 4)}{DateTime.Now.Millisecond.ToString()}";
return employeeID;
}
private string GetPartOfName(string name, int numberOfCharacters)
{
string output = name;
if (name.Length > numberOfCharacters)
{
output = name.Substring(0, numberOfCharacters);
}
return output;
}
Aktualizacja testów jednostkowych
Tim aktualizuje testy jednostkowe, aby odzwierciedlały zmiany w kodzie, takie jak modyfikacja oczekiwanej długości podciągów. Wyjaśnia, jak uruchomienie tych testów pomoże szybko zidentyfikować problemy i potwierdzić, że kod spełnia nowe wymagania. Tim dodaje nowe teorie, a następnie uruchamia testy jednostkowe, aby zweryfikować, czy wyniki są zgodne z oczekiwaniami:

Rozszerzenie wszechstronności za pomocą bibliotek .NET Standard
Tworzenie biblioteki .NET Standard
Aby zwiększyć wszechstronność swojej biblioteki klas, Tim Corey zaleca przejście z biblioteki klas .NET Framework na bibliotekę klas .NET Standard. Ta zmiana umożliwia kompatybilność biblioteki z różnymi platformami, w tym z:
- Platformy Windows: WinForms, WPF i aplikacje konsolowe
- Platformy wielopłatformowe: .NET Core, Xamarin (na iOS i Android), Linux i macOS
Kroki do stworzenia biblioteki .NET Standard:
- Dodaj nowy projekt: Kliknij prawym przyciskiem na swoim rozwiązaniu i wybierz dodanie nowego projektu.
-
Wybierz .NET Standard: Zamiast wybierać bibliotekę klas .NET Framework, wybierz .NET Standard. Ten typ biblioteki obsługuje szeroki zakres platform.

- Migracja kodu: Skopiuj i wklej istniejący kod (np. klasę EmployeeProcessor) do nowej biblioteki .NET Standard. Ten proces może wymagać drobnych dostosowań, ale podstawowa logika pozostaje spójna.
Konwertując na .NET Standard, udostępniasz swoją bibliotekę z różnych platform, ograniczając powtórzenia kodu w różnych typach aplikacji i oszczędzając wysiłek deweloperski.
Unikanie powtórzeń w kodzie i testowaniu
Redukowanie powtórzeń w rozwoju
Tim Corey podkreśla, że poprzez przyjęcie biblioteki .NET Standard, minimalizujesz powtarzalność kodu nie tylko w bazie kodu, ale także w procesie rozwoju. Zamiast dublować kod w ramach różnych projektów specyficznych dla platformy, centralizujesz go w jednej bibliotece, która działa w wielu środowiskach.
Zalety:
- Ujednolicona baza kodu: Jedna baza kodu dla różnych platform zmniejsza wysiłek wymagańy do utrzymania i aktualizacji kodu.
- Uproszczone testowanie: Z biblioteką .NET Standard możesz pisać testy jednostkowe raz i zapewnić ich zastosowanie do wszystkich obsługiwanych platform.
Testowanie i debugowanie: Tim wprowadza testy jednostkowe jako sposób na dalsze zmniejszenie wysiłku i powtarzalności. Testy zautomatyzowane weryfikują poprawność kodu bez potrzeby ręcznego testowania każdej iteracji aplikacji.
Wskazówki dotyczące stosowania DRY: wiedza, kiedy się zatrzymać
Tim Corey podkreśla, że choć przestrzeganie zasady DRY (Don't Repeat Yourself) jest kluczowe dla pisania kodu łatwego w utrzymaniu, ważne jest wiedzieć, kiedy i gdzie ją zastosować. Nie każdy scenariusz wymaga takiego samego podejścia, więc oto kilka praktycznych wskazówek zainspirowanych wglądami Tima:
-
Unikaj kodu w plikach kodu i interfejsach użytkownika: Tim odradza umieszczanie logiki bezpośrednio w plikach kodu lub interfejsach użytkownika. Na przykład logika biznesowa nie powinna być osadzana w formularzu ani zdarzeniu kliknięcia przycisku. Zamiast tego zachowaj taką logikę w oddzielnych klasach lub bibliotekach. To rozdzielenie pomaga utrzymywać czystą architekturę i czyni twój kod bardziej użytecznym w różnych interfejsach użytkownika.
-
Wykorzystaj biblioteki .NET Standard: Przy tworzeniu bibliotek Tim sugeruje korzystanie z bibliotek .NET Standard zamiast bibliotek .NET Framework, jeśli to możliwe. Biblioteki .NET Standard są bardziej wszechstronne, pozwalając na użycie kodu na różnych platformach, w tym .NET Core, Xamarin i więcej. To podejście zmniejsza duplikację kodu i zwiększa jego przenośność.
-
Oddziel kod specyficzny dla platformy: Niektóry kod może nie pasować do biblioteki .NET Standard z powodu wymagań specyficznych dla platformy, takich jak obsługa plików lub zarządzanie konfiguracją. Tim zaleca stworzenie dwóch bibliotek w takich przypadkach: jednej dla kodu .NET Standard i jednej dla kodu specyficznego dla platformy. W ten sposób można nadal używać głównej logiki, jednocześnie dostosowując się do potrzeb specyficznych dla platformy.
-
Podkreślanie testów jednostkowych: Tim zdecydowanie zachęca do pisania testów jednostkowych dla swojego kodu. Testy jednostkowe pomagają w identyfikacji błędów we wczesnym etapie i zapewniają, że kod działa zgodnie z oczekiwaniami. Mogą znacznie przyspieszyć proces debugowania, ponieważ można szybko zweryfikować zmiany bez ręcznego testowania całej aplikacji.
- Rozważ rozmiar projektu: W przypadku bardzo małych lub eksperymentalnych projektów Tim uznaje, że korzystanie z oddzielnych bibliotek i rozbudowanych testów jednostkowych może nie być konieczne. Jednak dla aplikacji produkcyjnych, rozpoczęcie od czystej architektury i testowania jednostkowego jest zalecane, ponieważ małe projekty często rosną i ewoluują z czasem.
Przestrzegając tych wskazówek, można skutecznie stosować zasadę DRY, równoważąc potrzebę ponownego użycia i łatwości w utrzymaniu kodu z praktycznymi rozważaniami.
Wnioski
Opanowanie zasady DRY poprzez wzorce projektowe jest niezbędne do pisania czystego i łatwego w utrzymaniu kodu C#. Jak pokazał Tim Corey, skuteczne stosowanie DRY obejmuje tworzenie metod wielokrotnego użycia, wykorzystywanie bibliotek klas oraz przyjęcie .NET Standard dla szerszej kompatybilności. Zrozumienie, kiedy i jak stosować te praktyki, może znacząco poprawić jakość i elastyczność kodu.
Dla bardziej szczegółowych informacji, sprawdź wideo Tima Coreya na ten temat tutaj. Aby być na bieżąco z najnowszą zawartością Tima, odwiedź jego kanał YouTube.


