Pokazywanie postów oznaczonych etykietą Zasady projektowania. Pokaż wszystkie posty
Pokazywanie postów oznaczonych etykietą Zasady projektowania. Pokaż wszystkie posty

BAD DESIGN czyli zły projekt

, niedziela, 17 kwietnia 2016 0 komentarze

Czym jest tzw. "zły projekt" (BAD DESIGN)
Niestety obecnie w dobie masy frameworków, bibliotek i innych gotowych komponentów korzystając z tych udogodnień zapomina się w ramach projektowania aplikacji czy nawet wielkich systemów o tak podstawowych zagadnieniach jak dobre zaprojektowanie systemu czy aplikacji. Napisałem "niestety", ponieważ złe zaprojektowanie prowadzi nieuchronnie do klęski w postaci:

  • błędów kodu trudno wykrywalnych lub niemożliwych do wykrycia (często rozwiązywanych np. poprzez cykliczne restarty systemów)
  • frustracji ludzi / programistów którzy są zmuszeni do utrzymywania systemu 
  • braku możliwości jakiegokolwiek rozwoju aplikacji czy systemu lub możliwości znacząco utrudnionych
  • ogromnych kosztów wymaganych na rozwój czy poprawę systemu / aplikacji

Wykorzystanie udogodnień jakie w dzisiejszych czasach występują znacząco ułatwia projektowanie systemów jednakże sprawia również, że używając ich następuje tzw. rozleniwienie i oczekiwanie, że zastosowane bibliotek / frameworków zwalnia od myślenia o dobrym projekcie. No dobrze, ale co to jest "złe zaprojektowanie" lub czym się charakteryzuje ? Zadając to pytanie, najprawdopodobniej padnie tyle odpowiedzi ilu ludzi będzie zaangażowanych w debatę i pewnie każdy będzie miał po części rację. Natomiast pytanie brzmi, czy są bezsporne kryteria złej architektury, z którymi są w stanie zgodzić się architekci / inżynierowie, a jeżeli tak to jakie to są kryteria. Niżej przedstawiam propozycję cech, zaczerpniętych z publikacji internetowych, którymi charakteryzuje się zła architektura (myślę, że do zaakceptowania dla każdego inżyniera):

  • system, który jest ciężki do zmiany z uwagi na fakt, że każda wprowadzona zmiana ma wpływ na znaczącą ilość elementów systemu (Rigidity)
  • system, w którym wprowadzenie zmiany w jakimś kawałku kodu powoduje błędy lub uszkodzenia w nieprzewidywalnych innych miejscach systemu (Fragility) 
  • system, w którym komponenty zostały tak zaprojektowane, że są niemożliwe do wyodrębnienia z tego systemu w celu użycia ich w innym systemie (Immobility) 

Oczywiście nie są to jedyne cechy błędnego projektu czy architektury, jednak gdy te w/w występują w projekcie systemu bezspornie można uznać ten projekt za błędy czyli przykład BAD DESIGN.


Single Responsibility Principle - zasada pojedynczej odpowiedzialności

czwartek, 29 lipca 2010 0 komentarze

Single Responsibility Principle - każdy obiekt powinien mieć pojedynczą odpowiedzialność i wszystkie jego usługi powinny być blisko sprzymierzone z tą odpowiedzialnością. Na pewnym poziomie Spójność (cohesion) jest uważana za synonim dla SRP.

Odpowiedzialność (responsibility) - powód do zmiany. W przypadku, gdy mamy więcej niż jeden powód do zmiany klasy, oznacza to, że klasa ma więcej niż jedną odpowiedzialność. Przykładowo mając interfejs poniżej widzimy cztery sensowne funkcje modemu:

interface Modem {
void dial(String phone);
void hangup();
void send(char ch);
char recv();
}


jednak, przy bliższym spojrzeniu można dostrzec dwie odpowiedzialności. Pierwsza z nich dotycząca zarządzania połączeniem oraz druga związana z przesyłaniem danych. Lepszym rozwiązaniem byłoby rozdzielenie tych dwóch odpowiedzialności pomiędzy dwa interfejsy, tak jak poniżej:

interface Connection {
void dial(String phone);
void hangup();
}

interface DataChannel {
void send(char ch);
char recv();
}

class ModemImpl implements DataChannel, Connection {
}



Spójność (cohesion) - miara jak silno-skojarzona jest funkcjonalność wyrażona przez kod źródłowy modułu oprogramowania. Metody pomiaru spójności zmieniają się od miar jakościowych klasyfikujących tekst źródła podlegającego analizie używając rubryki z hermeneutykami do jakościowych miar które badają tekstowe charakterystyki kodu źródłowego by wskazać wartość numeryczną spójności.

