Kilka uwag o projektowaniu obiektowym

Transkrypt

Kilka uwag o projektowaniu obiektowym
Kilka uwag o projektowaniu
obiektowym
Mateusz Kobos 17.03.2011
System komputerowy a rzeczywistość
●
Czasami można się spotkać ze stwierdzeniem, że „system komputerowy
modeluje rzeczywistość”. Takie stwierdzenie nie jest do końca poprawne,
bo
–
„rzeczywistość” zależy od punktu widzenia,
–
pojęcie rzeczywistości traci sens w przypadku programów, które
operują na programach (np. kompilatory, edytory),
–
w dzisiejszych czasach komputery i programy są częścią
modelowanej "rzeczywistości" (np. w systemach wspomagających
zarządzanie firmą),
–
system nie jest modelem rzeczywistości, w najlepszym przypadku
jest modelem modelu jakiejś części rzeczywistości.
2/21
System komputerowy a rzeczywistość cd.
●
●
●
Przy tworzeniu oprogramowania mamy do czynienia z kilkoma
„poziomami”:
–
rzeczywistość;
–
część rzeczywistości, który nas interesuje;
–
abstrakcyjny model tej części rzeczywistości;
–
system jest modelem tego abstrakcyjnego modelu.
Szpitalny system monitorowania pacjentów nie jest modelem szpitala, ale
praktycznym modelem czyjegoś punktu widzenia na to, jak powinny
wyglądać pewne aspekty zarządzania szpitalem.
Modelować znaczy odrzucać - zadaniem projektanta jest modelowanie tej
części rzeczywistości, która jest ważna dla budowanego systemu.
3/21
Cechy oprogramowania dobrej jakości
Poniżej przedstawiono podstawowe cechy jakimi charakteryzuje się
oprogramowanie dobrej jakości.
●
●
●
●
Niezawodność na którą składają się:
–
poprawność (ang. correctness) - najważniejsza cecha - jeśli program
nie robi tego, czego od niego oczekujemy to reszta jest nieważna;
–
odporność (ang. robustness) – odporność na nieprzewidziane (na
etapie specyfikacji/projektowania) sytuacje – program powinien
wypisać błąd i łagodnie zakończyć działanie.
Modułowość,na którą składają się:
–
rozszerzalność (ang. extendibility) - łatwość przystosowania
programu do zmian w specyfikacji;
–
przystosowanie do wielokrotnego użycia (ang. reusability) – kod
można wykorzystać w innych częściach programu lub w innych
programach.
Modułowość prowadzi do tworzenia elastycznej architektury systemu
złożonej z autonomicznych komponentów software-owych. Wykorzystuje
się moduły – samodzielne części kodu zorganizowane w stabilne struktury.
W przypadku programowania obiektowego to klasy są modułami.
4/21
Kryteria modułowości
By realizować pożądane cechy oprogramowania rozszerzalności i
przystosowania do ponownego użycia, metoda konstrukcji
oprogramowania powinna spełniać następujące kryteria.
●
●
●
●
●
Wspomaganie podejścia top-down – wspomaganie podejścia, w którym
rozbijamy problem software-owey na parę mniej skomplikowanych
podproblemów, które można analizować w miarę niezależnie.
Wspomaganie podejścia bottom-up – wspomaganie wytwarzania
komponentów software-owych, które mogą być ze sobą łatwo łączone
tworząc w ten sposób nowy system.
Zrozumiałość – by zrozumieć dowolny moduł, powinna wystarczyć analiza
tylko tego modułu, a w najgorszym przypadku również paru (niewielu)
innych.
Stabilność (alternatywna nazwa: ciągłość) - mała zmiana w specyfikacji
pociąga za sobą zmianę w jednym module albo w małej liczbie modułów.
Ochrona - w przypadku wystąpienia anormalnej sytuacji w module, jej
skutki ograniczają się tylko do tego modułu, a w najgorszym przypadku do
5/21
jeszcze paru (niewielu) innych.
Reguły modułowości
Uwaga: poniżej będziemy używać pojęcia intefejsu ale w znaczeniu
bardziej ogólnym niż np. interfejs w języku C#. Przez interfejs będziemy
tutaj rozumieć „kanał komunikacyjny” między modułami.
Z kryteriów modułowości wynikają następujące reguły.
●
●
Mapowanie bezpośrednie (ang. direct mapping) - struktura modułowa
programu powinna odpowiadać strukturze modułowej modelowanego
problemu opisanego w specyfikacji wymagań.
Niewiele interfejsów - każdy moduł powinien komunikować się z jak
najmniejszą liczbą innych modułów.
–
●
●
Nieduże interfejsy - jeśli 2 moduły komunikują się ze sobą, powinny
wymieniać tylko tyle informacji, ile jest niezbędne.
Jawne interfejsy – jeśli 2 moduły komunikują się ze sobą, musi to być
wyraźnie uwidocznione w kodzie źródłowym tych modułów.
–
●
umożliwia to tworzenie odpornych, rozszerzalnych architektur.
Tu taką ukrytą komunikacją może być korzystanie ze wspólnej
struktury danych (zamiast standardowego wywoływania nawzajem
swoich procedur przez moduły).
Ukrywanie informacji - tylko część modułu powinna być publiczna (bo
wtedy można łatwo zmieniać część prywatną).
6/21
Zasady modułowości
Z kryteriów i reguł modułowości wynikają następujące zasady.
●
●
Samodokumentowanie się kodu (ang. self-documentation) - trzeba
dążyć do tego, by wszystkie informacje o module były zawarte w module
(a nie w oddzielnych dokumentach).
Jednolity dostęp (ang. uniform access) – dostęp do
właściwości/elementów klasy powinien odbywać się przez odpowiednik
właściwości (ang. properties) z języka C# (by klient klasy nie wiedział,
czy uzyskuje bezpośredni dostęp do pola klasy, czy do pewnej funkcji
obliczającej wartość).
–
Zaleta: można łatwo zmienić implementację takiego dostępu do
klasy (wyliczać wartość dynamicznie lub przechowywać w polu
klasy)
7/21
Zasady modułowości cd.
●
●
Zasada otwarty zamknięty (ang. open-closed) - moduł powinien być
otwarty na rozszerzanie i zamknięty na modyfikacje.
–
„Otwartosć” oznacza tutaj, że można np. dodawać do modułu/klasy
nowe operacje/metody i nowe pola (co może być konieczne przy
zmianie specyfikacji wymagań).
–
„Zamkniętość” oznacza tutaj, że moduł ma stabilny, dobrze
zdefiniowany opis (intefejs) i dzięki temu może być używany przez
inne moduły.
–
Rozwiązaniem tego paradoksu jest np. zastosowanie dziedziczenia,
gdzie nie modyfikuje się napisanego już kodu, a można rozszerzać
jego funkcjonalność.
Pojedynczy wybór – gdy program obsługuje zbiór alternatyw, ich pełna lista
powinna być znana dokładnie jednemu modułowi.
–
Zaleta: dodanie nowej alternatywy (co może być konieczne gdy
zmieni się specyfikacja wymagań) powoduje zmiany tylko w
jednym module
8/21
Znajdowanie klas
●
●
●
●
●
●
Znajdowanie klas jest najważniejszym zadaniem w budowaniu systemu
obiektowo zorientowanego. Zadanie to wymaga talentu, doświadczenia i
szczęścia (jak w każdej dyscyplinie związanej z kreatywnością).
System projektujemy na podstawie specyfikacji wymagań.
Przykład: fragment specyfikacji wymagań: "Winda zamyka drzwi zanim
przemieści się na inne piętro".
Pytanie: czy klasa drzwi jest potrzebna?
Odpowiedź: to zależy, od tego, co ma robić program. Trzeba zadać sobie
pytanie czy
–
„drzwi” to oddzielny typ danych z jasno zdefiniowanymi operacjami
(klasa jest potrzebna) lub
–
czy może wszystkie operacje wykonywane na "drzwi" są już zawarte
w operacjach wykonywanych na innych typach danych takich jak
winda (klasa jest niepotrzebna).
Analogicznie, piętra możemy reprezentować po prostu za pomocą liczb.
Jeśli jednak piętro ma cechy inne niż liczba, czyli istotne operacje nie są
określone poprzez liczbę - np. niektóre piętra mogą mogą mieć określone
9/21
specjalne prawa dostępu - to należy utworzyć klasę piętro.
Znajdowanie klas cd.
Poniżej podano źródła, które mogą być przydatne przy znajdowaniu klas.
●
Istniejące programy/biblioteki.
●
Dokument wymagań
–
często pojawiające się terminy
–
terminy zdefiniowane explicite w tekście
–
terminy nie zdefiniowane precyzyjnie w tekście ze względu na swoją
oczywistość
●
Dyskusja z klientami i przyszłymi użytkownikami.
●
Rozmowa z doświadczonym projektantem.
●
Dokumentacja (np. manual) do innych systemów z tej dziedziny.
●
Literatura dotycząca systemów obiektowych oraz algorytmów i struktur
danych.
10/21
Dziedziczenie - własności
●
Na dziedziczenie można patrzeć z 2 perspektyw.
●
Perspektywa modułu – perspektywa bardziej implementacyjna.
–
●
Łatwo można tworzyć nowy moduł wskazując tylko to co jest dodane i
zmienione w nowym module (podklasa) względem starego
(nadklasa). W paru modułach (podklasy) można wydzielić wspólną
część (nadklasa) i dzięki temu nie duplikować kodu. Jest to zgodne
z zasadą otwarty-zamknięty.
Perspektywa typu – perspektywa bardziej abstrakcyjna. Dziedziczenie
reprezentuje relację „A jest B”/”każde A jest B”.
–
Co oznacza ta relacja?
●
●
Zawieranie się zbiorów obiektów: obiekty typu prostokąt są
podzbiorem obiektów typu wielokąt.
Podstawialność: każda operacja, która może być zastosowana
do klasy wielokąt może też być zastosowana do klasy
prostokąt.
11/21
Jak tworzyć hierarchię klas
●
●
●
Hierarchia klas tworzy strukturę klasyfikacji obiektów w systemie.
Zazwyczaj jest możliwych parę klasyfikacji dla danego problemu - należy
wybierać tą najbardziej odpowiadającą dziedzinie zastosowania.
Tworzenie hierarchii to kombinacja 2 procesów: specjalizacji i
generalizacji.
–
Czasami najpierw zauważamy abstrakcję i wnioskujemy o
przypadkach szczególnych jej użycia,
–
a czasami najpierw budujemy użyteczne klasy i zdajemy sobie, że
istnieje bardziej abstrakcyjny koncept, który je „spina”.
–
Jest to proces typu yo-yo: projektujemy abstrakcje, potem
implementacje i z powrotem abstrakcje.
12/21
Problem: sprawdzanie typu obiektu
●
●
W programach obiektowych b. rzadko zachodzi potrzeba sprawdzania typu
obiektu (tzn. sprawdzania instancją jakiej klasy jest obiekt).
Obowiązuje raczej zasada egoizmu: "Nie mów mi czym jesteś, powiedz mi
co masz – jak mogę cię wykorzystać” lub inaczej: „programuj biorąc pod
uwagę interfejs a nie implementację”. W programie wykorzystujemy
interfejs klasy (publiczne metody i właściwości), nie interesuje nas to z jaką
dokładnie klasą mamy do czynienia
void
voidmakeSound(Animal
makeSound(Animala){
a){
if(a
if(aisisCow)
Cow)((Cow)a).moo();
((Cow)a).moo();
else
if(a
is
Dog)
else if(a is Dog)((Dog)a).bark();
((Dog)a).bark();
else
if(a
is
Cat)
((Cat)a).meow();
else if(a is Cat) ((Cat)a).meow();
else
elsethrow
thrownew
newUnknownAnimal();
UnknownAnimal();
}}
void
voidmakeSound(Animal
makeSound(Animala){
a){
a.makeSound();
a.makeSound();
}}
13/21
Problem: sprawdzanie typu obiektu cd.
●
●
Komentarz do rysunku po lewej. Jeśli sprawdzamy typ obiektu i w
zależności od typu wykonujemy pewne działania, to podobny kod
sprawdzający ma tendencję do występowania w różnych miejscach
programu. Gdybyśmy dodali nową klasę zwierzęcia np. kurę, kod
prawdopodobnie należałoby zmienić w tych wszystkich miejscach, by
obsłużyć ten nowy typ. Byłoby to czasochłonne i podatne na popełnienie
błędu przez programistę. Jest to niezgodne z kryterium stabilności, i
niezgodne z zasadą pojedynczego wyboru (bo wiele różnych modułów
musiałoby mieć wiedzę o alternatywnych typach zwierzęcia).
Komentarz do rysunku po prawej. Jeśli natomiast postępujemy zgodnie z
„zasadą samolubności”, jest to zgodne z kryterium stabilności z z zasadą
pojedynczego wyboru – informacja o alternatywnych rodzajach zwierzęcia
jest wykorzystywana tylko w miejscu, w którym tworzone są obiekty typu
zwierzę. Dodanie nowego typu zwierzęcia jest tu o wiele łatwiejsze.
14/21
Problem: sprawdzanie typu obiektu na liście
●
Problem (analogiczny do poprzedniego): przy iterowaniu po elementach
listy, dla każdego elementu wykonujemy czynność zależną od jego typu
(klasy).
var
varl l==new
newArrayList<Animal>();
ArrayList<Animal>();
l.add(new
Dog(„Pluto”));
l.add(new Dog(„Pluto”));
l.add(new
l.add(newDog(„Butch”));
Dog(„Butch”));
l.add(new
Cow(„Megan”));
l.add(new Cow(„Megan”));
…
…
foreach(Animal
foreach(Animalaain
inl){
l){
if(a
is
Cow)
((Cow)a).moo();
if(a is Cow) ((Cow)a).moo();
else
elseif(a
if(aisisDog)
Dog)((Dog)a).bark();
((Dog)a).bark();
else
if(a
is
Cat)
((Cat)a).meow();
else if(a is Cat) ((Cat)a).meow();
else
elsethrow
thrownew
newUnknownAnimal();
UnknownAnimal();
}}
15/21
Problem: sprawdzanie typu obiektu na liście cd.
●
Nie ma dobrego i ogólnego rozwiązania tego problemu.
●
Rozwiązanie 1 - analogiczne jak poprzednio – wykorzystanie polimorfizmu
var
varl l==new
newArrayList<Animal>();
ArrayList<Animal>();
l.add(new
Dog(„Pluto”));
l.add(new Dog(„Pluto”));
l.add(new
l.add(newDog(„Butch”));
Dog(„Butch”));
l.add(new
Cow(„Megan”));
l.add(new Cow(„Megan”));
…
…
foreach(Animal
foreach(Animalaain
inl){
l){
a.makeSound();
a.makeSound();
}}
●
Wada tego rozwiązania: gdy chcemy np. zliczać obiekty każdego z typów
16/21
występujące na liście i tak musimy sprawdzić typ każdego obiektu.
Problem: sprawdzanie typu obiektu na liście cd.
●
●
●
Rozwiązanie 2 – nie trzymać na tej samej liście obiektów, które bardzo się
różnią – wykorzystać różne listy dla różnych typów obiektów.
Wada tego rozwiązania: to podejście może być nienaturalne i niezgodne z
filozofią reszty systemu. W szczególności możemy mieć do czynienia z
sytuacją, gdy dla każdego nowego typu obiektu należy tworzyć nową listę.
Czasami jedynym wyjściem jest stosowanie rozwiązania 1 połączonego ze
sporadycznym sprawdzaniem typu obiektów na liście (gdy np. chcemy
policzyć ile jest obiektów każdego z typów).
17/21
Problem: dziedziczenie czy typ wyliczeniowy?
●
●
Przykład: w systemie będzie występować parę różnych typów zwierząt:
pies, krowa, kot.
Pytanie: czy stosować dziedziczenie czy typ wyliczeniowy do
reprezentowania tych typów?
kontra
●
Odpowiedź:
–
Jeśli w systemie rodzaje zwierząt mają różne
cechy/funkcje/zachowanie (np. jest to system do zarządzania
zwierzętami farmerskimi, gdzie każde ze zwierząt wymaga innego
pokarmu, jest powiązane z innym lokum, wytwarza inne produkty),
to wprowadzenie hierarchii dziedziczenia jest uzasadnione.
–
W przeciwnym przypadku (np. jest to system raportowy, zliczający
tylko liczbę różnych rodzajów zwierząt) lepiej użyć typu
18/21
wyliczeniowego.
Problem: dziedziczenie czy zawieranie
●
Dziedziczenie i zawieranie to dwie podstawowe relacje występujące w
językach obiektowych. Kiedy należy stosować jedną a kiedy drugą?
kontra
●
Jeśli klasa A dziedziczy po klasie B, to powinna być zachowana relacja „A
jest B”, a bardziej szczegółowo: „każde A jest B” (np. każdy Samochód jest
Pojazdem).
–
Dzięki temu uszczegółowieniu możemy uniknąć trywialnego błędu:
●
●
Klasa SanFrancisco dziedziczy po klasie City – źle. Można
powiedzieć „San Francisco jest miastem”, ale już nie „Każde
San Francisco jest miastem”.
Jeśli obiekt A zawiera/posiada obiekt B, to powinna być zachowana relacja
„A zawiera/posiada B” (np. „samochód zawiera kierownicę”).
19/21
●
●
●
Uwaga: gdy zachodzi relacja "jest", można zamiast niej równie dobrze
zastosować relację "zawiera".
Przykład klasy "inżynier oprogramowania" i dwóch opisów świata.
–
"Każdy inżynier oprogramowania jest inżynierem.". Z tego opisu
wynika, że należy zastosować relację "jest", czyli dziedziczenie.
–
"W każdym inżynierze oprogramowania jest inżynier". Z tego opisu
wynika, że należy zastosować relację "zawiera/posiada", czyli
zawieranie.
Następująca reguła pomaga w wyborze odpowiedniej relacji (ale nie
zawsze wskazuje ją jednoznacznie). Pytamy czy klasa B powinna
zawierać obiekt klasy A, czy dziedziczyć po klasie A?
–
Stosować relację zawierania, jeśli w czasie wykonania programu
może zajść potrzeba zastąpienia obiektu klasy A obiektem innej
klasy.
●
–
Inżynier oprogramowania po paru latach może zmienić swój
komponent inżynier np. na poetę lub hydraulika.
Stosować relację dziedziczenia, jeśli może zajść potrzeba, by obiekty
klasy B były czasem traktowane jakby były obiektami klasy A.
●
Należy używać dziedziczenia, jeśli relacja między obiektami klas
20/21
A i B jest niezmienna.
Literatura
●
●
Bertrand Meyer, „Object Oriented Software Construction”, drugie wydanie,
2000
Bertrand Meyer, „Programowanie zorientowane obiektowo”, Helion. 2005
–
Nie polecane ze względu na słabe tłumaczenie
21/21

Podobne dokumenty