Zasady programowania
obiektowego
Prostota przede wszystkim
Zasady dla klas
Law of Demeter
• Demeter była boginią przyrody (Gleba-Matka), urodzajów, patronką
rolnictwa. Główny mit związany z Demeter opisuje jej poszukiwania zaginionej
córki (swojej i Zeusa) - Persefony (Kora - Dziewczyna). Bogini zostawiła ją na
łące, bawiącą się z nimfami (okeanidami). Odchodząc zabroniła jej zrywać
narcyzów - kwiatów związanych z bóstwami podziemnymi. Persefona nie
posłuchała matki. Z woli Zeusa wyrósł na łące wspaniały kwiat, w którym
z korzenia wyrastało sto łodyg o cudownym wyglądzie i zapachu. Gdy
dziewczyna odkryła kwiat zapomniała o przestrogach matki i zerwała go.
Otwarła się wówczas ziemia i Hades, bóg podziemi, porwał Persefonę
i zaczarowanym rydwanem wywiózł ją w dal. Lecieli ponad ziemią i morzem - do
Tartaru. Nikt nie słuchał krzyków Persefony. Dopiero po długim czasie dotarły
one do matki - Demeter.
• Zrozpaczona Demeter rozpoczęła poszukiwania. Przekazała swoją żałobę po
Persefonie polom. Zasiewy zmarniały, spiekota wysuszyła źródła i rzeki, padały
zwierzęta, a ludzi nawiedził głód. Nie było nawet z czego składać ofiar bogom.
Bogowie olimpijscy wystraszyli się, że jeżeli ludzie wyginą, to kto będzie składał
im ofiary. Zeus wysłał do Demeter muzy, charyty i kolejno innych olimpijczyków.
Ale nikomu nie udało się ubłagać bogini. W końcu Zeus wysłał Heraklesa do
Hadesa z żądaniem uwolnienia Persefony. Hades posłuchał rozkazu Zeusa, ale
rozstając się z Persefoną podał jej słodką pestkę granatu. Persefona zjadła ją,
nie wiedząc, że związała się tym na zawsze z podziemnym królestwem.
• Gdy Persefona wyszła z podziemi uradowana Demeter sprawiła, że wszystko na
ziemi rozkwitło. Z powodu zjedzonej pestki granatu Persefona odtąd trzecią
cześć roku musiała spędzać w królestwie Hadesa. Na ziemi panowała wtedy
zima - przerwa w wegetacji. Pozostałą część roku matka z córką spędzały
razem, obdarzając ziemię plonami.
Prawo Demeter
Law of Demeter
• Law of Demeter (LoD) (Don't Talk to
Strangers; or Principle of Least Knowledge)
• Treść: Niech obiekt rozmawia tylko z bliskimi
współpracownikami.
• Opis: Chodzi w niej o to, żeby obiekt nie
komunikował się (wywoływał metod, pobierał
wartości pól) z obiektów w dalekiej odległości w
grafie powiązań między obiektami. Oznacza to, że
obiekt powinien mieć minimalną wiedzę o innych
obiektach. Unika się dzięki temu „kruchych”
połączeń, co ogranicza wpływ miejscowych zmian
w projekcie na resztę projektu.
Law of Demeter – intuicja
Możesz się bawić:
• sam ze sobą,
• swoimi zabawkami (ale nie
wolno Ci rozbierać ich na części!),
• zabawkami, które dostaniesz od innych,
• zabawkami, które sam stworzysz, gdzie
zabawkami są po prostu obiekty, a
zabawa polega na wywoływaniu metod.
Law of Demeter -
formalizacja
public class Demeter { // Prawo Demeter dla klas
private A a;
private int func() { ... }
public void example (B b) {
// obiekt może wywołać tylko takie metody:
int k = func(); // <--------- tego samego obiektu
b.invert(); // <------------- parametru przesłanego do funkcji
a = new A();
a.setActive(); // <---------- obiektu stworzonego przez
Demeter
}
}
Law of Demeter -
naruszenie
class A {public: void m(); P p(); B b; };
class B {public: C c; };
class C {public: void foo(); };
class P {public: Q q(); };
class Q {public: void bar(); };
void A::m() {this.b.c.foo();
this.p().q().bar();}
Zmniejszenie powiązań
między obiektami. Przykład.
• Kiepskie rozwiązanie dla wyszukiwania
wina
• W= ListaWin.dajWina();
• /* Wyszukiwanie liniowe w liście win... */
• Odwołujemy sie do pola klasy ListaWin.
• Nadopiekuńczość – chcemy wykonać
pracę za kogoś innego, bo na przykład
uważamy, że lepiej to zrobimy.
Zmniejszenie powiązań
między obiektami. Przykład.
• Związane są z tym dwa potencjalne problemy:
• Klasa ListaWin może zawierać struktury
przyśpieszające wyszukiwanie win po nazwie,
w czasie logarytmicznym, dlatego liniowe
przeglądanie listy jest nieefektywne.
• Po pewnym czasie może się okazać, że
będziemy chcieli przechowywać listę win w
innej strukturze danych (np. w posortowanej
tablicy) i trzeba będzie przerabiać wszystkie
wyszukiwania win w programie.
Korzyści ze stosowania LoD
Dwie główne korzyści, które daje nam
stosowanie prawa Demeter to:
• Nie musimy znać struktury innych
obiektów.
• Wszelkie zmiany w innych obiektach
nie maja wpływu na naszą metodę.
Korzyści cd.
• Wnioski płynące ze stosowania prawa Demeter,
potwierdzone empirycznie podczas badań na
rzeczywistych programach wskazują, że ułatwia
ono pielęgnację, zmniejsza gęstość błędów, a
także ogranicza liczbę i rodzaj powiązań pomiędzy
obiektami, a co za tym idzie – wzmacnia
hermetyzację i abstrakcję klasy.
• Zastosowanie prawa Demeter na poziomie kodu
programu powoduje przeniesienie
odpowiedzialności za dostęp do metod i atrybutów
klasy z obiekt odwołującego się do nich do ich
właściciela.
LoD króciutko
• Rumbaugh podsumował zasadę Law
of Demeter w następujący sposób:
Metoda powinna mieć ograniczoną
wiedzę o modelu obiektów.
Cienie
• Konsekwentne stosowanie prawa
Demeter, może skutkować nadmierną
liczbą metod, których jedynym zadaniem
jest przedłużenie wywołania metody
• class Samochód{
• private Silnik silnik;
• public void uruchom() { silnik.uruchom;}
• }
A co z tym zrobić?
• System.Console.Writeln(„Hello World”);
• Żeby prawo Demeter nie rodziło takich
sprzeczności, zezwala na odwoływanie
sie do pól i metod obiektów globalnych
(w szczególności do różnych
przestrzeni nazw; w Javie pakietów).
Law of Demeter
Istnieją dwie postaci prawa Demeter:
silna i słaba.
W przypadku wersji słabej prawo to
jest rozszerzone także na podklasy
klasy analizowanej
Single-Responsibility
Principle
• Zasada ta jest silnie powiązana z zasadą
skupienia (high cohesion) ze wzorów GRASP
(General Responsibility Assignment Software
Patterns ). Chodzi o to, żeby klasa miała
tylko jedną odpowiedzialność (responsibility),
ponieważ odpowiedzialność jest
potencjalnym źródłem zmian. Jeżeli klasa ma
więcej niż jedną odpowiedzialność to zmiana
jednej z nich będzie wpływać na
implementację reszty.
Single-Responsibility
Principle
• Prostym przykładem złamania tej zasady
jest klasa, która jednocześnie zawiera logikę
(np. obliczanie powierzchni figury) oraz kod
związany z grafiką (np. odpowiedzialny za
rysowanie tej figury). Trochę lepszym
przykładem złamania zasady SRP jest klasa
zawierająca jednocześnie kod wybierający
dane z bazy oraz logikę odpowiedzialną za
obliczanie pozycji faktury (typowy i częsty
przykład pomieszania logiki biznesowej)
Single-Responsibility
Principle
• class Suma
{
private int wynik;
public void Dodaj(int a, int b)
{
wynik = a + b;
}
public void Wypisz()
{
Console.WriteLine(wynik);
}
}
Single-Responsibility
Principle
• interface IWyświetl
{
void Wyświetl(int wynik);
}
class Suma{
private int wynik;
public int Wynik
{
get
{
return wynik;
}
• }
public void Dodaj(int a, int
b)
{
wynik = a + b;
}
}
• class SumaUI : IWyświetl
{
public void Wyświetl(int
wynik)
{
Console.WriteLine(wynik);
}
}
//UI – User Interface
• class Program
{
static void Main(string[] args)
{
Suma s = new Suma();
s.Dodaj(2,2);
IWyświetl w = new
SumaUI();
w.Wyświetl(m.Wynik);
}
}
Open/close principle
• Treść: Elementy oprogramowania powinny być otwarte na
rozszerzenia, ale zamknięte na modyfikacje.
Opis: Podstawą dla tej zasady jest spostrzeżenie, że każdy kod (klasa,
moduł, itp.) zmienia się z czasem i ma więcej niż jedną wersję. Dobre
stosowanie tej zasady powoduje, że wprowadzenie rozszerzeń w jednej
klasie nie spowoduje całej kaskady zmian w innych klasach.
Trzeba zatem tworząc kod od razu mieć na uwadze możliwość
przyszłych rozszerzeń. Trzeba przewidzieć możliwość tworzenia
rozszerzeń i zmiany zachowania kodu, ale nie powinno się to odbywać
poprzez zmianę już istniejącego kodu.
Można powiedzieć, że OCP to główna zasada projektowania
obiektowego, która pozwala spełniać jego slogany reklamowe
(flexibility, reusability, maintainability).
Przykład: stosowanie się do tej zasady zazwyczaj oznacza bazowanie
na abstrakcji (klasy abstrakcyjne) i stosowanie polimorfizmu
(oczywiście nie tylko do tego). Dobrym przykładem złamania tej zasady
jest tworzenie logiki zawierającej instrukcje „switch..case”
rozróżniającej typy obiektów.
Open/close principle
class
Shape
{
private
Punkt _center;
#region
Center Get / Set
}
class
Circle : Shape
{
private
int
_radius;
#region
Radius Get / Set
}
class
Square : Shape
{
private
int
_side;
#region
Side Get / Set
}
Open/close principle
class
DrawManager
{
private
List
<
Shape
>
shapeList;
public
DrawManager(){…}
public
void
add(Shape s){…}
public
void
drawShapes(){->}
private
void
drawCirle(Circle c){..}
private
void
drawSquare(Square sq){..}
}
public
void
drawShapes()
{
foreach(Shape s
in
shapeList)
{
if
(s is Circle)
{
drawCirle((s as Circle));
}
else
if
(s is Square)
{
drawSquare((s as Square));
}
}
}
Open/close principle
abstract
class
Shape
{
private
Punkt _center;
public
abstract
void
Draw();
};
class
Circle : Shape
{
private
int
_radius;
public
override
void
Draw()
}
class
Square : Shape
{
private
int
_side;
public
override
void
Draw()
}
class
DrawManager
{
//......
public
void
drawShapes()
{
foreach (Shape s
in
_shapeList)
s.Draw();
}
//......
}
OCP - metody
• Podstawowymi mechanizmami
umożliwiającymi stosowanie zasady
OCP są abstrakcja i polimorfizm.
• W językach programowania ze
statyczną obsługą typów (C#) ważny
jest mechanizm dziedziczenia
(hierarchia).
OCP na koniec
• Stosowanie programowania
obiektowego, bezmyślne wykorzystanie
abstrakcji we wszelkich możliwych
miejscach to nie jest zasada OCP.
• Najlepszym wyjściem jest definiowanie
abstrakcji tylko w tych obszarach
programowania, co do których istnieje
przypuszczenie szczególnie częstych
zmian.
Problem testowy
• CA14
• Lsp+Ocp.doc
Liskov Substitution
Principle
• Treść: [W hierarchii dziedziczenia] podtypy muszą
być „podstawialne” za typy bazowe.
Opis: Zasada ta umożliwia wykrycie źle stworzonej
hierarchii dziedziczenia, w szczególności źle
zaimplementowaną klasę pochodną. Chodzi o to,
żeby wszystkie podtypy zachowywały się tak jak
zakłada się, że zachowuje się typ bazowy.
Zazwyczaj domyślnie się zakłada, że za typ bazowy
można podstawić klasę pochodną i zachowanie
będzie zmodyfikowane, ale będzie poprawne z
punktu widzenia „klientów” klasy bazowej.
Zasada ta jest nawet silniejsza niż podstawowa
zasada dziedziczenia „is-a” (jest).
Liskov Substitution
Principle
• 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.
Przecież kwadrat jest (IS-A)
prostokątem
•
public
class
Rectangle
•
{
•
private
double
width;
•
private
double
height;
•
•
public virtual double
Width
•
{
•
get
{
return
width; }
•
set
{ width = value;}
•
}
•
•
public
virtual
double
Height
•
{
•
get
{
return
height; }
•
set
{ height = value;}
•
}
•
•
}
public class Square: Rectangle
{
public override double Width
{
set
{
base.Width = value;
base.Height = value;
}
}
public override double Height
{
set
{
base.Height = value;
base.Width = value;
}
}
}
public class RectangleTests
{
public void AreaOfRectangle()
{
Rectangle r = new Rectangle();//wstawmy Square
r.Width = 5;
r.Height = 2;
if (r.Area() != 10) throw new Exception("ZŁe pole");
}
}
Relacja IS-A
• LSP mówi, że ta relacja musi
zachowywać zachowania
• Zachowanie prostokąta: w czasie
zmiany jednego z boków, drugi nie
może być zmieniony
• Zachowanie kwadratu: gdy
zmieniamy jeden bok to drugi też
DBC
• Sposób projektowania
oprogramowania zaproponowany
przez Bertranda Meyera w latach 80
• Wspierany przez język Eiffel
• Idea:
• Kontrakt między klientem (miejsce
wywołania procedury) i dostawcą
(procedurą)
Projektowanie przez
kontrakt
• Warunki wstępne
• Warunki końcowe
• Przestrzeganie kontraktu - asercje
• Dla prostokąta i kwadratu
• WW - puste
• Width=w;
• WK : width==w &&
height==old.height
Proste i odcinki
public class Prosta
{
private Punkt p1;
private Punkt p2;
public Prosta(Punkt p1, Punkt p2)
{this.p1=p1; this.p2=p2;}
public Punkt P1 { get { return p1; } }
public Punkt P2 { get { return p2; } }
public double Slope { get {/* kod */} }
public double YIntercept { get {/* kod
*/} }
public virtual bool JestNa(Punkt p) {/*
kod */}
}
public class Odcinek : Prosta
{
public Odcinek(Punkt p1, Punkt
p2) : base(p1, p2) {}
public double Length() {
get {/* kod */} }
public override bool
JestNa(Punkt p) {/* kod */}
}
I co teraz – naruszenie LSP
• Przymknąć oko. W końcu czy musimy
używać klasy bazowej w miejsce
pochodnej?
• Ale to świadczy o niezbyt wysokiej
jakości naszego kodu. Trzeba
przemyśleć.
• Wyodrębnić (factor) !
To jest to
public abstract class ObiektLiniowy
{
private Punkt p1;
private Punkt p2;
public ObiektLiniowy(Punkt p1, Punkt p2)
{this.p1=p1; this.p2=p2;}
public Punkt P1 { get { return p1; } }
public Punkt P2 { get { return p2; } }
public double Slope { get {/* kod */} }
public double YIntercept { get {/* kod
*/} }
public virtual bool IsOn(Punkt p) {/* kod
*/}
}
public class Prosta : ObiektLiniowy
{
public Prosta(Punkt p1, Punkt p2) :
base(p1, p2) {}
public override bool IsOn(Punkt p) {/* kod
*/}
}
public class Odcinek : ObiektLiniowy
{
public Odcinek(Punkt p1, Punkt p2) :
base(p1, p2) {}
public double GetLength(){/* kod */}
public override bool IsOn(Punkt p) {/* kod
*/}
}
//Promień
Klasyczne naruszenie LSP
• class Bazowa
• {
public virtual void f() {/*jakiś kod*/}
• }
• class Pochodna
• {
• public override void f(){}
• }
OCP a LSP
• OCP jest jedną z podstawowych reguł
programowania obiektowego
• LSP jest jednym z warunków OCP
• Czynnikiem który naprawdę definiuje
podtyp, jest możliwość zastępowania
wynikająca z jawnego bądź
niejawnego kontraktu.
Dependency-Inversion
Principle
• Treść: Moduły wysokopoziomowe nie powinny zależeć
od modułów niskopoziomowych. Oba powinny zależeć
od abstrakcji. Abstrakcje nie powinny zależeć od
detali. Detale powinny zależeć od abstrakcji.
Opis: Główną ideą tej zasady jest wymuszenie, aby
moduły czy klasy nie zależały od bardziej
niskopoziomowych elementów, a raczej, aby
niskopoziomowe klasy czy moduły bazowały na
klasach abstrakcyjnych, czy wyższych modułach.
Dzięki temu niskopoziomowe zmiany nie będą miały
wpływu na wyższy poziom aplikacji. Zastosowanie tej
zasady najważniejsze jest przy podziale aplikacji na
warstwy, ale także powinno się ją stosować przy
projektowaniu klas.
Dependency-Inversion
Principle
• Tam gdzie to tylko możliwe bądź zależny
od abstrakcji a nie konkretów. Czyli jeśli
chcesz mieć w klasie pole typu lista, to nie
deklaruj LinkedList, ArrayList ani nic
takiego, tylko po prostu List. To ułatwi ci
refaktoryzację jeśli okaże się, że trzeba
skorzystać z innej implementacji.
• Typowym naruszeniem DIP jest pobieranie
z formy danych (grid) i obliczanie np. sumy
w tej samej funkcji
Dependency-Inversion
Principle
•
class B
{
public void ZrobCos() { }
}
class A
{
public void Metoda()
{
B b = new B();
b.ZrobCos();
}
}
Jakakolwiek zmiana w klasie B może mieć wpływ na zachowanie klasy A. Poza tym, klasa A może tylko sterować
zachowaniem B. Jak to zmienić?
interface IB
{
void ZrobCos() { }
}
class B : IB
{
public void ZrobCos() { }
}
class A
{
public void Metoda()
{
IB b = new B();
b.ZrobCos();
}
}
Co osiągnęliśmy?
-klasa A może sterować dowolnymi klasami implementującymi interface IB
Dependency-Inversion
Principle
Interface Segregation
Principle
• Treść: klient nie powinien być zmuszony do
zależności od metod, których nie używa.
Opis: jest to chyba najrzadziej łamana zasada
ze wszystkich dotychczas przedstawionych.
Postuluje ona, aby interfejsy były jak
najbardziej skupione na jednym celu, a tym
samym nie były „przeładowane” metodami.
Jeżeli klasa implementuje interfejs, który ma
bardzo wiele metod to możliwe jest, że nie
wszystkie będzie wykorzystywać, a co gorsze
będzie musiała, co spowoduje złamanie SRP.
Interface Segregation
Principle -naruszenie
• Najprostszym złamaniem ISP byłoby
umieszczenie w jednym interfejsie metod
służących do operacji graficznych (np.:
Draw(), Rotate()) oraz odpowiedzialnych za
trwałość (np.: Save(), Load()).
• Każdorazowo, gdy implementujemy
metodę interfejsu (bo musimy) i
zostawiamy ją pustą to oznacza
nadmiernie rozbudowany interfejs.
• Interface pollution