Separation of Concerns - separacja zagadnień

środa, 28 lipca 2010 0 komentarze

Separation of Concerns jest procesem podziału programu komputerowego na odrębne cechy (moduły), które pokrywają się wzajemnie z punktu widzenia funkcjonalnego tak mało jak to tylko możliwe.

Mianem Concern (zagadnienie) określany jest pojedynczy element zainteresowania lub skupienia w programie. Typowo, zagadnienia są synonimami dla cech / właściwości lub zachowań.

Separacja pozwala między innymi na:
- indywidualną pracę w izolacji grupy ludzi nad kawałkami systemu,
- ułatwienie reużywalności,
- zapewnienie obsługiwalności systemu,
- dodawanie nowych właściwości w prosty sposób,
- zapewnienie każdemu lepszego zrozumienia systemu.

SoC nie jest przypisana jedynie do warstwy architektonicznej, często stosowana jest również do wielu innych zagadnień, takich jak:
- obiekt który reprezentuje zagadnienie z punktu widzenia języka,
- SOA może separować zagadnienia w usługi, wydzielając w ten sposób zachowanie jako zagadnienie w logiczne jednostki itd.

Zasada Separation of Concerns stanowi, że elementy systemu powinny mieć rozłączne i osobliwe (jednostkowe) zastosowanie. Inaczej mówiąc, żaden z elementów nie powinien współdzielić odpowiedzialności z innym elementem. Separację zagadnień można osiągnąć poprzez wytyczenie granic, gdzie granicą określamy logiczne lub fizyczne ograniczenie, które wytycza określony zestaw odpowiedzialności. Proces osiągania separacji zagadnień (SoC) zawiera podział zestawu odpowiedzialności, gdzie celem nie jest redukcja systemu na poszczególne części, a organizacja systemu w elementy niepowtarzalnych zestawów spójnych odpowiedzialności.

Celem SoC jest osiągnięcie dobrze zorganizowanego systemu, gdzie każda jego część pełni znaczącą i intuicyjną rolę przy maksymalizacji jej zdolności do adaptacji do zmian.


"Make everything as simple as possible, but not simpler."
Albert Einstein

poniedziałek, 26 maja 2008 0 komentarze

Zasady projektowania OOD (Object-Oriented Design)

Programowanie obiektowe (OOP) jest metodyką modelowania i projektowania oprogramowania, które obejmuje podstawowe pojęcia hermetyzacji, abstrakcji, dziedziczenia i polimorfizmu. Ta metodyka kieruje się zestawem zasad zwanych zasadami projektowania. Zasady te pokazują właściwy kierunek projektowania i pozwalają uniknąć kosztownych pomyłek na etapie projektowania.

Stosowanie zasad projektowania jest ważne nie tylko z punktu widzenia projektowania oprogramowania, ale również z punktu widzenia biznesowego, gdyż umożliwiają one tworzenie elastycznych projektów, które mogą ewaluować wraz z wymaganiami biznesowymi przy minimalnym wysiłku, czasie i kosztach.

Poniżej zamieszczony został opis dotyczący niektórych zasad.

Zasady dotyczące projektu klas - --- SOLID ---

Open Close Principle (OCP)
Zasada ta stanowi najbardziej podstawową regułę uelastycznienia oprogramowania. Wszystkie inne zasady, metodyki i konwencje OOP koncentrują się wokół tej zasady. OCP określa co następuje:

Elementy oprogramowania powinny być otwarte na rozszerzania (extensions) natomiast zamknięte na modyfikacje (modifications).

Na pierwszy rzut oka stwierdzenie powyższe wydaje się to wewnętrznie sprzeczne. W celu wyjaśnienia zdefiniujmy następujące terminy:
  • Encje oprogramowania w projekcie to funkcje, metody, moduły, klasy.
  • Otwarty na rozszerzanie oznacza, że jego zachowanie może zostać rozszerzone lub zmienione w zależności od potrzeb
  • Zamknięte na modyfikacje oznacza ograniczenie zmian kodu źródłowego modułu do rozszerzenia lub zmiany jego zachowania.
