Zasady SOLID to pięć głównych zasad, według których należy się kierować podczas programowania obiektowego. Skrót SOLID pochodzi od pierwszych liter każdej z zasady. Zasady te zostały przedstawione na rysunku poniżej.

1. Single responsibility.
Pierwsza to zasada pojedynczej odpowiedzialności . Mówi ona o tym, że każda klasa powinna być odpowiedzialna za jedną rzecz. W praktyce zwiększa nam to ilość klas w projekcie. Kod jest wtedy bardziej czytelny, przejrzysty oraz w przyszłości prościej rozwijać taki kod. Poniżej przykład klasy nie prowadzonej zgodnie z zasadą pojedynczej odpowiedzialności i odpowiada za kilka rzeczy na raz oraz przykład poprawny w którym jedna klasa jest rozbita na kilka mniejszych klas. Jak widać w złym przykładzie klasa ma zbyt dużo funkcji. Klasa Adresat powinna tylko posiadać dane o danym adresacie, nie powinna być odpowiedzialna za walidacje emaila czy też dodawanie/usuwanie/edycje adresatów. Dlatego kod ten można zrefaktoryzować przez rozbicie go na mniejsze klasy o mniejszej odpowiedzialności, np. osobna klasa do walidacji emaila, osobna klasa odpowiedzialna za działania z adresatami. Dodatkowo można też stworzyć osobną klasę która będzie adres.
/* --- ZŁY PRZYKŁAD --- */
class Adresat {
string imie, nazwisko, email, kraj, miejscowość;
public:
void ustawImie(string noweImie);
void ustawNazwisko(string noweNazwisko);
void ustawEmail(string nowyEmail);
void ustawKraj(string nowyKraj);
void ustawMiejscowosc(string nowaMiejscowosc);
string pobierzImie();
string pobierzNazwisko();
string pobierzEmail();
string pobierzKraj();
string pobierzMiejscowosc();
bool validateEmail(string email);
void dodajAdresata();
void usunAdresata();
void edytujAdresata();
};
/* --- DOBRY PRZYKŁAD --- */
class Adres{
string kraj, miejscowość;
public:
void ustawKraj(string nowyKraj);
void ustawMiejscowosc(string nowaMiejscowosc);
string pobierzKraj();
string pobierzMiejscowosc();
};
class Adresat {
string imie, nazwisko, email;
Adres adresAdresata;
public:
void ustawImie(string noweImie);
void ustawNazwisko(string noweNazwisko);
void ustawEmail(string nowyEmail);
void ustawAdres(Adres nowyAdres);
string pobierzImie();
string pobierzNazwisko();
string pobierzEmail();
Adres pobierzAdres();
};
class EmailWalidacja{
public bool validateEmail(string email);
}
class AdresatManager{
vector <Adresat> adresaci;
public:
void dodajAdresata();
void usunAdresata();
void edytujAdresata();
};
2. Open/Closed principle.
Zasada otwarta/zamknięta mówi o tym, że każda klasa powinna być otwarta na rozbudowę, ale zamknięta na modyfikację. Modyfikacje w programach są unikane, ponieważ zmodyfikowanie kodu w jednym miejscu może spowodować awarie w innym miejscu, bądź konieczność zmiany kodu w wielu miejscach. Użycie polimorfizmu dla tej zasady może być fundamentalne. Zobaczmy prosty przykład z kalkulatorem do liczenia pól różnych figur. Widzimy w złym przykładzie, że gdy np. chcielibyśmy dodać figurę trójkąt musielibyśmy zmodyfikować kod w klasie Kalkulator co jest zabronione. Dlatego w takiej sytuacji pomoże nam polimorfizm. Pozwoli nam to stworzyć abstrakcyjną klasę Kształt które może być rozwijana bez konieczności modyfikacji.
/* --- ZŁY PRZYKŁAD --- */
class Kwadrat
{
public int A;
}
class Prostokat
{
public int A, B;
}
class Kalkulator
{
public int Pole(object ksztalt)
{
if (ksztalt is Kwadrat)
{
Kwadrat kwadrat= (Kwadrat)ksztalt;
return kwadrat.A * kwadrat.A;
}
else if (ksztalt is Prostokat)
{
Prostokat prostokat= (Prostokat)ksztalt;
return prostokat.A * prostokat.B;
}
return 0;
}
}
/* --- DOBRY PRZYKŁAD --- */
abstract class Ksztalt
{
public abstract int Pole();
}
class Kwadrat: Ksztalt
{
public int A;
public override int Pole()
{
return A * A;
}
}
class Prostokat: Ksztalt
{
public int A, B;
public override int Pole()
{
return A * B;
}
}
class Calculator
{
public int Pole(Ksztalt ksztalt)
{
return ksztalt.Area();
}
}
3. Liskov’s substitution principle.
Trzecia zasada podstawienia Liskova mówi, że w miejscu klasy bazowej można użyć dowolnej klasy pochodnej (zgodność wszystkich metod). Zasada ta jest trochę cięższa do zrozumienia i jej nieprzestrzeganie jest powiązane z drugą zasadą open/close. Łamanie tej metody najczęściej jest skutkiem złego rozplanowania mechaniki dziedziczenia przez programiste, brak użycia polimorfizmu, klasy pochodne nadpisują logikę metod klasy bazowej. W dobrze zaplanowanym dziedziczeniu klasy pochodne powinny nadpisywać metody klasy bazowej, a nie je zastępować. Poniższy zły przykład pokazuje klasę Zwierzęta który posiada metodę Spaceruj(). Niestety gdy stworzymy z klasy Zwierzęta klasę Rybe to metoda Spaceruj() nie będzie do niej pasować i dojdzie to złamania zasady Liskova. Dlatego ważne jest dobre rozplanowanie mechanizmu dziedziczenia.
abstract class Zwierze
{
public string Name;
public abstract void Spaceruj();
}
class Kot: Zwierze
{
public override void Spaceruj()
{
cout << "Kot może spacerować";
}
}
class Ryba: Zwierze
{
public override void Spaceruj()
{
cout << "Ryby tylko pływają!";
}
}
4. Interface segregation.
Zasada segregacji interfejsów mówi o tym, aby każdy interfejs był konkretny i najmniejszy. Dla przykładu interfejs po lewej posiada kilka wydruków w różnych formatach. Dany dokument może nie posiadać któregoś z formatu wydruków, dlatego lepiej taki interfejs rozbić na kilka dedykowanych dla każdego wydruku.
/* --- ZŁY PRZYKŁAD --- */
interface Printer{
void PrintA3();
void PrintA4();
void PrintA5();
}
class Dokument1: Printer
{
/* posiada każdy format wydruku */
}
class Dokument2: Printer
{
/* posiada tylko format wydruku A4 */
}
/* --- DOBRY PRZYKŁAD --- */
interface A3{
void PrintA3();
}
interface A4{
void PrintA4();
}
interface A5{
void PrintA5();
}
class Dokument1: A3, A4, A5
{
/* posiada każdy format wydruku */
}
class Dokument2: A4
{
/* posiada tylko format wydruku A4 */
}
5. Dependency Inversion.
Ostatnia z zasad to zasada odwrócenia zależności. Mówi o tym, że wszystkie zależności powinny w jak największym stopniu zależeć od abstrakcji a nie od konkretnego typu. Jeżeli mamy jakiś parametr funkcji, który przyjmuje jakiś konkretny obiekt to znacznie lepszym rozwiązaniem jest, aby przyjmował on interfejs bądź klasę abstrakcyjną tego obiektu. Dzięki temu nie uzależniamy pojedynczej metody od konkretnego typu. Poniższy przykład zły pokazuje, że kontroler jest związany z konkretnym obiektem BookRepository. W dobrym kodzie wprowadzamy interfejs, aby zmniejszyć powiązanie między klasami.
/* --- ZŁY PRZYKŁAD --- */
class BookController
{
private BookRepository bookRepository;
public BookController(BookRepository bookRepository)
{
this.bookRepository = bookRepository;
}
}
/* --- DOBRY PRZYKŁAD --- */
interface IRepository<T>
{
IEnumerable<T> GetAll();
T GetById(int id);
}
class BookRepository: IRepository<Book>
{
public IEnumerable<Book> GetAll() { ... }
public Book GetById(int id) { ... }
}
class BookController
{
private IRepository<Book> bookRepository;
public BookController(IRepository<Book> bookRepository)
{
this.bookRepository = bookRepository;
}
}
Podsumowując, przestrzegając zasad SOLID jesteśmy w stanie pisać kod w sposób przejrzysty, możliwy do ciągłego rozwijania oraz taki który możemy wykorzystać w wielu projektach. Pomaga to też w przypadku gdy inna osoba przegląda nasz kod, który jest napsiany wg. zasad SOLID i jest w stanie w nim się odnaleźć.