Krzysztof Olszewski
Dyrektor Technologii i Architektury Oprogramowania
Krzysztof Olszewski
Dyrektor Technologii i Architektury Oprogramowania
We wczesnych latach siedemdziesiątych poprzedniego stulecia, Dan Ingalls, zdolny architekt i programista pracujący w laboratoriach firmy Xerox (tej samej od której Steve Jobs i Bill Gates „pożyczyli” ideę pulpitu, okien, myszki, ikonek itd.) w krótkim czasie zaimplementował pierwszą wersję języka Smalltalk. Język Smalltalk to pierwszy w pełni obiektowy język programowania, którego podstawowy zbiór cech wygląda nawet na dzisiejsze czasy imponująco:
- wszystko jest obiektem,
- programy działają na maszynie wirtualnej,
- pamięć jest automatycznie oczyszczana za pomocą Garbage Collector’a,
- dostępna jest refleksja poprzez meta-model,
- każdy obiekt może wyłącznie przechowywać stan, odbierać komunikaty i wysyłać komunikaty.
W konsekwencji swoich cech Smalltalk stał się bardzo wpływowym językiem. Jeżeli ktoś ma jeszcze wątpliwości skąd architekci czerpali inspiracje przy tworzeniu Java’y czy Python’a, to już nie powinien mieć :).
Na tle tych faktów, chciałbym poruszyć ważną kwestię dotyczącą komunikacji pomiędzy obiektami. Powyższy Smalltalk’owy początek – co bardziej spostrzegawczy mogli zauważyć – zawiera jeden nieoczywisty punkt:
każdy obiekt może wyłącznie przechowywać stan, odbierać komunikaty i wysyłać komunikaty
Z tym stanem to sprawa jest dosyć jasna, uczy się wszystkich, że obiekt jako instancja klasy powinien zawierać i chronić (enkapsulować) stan. Natomiast z tymi komunikatami … to jakieś novum. Wszyscy wiemy, że oprócz stanu, klasy i obiekty mają metody, które można wywoływać przekazując im parametry i odbierając wyniki. W zasadzie takie metody są to funkcje wbudowane w obiekty, które „widzą” wnętrze obiektu. Tak nas uczą. Jak widać, wzorując się na Smalltalk’u architekci nie odrobili lekcji do końca, lub odrobili i świadomie zrezygnowali z implementacji komunikatów. Jakie do daje konsekwencje. Nasz kod wygląda jak klasyczny kod proceduralny, wykonuje się linia po linii. Jest prosty, czytelny, łatwo się śledzi. Jednak jedna dosyć znaczna wada tego podejścia, każe nam szukać w pewnych obszarach innych rozwiązań. Nasz kod jest synchroniczny. Na każde „coś” co długo trwa musimy czekać. Jeżeli wywołanie metody w linii 431 trwa długo, to instrukcja w linii 432 musi czekać aż ta w 431 się skończy. Z różnych względów coraz częściej zależy nam aby było inaczej.
Z pomocą przychodzą nam różne konstrukcje językowe głównie oparte o współbieżność, wątki, nieblokujące I/O, programowanie reaktywne. Wszystko to nie jest proste, znacząco komplikuje architekturę i sam kod. Gdzie zatem leży przyczyna? Jedną z przyczyn jest synchroniczne zwracanie prostego, oczekiwanego rezultatu z metody, np.
public class SaleDocument {
//...
public BigDecimal calculateDocumentTotal() {
//...
}
Od razu widać problem. Możemy kazać dokumentowi policzyć swoją sumę, i jednocześnie musimy czekać aż on to zrobi, bo nasze polecenie okraszone jest faktem, że w wyniku jego działania ma zostać zwrócona jakaś liczba. Domyślamy się, że ta liczba to będzie ten „DocumentTotal”, ale pewności nie mamy, pewnie w dokumentacji będzie to napisane (uff!!!). Gdyby użyć „void” i zmienić deklarację na
public class SaleDocument {
//...
public void calculateDocumentTotal() {
//...
}
Wygląda to lepiej. Od razu widzimy że metoda ta każe wykonać obliczenia i nic więcej. W zasadzie ktoś kto ją wywołuje zastanawia się po co ma czekać na wynik, skoro nie może go sobie zapisać w zmiennej. Niby tak, ale przecież metoda ta mogła zostać wywołana po to, aby w kolejnej linii wywołać metodę
public class SaleDocument {
//...
public BigDecimal getDocumentTotal() {
//...
}
Gdyby tak było, to nie mamy żadnego zysku zamiast wołać
//...
BigDecimal total = doc.calculateDocumentTotal();
//...
wołamy
//...
doc.calculateDocumentTotal();
BigDecimal total = doc.getDocumentTotal();
//...
czyli bardzo źle. Bo po pierwsze dwie linie zamiast jednej, a po drugie gdyby ktoś wywołał getDocumentTotal() bez wcześniejszego wywołania calculateDocumentTotal() mógłby otrzymać zły wynik (sic!).
Wygląda na to, że chyba pogorszyliśmy sytuację (i tak jest). Jednak wróćmy do samego początku i jak przystało na programistów obiektowych, zastanówmy się, jakie są kompetencje klasy SaleDocument a jakie nas, czyli innej klasy będącej jej klientem np. Client.
SaleDocument:
- wie czy jest podliczony czy nie, zna swój stan,
- umie obliczać swoją wartość,
- w razie obliczeń wie kiedy skończył i wie kiedy ma gotowy wynik (może mieć od razu, np. nie zmieniła się lista pozycji od czasu ostatniej kalkulacji, albo musi liczyć na nowo).
Client:
- umie poprosić o wartość dokumentu,
- wie co zrobić jak ta wartość jest gotowa.
Zgodnie z tym, zaproponujmy interfejs klasy SaleDocument
public class SaleDocument {
//...
public void getDocumentTotal(Listener l) {
//...
}
i klasy Client
public class Client {
//...
public void documentTotalReady(BigDecimal total) {
//...
}
Przy takich interfejsach, obiekt klienta gdzieś w swoim kodzie może poprosić dokument o wartość. Dokument jak będzie miał gotowe dane z podliczenia to poinformuje o tym klienta. Żadna metoda nie zwraca wyników. Wynikiem działania metody jest komunikat zwrotny przekazany albo od razu albo po skomplikowanych obliczeniach, nie ważne bo za każdym razem przekazany jest tym samym sposobem. A może będzie wyliczany na osobnym wątku … kto wie, może tak może nie. A może dzisiaj nie, a za jakiś czas tak. Nie ma to dla nas znaczenia. Dowolna zmiana w implementacji dokumentu nie zmieni interfejsu i nie zmieni kodu klienta. Rewelacja.
Gdyby obiekty od początku przyjęły architekturę komunikacji wyłącznie za pomocą komunikatów, rozwiązało by to bardzo wiele problemów. Pozwoliło by to uprościć interfejsy, zachować większą spójność czy w końcu dynamicznie (np. w runtime) migrować z wywołań synchronicznych do asynchronicznych i odwrotnie. W dodatku komunikaty można logować, filtrować, wzbogacać, odtwarzać, transformować, itd. itp.
Bardziej uważny czytelnik zauważył pewien zgrzyt w samej koncepcji. Aby ją zrealizować trzeba uciekać się do różnych rozwiązań pomocniczych. W przykładzie metoda
public void calculateDocumentTotal(Listener l)
otrzymuje argument Listener. Niestety nie jest on związany z biznesowym kontraktem metody, jest to argument techniczny, przekazany po to aby wiadomo było kogo poinformować o końcu obliczeń. Oczywiście Listener’a można przekazać w konstruktorze, czy jakoś inaczej, ale faktem pozostaje, że potrzebny jest cały ten „stuff” związany z obsługą komunikacji, taki czy inny. Oczywiście użycie wzorca Listener jest tu tylko jedną z możliwości. Nie zmienia to faktu, że nie jest to eleganckie.
Czy zatem możemy pozbyć się tego rodzaju zakłóceń w kodzie? Do pewnego stopnia tak. Podjęto wiele prób rozwiązania problemu, niektóre zakończyły się jeżeli nie pełnym to prawie pełnym sukcesem. Polecam zapoznać się z biblioteką „Akka” z języka Scala. W elegancki sposób implementuje ona ideę komunikacji za pomocą komunikatów, wykraczając mocno poza bazowe wymagania. W sferze marzeń pozostaje natywne wsparcie już na poziomie samych języków, może się doczekamy (a może czegoś nie wiem).
Czy zatem we własnym kodzie powinniśmy myśleć o zmianie podejścia, z klasycznego na „komunikatowe”? To zależy. Powinniśmy, ale tylko tam gdzie przyniesie to korzyści większe niż koszty (cóż za odkrycie :)). Te koszty wciąż nie są zerowe, więc decyzja nie jest oczywista. Pewnym wskazaniem jest aby stosować architektury oparte o komunikaty, czy o asynchroniczność, w tych obszarach aplikacji czy systemów gdzie wykraczamy poza główną domenę z danego kontekstu. Im bardziej jesteśmy blisko i wewnątrz jakiegoś kontekstu, implementując jego główne mechanizmy tym bardziej możemy pozwolić sobie na zwykły, prosty kod. Im bardziej na zewnątrz, gdy wręczy łączymy różne konteksty, tym bardziej można myśleć o komunikatach czy asynchroniczności.
Na koniec refleksja. Mija kilkadziesiąt lat od opublikowania założeń i samego języka Smalltalk, a idee zrodzone w umysłach jego twórców są żywe do dzisiaj. Pojawiają się w językach których używamy i być może niedługo ożyją znowu w jakimś języku który pozwoli używać ich bez ponoszenia żadnych dodatkowych kosztów.