Otwartość na rozszerzanie pozwala na zaspokojenie rzeczywistej potrzeby zmian w oprogramowaniu spowodowanej zmianą wymagań biznesowych bez konieczności modyfikacji kodu źródłowego. Dlaczego nie powinno być zmian w kodzie źródłowym ? Aby zdać sobie sprawę z korzyści otwartości rozważymy kod składający się z logiki if-then-else w przykładzie dotyczącym logowania. Załóżmy, że mamy przygotowaną i działającą następującą funkcję logującą:
void log() {
prepareMessage();
writeMessageToFile();
}
Biznes teraz wymaga od nas, by logowanie było możliwe nie tylko do pliku ale również do bazy danych. Tak więc robimy modyfikację w kodzie źródłowym:

void log() {
prepareMessage();
if (target is file) {
writeMessageToFile();
} else {
writeMessageToDatabase();
}
}
Wszystko działa, natomiast problem polega na tym, że w przyszłości, gdy wymagania się znowu zmienią np. logowanie do pliku XML, będzie musiał być również znowu zmieniony kod źródłowy. Natomiast każda modyfikacja w kodzie źródłowym wprowadza potencjalne błędy, jak również prowadzi do zwiększenia ilości testów w celu zapewnienia poprawności działania i sprawdzenia czy przy okazji modyfikacji coś co działało poprawnie nie zostało zepsute. W szczególności, gdy zmiana została wykonana w części core-owej projektu, będzie musiał być wykonany cały zestaw testów.

Open-Closed Principle zaleca realizację elastycznego oprogramowania poprzez podejście polegające na inkrementacji kodu. Innymi słowy raczej dodawanie nowego kodu niż zmiana w kodzie istniejącym i działającym.

Powracając do przykładu z logowaniem, dobry projekt z punktu widzenia OCP powinien być zrealizowany poprzez przygotowanie interfejsu oraz klas specyficznych w odniesieniu do specyficznej realizacji logowania. Patrz diagram poniżej:

Główna klasa logująca nie musi być zmieniana w celu dodania nowego punktu przeznaczenia. Jest to przykład otwartości na rozszerzenie jednak zamknięcia na modyfikacje.


Liskov Substitution Principle (LSP)
Zasada OCP wymieniona wcześniej jest główną zasadą OOD i można powiedzieć o niej, że jest sercem OOD. Tak jak zostało wcześniej opisane, zaleca ona stosowanie abstrakcji w celu umożliwienia rozszerzalności obiektów, która typowo implementowana jest wykorzystując mechanizm dziedziczenia. Dziedziczenie samo w sobie wprowadza wiele możliwości, z których część może prowadzić do błędnego projektu. Zasada Liskov Substitution Principle (LSP) wprowadza wskazówki dotyczące projektowania używając dziedziczenia. Została sformułaowana przez Barbarę Liskov i w wolnym tłumaczeniu brzmi następująco:

Jeżeli dla każdego obiektu o1 typu S istnieje obiekt o2 typu T, taki że dla wszystkich programów P zdefiniowanych używając T, zachowanie P pozostaje niezmienne po zamianie o1 na o2 to typ S jest podtypem typu T.

Powyższe stwierdzenie dostarcza definicji podtypu lub klasy pochodnej. Z punktu widzenia programowania, można to zinterpretować tak, że zachowanie funkcji lub metody używającej obiektu klasy bazowej powinno pozostać niezmienne po zmianie na obiekt klasy pochodnej wywodzącej się z tej klasy bazowej.

Co z tego wynika możemy prześledzić na przykładzie. Załóżmy, że mamy zdefiniowaną klasę Rectangle która definiuje prostokąt. Następnie chcielibyśmy przygotować klasę Square definiującą typ kwadrat. Wiemy, że kwadrat to prostokąt o równych bokach, tak więc naturalnym podejściem jest zaimplementowanie dziedziczenia klasy Square z klasy Rectangle. Podejście jest jak najbardziej słuszne, jednak jak zostanie to pokazane dalej może prowadzić do niespełnienia zasady LSP. Poniżej został zaprezentowany diagram klas dla opisywanego przypadku:
Jak wynika z diagramu prostokąt ma 4 metody 2 do pobierania długości boków oraz 2 do ustawiania długości boków. Ponieważ kwadrat dziedziczy z prostokąta, dziedziczy również te 4 metody. Teraz, aby zadbać o wewnętrzną spójność kwadratu możemy dodać kod ustawiający długość dla obydwu boków przy ustawieniu długości któregokolwiek z nich, tak jak poniżej:
public void setLengthA(int lengthA) {
this.lengthA = lengthA;
this.lengthB = lengthA;
}

public void setLengthB(int lengthB) {
this.lengthB = lengthB;
this.lengthA = lengthB;
}
Wszystko jest niby OK, ale zasada LSP nie jest spełniona. Można by zapytać w czym to przeszkadza. Otóż, załóżmy że w celu testu zostanie przygotowany następujący kod:

