Zasady SOLID opisują pięć podstawowych reguł programowania obiektowego, których celem jest tworzenie zrozumiałego, elastycznego i łatwiejszego do utrzymania kodu. Mają zastosowanie nie tylko w projekcie zorientowanym obiektowo, ale także mogą stanowić fundamenty dla metodologi pracy takich jak np. metodyki zwinne.
Single responsibility
Zasada pojedynczej odpowiedzialności (Single responsibility principle) mówi, że nigdy nie powinno być więcej niż jednego powodu do modyfikacji klasy. Innymi słowy każda klasa powinna być odpowiedzialna za realizacje pojedynczego tematu. Jest jednym z podstawowych elementów refaktoryzacji i skutkuje wydzieleniem mniejszych klas z jednej większej. Tworzenie kodu w oparciu o wiele małych klas zamiast kilku dużych pozwala przede wszysktim na uniknięcie powtórzeń tego samego fragmentu kodu. Modularyzacja znacząco przyczynia się do budowania zrozumiałej struktury projektu łatwiejszego w rozwoju i utrzymaniu.
Klasa Person posiada zbiór właściwości, które mogłyby zostać podzielone na mniejsze obiekty, np. klasy Address i ContactDetails. Pociąga to za sobą również przeniesienie metod isEmailValid i isPhoneNumberValid do odpowiadającej im klasy ContactDetails ponieważ nie leżą one teraz w obowiązku typu Person. Ponadto metoda createPdfSummary realizująca generowanie pliku przypisuje dodatkową odpowiedzialność w związku z czym należy ją oddelegować do klasy SummaryGenerator. Dzięki takiemu podziałowi możliwe jest ponowne wykorzystanie klas w innych miejscach aplikacji bez tworzenia nadmiarowego kodu.
Open-close
Zasada otwarte-zamknięte (Open-close principle) głosi, że elementy systemu (klasy, moduły, funkcje) powinny być otwarte na rozszerzenia i zamknięte na modyfikacje. Oznacza to iż zmiana zachowania encji ma być możliwa w wyniku dodania nowego kodu, który nie zmienia struktury bieżącego ponieważ modyfikacja któregokolwiek elementu może spowodować awarię w innym miejscu. W wyniku tego sposób funkcjonowania systemu jest rozszerzony i pozostaje niezmieniony. Dzięki temu zmniejsza się ryzyko wprowadzenia błędu do aktualnie działającego oprogramowania oraz zachowuje zgodność z bieżącym zestawem testów wymagających jedynie uzupełnienia. Jest to szczególnie istotne w środowisku produkcyjnym np. biblioteki.
Dodanie nowej klasy z możliwością obliczenia pola i obwodu lub modyfikacja bieżących może skutkować zmianą w klasie Calculator co nie pozwoli na rozszerzenie funkcjonalności bez naruszenia bieżącej struktury klasy. W takiej sytuacji należy posłużyć się wspólnym typem bazowym Figure oraz wymusić implementacje szczegółów w klasach pochodnych Rectangle, Triangle, Circle. Następnie w klasie Calculator wystarczy wywołać metody typu bazowego co pozwoli na dodanie nowych figur do kalkulatora bez konieczności modyfikacji klasy Calculator.
Liskov substitution
Zasada podstawienia Liskov (Liskov substitution principle) mówi, że funkcje które używają wskaźników lub referencji do klas bazowych, muszą być w stanie używać również obiektów klas dziedziczących po klasach bazowych, bez dokładnej znajomości implementacji. Innymi słowy klasa dziedzicząca powinna rozszerzać funkcjonalność klasy bazowej bez dokonywania modyfikacji co pozwala na użycie w miejscu klasy bazowej dowolnej klasy pochodnej. Takie podejście wymaga zachowania zgodności interfejsu oraz metod.
Klient NotesView wykorzystuje zbiór obiektów typu Note w celu odtworzenia multimedialnej zawartości notatki bezpośrednio z widoku listy. Jednakże nie każdy obiekt rozszerzający typ bazowy posiada plik multimedialny. Przykładem takiej klasy jest TextNote której obiekty nie są zdolne do odtworzenia zawartości pomimo zgłoszonej deklaracji. NotesView nie musi znać szczegółów implementacji, oczekuje od wszystkich obiektów typu Note poprawnej implementacji i zdolności do wykonania zadania. Jednym z rozwiązań tego problemu może być stworzenie dodatkowego typu podstawowego dla notatek multimedialnych MediaNote rozszerzającego klasę Note, który przejmuje definicję funkcji play. Klient NotesView może teraz bez przeszkód wykorzystać wszystkie obiekty typu MediaNote do odtworzenia zawartości.
Interface segregation
Zasada segregacji interfejsów (Interface segregation principle) stwierdza, że żaden klient nie powinien być zmuszony do polegania na nieużywanych przez niego metodach. Realizacja tej zasady polega na dzieleniu dużych interfejsów na jak najmniejsze i szczegółowe dzięki czemu klient będzie mógł implementować tylko wymagane przez niego metody. Pozwala to na utrzymanie niezwiązanego systemu, łatwiejszego do refaktoryzacji i wprowadzania zmian.
Klasy Exam oraz ExamResults implementują interfejs Printable, który deklaruje możliwość generowania dokumentu tekstowego (printDocument) oraz arkusza kalkulacyjnego (printSheet). Jednakże eksportowanie obiektu klasy Exam do arkusza kalkulacyjnego nie ma sensu w związku z czym implementacja metody printSheet zgłosi wyjątek lub pozostanie pusta. Klient oczekujący stworzenia pliku arkusza kalkulacyjnego dla egzaminu spotka się z nieoczekiwanym zachowaniem lub wystąpieniem błędu. W związku z czym warto podzielić interfejs Printable na mniejsze dedykowane interfejsy PrintableDocument i PrintableSheet.
Dependency inversion
Zasada odwrócenia zależności (Dependency inversion principle) polega na tym, że elementy wysokiego poziomu nie powinny zależeć od jednostek niskiego poziomu, a zależności między nimi wynikają z abstrakcji. Abstrakcje nie powinny zależeć od szczegółów lecz to szczegóły powinny zależeć od abstrakcji. Mówiąc w skrócie zależności powinny w jak największym stopniu zależeć od abstrakcji, a nie konkretnej implementacji. Przeważnie zostaje to osiągnięte za pomocą interfejsów i klas abstrakcyjnych.
Klasa Downloader odpowiedzialna za pobieranie informacji z serwera dokonuje szczegółowej implementacji zachowania, polegając tym samym na zależnościach niskiego poziomu. Takie podejście nie jest wskazane, gdyż silnie łączy klasę wysokiego poziomu z niskopoziomowymi obiektami. Alternatywnym podejściem będzie wprowadzenie warstwy abstrakcji w postaci interfejsu Network wywoływanej z klienta Downloader. Szczegóły implementacji niskopoziomowych obiektów zostają przeniesione do klas RestNetwork i SocketNetwork implementujących interfejs Network co pozwala ukryć zależności niskiego poziomu przed klientem.