Na blogu nuda, aż piszczy. Pora się otrząsnąć i wyjść naprzeciw oczekiwaniom tym kilku osobom, które z utęsknieniem wyczekiwały nowych wpisów na mojej stronie i tak mocno się rozczarowały 😉 Często umieszczam to czego sam aktualnie się uczę. Zauważyłem, że czegoś w moim kodzie brakuje. Po chwili namysłu stwierdziłem, że tym czymś jest swoisty zbiór reguł i zasad, który usystematyzowałby i poprawił mój kod. Taki, który brutalnie wziąłby klasy za fraki. Przedstawiam wam zasady Solid.
Czym są zasady Solid?
Jest to zbiór 5 reguł autorstwa prawdziwego guru „zero-jedynkowego” porządku, kodowego pedanta, autora książki „Czysty kod” – Roberta Martina, których przestrzeganie ma pomagać w utrzymywaniu porządku w kodzie języków obiektowych i w uniknięciu niepotrzebnych problemów w dłuższej perspektywie czasowej. Zasady Solid mają proste założenie: twórz taki kod, aby osoba spoza projektu nie miała problemów z jego zrozumieniem teraz zarówno teraz, jak i za parę miesięcy czy lat. Nie sztuką jest napisać coś co się kompiluje, uruchamia i działa. Kwintesencja umiejętnego pisania kodu jest jego właściwe formatowanie. Program ma nie sprawiać problemów w rozwijaniu i udoskonalaniu go.
Bardzo prawdopodobne, że program pisany z przymrużeniem oka na jakikolwiek z postulatów zasad Solid, prędzej, czy później i tak potrzebować będzie refaktoryzacji. Dlatego piszmy tak, aby możliwie jak najbardziej ograniczyć konieczność poprawy kodu za X miesięcy.
Zasady Solid są wręcz kluczową sprawą w projektach rozwijanych przez firmy. Jeśli jako Junior Developer, reguły wujaszka Boba są Ci obce to polecam jak najszybciej się z nimi zapoznać, a samą książkę „Czysty kod” warto dopisać do swojej listy książek, które koniecznie musisz przeczytać 😉. Nic nie stracisz, a na pewno sporo zyskasz. Sam zacząłem wprowadzać zasady Solid do swoich projektów i liczę na to, że z biegiem czasu hołdowanie tym zasadom stanie się dla mnie tak oczywiste i naturalne jak oddychanie 😊.
Zasady Solid – 5 przykazań schludnego programisty
Skrót SOLID rozwija się następująco:
- Single responsibility principle – zasada jednej odpowiedzialności,
- Open/closed principle – zasada otwarte-zamknięte,
- Liskov substitution principle – zasada podstawienia Liskov,
- Interface segregation principle – zasada segregacji interfejsów,
- Dependency inversion principle – zasada odwrócenia zależności.
Co stoi za tymi niejasno brzmiącymi hasłami? O tym poniżej. Krótko, rzeczowo i w przyswajalny sposób.
Single responsibility principle – zasada jednej odpowiedzialności
Pierwsza z zasad SOLID jest lekka, prosta, genialna i przyjemna. Założeniem jest, aby każda z klas miała tylko jeden cel, jedno przeznaczenie i jedną odpowiedzialność. Lepszym rozwiązaniem jest stworzenie kilku mniejszych klas, każda odpowiedzialna za coś innego. Ta praktyka jest szczególnie pomocna w utrzymaniu kodu i jego dalszym rozwoju. Zwiększasz jego czytelność i użyteczność.
Przykłady będą wręcz trywialne. Mają w prosty sposób przekazać istotę zasady SRP. Mamy taką oto klasy napisane niezgodnie z powyższą zasadą:
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 |
class Order { public: string _name; string _surname; string _city; string _postcode; string _street; string _housenumber; int _productID; int _amount; Order(string name, string surname, string city, string postcode, string street, string housenumber, int productID, int amount) { _name=name; _surname=surname; _city=city; _postcode=checkPostcode(postcode); _street=street; _housenumber=housenumber; _productID=productID; _amount=amount; } private: string checkPostcode(postcode) { if(postcode[2]=='-') return postcode; else { string exception="Wrong postcode :/"; throw exception; } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
package matura; public class Order { public String name, surname, city, postcode, street, housenumber; public int productID, amount; public Order(String Name, String Surname, String City, String PostCode, String Street, String Housenumber, int pid, int Amount) { name = Name; surname = Surname; city = City; postcode = PostCode; street = Street; housenumber = Housenumber; productID = pid; amount = Amount; } private String checkPostcode(String postcode) throws Exception { if(postcode.charAt(2) == '-') return postcode; else { throw new Exception("Wrong postcode: " + postcode); } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class Oder: name, surname, city, postcode, street, housenumber, productID, amount = None def __init__(self, Name, Surname, City, PostCode, Streent, Housenumber, PID, Amount): self.name = Name self.surname = Surname self.city = City self.postcode = PostCode self.housenumber = Housenumber self.productID = PID self.amount = Amount def _checkPostcode(self, postcode): if postcode[2] == '2': return postcode else: raise Exception("Wrong postcode") |
Jak można je zrefaktoryzować? Dobrym pomysłem będzie rozbicie klasy na dwie mniejsze. W ten sposób w późniejszej fazie projektu będzie można skorzystać z możliwości utworzenia obiektu dla klienta, niezwiązanego w żaden sposób z klasą zamówienie. A co z metodą sprawdzającą poprawność kodu pocztowego? Zawrzemy ja w nowoutworzonej klasie klienta? Otóż nie. Zasada jednej odpowiedzialności mówi, że może istnieć tylko jeden powód do zmian w klasie. Gdybyśmy chcieli dodać dodatkową informacje o kliencie i zmienić np. warunek w metodzie sprawdzającą kod pocztowy to mielibyśmy już dwa powody do zmiany klasy, wbrew SRP. Zatem tworzymy oddzielną klasę sprawdzającą tenże kod pocztowy.
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 40 41 42 43 44 45 46 47 |
class Client { public: string _name; string _surname; string _city; string _postcode; string _street; string _housenumber; Client(string name, string surname, string city, string postcode, string street, string housenumber) { _name=name; _surname=surname; _city=city; _postcode=postcode; _street=street; _housenumber=housenumber; } } class Order { public: Client ClientData; int _productID; int _amount; Order(Client clientData, int productID, int amount) { ClientData=clientData; _productID=productID; _amount=amount; } } class PostcodeValidator { public: bool checkPostcode(postcode) { if(postcode[2]=='-') return 1; else { return 0; } } } |
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 |
package matura; public class Client { public String name, surname, city, postcode, street, housenumber; public Client(String Name, String Surname, String City, String PostCode, String Street, String Housenumber) { name = Name; surname = Surname; city = City; postcode = PostCode; street = Street; housenumber = Housenumber; } } class Order{ public Client clientData; int productID, amount; public Order(Client client, int PID, int Amount) { clientData = client; productID = PID; amount = Amount; } } class PostcodeValidator{ public boolean checkPostcode(String postcode) { if(postcode.charAt(2) == '-') return true; else { return false; } } } |
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 |
class Client: name, surname, city, postcode, street, housenumber = "" def __init__(self, Name, Surname, City, PostCode, Street, Housenumber): self.name = Name self.surname = Surname self.city = City self.postcode = PostCode self.housenumber = Housenumber self.street = Street def _checkPostcode(self, postcode): if postcode[2] == '2': return postcode else: raise Exception("Wrong postcode") class Order: productID, amount = 0 clientData = None def __init__(self, client, PID, Amount): self.clientData = client self.productID = PID self.amount = Amount class PostcodeValidator: def checkPostcode(self, postcode): if postcode[2] == '2': return True else: return False |
Myślałem, że uniknę tego porównania, ale jest zbyt obrazowe, aby go nie użyć. Mianowicie, duża klasa jest jak taki gruby, wielofunkcyjny scyzoryk. Zmierzać mamy do tego, aby jego elementy rozbijać na możliwie jak najmniejsze.
Open/closed principle – zasada otwarte-zamknięte
Zasada ta mówi, że „każda klasa czy funkcja powinna być otwarta na rozszerzenia, ale zamknięta na modyfikacje”. Co to oznacza w praktyce? Od początku mamy pisać w ten sposób, aby możliwe było dodawanie nowych metod w klasie, rozwijanie jej funkcjonalności bez jakichkolwiek późniejszych zmian w systemie. W przypadku rozbudowanych systemów, zawierających sporo klas i funkcji, modyfikacja nawet jednej małej metody może spowodować nieprawidłowe działanie innych elementów programu.
Spójrzmy na następujące przykłady:
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 40 41 |
#include <iostream> #include <typeinfo> using namespace std; class Criminal { public: string _type; Criminal(string type){_type=type;} }; class Judge { public: void Judgement(Criminal criminal) { if(criminal._type=="thief") cout << "One year in prison\n"; if(criminal._type=="murderer") cout << "Thirty year in prison\n"; } }; int main() { Criminal t("thief"); Criminal m("murderer"); Judge Anna_maria; Anna_maria.Judgement(t); Anna_maria.Judgement(m); system("pause"); return 0; } |
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 |
package matura; class Criminal{ public String type; public Criminal(String criminalType) { type = criminalType; } } class Judge{ public void Judgement(Criminal criminal) { if(criminal.type=="thief") System.out.println("One year in prison"); if(criminal.type=="murderer") System.out.println("Thirty year in prison\n"); } } public class Main { public static void main(String[] args){ Criminal t = new Criminal("thief"); Criminal m = new Criminal("murderer"); Judge Anna_maria = new Judge(); Anna_maria.Judgement(t); Anna_maria.Judgement(m); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
class Criminal: type = "" def __init__(self, Type): self.type = Type class Judge: def Judgement(self, criminal): if criminal.type == "thief": print("One year in prison") if criminal.type == "murderer": print("Thirty years in prison") t = Criminal("thief") m = Criminal("murderer") judge = Judge() judge.Judgement(t) judge.Judgement(m) |
Za każdym razem, gdy dodać będziemy chcieli wyrok dla nowego rodzaju przestępcy to musimy edytować metodę Judgement klasy sędziego. Co gdy przewidujemy nieustanne dodawanie nowych rodzajów przestępców i wyroków dla nich? Z pomocą przychodzi polimorfizm. Jeden z fundamentów obiektowego podejścia do programowania. Powinienem kiedyś przybliżyć wam z czym to się je, ale tymczasem spójrzcie:
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 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
#include <iostream> #include <cstdlib> using namespace std; class Criminal { public: virtual void Judgement() { cout << "--\n"; } }; class Thief : public Criminal { public: void Judgement() override { cout << "One year in prison\n";} }; class Murderer : public Criminal { public: void Judgement() override { cout << "Thirty year in prison\n";} }; class Burglar : public Criminal { public: void Judgement() override { cout << "Sixteen months in prison\n";} }; class Judge { public: void Judgement(Criminal &criminal) { criminal.Judgement(); } }; int main() { Criminal c; Burglar b; Murderer m; Thief t; Judge Anna_maria; Anna_maria.Judgement(c); Anna_maria.Judgement(t); Anna_maria.Judgement(m); Anna_maria.Judgement(b); system("pause"); return 0; } |
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 40 41 42 43 44 45 46 47 48 49 |
package matura; class Criminal{ public void Judgement() { System.out.println("--"); } } class Thief extends Criminal{ public void Judgement() { System.out.println("One year in prison"); } } class Murderer extends Criminal{ public void Judgement() { System.out.println("Thirty years in prison"); } } class Burglar extends Criminal{ public void Judgement() { System.out.println("Sixteen years in prison"); } } class Judge{ public void Judgement(Criminal criminal) { criminal.Judgement(); } } public class Main { public static void main(String[] args){ Criminal c = new Criminal(); Thief t = new Thief(); Murderer m = new Murderer(); Burglar b = new Burglar(); Judge Anna_maria = new Judge(); Anna_maria.Judgement(c); Anna_maria.Judgement(t); Anna_maria.Judgement(m); Anna_maria.Judgement(b); } } |
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 |
class Criminal: def Judgement(self): print("--") class Thief(Criminal): def Judgement(self): print("One year in prison") class Murderer(Criminal): def Judgement(self): print("Thirty years in prison") class Burglar(Criminal): def Judgement(self): print("Sixteen years in prison") class Judge: def Judgement(self, criminal): criminal.Judgement() c = Criminal() t = Thief() m = Murderer() b = Burglar() judge = Judge() judge.Judgement(c) judge.Judgement(t) judge.Judgement(m) judge.Judgement(b) |
Po refaktoryzacji możemy dodawać niezliczoną ilość nowych klas dla przestępców nie modyfikując przy tym klasy sędziego.
Mi osobiście zasada OCP kojarzy się z pewną regułą, którą stosują matematycy i fizycy -> „Nowe nie może burzyć starego”.
Liskov substitution principle – zasada podstawienia Liskov
Kolejna wdzięczna reguła paradygmatu SOLID. Jej przestrzeganie wymaga rozumnego przemyślenia schematu mechanizmu dziedziczenia w naszym kodzie. Właściwie wymuszając zastosowanie polimorfizmu. Zasada podstawienia Liskov brzmi następująco:
„Funkcje, korzystające z obiektów klasy bazowej, muszą być w stanie używać również obiektów klasy pochodnej, bez dokładnej znajomości tych obiektów.”
Idąc tropem naszych przestępców rozważmy kolejne przykłady.
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 |
#include <iostream> #include <cstdlib> using namespace std; class Murderer { public: void Judgement() { cout << "Thirty year in prison\n";} }; class SerialKiller : public Murderer { public: void Judgement() { cout << "Capital punishment\n";} }; void judge (Murderer &criminal) { criminal.Judgement(); } int main() { SerialKiller killer; Murderer seba; judge(seba);//Thirty year in prison judge(killer);//Thirty year in prison system("pause"); return 0; } |
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 |
package matura; class Murderer{ public void Judgement() { System.out.println("Thirty years in prison"); } } class SerialKiller extends Murderer{ public void Judgement() { //w Javie metoda ta w sposob ukryty nadpisuje metode rodzica System.out.println("Capital punishment"); } } public class Main { public static void judge(Murderer criminal) { criminal.Judgement(); } public static void main(String[] args){ Murderer seba = new Murderer(); SerialKiller killer = new SerialKiller(); judge(seba); judge(killer); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class Murderer: def Judgement(self): print("Thirty years in prison") class SerialKiller(Murderer): def Judgement(self): #w Pythonie metoda dziecka automatycznie nadpisuje metode rodzica print("Capital punishment") def judge(murderer): murderer.Judgement() seba = Murderer() killer = SerialKiller() judge(seba) judge(killer) |
Chcielibyśmy, aby przy wywoływaniu metody klasy pochodnej SerialKiller przysłoniła metodę klasy bazowej Murderer. Zamiast tego funkcja judge() wymusza na nas użycie metody obiektu klasy podanej bazowej podanej jako typ argumentu funkcji.
Co zrobić z tym fantem? Z pomocą przychodzi polimorfizm:
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 |
#include <iostream> #include <cstdlib> using namespace std; class Murderer { public: virtual void Judgement() { cout << "Thirty year in prison\n";} }; class SerialKiller : public Murderer { public: void Judgement() override { cout << "Capital punishment\n";} }; void judge (Murderer &criminal) { criminal.Judgement(); } int main() { SerialKiller killer; Murderer seba; judge(seba);//Thirty year in prison judge(killer);//Capital punishment system("pause"); return 0; } |
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 |
package matura; class Murderer{ public void Judgement() { System.out.println("Thirty years in prison"); } } class SerialKiller extends Murderer{ @Override //powinno dodac sie ta adnotacje, aby inny programista widzial, ze w tym miejscu nadpisujemy metode public void Judgement() { System.out.println("Capital punishment"); } } public class Main { public static void judge(Murderer criminal) { criminal.Judgement(); } public static void main(String[] args){ Murderer seba = new Murderer(); SerialKiller killer = new SerialKiller(); judge(seba); judge(killer); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class Murderer: def Judgement(self): print("Thirty years in prison") class SerialKiller(Murderer): def Judgement(self): #w Pythonie metoda dziecka automatycznie nadpisuje metode rodzica print("Capital punishment") def judge(murderer): murderer.Judgement() seba = Murderer() killer = SerialKiller() judge(seba) judge(killer) |
Obiekt klasy pochodnej może być od teraz używany przemiennie z obiektem klasy bazowej. Opisywana zasada bardzo dobrze wpisuje się w meandry reguły Open/Closed – „nowe nie może burzyć starego”.
Interfaces segregation principle – zasada segregacji interfejsów
Wiele dedykowanych interfejsów jest lepsze niż jeden ogólny. Krótka piłka.
Żeby zrozumieć jak praktycznie można wykorzystać tą zasadę spójrzmy od razu na przykłady, bez owijania w bawełnę. Wiele więcej do zrozumienia ISP nie trzeba.
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 |
#include <iostream> #include <cstdlib> using namespace std; class InputOutput { public: virtual void print()=0; virtual void scan()=0; }; class MultiFuncDevice : InputOutput { public: void print() { //printing something } void scan() { //scanning something } }; class Printer : InputOutput { public: void print() { //printing something } }; |
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 |
interface InputOutput{ public void print(); public void scan(); } class MultiFuncDevice implements InputOutput{ @Override public void print() { // printing something } @Override public void scan() { // scanning something } } class Printer implements InputOutput{ @Override public void print() { // printing something } @Override public void scan() { return; //unnecessary method - returning } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
class InputOutput: def print(self): pass def scan(self): pass class MultiFuncDevice(InputOutput): def print(self): print("printing something") def scan(self): print("scanning something") class Printer(InputOutput): def print(self): print("printing something") |
Mamy interfejs wejścia/wyjścia z dwiema metodami wirtualnymi i dwie klasy dziedziczące po interfejsie z tym, że tylko jeden używa wszystkich funkcji interfejsu. Dążyć należy do sytuacji, w której każda klasa wykorzystuje wszystkie metody otrzymane w spadku. Dlatego rozbijemy klasę Input/Output na dwie mniejsze. W końcu nasza drukarka i tak nie korzysta z opcji skanowania.
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 40 |
#include <iostream> #include <cstdlib> using namespace std; class Input { public: virtual void print()=0; }; class Output { public: virtual void scan()=0; }; class MultiFuncDevice : Input, Output { public: void print() { //printing something } void scan() { //scanning something } }; class Printer : Output { public: void print() { //printing something } }; |
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 |
interface Input{ public void scan(); } interface Output{ public void print(); } class MultiFuncDevice implements Input, Output{ @Override public void print() { // printing something } @Override public void scan() { // scanning something } } class Printer implements Output{ @Override public void print() { // printing something } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
class Input: def scan(self): pass class Output: def print(self): pass class MultiFuncDevice(Input, Output): def print(self): print("printing something") def scan(self): print("scanning something") class Printer(Output): def print(self): print("printing something") |
Dependency inversion principle – zasada odwrócenia zależności
Piąta i ostatnia zasada SOLID formułuje się następująco:
„Wysokopoziomowe moduły nie powinny zależeć od modułów niskopoziomowych – zależności między nimi powinny wynikać z abstrakcji.”
DIP zaleca korzystanie z abstrakcyjnych klas w postaci buforu pomiędzy wspomnianymi wysoko i niskopoziomowymi modułami. Sugeruje używać polimorfizmu najczęściej jak to możliwe. Dzięki tej zasadzie kod staje się bardziej elastyczny i mnie „przyszyty” do konkretnej implementacji.
Za przykład posłużą mi klasy użyte przy okazji omawiania zasady otwarte-zamknięte.
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 Criminal { public: virtual void Judgement() { cout << "--\n"; } }; class Thief : public Criminal { public: void Judgement() override { cout << "One year in prison\n";} }; class Murderer : public Criminal { public: void Judgement() override { cout << "Thirty year in prison\n";} }; class Burglar : public Criminal { public: void Judgement() override { cout << "Sixteen months in prison\n";} }; class Judge { public: void Judgement(Criminal &criminal) { criminal.Judgement(); } }; |
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 Criminal{ public void Judgement() { System.out.println("--"); } } class Thief extends Criminal{ public void Judgement() { System.out.println("One year in prison"); } } class Murderer extends Criminal{ public void Judgement() { System.out.println("Thirty years in prison"); } } class Burglar extends Criminal{ public void Judgement() { System.out.println("Sixteen years in prison"); } } class Judge{ public void Judgement(Criminal criminal) { criminal.Judgement(); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
class Criminal: def Judgement(self): print("--") class Thief(Criminal): def Judgement(self): print("One year in prison") class Murderer(Criminal): def Judgement(self): print("Thirty years in prison") class Burglar(Criminal): def Judgement(self): print("Sixteen years in prison") class Judge: def Judgement(self, criminal): criminal.Judgement() |
Metoda Judgement() klasy Judge nie jest zupełnie zależny od konkretnego typu. Klasa Criminal pełni tutaj rolę interfejsu właśnie. Dzięki niemu sędzia może orzekać wyrok dla każdego z przestępców bez konieczności dostosowania implementacji tejże metody.
A komu to potrzebne? A po co?
Początkowo wprowadzenie wszystkich tych pięciu zasad może okazać się kłopotliwe i zbędne. Po czasie wchodzi w krew, ale i tak niekoniecznie widzisz słuszność w tym co robisz. Zasady SOLID docenia się w trzech przypadkach:
- Wracając po jakimś czasie do kodu,
- W sytuacji gdy projekt się rozwijany na bieżąco,
- Przy pracy zespołowej, gdy kod nie jest tylko twój -> jest wspólny i należy o niego dbać lepiej niż o żonę 😉 #wyolbrzymienie
To moje rozumienie zasad Roberta Martina. Dajcie znać co o tym sądzicie. Może udało mi się komuś „rozjaśnić” w głowie, a może coś dało się wytłumaczyć lepiej? Zapraszam do komentowania 😊.
Arek
says:SRP nie mówi o tym, że klasa ma „robić jedna rzecz” (mówi o tym sam Robert Martin w czystej architekturze) tylko, że ma mieć jeden powód do zmiany. To nie są równoznaczne rzeczy.
Lukasz
says:Hej, prześwietny artykuł.
Pierwszy raz widzę tak dobrze i zrozumiale wytłumaczone zasady SOLID!
Dodatkowo dużym atutem jest kod w 3 różnych językach.
Pozwala to się czuć dobrze przy zrozumieniu nowego tematu każdemu programiśćie 😀
Początkujący Programista
says:Bardzo fajny artykuł dedykowany dla WSZYSTKICH programistów 🙂 Mam tylko takie pytanko, czy takie rozdrobnienie kodu nie spowoduje, że program będzie „cięższy” i bardziej pamięciożerny ?
Łukasz Kosiński
says:To bardzo trafne pytanie 😉 Ogólnie rzecz biorąc to kod rzeczywiście może stracić na wydajności z uwagi na dużo warstw abstrakcji, ale to nie zawsze jest zasada. Na szybkość programu wpływają głównie (to jest jakiś duży procent) tzw. wąskie gardła – źródła wszelkiego zła. Na nich skupiasz się najpierw, potem ew. obwiniasz Solid 😀
W skrócie SOLID może, ale nie musi obciążać programu.