public void test(Rectangle rect) {
rect.setLengthA(2);
rect.setLengthB(3);

asert(6 == rect.getLengthA() * rect.getLengthB());
}
Test w przypadku wykonania na obiekcie klasy Rectangle działa OK, natomiast gdy to metody test() przekażemy obiekt klasy Square niestety test nie powiedzie się. W tym przypadku oczekiwanie jak najbardziej słuszne osoby piszącej test, nie jest spełnione. W tym przypadku można powiedzieć, że klasa Square nie spełnia zasady LSP.

Design by Contract (DBC)
Zasada DBC jest skojarzona z zasadą LSP. DBC określa, że każda encja oprogramowania jest zobowiązana do ciągłego dostarczania usługi innym encjom. Umowa (contract) jest definiowana przez warunki początkowe (preconditions) oraz warunki końcowe (postconditions), które w programowaniu przenoszą się na sygnaturę funkcji czy metody. Element wywołujący zapewnia spełnienie określonych warunków początkowych na podstawie, których element wywoływany zapewnia spełnienie warunków końcowych. DBC określa:

W przypadku przedefiniowania procedury [w elemencie pochodnym] można jedynie zmienić jej warunki początkowe na słabsze oraz jej warunki końcowe na mocniejsze.


W przykładzie opisywanym w LSP dotyczącym prostokątów i kwadratów, załóżmy, że klasa Rectangle ma metodę setDimension(lenghtA, lengthB), która w klasie Square może być przetransformowana do setDimension(length). W tym przypadku klasa Square zamienia warunki początkowe zdefiniowane w klasie Rectangle na mocniejsze powodując złamanie zasady DBC.

Dependency Inversion Principle (DIP)
Projekt oprogramowania definiuje obiekty oraz komunikację pomiędzy nimi. Ostatecznie dostajemy sieć wewnętrznych połączeń pomiędzy tymi obiektami, która w połączeniu z ograniczeniami biznesowymi takimi jak czas czy budżet, prowadzi do złego projektu oprogramowania. Robert Martin zdefiniował zły projekt oprogramowania trzema podstawowymi atrybutami przedstawionymi poniżej.

Część oprogramowania spełniająca swoje wymagania i wykazująca jeszcze którąkolwiek lub wszystkie z następujących trzech cech posiada zły projekt (design):
  1. Jest bardzo trudna do zmiany, ponieważ każda zmiana ma wpływ na wiele części systemu - rigidity.
  2. W przypadku wprowadzenia zmiany, nieoczekiwane części systemu ulegają zepsuciu - fragality.
  3. Jest bardzo ciężko ją użyć ponownie w innej aplikacji, ponieważ nie może być wyciągnięta z aktualnej aplikacji - immobility.
Gdziekolwiek funkcjonalność jest zaimplementowana, może ona być podzielona na moduły wysokopoziomowe i niskopoziomowe. Moduły wysokopoziomowe są zwykle reprezentatywne dla koncepcji aktywności, podczas gdy niskopoziomowe aktywności są szczegółami modułu wysokopoziomowego. Przykładowo aktywność logowania (moduł wysokopoziomowy) może być podzielona na:
  • sformułowanie komunikatu do zalogowania
  • zapis komunikatu do miejsca przeznaczenia np. pliku, bazy danych

W implementacji, moduły niskopoziomowe mogą mieć specyficzny kod np. moduł do zapisu komunikatu może zapisywać go do pliku:

prepareMessage
writeMessageToFile
Później, gdy będzie wymagane logowanie do bazy danych, algorytm ulegnie zmianie do następującej wersji:
prepareMessage
writeMessageToDatabase
W opisywanym przypadku zmiana w module niskopoziomowym powoduje zmianę w module wysokopoziomowym. Oznacza to, że nie można użyć ponownie modułu logującego nie używając implementacji modułu niskopoziomowego do zapisu komunikatu.

Na poziomie koncepcyjnym wszystko jest w porządku, problemy pojawiają się podczas implementacji. Dlaczego nie włączyć koncepcji w samym projekcie ? Może to być wykonane poprzez wprowadzenie abstrakcji, jak w algorytmie poniżej:

logging:
prepareMessage
writeMessage

writeMessage:
writeMessageToFile
Został tutaj wprowadzony poziom pośredni, moduł logowania nie komunikuje się bezpośrednio z modułem zapisu komunikatu na dysk. Komunikuje się on z modułem zapisu komunikatu, który ukrywa całość zmian swojej implementacji wewnętrznie. W tym przypadku moduł logowania jest niezależny od implementacji zapisu komunikatu i może być użyty ponownie.

