Uvod
SOLID je mnemonički akronim za pet načela objektno orijentiranog dizajna koje je uveo Robert C. Martin, popularno poznat kao Uncle Bob. Ova načela osmišljena su kako bi pomogla dizajnerima softvera, arhitektima, inženjerima i programerima u stvaranju fleksibilnijeg softvera koji je lakše održavati i skalirati. Slijedeći ova načela, možete dizajnirati klase koje je lakše testirati, refaktorirati, ponovno upotrijebiti i proširiti.
Akronim SOLID označava:
S – Načelo jedinstvene odgovornosti
O – Načelo otvorenosti/zatvorenosti
L – Načelo Liskovljeve zamjene
I – Načelo segregacije sučelja
D – Načelo inverzije ovisnosti
U ovom članku objasnit ćemo svako načelo pojedinačno kako bismo razumjeli kako vam može pomoći u pisanju boljeg koda. Osim toga, dodat ćemo isječke koda za svako načelo kako bismo vam pokazali kako ih možete primijeniti u svom programiranju, kao i što biste trebali izbjegavati u arhitekturi čistog koda. Za demonstraciju koncepata koristit ćemo programski jezik Kotlin koji su razvili JetBrains i suradnici otvorenog koda.
Načelo jedinstvene odgovornosti
Načelo jedinstvene odgovornosti (SRP) je načelo dizajna softvera koje nalaže da svaka klasa ili modul u programu treba imati jedinstvenu, dobro definiranu odgovornost. To znači da bi klasa trebala imati samo jedan razlog za promjenu i trebala bi biti odgovorna za samo jedan dio funkcionalnosti programa.
Razmotrite sljedeći isječak koda kao primjer kako se ovo načelo može primijeniti u jeziku Kotlin:
|
1 2 3 4 5 6 7 8 9 |
data class User( val firstName: String, val lastName: String, val email: String ) |
Ključna riječ data u Kotlinu označava da je ova klasa podatkovna klasa, što znači da je namijenjena pohrani podataka i nema nikakvo složeno ponašanje. Koristeći ovu podatkovnu klasu, možemo stvoriti UserService klasu za upravljanje korisnicima kao što je prikazano u nastavku:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
class UserService { fun createUser(user: User) { // kod za kreiranje korisnika u bazi podataka } fun deleteUser(user: User) { // kod za brisanje korisnika iz baze podataka } fun updateUser(user: User) { // kod za ažuriranje korisnika u bazi podataka } fun getUser(id: Int): User { // kod za dohvaćanje korisnika iz baze podataka } } |
U ovom primjeru, klasa UserService ima jedinstvenu odgovornost: upravljanje korisnicima u bazi podataka. Svaki korisnik je predstavljen s data class User kodom koji je ranije podijeljen. Svi metodi u klasi UserService povezani su s ovom odgovornošću i stoga su kohezivni. To olakšava razumijevanje i održavanje klase, jer je jasno da su svi metodi u klasi povezani s jednim, dobro definiranim zadatkom.
Metoda koja krši Načelo jedinstvene odgovornosti (SRP) u klasi UserService bila bi ona koja nije povezana s primarnom odgovornošću klase, a to je upravljanje korisnicima u bazi podataka. Na primjer, razmotrite sljedeću varijaciju klase UserService :
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
class UserService { fun createUser(user: User) { // kod za kreiranje korisnika u bazi podataka } fun deleteUser(user: User) { // kod za brisanje korisnika iz baze podataka } fun updateUser(user: User) { // kod za ažuriranje korisnika u bazi podataka } fun getUser(id: Int): User { // kod za dohvaćanje korisnika iz baze podataka } fun sendEmail(user: User, subject: String, message: String) { // kod za slanje e-pošte korisniku } } |
U ovom primjeru, sendEmail metoda ne odnosi se na primarnu odgovornost UserService klase, a to je upravljanje korisnicima u bazi podataka. Ova metoda je odgovorna za slanje e-pošte, što je zasebna briga od upravljanja korisnicima. Kao rezultat toga, ova metoda krši SRP, jer uvodi drugi razlog za promjenu klase UserService.
Kako bismo se pridržavali načela jedinstvene odgovornosti, bilo bi bolje odvojiti funkcionalnost slanja e-pošte u zasebnu klasu, kao što je EmailService klasa. To bi omogućilo klasi UserService da se usredotoči na svoju primarnu odgovornost upravljanja korisnicima, a klasi EmailService da se usredotoči na svoju odgovornost slanja e-pošte.
Treba napomenuti da se načelo jedinstvene odgovornosti ne odnosi na broj metoda koje klasa ima, već na kohezivnost metoda i jasno razdvajanje odgovornosti unutar klase.
Načelo otvorenosti/zatvorenosti
Načelo otvorenosti/zatvorenosti (OCP) je načelo dizajna softvera koje navodi da bi softverski entiteti (kao što su klase, moduli ili funkcije) trebali biti otvoreni za proširenje, ali zatvoreni za izmjene. To znači da bi trebalo biti moguće dodati novu funkcionalnost klasi ili modulu bez mijenjanja njezinog postojećeg koda.
Razmotrimo klasu UserService iz prethodnog primjera. Pretpostavimo da želimo dodati novu značajku u klasu UserService koja nam omogućuje pretraživanje korisnika po adresi e-pošte. Jedan od načina da to učinimo bio bi dodavanje nove metode u klasu UserService kao što je istaknuto u kodu u nastavku:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
class UserService { fun createUser(user: User) { // kod za kreiranje korisnika u bazi podataka } fun deleteUser(user: User) { // kod za brisanje korisnika iz baze podataka } fun updateUser(user: User) { // kod za ažuriranje korisnika u bazi podataka } fun getUser(id: Int): User { // kod za dohvaćanje korisnika iz baze podataka } fun searchUsersByEmail(email: String): List<User> { // kod za pretraživanje korisnika po adresi e-pošte } } |
Ovaj pristup radi, ali krši načelo otvorenosti/zatvorenosti, jer smo morali modificirati postojeću klasu UserService kako bismo dodali novu značajku. Bolji pristup bio bi korištenje nasljeđivanja ili kompozicije za proširenje funkcionalnosti klase UserService bez mijenjanja njezinog koda.
Kako bismo to postigli, mogli bismo kreirati novu klasu pod nazivom UserSearchService koja proširuje klasu UserService i dodaje funkcionalnost pretraživanja po e-pošti:
|
1 2 3 4 5 6 7 8 9 |
class UserSearchService(private val userService: UserService) : UserService() { fun searchUsersByEmail(email: String): List<User> { // kod za pretraživanje korisnika po adresi e-pošte } } |
U ovom primjeru, klasa UserSearchService otvorena je za proširenje, jer pruža dodatnu funkcionalnost izvan onoga što nudi klasa UserService klasa. Istovremeno, UserService klasa ostaje zatvorena za izmjene, jer nismo morali mijenjati njezin kod kako bismo dodali značajku pretraživanja po e-pošti.
Liskovljev princip zamjene
Liskovljev princip zamjene (LSP) je princip dizajna softvera koji navodi da bi objekti nadklase trebali moći biti zamijenjeni objektima podklase bez utjecaja na ispravnost programa. To znači da bi podklasa trebala biti valjana zamjena za svoju nadklasu i trebala bi se ponašati na isti način kao i nadklasa kada se koristi u istom kontekstu.
Nastavit ćemo demonstraciju koristeći User i UserService klase iz prethodnih primjera. Kako bismo omogućili proširenje klase User, koristimo ključnu riječ open u Kotlinu:
|
1 2 3 4 5 6 7 8 9 |
open class User( val firstName: String, val lastName: String, val email: String ) |
Ovdje je originalna UserService klasa koja koristi User podatkovnu klasu iznad:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
class UserService { fun createUser(user: User) { // kod za kreiranje korisnika u bazi podataka } fun deleteUser(user: User) { // kod za brisanje korisnika iz baze podataka } fun updateUser(user: User) { // kod za ažuriranje korisnika u bazi podataka } fun getUser(id: Int): User { // kod za dohvaćanje korisnika iz baze podataka } } |
Pretpostavimo da želimo kreirati podklasu od User pod nazivom AdminUser koja predstavlja korisnike s administrativnim privilegijama. To bismo mogli učiniti ovako:
|
1 2 3 4 5 6 7 8 9 |
class AdminUser( firstName: String, lastName: String, email: String, ) : User(firstName, lastName, email) |
U ovom primjeru, klasa AdminUser valjana je zamjena za klasu User jer se ponaša na isti način kao i klasa User i može se koristiti svugdje gdje se očekuje User objekt. Na primjer, možemo koristiti klasu AdminUser s klasom UserService na ovaj način:
|
1 2 3 4 5 6 7 8 9 |
fun main() { val userService = UserService() val adminUser = AdminUser("John", "Doe", "john.doe@example.com") userService.createUser(adminUser) } |
Ovaj kod je ispravan, jer je klasa AdminUser valjana zamjena za klasu User i može se koristiti na isti način kao i User objekt.
Važno je napomenuti da se Liskovljev princip zamjene ne odnosi samo na nasljeđivanje. Radi se o osiguravanju da se objekti podklase ponašaju na isti način kao i objekti nadklase, bez obzira na to kako je podklasa implementirana. Na primjer, kada bi se klasa AdminUser ponašala drugačije od klase User na neki način, to bi prekršilo Liskovljev princip zamjene, jer ne bi bila valjana zamjena za klasu User klasu.
Princip segregacije sučelja
Princip segregacije sučelja (ISP) je princip dizajna softvera koji navodi da klijenti ne bi trebali biti prisiljeni ovisiti o sučeljimakoja ne koriste. To znači da je općenito dobra ideja kreirati mala, fokusirana sučelja koja dobro rade jednu stvar, umjesto kreiranja velikih sučelja koja pokušavaju raditi mnogo stvari.
Evo primjera kako se ovaj princip može primijeniti ponovnim pisanjem User i UserService klasa iz prethodnih primjera:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
interface UserService { fun createUser(user: User) fun deleteUser(user: User) fun updateUser(user: User) fun getUser(id: Int): User } class DatabaseUserService : UserService { override fun createUser(user: User) { // kod za kreiranje korisnika u bazi podataka } override fun deleteUser(user: User) { // kod za brisanje korisnika iz baze podataka } override fun updateUser(user: User) { // kod za ažuriranje korisnika u bazi podataka } override fun getUser(id: Int): User { // kod za dohvaćanje korisnika iz baze podataka } } |
U ovom primjeru, UserService sučelje definira četiri metode koje su povezane s upravljanjem korisnicima u bazi podataka. DatabaseUserService klasa implementira ovo sučelje i pruža konkretne implementacije za ove metode.
Pretpostavimo da želimo dodati novu značajku u UserService sučelje koje nam omogućuje pretraživanje korisnika po adresi e-pošte. Jedan od načina da to učinimo bio bi dodavanje nove metode u UserService sučelje:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
interface UserService { fun createUser(user: User) fun deleteUser(user: User) fun updateUser(user: User) fun getUser(id: Int): User fun searchUsersByEmail(email: String): List<User> } |
Vaš se kod neće pokrenuti, osim ako također ne implementirate ovu metodu u svim klasama koje implementiraju UserService sučelje:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
class DatabaseUserService : UserService { override fun createUser(user: User) { // kod za kreiranje korisnika u bazi podataka } override fun deleteUser(user: User) { // kod za brisanje korisnika iz baze podataka } override fun updateUser(user: User) { // kod za ažuriranje korisnika u bazi podataka } override fun getUser(id: Int): User { // kod za dohvaćanje korisnika iz baze podataka } override fun searchUsersByEmail(email: String): List<User> { // kod za pretraživanje korisnika po adresi e-pošte } } |
Iako ovaj pristup radi, on krši princip segregacije sučelja (Interface Segregation Principle), jer prisiljava DatabaseUserService klasu da implementira metodu ( searchUsersByEmail ) koja joj možda nije potrebna ili je ne koristi.
Bolji pristup bio bi stvaranje zasebnog sučelja za funkcionalnost pretraživanja po e-pošti:
|
1 2 3 4 5 |
interface UserSearchService { fun searchUsersByEmail(email: String): List<User> } |
Sada imamo zasebna, mala i usmjerena sučelja, tj. UserService i UserSearchServicekoja imaju jedinstvenu odgovornost. Klasa koja zahtijeva sve funkcionalnosti ovih sučelja može ih implementirati kao što je prikazano u isječku koda u nastavku:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
class DatabaseUserService : UserService, UserSearchService { override fun createUser(user: User) { // kod za kreiranje korisnika u bazi podataka } override fun deleteUser(user: User) { // kod za brisanje korisnika iz baze podataka } override fun updateUser(user: User) { // kod za ažuriranje korisnika u bazi podataka } override fun getUser(id: Int): User { // kod za dohvaćanje korisnika iz baze podataka } override fun searchUsersByEmail(email: String): List<User> { // kod za pretraživanje korisnika po adresi e-pošte } } |
To je u skladu s načelom segregacije sučelja, jer osigurava da klijenti (kao što je DatabaseUserService klasa) nisu prisiljeni ovisiti o sučeljima koja ne koriste.
Kako bismo bolje razumjeli ovaj koncept, pretpostavimo da imamo drugu klasu pod nazivom MemoryUserService koja implementira sučelje UserService ali ne treba funkcionalnost pretraživanja po e-pošti, kod možemo napisati ovako:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
class MemoryUserService : UserService { private val users = mutableListOf<User>() override fun createUser(user: User) { users.add(user) } override fun deleteUser(user: User) { users.remove(user) } override fun updateUser(user: User) { val index = users.indexOf(user) if (index >= 0) { users[index] = user } } override fun getUser(id: Int): User { return users[id] } } |
U ovom primjeru, klasa MemoryUserService treba samo implementirati metode definirane u sučelju UserService i ne mora brinuti o funkcionalnosti pretraživanja po e-pošti. To omogućuje klasi MemoryUserService da se usredotoči na svoju primarnu odgovornost upravljanja korisnicima u memoriji, umjesto da bude prisiljena implementirati nepovezanu funkcionalnost.
Načelo inverzije ovisnosti (Dependency Inversion Principle)
Načelo inverzije ovisnosti (DIP) je načelo dizajna softvera koje navodi da moduli visoke razine ne bi trebali ovisiti o modulima niske razine, već bi oboje trebali ovisiti o apstrakcijama. To znači da je općenito dobra ideja dizajnirati softver tako da komponente visoke razine nisu vezane uz specifične implementacije komponenti niske razine, već ovise o apstrakcijama (kao što su sučelja ili apstraktne klase) koje se mogu implementirati na različite načine.
Pogledajmo primjer kako se ovo načelo može primijeniti na klase User i UserService korištene u prethodnim isječcima koda:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
interface UserRepository { fun create(user: User) fun delete(user: User) fun update(user: User) fun get(id: Int): User } class UserService(private val repository: UserRepository) { fun createUser(user: User) { repository.create(user) } fun deleteUser(user: User) { repository.delete(user) } fun updateUser(user: User) { repository.update(user) } fun getUser(id: Int): User { return repository.get(id) } } |
U ovom primjeru, UserService klasa ovisi o apstrakciji zvanoj UserRepository sučelje, umjesto da ovisi o specifičnoj implementaciji repozitorija korisnika. To nam omogućuje implementaciju UserRepository sučelja na različite načine, primjerice s bazom podataka ili pohranom u memoriji, bez utjecaja na UserService klasu.
Na primjer, evo implementacije UserRepository sučelja koje koristi bazu podataka:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
class DatabaseUserRepository: UserRepository { override fun create(user: User) { // kod za kreiranje korisnika u bazi podataka } override fun delete(user: User) { // kod za brisanje korisnika iz baze podataka } override fun update(user: User) { // kod za ažuriranje korisnika u bazi podataka } override fun get(id: Int): User { // kod za dohvaćanje korisnika iz baze podataka } } |
Evo još jedne implementacije UserRepository sučelja koje koristi pohranu u memoriji:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
class MemoryUserRepository : UserRepository { private val users = mutableListOf<User>() override fun create(user: User) { users.add(user) } override fun delete(user: User) { users.remove(user) } override fun update(user: User) { val index = users.indexOf(user) if (index >= 0) { users[index] = user } } override fun get(id: Int): User { return users[id] } } |
To čini sustav fleksibilnijim i lakšim za održavanje, jer nam omogućuje promjenu implementacije repozitorija bez utjecaja na ostatak sustava. Također olakšava testiranje klase UserService, jer možemo simulirati ovisnost UserRepository u našim testovima.
Zaključak
U ovom smo članku govorili o pet načela SOLID kod i dijeljene isječke koda koji zadovoljavaju svako načelo. Pridržavanje ovih načela može vam pomoći u dizajniranju softverskih sustava koji su fleksibilniji, lakši za održavanje i skalabilniji. Međutim, važno je imati na umu da su ova načela smjernice, a ne stroga pravila, te je na razvojnom programeru da odluči kada će ih i kako primijeniti u kontekstu svog specifičnog projekta. Nastavite učiti posjetom našem blogu za detaljnije i ažurnije članke i vodiče o računalstvu u oblaku i DevOpsu, dizajnu i razvoju softvera, tehnološkim trendovima na koje treba obratiti pozornost i još mnogo toga.
Sretno kodiranje!
Komentari
Još nema komentara. Budite prvi.