Architektura systemów to nie jest najprostsze zagadnienie. Zadanie architekta jest trudne. Trzeba niczym Gary Kasparov przewidywać kilka kroków do przodu co się wydarzy, aby poprawnie zacząć. O tym jak pisać solidny, czysty i poprawny kod powstało już wiele książek.
Solidny kod to przede wszystkim kod napisany w prosty i czytelny sposób. Tu pojawia się reguła KISS mówiąca o tym, aby utrzymać kod maksymalnie prostym. Oprócz tego ważne, aby kod był utrzymywalny. Co to oznacza? A no to, że trzeba pisać kod tak, żeby za pół roku nie płakać podczas dodawania nowych funkcji do systemu.
Co zatem dokładnie zrobić, aby kod wyglądał tak, jak należy? Warto zacząć od postaw.
SOLID – to akronim stworzony z pierwszych liter zasad, którymi powinniśmy się kierować, pisząc kod.
Single Responsibility Principle
Powyższa reguła mówi, aby jednostka kodu była odpowiedzialna tylko za jedną rzecz. Co mam na myśli pisząc “jednostka kodu”? Zdania są podzielone odnośnie tego co powinno pełnić pojedynczą rolę. Jedni mówią, że to klasa ma pełnić tylko jedną funkcję, a inni mówią, że to metody w klasie powinny działać w ten sposób. Jak to bywa w programowaniu, prawidłowych odpowiedzi jest kilka. Ja proponuje podchodzić do tego po prostu racjonalnie.
Dla przykładu, jeśli mamy klasę, która jest odpowiedzialna za obsługę plików, to niech ona zajmuje się tylko i wyłącznie obsługą plików. Jeśli chcemy tej klasy użyć do tworzenia plików tekstowych i struktury katalogów to niech będą do tego stworzone osobne metody, a nie jedna.
Zamiast:
class FileManager {
void createDirectoryStructureAndTextFile(String path, String file){
}
}
Być może lepiej:
class FileManager {
void createTextFile(String path, String file){
}
void createDirectoryStructure(String path){
}
}
Dodatkowo warto zainteresować się podejściem DDD do tworzenia systemów. Dobrze przeprowadzona analiza w tym modelu pozwala zbudować system, w którym obszary biznesowe będą od siebie oddzielone jakimś fizycznym bytem, spełniając przy tym pierwszą zasadę SOLID na nieco wyższym poziomie.
Open-Close Principle
Reguła open-close z solid mówi o tworzeniu kodu otwartego na rozszerzanie a zamkniętego na zmiany. Oznacza to wymaganie tworzenia kodu w taki sposób, aby w przypadku dodawania nowej funkcjonalności nie było konieczności modyfikacji istniejących elementów.
Istnieje wiele metod pisania rozszerzalnego kodu i chyba większość z nich opiera się o zdrowy rozsądek i doświadczenie, ale jeden ze wzorców projektowych również nam w tym pomoże. Chodzi mi o wzorzec strategia.
Przeanalizujmy kod do walidacji danych wejściowych. Najpierw wersja, która wymusza na nas modyfikację klasy.
Piszemy pierwszą walidację – sprawdzanie czy użytkownik podał e-mail.
class UserDataValidator {
public void validate(User input)throws ValidationException {
if(input.getEmail()==null || input.getEmail().lenght()==0){
throw new ValidationException("Email");
}
}
}
Teraz załóżmy sytuację, że jakiś czas później musimy dodać kolejne sprawdzenia np. czy wiek jest większy niż 16 oraz, czy hasło ma przynajmniej 10 znaków.
class UserDataValidator {
public void validate(User input)throws ValidationException {
if(input.getEmail()==null || input.getEmail().lenght()==0){
throw new ValidationException("Email");
}
if(input.getAge()<=16){
throw new ValidationException("Age");
}
if(input.getPassword().lenght()<10){
throw new ValidationException("Password too short");
}
}
}
Jak widać, trzeba było zmodyfikować istniejącą klasę i tak będzie za każdym razem, gdy będzie konieczność dodawania nowych walidacji.
Teraz uczyńmy ten kod zgodnym z Open-Close principle. Zacznijmy od przygotowania interface dla naszej strategii.
interface UserDataValidator {
void validate(User input)throws ValidationException;
}
class EmailValidator implements UserDataValidator {
void validate(User input)throws ValidationException {
if(input.getEmail()==null || input.getEmail().lenght()==0){
throw new ValidationException("Email");
}
}
}
Ten mechanizm użyjemy w następujący sposób.
class UserService {
private UserDataValidator validator = new EmailValidator();
public void createUser(User input){
validator.validate(input);
}
}
Mamy już walidację e-maila. Teraz dodajmy kolejne walidacje, wystarczy, że dopiszemy kolejne klasy.
class AgeValidator implements UserDataValidator {
void validate(User input)throws ValidationException {
if(input.getAge()<=16){
throw new ValidationException("Age");
}
}
}
class PasswordValidator implements UserDataValidator {
void validate(User input)throws ValidationException {
if(input.getPassword().lenght()<10){
throw new ValidationException("Password too short");
}
}
}
Teraz musimy zmodyfikować miejsce użycia walida torów, aby można było korzystać z wielu.
class UserService {
private List<UserDataValidator> validators = List.of(
new EmailValidator(),
new PasswordValidator(),
new AgeValidator()
);
public void createUser(User input){
validators.forEach(v -> v.validate(input));
}
}
W tym momencie dodanie nowego walidatora będzie wymuszało dodanie go do listy i modyfikację klasy UserService. Zmieńmy to w taki sposób, aby to nie było konieczne. Jedyne co trzeba zrobić to przekazać listę walidatorów przez konstruktor.
class UserService {
private final List<UserDataValidator> validators;
public UserService(List<UserDataValidator) validators){
this.validators = validators;
}
public void createUser(User input){
validators.forEach(v -> v.validate(input));
}
}
//użycie
UserService userService = new UserService(List.of(
new EmailValidator(),
new PasswordValidator(),
new AgeValidator()
));
Jak widać, kod w tym momencie jest dość odporny na modyfikację. W przypadku modyfikacji i tak coś trzeba będzie zmienić, ale będzie to tylko dodanie klasy do listy walidatorów. W przypadku korzystania z frameworku Spring nawet to nie będzie konieczne, to narzędzie zrobi tę operację za nas automatycznie.
Zapisz się na newsletter, aby otrzymywać informacje o nowych artykułach oraz inne dodatki.
Liskov Substitution Principle
LSP to według mnie najtrudniejsza reguła do zrozumienia ale bardzo łatwa do wdrożenia. Mówi ona o tym, że obiekty, które stanowią podtypy jakiejś klasy powinny dać się w łatwy sposób zastąpić ich superklasą (typem bazowym) bez popsucia aplikacji. To wymaga, aby obiekty podklas zachowywały się w taki sam sposób jak obiekty typu bazowego. Można to osiągnąć poprzez stosowanie kilku zasad przypominających design by contract.
Pokrótce zasady te mówią o tym, aby w przypadku nadpisywania metod zachować te same typy przyjmowanych parametrów i te same typy zwracane (lub typ pochodny od typu zwracanego w metodzie znajdującej się w klasie bazowej).
Interface Segregation Principle
ISP to moja ulubiona zasada ze zbioru solid. Mówi ona o segregowaniu interfejsów na potrzebny konsumentów. Dla przykładu opiszę abstrakcyjny przypadek, który zawsze opisywałem na swoich pierwszych rozmowach kwalifikacyjnych.
Załóżmy, że mamy klasę Child oraz klasy Teacher i Parent. Odpowiedzialnością dziecka jest zarówno uczenie się w szkole tego co zaleci nauczyciel jak i słuchanie tego co mówią rodzice. Rodzic inaczej będzie się komunikował ze swoim dzieckiem niż nauczyciel, wiec powinni oni mieć udostępnione inne wersje interfejsów mimo, że rozmawiamy o tym samym człowieku.
Zasada Interface Segregation Principle bardzo dobrze wpasowała się w architekturze heksagonalnej, gdzie dany moduł udostępnia wiele metod poprzez implementację wielu różnych interfejsów zwanych w tej architekturze portami. Inne moduły korzystające z tego modułu, posługują się portami, a więc zbiorem metod, które są im potrzebne. Mimo że implementacja danego portu (adapter) może fizycznie zawierać o wiele więcej metod, udostępnione będą tylko te, które znajdują się w danym interfejsie (porcie).
Te zasada w dużej mierze pozwala nam na utrzymanie porządku w aplikacji oraz pomaga w zachowaniu hermetyzacji kodu.
Dependency Inversion Principle
Moduły wysokiego poziomu nie powinny zależeć od modułów niskiego poziomu. Oba powinny zależeć od abstrakcji.
Abstrakcje powinny być zależne od detali. Detale powinny być zależne od abstrakcji.
Powyższe zdania są stwierdzeniami Wujka Boba, który słusznie zauważa, że pisząc aplikacje, powinniśmy posługiwać się abstrakcją, a nie konkretną implementacją. Myśląc w sposób abstrakcyjny, dużo prościej będzie nam budować elastyczne systemy, w których kawałki kodu (moduły) są łatwo wymienne.
Podsumowanie
W stosowaniu powyższych zasad ważne jest zdrowe podejście do tematu, a nie kurczowe trzymanie się wszystkich reguł spisanych w internecie. Solid to zbiór reguł, który powinien być znany każdemu programiście. Reguły solid pomagają pisać kod z myślą o przyszłości i kolejnych zmianach dlatego jest tak ważne i nie wyobrażam sobie, aby ktoś piszący kod komercyjnie nie znał tych zasad. Z biznesowego punktu widzenia pisanie kodu otwartego na łatwe modyfikacje zmniejsza koszt prowadzenia utrzymania aplikacji i po prostu prowadzi do oszczędności.
Pisząc solidny kod, pomagasz nie tylko organizacji poprzez oszczędności pieniędzy wynikające w łatwiejszego utrzymania, pomagasz również swoim kolegom i koleżankom, którzy w przyszłości będą musieli utrzymywać napisany przez Ciebie kod.
Jeśli artykuł Ci się podobał, zapraszam do polubienia profilu na facebooku oraz obserwowania na instagramie. Zapraszam również do grupy Wsparcie w programowaniu i do kontaku.