Pisałem już kiedyś o błędach popełnianych przez młodych programistów. Byłem wtedy na początku swojej programistycznej kariery i myślałem, że już dużo wiem. Teraz wiem ile jeszcze nie wiem 😉 Niemniej jednak z większością z tamtych punktów zgadzam się do dziś. Postanowiłem więc, po kilku latach jeszcze raz zebrać kilka najczęstszych błędów, które zauważyłem już w aplikacjach enterprise. Poniższe punkty nazwałem błędami architektonicznymi, czyli odnoszącymi się bezpośrednio do kodu programu. Jak wiadomo praca programisty składa się jeszcze z wielu innych aktywności, ale o nich napiszę w innym poście.
1. Nadużywanie statycznych metod
Statyczne metody nie powinny być zbyt często używane. Programiści często idą na skróty i dopisują funkcje statyczne, które zawierają logikę biznesową bądź zmieniają stan aplikacji. Logika biznesowa w metodach statycznych to najgorsze, co może być. Dlaczego? Dlatego, że dla takiej metody jest bardzo trudno napisać jakikolwiek test jednostkowy. Metody statyczne powinny być używane dla bardzo prostych helperów, służących tylko do prostych obliczeń, niewymagających testowania. Chodź i tak do tego lepiej nadają się extension methods. Przykład błędnego użycia metody statycznej znajdziecie poniżej.
var user = User.GetUserById(userId);
public class User
{
public static User GetUserById(int userId)
{
//Implementation
}
}
Zamiast tego powinno się używać serwisów, które mogą być wstrzyknięte poprzez DI i z łatwością zmockowane.
2. Niekorzystanie z interfejsów
Punkt ten często wiąże się z poprzednim, gdyż metod statycznych nie można umieścić w interfejsie. Często myślimy, że jeśli aktualnie do dostępu do bazy używamy ADO.NET, to tak będzie już zawsze i tworzymy metody pobierające/zapisujące dane w klasie, do której odwołujemy się bezpośrednio. Niestety takie podejście ma bardzo wiele wad. Przede wszystkim, tak samo jak w przypadku metod statycznych bardzo ciężko jest testować taki kod. Nie można stworzyć żadnego mocka, który symulowałby nam dostęp do bazy danych. Drugim problemem z tym związanym jest brak możliwości zmiany aktualnie używanego rozwiązania. Jeśli już w całym programie używa się bezpośrednich połączeń do metod z ADO.NET, to nie przepiszemy wszystkiego na Entity Framework w jeden tydzień.
To co powinno się robić, to używać interfejsów zawsze. Nawet jeśli sposób implementacji w danym momencie jest tylko jeden.
3. Wrzucanie funkcji do jednego worka
Jeśli myślimy zbyt obiektowo, nadchodzi czas gdy biznes przerasta nasze obiektowe pojęcie. Wtedy właśnie zdajemy sobie sprawę, że nasza klasa User trochę się rozrosła i każda operacja mająca cokolwiek wspólnego z użytkownikiem, nie powinna znajdować się w tej jednej klasie. Często osoby, które nie stosują żadnych wzorców architektonicznych, popadają w pułapkę klas Bogów. Zamiast myśleć o obiektach jako encjach bazodanowych, należy pomyśleć o obiektach w kontekście biznesu. Bardzo przydatna w zrozumieniu tego zagadnienia jest książka Domain Driven Design. Pomocne w rozdzieleniu funkcji mogą być również wzorce projektowe, takie jak CQRS.
4. Wykorzystywanie tych samych DTO w wielu, niepowiązanych miejscach.
Najtrudniejsze w programowaniu jest wymyślanie nazw. Dlatego często idziemy na łatwiznę i nadajemy nic nie znaczące nazwy. Załóżmy, że mamy klasę do edycji użytkownika, więc nazwiemy ją UserDto. Po kilku miesiącach zapominamy, piszemy inną funkcjonalność i dajmy na to, że musimy wyświetlić wszystkich użytkowników z daną rolą. Jakiej klasy użyjemy? Prawdopodobnie UserDto, pomijając, że klasa ta posiada znacznie więcej pól niż potrzeba przy prostym wyświetleniu użytkowników. Jest to kolejny błąd, który ciężko naprawić, bo im więcej odwołań do danej klasy tym ta klasa jest trudniej edytowalna.
5. Pisanie nieczytelnych funkcji
Temat pisania nieczytelnych funkcji, poruszany jest w wielu wątkach i opisywany w niejednej książce, mimo to myślę, że wciąż są problemy z czytelnym pisaniem funkcji. Przede wszystkim problemy występują w instrukcjach warunkowych, które potrafią być całkiem sporym kawałkiem logiki. Istnieje nawet kampania anty-ifowa, której członkowie namawiają do programowania bez użycia instrukcji if oraz switch. Nie jestem za popadaniem ze skrajności w skrajność i wiem, że czasami użycie if jest czytelniejsze niż przerobienie architektury, niemniej jednak należy zwrócić uwagę na to, czy inni programiści zrozumieją, kiedy kod wchodzi do bloku if.
Poniższy przykład pokazuje świetnie ewolucje oprogramowania. Pierwsza linijka jest akceptowalna, bo to tylko jeden warunek, do tego w miarę czytelny i logiczny- ma pieniądze, więc może kupić. Po dodaniu funkcjonalności ról, umieszczamy kolejny warunek i już zaczyna się robić mało czytelnie. W tym momencie powinniśmy opakować cały warunek w jakąś ładnie brzmiącą metodę. Niestety wiele razy widziałem, że ostatni krok jest pomijany i zostaje taki tasiemiec z instrukcji warunkowych, nad którym trzeba spędzić kilka minut, żeby go zrozumieć.
// this is readable
if (user.AccountBalance >= productCost) { }
// we add Roles to program and code is ugly
if (user.AccountBalance >= productCost && user.Roles.Any(r => r.AllowInAppPurchases)) { }
// after refactor it is better
if (user.CanBuyProduct(productCost)) { }
To tylko kilka uwag, które zamierzałem przedstawić, żeby przestrzec innych. Często w pośpiechu sam zapominam o niektórych zasadach, ale mam nadzieję, że dzięki opisaniu ich tutaj, sam zacznę się bardziej pilnować. 🙂