To co zostało opisane powyżej jest ogólnym sformułowaniem zasady DIP, która stanowi:

Moduły wysokopoziomowe nie powinny zależeć od modułów niskopoziomowych. Obydwa moduły natomiast powinny zależeć od abstrakcji. Abstrakcje nie powinny zależeć od szczegółów. Szczegóły powinny natomiast zależeć od abstrakcji.

W rzeczywistości oznacza to, że odseparowana została koncepcja od implementacji poprzez wprowadzenie abstrakcji. Jest to definicją reguły projektowania i programowania, która mówi, by projektować interfejsy a nie implementacje. Klasa potrzebująca funkcjonalności logowania powinna być projektowana z wykorzystaniem modułu wysokopoziomowego zamiast modułu niskopoziomowego.

Law of Demeter (LoD)
LoD nazywane jest zasadą minimalnej wiedzy o innych obiektach. Głównym celem LoD jest kontrola informacji przeładowanej (overload) poprzez definicję reguł dla interakcji pomiędzy obiektami - "Rozmawiaj tylko z najbliższymi przyjaciółmi". Bardziej powszechną formą zasady jest:

Każda jednostka powinna mieć jedynie ograniczoną wiedzę o innych jednostkach. Powinna mieć wiedzę dotycząca jedynie jednostki "blisko" z nią skojarzonej.

Każda jednostka powinna jedynie rozmawiać z przyjaciółmi, a nie rozmawiać z nieznajomymi.

W przypadku OOP, jednostką jest metoda a blisko powiązanymi jednostkami metody są:

  • argumenty przekazane do metody
  • pola klasy, do której należy, ale nie ich pola
  • obiekty, które sama tworzy
  • elementy klasy do której należy
Kiedykolwiek metoda wywołuje metody należące do innych obiektów, zakłada znajomość struktury ich klas. Idąc dalej, taka sama sytuacja występuje, gdy używa obiektów zwracanych z tych metod i wywołuje na nich metody. Zmiana w strukturze wyżej wymienionych obiektów może powodować zmiany w opisywanej metodzie. LoD określa zakres obiektów, których struktura może być znana i do których mogą odwoływać się metody. Przedstawiony zostanie teraz przykład łamiący LoD:
obj1 = obj2.getObj1();
obj3 = obj1.getObj3();
Zakładając, że obj2 został przekazany jako argument metody, dopuszczalna jest widza o strukturze klasy dla obiektu obj2. Została jednak również wywołana metoda na obiekcie obj1 odkrywając w ten sposób jego strukturę. Jeżeli struktura klasy dla obj1 zmieni się, istnieje możliwość, że przedstawiony kod także będzie musiał ulec zmianie. Przedstawioną powyżej sekwencję można zastąpić przez:
obj3 = obj2.getObj3()
Z tego, że obj2 zwraca obiekt obj1 wynika, że obj2 zna strukturę obj1 i może wywoływać jego metody. W wymienionym przypadku obj2 jest jednostką ściśle powiązaną, podczas gdy obj1 jest elementem obcym.

Rumbaugh podsumował zasadę Law of Demeter w następujący sposób:

Metoda powinna mieć ograniczoną wiedzę o modelu obiektów.

Interface Segrgation Principle (ISP)
Wiele małych interfejsów jest znacznie lepsze od jednego wielkiego.



Zasady dotyczące projektu pakietów.

Reuse/Release Equivalency Principle (REP)

The Common Reuse Principle (CRP)

Common Closure Principle (CCP)
Klasy w pakietach powinny być wspólnie zamknięte względem tego samego rodzaju zmian. Zmiana, która wpływa na pakiet dotyczy wszystkich klas w tym pakiecie.

Zasada określa, że klasy, które współistnieją razem powinny być grupowane razem. Co to oznacza ? Oznacza to tyle, że klasy, które zmieniają się wspólnie po zmianie wymagań, powinny być umieszczane w tym samym pakiecie. Zasada minimalizuje liczbę pakietów do zmiany w przypadku zmiany wymagań.


Acyclic Dependencies Principles (ADP)
Zależności pomiędzy pakietami nie mogą pod żadnym pozorem być cykliczne.

Stable Dependencies Principle (SDP)

Stable Abstractions Principle (SAP)

GlossyBlue Blogger by Black Quanta. Theme & Icons by N.Design Studio
Entries RSS Comments RSS