Sari la conținutul principal

Laborator 2 - principiile SOLID

Cele mai comune practici utilizate în dezvoltarea modernă de software sunt principiile SOLID. Deși sunt foarte generale și se aplică în principal pentru dezvoltarea într-un limbaj orientat pe obiecte, ele sunt folosite astăzi pentru majoritatea aplicațiilor de tip enterprise.

S. Principiul responsabilității unice (Single-responsibility principle)

Nu ar trebui să existe mai mult de un motiv pentru care o clasă să fie modificată.

Implementarea mai multor funcționalități într-o clasă este dăunătoare pentru gestionarea codului, deoarece cu cât o clasă face mai multe, cu atât va fi mai prezentă în cod și orice schimbare asupra ei poate afecta multe locații, ceea ce, uneori, poate duce la erori și la supraîncarcarea în adaptarea codului existent. Prin urmare, în aplicațiile moderne, componentele software sunt împărțite în categorii pentru a respecta principiul responsabilității unice, (ex. entități de bază de date, obiecte de transfer de date, obiecte de valoare etc).

O. Principiul deschidere-închidere (Open–closed principle)

Entitățile software ar trebui să fie deschise pentru extindere, dar închise pentru modificare.

Când o componentă software este implementată și utilizată, dezvoltatorul face anumite presupuneri despre utilizarea ei și o testează în consecință; orice modificare ulterioară a acelei componente poate duce la erori sau comportament nedefinit. Așadar, componentele software, odată implementate, nu ar trebui să fie modificate, ci să-și extindă funcționalitatea prin alte mijloace. Cele mai folosite exemple de extindere a funcționalității sunt moștenire, metode de extensie sau programarea orientată pe aspecte, unde clasele, metodele sau câmpurile sunt adnotate și câștigă funcționalități suplimentare.

L. Principiul substituției Liskov (Liskov substitution principle)

Funcțiile care utilizează pointeri sau referințe la clase de bază trebuie să poată folosi obiecte ale claselor derivate fără să știe acest lucru.

Acest principiu este util în special pentru testarea unitară și de integrare; în aplicațiile bine scrise, atunci când componentele referențiază alte componente, ele dețin o referință la o interfață sau alt tip de abstractizare, ascunzând astfel implementarea. În scenariile de testare, acest lucru este util deoarece implementările de producție pot fi înlocuite cu implementări mock pentru a izola cazurile de testare, cum ar fi în cazul consumatorilor unui API.

I. Principiul segregării interfețelor (Interface segregation principle)

Clienții nu ar trebui să fie forțați să depindă de interfețe pe care nu le folosesc.

În general, când se dezvoltă o componentă software cu o interfață, aceasta este proiectată pentru a rezolva o nevoie punctuală; adăugarea unor funcționalități suplimentare ar duce implicit la un cod mai greu de gestionat și mai puțin lizibil. Segregarea interfețelor cu funcționalitățile lor coezive (ex. gestionarea utilizatorilor) face ca codul să fie modular, un avantaj pentru echipele de dezvoltatori care lucrează la proiecte mari, în special când se folosește o metodologie Agile.

D. Principiul inversării dependenței (Dependency inversion principle)

Depindeți de abstracții, nu de concretizări.

În loc ca fiecare componentă software să depindă direct de alta, fiecare implementează o interfață care este referențiată în locul ei. Această decuplare a componentelor face aplicațiile mai modulare și mai ușor de schimbat. De exemplu, dacă o componentă software este mutată într-un serviciu extern din locația originală, putem păstra pur și simplu interfața fără a schimba implementarea care face referință la interfață și implementăm un client pentru serviciul extern.

Exerciții

Pentru a exemplifica aceste principii ne vom folosi codul următor pentru a-l îmbunătății. Codul de mai jos reprezintă o lampă care are o funcționalitate de aprindere și una de observare dacă e sau nu aprinsă.

Aceste exemplu are ca scop să ințelegeți cum putem extinde ușor cod dacă respectăm principiile date.

Lamp.cs
public class Lamp
{
public bool IsOn { get; private set; }
public string State => IsOn ? "On" : "Off";

public void Toggle() => IsOn = !IsOn;

public void Observe() => Console.WriteLine("The lamp is {0}!", State);
}
Program.cs
internal static class Program
{
public static async Task Main()
{
var lamp = new Lamp();

var cancellationTokenSource = new CancellationTokenSource();
var cancellationToken = cancellationTokenSource.Token;

var read = Task.Run(() =>
{
while (!cancellationToken.IsCancellationRequested)
{
var line = Console.ReadLine();

switch (line)
{
case "toggle":
lamp.Toggle();
break;
case "exit":
cancellationTokenSource.Cancel();
break;
}
}

}, CancellationToken.None);

var observer = Task.Run(async () =>
{
while (!cancellationToken.IsCancellationRequested)
{
lamp.Observe();

try
{
await Task.Delay(TimeSpan.FromSeconds(3), cancellationToken);
}
catch (TaskCanceledException)
{
Console.WriteLine("Shutting down...");
}
}
}, CancellationToken.None);

await read;
await observer;
}
}
  1. Vrem să segregam cele două funcționalități din clasa Lamp și sa avem clasa Lamp care doar să o observăm și clasa Button care să schimbe starea.
  2. Pe cele două implementări vrem să le abstractizăm și lasăm fiecare clasă să implementeze interfețele corespunzătoare fiecărei funcționalități.
  3. La clasa Button trebuie să adăugăm noi funcționalități, să adaugăm metode pentru aprinderea lămpii și oprirea ei dacă nu se află deja în stările respective și folosiți-le în comenzile date din linia de comandă. Folosiți pentru asta metode de extensie.
  4. Instalați pachetul Microsoft.Extensions.DependencyInjection și folosiți clasa ServiceCollection și ServiceProvider pentru a construi obiectele necesare prin dependency injection. O dată creat un ServiceCollection trebuie folosita funcția AddScoped<Interface, Implementation>() și BuildServiceProvider() pentru a crea un ServiceProvider. Cu ServiceProvider se pot instanția obiectele necesare prin metoda GetRequiredService<Interface>().
  5. Adaugați in cod alte două implementări FancyLamp care schimbă textul afișat la observare și FancyButon care are trei stări, una oprită și două aprinse de culori diferite pentru lampă. Modificați codul asfel incât să fie ales din linie de comandă ce tip de obiect trebuie instanțiat.