SOLID Partea 1 - Principiul responsabilității unice

Responsabilitatea unică (SRP), Deschidere / Închidere, Înlocuirea lui Liskov, Segregarea interfeței și Inversiunea dependenței. Cinci principii agile care ar trebui să vă ghideze de fiecare dată când scrieți cod.

Definitia

O clasă ar trebui să aibă doar un singur motiv pentru a se schimba.

Definit de Robert C. Martin în cartea sa Agile Software Development, Principii, modele și practici și republicată ulterior în versiunea C # a cărții Agile Principles, Patterns and Practices în C #, este unul dintre cele cinci principii agile SOLID. Ceea ce afirmă este foarte simplu, însă realizarea acelei simplități poate fi foarte dificilă. O clasă ar trebui să aibă doar un singur motiv pentru a se schimba.

Dar de ce? De ce este atât de important să ai doar un singur motiv pentru schimbare?

În limbi statice și compilate, mai multe motive pot duce la mai multe redistări nedorite. Dacă există două motive diferite de schimbare, se poate presupune că două echipe diferite pot lucra pe același cod din două motive diferite. Fiecare va trebui să implementeze soluția sa, care, în cazul unei limbi compilate (cum ar fi C ++, C # sau Java), poate duce la module incompatibile cu alte echipe sau alte părți ale aplicației.

Chiar dacă este posibil să nu utilizați un limbaj compilat, poate fi necesar să retestați aceeași clasă sau modul din diferite motive. Aceasta înseamnă mai multă muncă, timp și efort.

Audienta

Stabilirea unei singure responsabilități pe care ar trebui să o aibă o clasă sau un modul este mult mai complexă decât să se uite la o listă de verificare. De exemplu, un indiciu pentru a găsi motivele pentru schimbare este analiza publicului pentru clasa noastră. Utilizatorii aplicației sau sistemului pe care îl dezvoltăm, care sunt deserviți de un anumit modul, vor fi cei care solicită modificări. Cei deserviți vor cere schimbarea. Iată câteva module și audiențele lor posibile.

  • Modul de persistență - Audiența include DBA-urile și arhitecții software.
  • Modulul de raportare - Audiența include funcționari, contabili și operațiuni.
  • Module de calculare a plăților pentru un sistem de salarizare - Audiența poate include avocați, manageri și contabili.
  • Book Search Module pentru un sistem de management al bibliotecii - Audiența poate include bibliotecarul și / sau clienții înșiși.

Roluri și actori

Asocierea persoanelor concrete cu toate aceste roluri poate fi dificilă. Într-o companie mică, o singură persoană poate avea nevoie să îndeplinească mai multe roluri, în timp ce într-o companie mare pot exista mai multe persoane alocate unui singur rol. Deci, pare mult mai rezonabil să ne gândim la roluri. Dar rolurile de la sine sunt destul de greu de definit. Care este un rol? Cum o găsim? Este mult mai ușor să ne imaginăm actorii care fac acele roluri și asociază audiența noastră cu acei actori.

Deci, dacă audiența noastră definește motivele schimbării, actorii definesc publicul. Acest lucru ne ajută foarte mult să reducem conceptul de persoane concrete cum ar fi "arhitectul Ioan" în Arhitectură, sau "Maria referentul" la Operații.

Deci, o responsabilitate este o familie de funcții care servește unui anumit actor. (Robert C. Martin)

Sursa schimbării

În sensul acestui raționament, actorii devin o sursă de schimbare pentru familia de funcții care le servește. Pe măsură ce nevoile lor se schimbă, familia specifică de funcții trebuie să se schimbe și pentru a-și satisface nevoile.

Un actor pentru responsabilitate este singura sursă de schimbare pentru această responsabilitate. (Robert C. Martin)

Exemple clasice

Obiecte care se pot "imprima" pe sine

Să presupunem că avem a Carte clasă încapsulând conceptul de carte și funcționalitățile sale.

clasa de carti function getTitle () retur "o carte mare";  funcția getAuthor () return "John Doe";  funcția turnPage () // pointer la pagina următoare funcția printCurrentPage () echo "conținutul paginii curente"; 

Aceasta ar putea părea o clasă rezonabilă. Avem carte, poate oferi titlul, autorul și poate transforma pagina. În cele din urmă, este de asemenea capabil să imprime pagina curentă pe ecran. Dar există o mică problemă. Dacă ne gândim la actorii implicați în operarea Carte obiect, cine ar putea fi? Putem să ne gândim cu ușurință la doi actori diferiți: gestionarea cărților (cum ar fi bibliotecarul) și mecanismul de prezentare a datelor (cum ar fi modul în care vrem să furnizăm conținutul utilizatorului - pe ecran, interfața grafică, interfața UI, poate tipărirea) . Acestea sunt doi actori foarte diferiți.

Amestecarea logicii de afaceri cu prezentarea este rău pentru că este împotriva principiului responsabilității unice (SRP). Uitați-vă la următorul cod:

clasa de carti function getTitle () retur "o carte mare";  funcția getAuthor () return "John Doe";  funcția turnPage () // pointer la pagina următoare funcția getCurrentPage () retur "conținutul paginii curente";  interfață Imprimanta function printPage ($ page);  clasa PlainTextPrinter implementează Printer function printPage ($ page) echo $ page;  clasa HtmlPrinter implementează Printer function printPage ($ page) echo '
". $ pagina. '
„;

Chiar și acest exemplu foarte simplu arată modul în care separarea prezentării de logica de afaceri și respectarea SRP oferă avantaje mari în flexibilitatea designului nostru.

Obiectele care se pot "salva" pe ele însele

Un exemplu similar cu cel de mai sus este atunci când un obiect poate salva și se poate recupera din prezentare.

clasa de carti function getTitle () retur "o carte mare";  funcția getAuthor () return "John Doe";  funcția turnPage () // pointer la pagina următoare funcția getCurrentPage () retur "conținutul paginii curente";  funcția salvați () $ filename = '/ documents /'. $ This-> getTitle (). "-". $ This-> getAuthor (); file_put_contents ($ filename, serialize ($ this)); 

Putem identifica din nou mai mulți actori, cum ar fi Sistemul de gestionare a cărților și persistența. Ori de câte ori dorim să schimbăm persistența, trebuie să schimbăm această clasă. Ori de câte ori dorim să schimbăm modul în care ajungem de la o pagină la alta, trebuie să modificăm această clasă. Există mai multe axe de schimbare aici.

clasa de carti function getTitle () retur "o carte mare";  funcția getAuthor () return "John Doe";  funcția turnPage () // pointer la pagina următoare funcția getCurrentPage () retur "conținutul paginii curente";  clasa SimpleFilePersistence function save (Cartea $ book) $ filename = '/ documents /'. $ book-> getTitle (). "-". $ Book> getAuthor (); file_put_contents ($ filename, serialize ($ book)); 

Deplasarea operației de persistență într-o altă clasă va separa în mod clar responsabilitățile și vom fi liberi să schimbăm metode de persistență fără a ne afecta pe noi Carte clasă. De exemplu, punerea în aplicare a DatabasePersistence clasa ar fi trivială și logica noastră de afaceri construită în jurul operațiunilor cu cărți nu se va schimba.

Vizualizare la nivel superior

În articolele anterioare am menționat și prezentat frecvent schema arhitecturală de nivel înalt care poate fi văzută mai jos.


Dacă analizăm această schemă, puteți vedea modul în care este respectat principiul responsabilității unice. Crearea obiectelor este separată în partea dreaptă în fabrici și punctul principal de intrare al aplicației noastre, un singur actor este responsabil. Persistența este, de asemenea, îngrijită în partea de jos. Un modul separat pentru responsabilitatea separată. În cele din urmă, în stânga, avem prezentare sau mecanismul de livrare, dacă doriți, sub formă de MVC sau orice alt tip de interfață utilizator. SRP a respectat din nou. Tot ce rămâne rămâne să ne dăm seama ce să facem în interiorul logicii noastre de afaceri.

Considerații privind proiectarea software-ului

Când ne gândim la software-ul pe care trebuie să-l scriem, putem analiza multe aspecte diferite. De exemplu, mai multe cerințe care afectează aceeași clasă pot reprezenta o axă de schimbare. Aceste axe ale schimbării pot fi un indiciu pentru o singură responsabilitate. Există o mare probabilitate ca grupurile de cerințe care afectează același grup de funcții să aibă motive să se schimbe sau să fie specificate în primul rând.

Valoarea principală a software-ului este ușurința schimbării. Funcția secundară este funcționalitatea, în sensul satisfacerii cât mai multor cerințe, satisfacerea nevoilor utilizatorului. Cu toate acestea, pentru a obține o valoare secundară superioară, o valoare primară este obligatorie. Pentru a menține valoarea noastră primară mare, trebuie să avem un design ușor de schimbat, de extins, de adaptat la noi funcționalități și de a asigura respectarea SRP.

Putem raționa într-o manieră pas cu pas:

  1. Valoarea primară mare duce în timp la o valoare secundară mare.
  2. Valoarea secundară înseamnă nevoile utilizatorilor.
  3. Nevoile utilizatorilor înseamnă necesitățile actorilor.
  4. Nevoile actorilor determină nevoile schimbărilor acestor actori.
  5. Nevoile de schimbare a actorilor ne definesc responsabilitățile.

Atunci când proiectăm software-ul nostru, ar trebui:

  1. Găsiți și definiți actorii.
  2. Identificați responsabilitățile care le servesc pe acești actori.
  3. Grupează funcțiile și clasele noastre astfel încât fiecare să aibă doar o singură responsabilitate alocată.

Un exemplu mai puțin evident

clasa de carti function getTitle () retur "o carte mare";  funcția getAuthor () return "John Doe";  funcția turnPage () // pointer la pagina următoare funcția getCurrentPage () retur "conținutul paginii curente";  funcția getLocation () // returnează poziția în bibliotecă // ie. numărul raftului și numărul camerei

Acum, acest lucru poate părea perfect rezonabil. Nu avem nici o metodă care să se ocupe de persistență sau de prezentare. Avem pe noi turnPage () funcționalitate și câteva metode pentru a furniza informații diferite despre carte. Cu toate acestea, este posibil să avem o problemă. Pentru a afla mai bine, am putea analiza aplicația noastră. Functia getLocation () poate fi problema.

Toate metodele Carte clasa sunt despre logica de afaceri. Deci, perspectiva noastră trebuie să fie din punctul de vedere al afacerii. Dacă cererea noastră este scrisă pentru a fi folosită de adevărații bibliotecari care caută cărți și ne dau o carte fizică, atunci SRP ar putea fi încălcată.

Putem considera că operațiile actorilor sunt cele interesate de metode getTitle (), getAuthor () și getLocation (). Clienții pot avea, de asemenea, acces la aplicație pentru a selecta o carte și pentru a citi primele câteva pagini pentru a obține o idee despre carte și a decide dacă o doresc sau nu. Așadar, cititorii de actori ar putea fi interesați de toate metodele, cu excepția getLocations (). Un client obișnuit nu are grijă de locația în care se află biblioteca. Cartea va fi predată clientului de către bibliotecar. Deci, într-adevăr, avem o încălcare a SRP.

clasa de carti function getTitle () retur "o carte mare";  funcția getAuthor () return "John Doe";  funcția turnPage () // pointer la pagina următoare funcția getCurrentPage () retur "conținutul paginii curente";  clasa BookLocator localiza funcția (Book book book) // returnează poziția în bibliotecă // ie. numărul raftului și numărul camerei $ libraryMap-> findBookBy ($ book-> getTitle (), $ book-> getAuthor ()); 

Prezentarea BookLocator, bibliotecarul va fi interesat de BookLocator. Clientul va fi interesat de Carte numai. Desigur, există mai multe modalități de implementare a BookLocator. Poate să utilizeze autorul și titlul sau un obiect de carte și să obțină informațiile necesare din Carte. Depinde întotdeauna de afacerea noastră. Ceea ce este important este că dacă biblioteca este schimbată și bibliotecarul va trebui să găsească cărți într-o bibliotecă diferită organizată, Carte obiect nu va fi afectat. În același mod, dacă decidem să oferim cititorilor un rezumat precompilat în loc să le permită să navigheze paginile, acest lucru nu va afecta bibliotecarul și nici procesul de găsire a raftului pe care se află cărțile.

Cu toate acestea, dacă afacerea noastră este de a elimina bibliotecarul și de a crea un mecanism de auto-service în biblioteca noastră, atunci putem considera că SRP este respectată în primul exemplu. Cititorii sunt, de asemenea, bibliotecarii noștri, trebuie să meargă și să găsească cartea în sine și apoi să o verifice la sistemul automat. Aceasta este și o posibilitate. Este important să vă amintiți aici că trebuie să vă gândiți întotdeauna la afacerea dvs. cu atenție.

Gândurile finale

Principiul responsabilității unice ar trebui să fie întotdeauna luat în considerare atunci când scriem cod. Designul de clase și module este foarte afectat de acesta și duce la un design redus cuplat, cu dependințe mai mici și mai ușoare. Dar, ca orice monedă, are două fețe. Este tentant să proiectăm încă de la începutul aplicației noastre SRP. Este, de asemenea, tentant să identificăm cât de mulți actori ne dorim sau avem nevoie. Dar acest lucru este de fapt periculos - din punct de vedere al designului - de a încerca să se gândească la toate părțile încă de la început. Examinarea SRP excesivă poate duce cu ușurință la optimizarea prematură și, în locul unui design mai bun, poate duce la o risipire în care responsabilitățile clare ale clasei sau modulelor pot fi greu de înțeles.

Deci, ori de câte ori observați că o clasă sau un modul începe să se schimbe din diferite motive, nu ezitați, luați pașii necesari pentru a respecta SRP, cu toate acestea nu lăsați-o în întârziere, deoarece optimizarea prematură vă poate înșela cu ușurință.

Cod