wtorek, 25 maja 2010

Object Oriented czy jedynie Class Oriented?

Dziś będzie o "nowym" podejściu do Object Oriented.
Jeżeli komuś wydaje się, że już nic nowego nie można w tym jakże oklepanym temacie dodać, to zachęcam do zapoznania się prezentacjami zawartymi w tekście. Dowiecie się również dlaczego nowym napisałem w cudzysłowie.

Od jakiegoś czasu z wielu stron pojawiają się zachęty do zainteresowania się nowymi językami, takimi jak Scala, Groovy, Ruby,...

Do tej pory wszyscy orędownicy pokazują jak to fajnie można sobie operować na kolekcjach (sortować, wyszukiwać) w jednej linijce kodu dzięki możliwości "doklejania" nowych metod do istniejących klas. Z całym szacunkiem, ale jest to nic innego jak objaw onanizmu technicznego. Jeżeli mam zainteresować się nowym językiem tylko po to aby uniknąć klepania pętelek to jednak zastosuję "cwanego" utilsa. Czy aby na pewno nowe języki powstają tylko po to aby sobie dokleić do klasy String 100 nowych metodek, o których istnieniu wie tylko doklejający?

Jeszcze inni odkryli ponownie możliwość dynamicznego dodawania "w locie" do encji metod zapisujących i wczytujących je. Myślałem, że niesławny Active Record odszedł już dawno w niepamięć. Ale jednak nie - okazuje się, że jest sexi w niektórych frameworkach.

Podczas niedawnej sesji researchu trafiłem na ciekawą koncepcję, która nadaje prawdziwy i pragmatyczny sens tym wszystkim konstruktom językowym.Pojawił się oto "nowy" paradygmat programowania: Data Context Interaction, który wskazuje zastosowanie dynamicznych konstruktów do lepszego (a przynajmniej innego) modelowania problemów. Czyli mamy coś więcej niż sortowanie listy, mamy nową jakość myślenia o modelowanej strukturze.

Twórcy DCI twierdzą, że mainstreamowe podejście do OO to jedynie kilka % prawdziwego OO. Klasyczne podejście skupia się na klasach, czyli na pewnych strukturach. Stawiają oni klasycznemu podejściu zarzuty, że skupia się ono na klasach zamiast na obiektach, co jakoby powoduje "rozsmarowanie" logiki Use Case po wielu klasach, dzięki czemu w nietrywialnych aplikacjach z czasem coraz ciężej przychodzi połapanie się w logice.

Czy zatem czas na powrót do paradygmatu proceduralnego, gdzie mamy cały "flow" w 1 miejscu?
Niekoniecznie.
Spójrzmy ma poniższy rysunek:


źródło: http://www.underbjerg.com/2009/11/16/oredev-2009-impressions-and-dci-architecture/

Dane opisują przy pomocy Klas pewną niezmienną strukturę - można powiedzieć core modelu (może mieć on pewne bazowe, ogólne zachowania).
Interakcje są wyniesieniem zachowania do poziomu głównych bytów, dzięki czemu lepiej odpowiadają modelowi mentalnemu usera. Są pewnymi rolami, które mogą być przyjęte przez obiekty. Interakcje operują na Danych ponieważ są do nich mixowane w...
Kontekście - kontekst odpwiada Use Caseom lub ich krokom. To w pewnym kontekście dane są łączone z rolami (zachowaniem) tworząc dopiero obiekty.

Intuicyjny przykład: jestem człowiekiem, opisuje mnie zestaw standardowych parametrów. Ale w zależności od kontekstu przyjmuję niektóre role (a wraz z nimi zachowania): Programista, Manager, Ojciec, Kierowca, Gracz, Klient, ...


Jeżeli udało mi się chociaż trochę zainteresować Was koncepcją DCI (nie mylić z nieco mniej innowacyjnym CDI;) to polecam następujące materiały:

- Architektura i ciekawy przykład w Ruby - aby zaintrygować dodam, że znajdziecie tutaj ostrą krytykę TDD.
- Ciekawe podejście do modelowania - mamy tutaj przykład niesamowitej jasności myślenia i pokaz tragikomicznego frameworka Qi4j (Java się jednak nie nadaje do DCI)
- Niezbyt porywające wprowadzenie teoretyczne - z którego dowiemy się, że te koncepcje mają już kilkadziesiąt lat, tylko gdzieś się zapodziały w przemyśle.
- Niekoniecznie świadome DCI w Scali - plus durne przykłady sortowania kolekcji;)


Ogólnie polecam materiały z konferencji Øredev - jak widać tematyka jest jest bardzo ciekawa i mocno wykracza poza mainstream.

A w inkubatorze (póki co) Eclipse mamy coś bardzo podobnego: Object Teams


//==========================

Ciekawie DCI podsumował jeden ze znajomych architektów (przy okazji "snajper", który potrafi trafić człowieka z granatnika z odległości całej planszy w Modern Warfare 1): "ale trzeba mieć jaja aby zakwestionować standardy i przedstawić coś tak odmiennego".

23 komentarze:

Paweł Lipiński pisze...

Ten co ma jaja, to autor wzorca MVC (gdzieś z lat 70tych), aktualnie 80-letni staruszek, więc może sobie pozwolić na innowacje ;-) A tak serio, to niesamowite i godne najwyższego szacunku, że człowiek w tym wieku nie odcina kuponów, tylko wciąż jest kreatywny.
Drugi z jajami to Jim Coplien, który znany jest z kontrowersyjnych wypowiedzi (java is a toy language; agile is like a teenagers sex - everybody talks about it, a few really do it; tdd will destroy your application).

Pomysł jest faktycznie ciekawy, ale tak jak piszesz zupełnie do javy nie pasuje. Wszelkie łatki w rodzaju Qi4j powodują, że kod staje się mniej czytelny zamiast bardziej. A przecież w OO chodzi m.in. o czytelność.

Zainteresowanym dokładniejszym opisem DCI odsyłam do http://www.artima.com/articles/dci_vision.html

Sławek Sobótka pisze...

80 lat... nie wygląda... chyba każdy chciałby być w takim wieku w takie formie umysłowej.

Co do jaj, to kiedyś przeglądałem biografię Rickarda Öberga (Qi4j) - założył JBossa, po czym uznał, że EE nie ma sensu:)

iirekm pisze...

Czy jest to takie nowe podejście?
Dużo niepotrzebnego gadania, a tak naprawdę to wszystko sprowadza się do umiejętnego stosowania rzeczy jak mixiny (które lepiej zastąpić wzorcami GoF), AOP czy Bounded Context.

Sławek Sobótka pisze...

Nowe jest zastosowanie niektórych z wymienionych przez Ciebie technik do modelowania domeny.

Mixiny zwykle pokazuje się na przykładzie tych nieszczęsnych kolekcji czy Active Record. Pewnie niezbyt szukałem, ale nie znalazłem tak odważnego ich wykorzystania w warstwie logiki. Mało tego - takie podejście mentalne musi być wykorzystywane nie tylko na poziomie developmentu, ale również na poziomie analizy.

Wzorce GoF... można wyciągać role do strategii, ale Data musi wiedzieć o Interaction. Aby tego uniknąć weszlibyśmy pewnie w jakieś dekoratory albo paskudne Proxy.

AOP - zawsze musisz mieć punk złączenia, który pozwoli na nałożenie porady. Przez co Dane będą sztucznie "obłożone" takimi punktami.

Bounded Context - ok jestem w stanie uznać tą odpowiedź... W DDD otworzymy koncepcyjne "wyspy" hermetyzujące strukturę. A globalny model to antywzorzec. W DCI jest odwrodnie - struktura jest globalna. Więc są to diametralnie różne podejścia. Nie czuję się na siłach aby je teraz wartościować - wszystko jak zwykle zależy:)

iirekm pisze...
Ten komentarz został usunięty przez autora.
iirekm pisze...

Trudno o tym gadać bez rzeczywistych przykładów - niestety na tych prezentacjach prawie nic nie ma, a te co są używają właśnie mixinów. Ponieważ mixiny mogą być upierdliwe (podobnie jak proxy czy dekorator), lepiej używać strategii + AOP.
Ale ogólnie, to chyba nic nowego:

- data - to obiekty domenowe, np. w stylu DDD. To po prostu warstwa domenowa.

- context - wzorce strategia+dependency injection. To strategie powstrzykiwane obiektom domenowym by zmienić ich zachowanie zależnie od kontekstu. Tak naprawdę po to właśnie te wzorce są. Czasami tworzenie strategii byłoby czynnością mechaniczną (np. nakładanie tranzakcji) - wtedy lepiej użyć AOP.

- interaction - to wysokopoziomowy kod scalający to wszystko razem - znany jako fasada, warstwa aplikacji czy usecase controller.

Sławek Sobótka pisze...

Racja, bez przykładów możemy błądzić we własnych urojeniach;)
Zaciekawiło mnie to co piszesz o problemach z mixinami - w wolnej chwili napisz coś więcej.

Odnośnie porównań to czegoś innych rzeczy to jest to efektywna technika poznawcza, dopóki nie mamy do czynienia z czymś zupełnie innym/nowym - wówczas takie porównywanie to jeden z typowych błędów kognitywnych. Powoduje uwięzienie umysłu.

- Data to owszem warstwa domenowa. Ale w DDD encje, agregaty i VO mają odpowiedzialność biznesową. W DCI dane są "głupie" - może nie aż tak jak w mainstremowym podejściu proceduralnym ponieważ mogą mieć proste, wspólne zachowania. Ale cała logika biznesowa siedzi w Interaction.

- Interaction jak pisałem powyżej
zawiera logikę biznesową.
/*Z tego co rozumiem bo jak uznaliśmy obaj - bez przykładu możemy dryfować. Być może to ja popełniam błąd kognitywny dopasowując DCI do dłasnych wyobrażeń;)*/ Warstwa aplikacji zawiera logikę aplikacji.

iirekm pisze...

Noo, dobry przykład podobnie jak dobry rysunek byłby wart więcej niż 1000 słów czy (w sumie) 4 godziny prezentacji o DCI z Oredev 2009. :-)

Sławek Sobótka pisze...

hehehe
true, true

Paweł Badeński pisze...

Wygląda to tak, że rzesze programistów twierdzają że znają OO, odwołując się do umiejętności programowania w Javie, C++ czy C#. Tymczasem na przykład po dziś dzień nie powstał (chyba) język, który w równie umiejętny sposób jak Eiffel implementuje Design by Contract (a chodzi mi m. in. o wsparcie zasady podstawienia Liskovej na poziomie semantyki operacji). Przy okazji - dość trudno jest mówić o 100% OO, co Meyer mówi wprost w swojej księdze (przyznacie, że trudno nazwać ją książką :P), a o tym co różni badacze uznają za OO można poczytać u Armstrong (The Quarks of Object-Oriented Development). Część osób może zaciekawić również flame OO w wykonaniu Jonathana Rossa http://www.eros-os.org/pipermail/e-lang/2001-October/005852.html (sugeruję nie zniechęcać się pierwszym na wpół zrozumiałym postem i przeczytać cały wątek). Ogólnie oceniłbym świadomość przemysłu w temacie OO na 3.5 w skali akademickiej, zwłaszcza że duża liczba osób po studiach nie potrafi pisać OO w (sic!) Javie. Cieszę się, że wreszcie ruszamy na przód, zwłaszcza pokładam nadzieję w Scali, która pozwala nam wyjść z obiektowego żłobka.

Co do DCI uważam, że to genialny pomysł. Muszę powiedzieć, że chyba z 2 tygodnie chodziłem podjarany jak się o tym dowiedziałem ;D Nie pamiętam jak to wygląda w koncepcji Trygve, ale jeśli chodzi o implementację w Scali, to mi osobiście jeszcze brakuje zupełnego oddzielenia warstwy danych od zachowania, czyli możliwości dynamicznego podpinana zachowania pod jakąś klasę (dla osób znających Scalę: "var a = new A; foo(a with SomeBehaviour)"). Nie wiem jak to działałoby w praktyce, ale uważałbym eksperyment z takim mechanizmem za ciekawy :P

P.S. Przepraszam. Temat OO ostatnio stał się moim uzależnieniem ;]

iirekm pisze...

>>>> mi osobiście jeszcze brakuje zupełnego oddzielenia warstwy danych od zachowania, czyli możliwości dynamicznego podpinana zachowania pod jakąś klasę (dla osób znających Scalę: "var a = new A; foo(a with SomeBehaviour)") <<<<

hehe, dynamiczne podpinanie zachowania to przecież wzorzec Strategy :-)
fakt, strategia wymaga ciut więcej roboty niż np. mixiny (traity) w Scali, ale uważam że się opłaca, bo to tylko CIUT więcej roboty, a zysk ogromny, np. owa dynamiczna zmiana

dlatego jestem raczej przeciwny mixinom jako elementom języków programowania - wystarczy wiedzieć jak robić 'mixiny' za pomocą strategii czy template method

luknij tu:
- http://coding-masters.blogspot.com/2010/04/write-reusable-java-code-with-mixins.html
- http://coding-masters.blogspot.com/2010/04/mastering-mixins.html

Paweł Badeński pisze...

Dynamicznego podpinania zachowania, a nie dynamicznej zmiany zachowania. Może zbyt lakonicznie przedstawiłem pomysł. Mówię o dynamicznym (czyli de facto kontekstowym i tymczasowym) rozszerzeniu interfejsu obiektu. Po wywołaniu "foo with SomeBehaviour" obiekt potrafiłby odbierać więcej komunikatów niż sam "foo". Trudno tu znaleźć analogię z istniejącym wzorcem, zwłaszcza na poziomie filozofii mechanizmu.

Wracając do mixinów, to przedstawiłeś argument "dlatego jestem raczej przeciwny mixinom jako elementom języków programowania - wystarczy wiedzieć jak robić 'mixiny' za pomocą strategii czy template method", którą pozwolę sobie uogólnić jako "dlatego jestem raczej przeciwny XXX jako elementom języków programowania - wystarczy wiedzieć jak robić XXX za pomocą YYY". Większość GPL jest Turing-complete, ale chyba nie o to chodzi ;) Niefajnie, że nie posiadamy metodyki porównywania mechanizmów języków (jeśli czyta to jakiś student - temat na mgr :P), bo w takich przypadkach jak ten na ogół wszyscy pokazują "czy XXX w A da się zrobić w B". A pozostaje chociażby kwestia złożoności poznawczej czy kosztów pielęgnacji (prawdopodobnie etc.).

iirekm pisze...

>>>> Po wywołaniu "foo with SomeBehaviour" obiekt potrafiłby odbierać więcej komunikatów niż sam "foo" <<<<
Masz na myśli dodawanie metod do już utworzonych obiektów?
new SomeBehaviour(foo) - klasa SomeBehaviour zawiara dodatkowe metody dla foo. Na dodawanie nowych metod do już istniejących obiektów pozwala wzorzec Visitor (choć szczególnie go nie lubię). Może też wystarczyć 'cwany utils':
void SomeBehaviour.nowaMetoda(Foo foo, parametry...)


>>>> Większość GPL jest Turing-complete, ale chyba nie o to chodzi ;) <<<<
są elementy języka które są bardzo użyteczne i zaoszczędzają wielu kłopotów, klepania i poprawiają jakość kodu (np. pamięć zarządzana przez GC, klasy, generyki, adnotacje, closures, LINQ, foreach, ...), są też elementy które niewiele wnoszą, lub wręcz sprawiają problemy i do tego niepotrzebnie komplikują język (wielokrotne dziedziczenie, większość 'cwanych' rzeczy które są w Groovym czy w Perlu, duck typing, yield z C#, preprocesor)
ja bym mixiny raczej zaliczył do tych, które niewiele wnoszą - nie są obowiązkowe w składni języka; dobrze napisane mixiny (=traity) wymagają wiele tzw. 'glue code', a tego za programistę kompilator nie zrobi

Paweł Badeński pisze...

W sprawie pierwszej kwestii powtórzę tylko, że tu nie o to chodzi, że to się da tylko w jaki sposób to zmienia postrzeganie struktury modelu (myślę o czymś w kontekście Naurowego "programming as theory building"). Zresztą np. foo with A with B już tak prosto nie zrobisz. Nasza dyskusja utwierdza mnie w przekonaniu, że bez jakiegoś sensownego modelu do porównywania struktur języka daleko się nie ruszymy. Z tego co patrzyłem ostatnia praca o zbliżonej tematyce jest z roku '93, więc ponawiam apel do ludzi nauki (i studentów :P).

iirekm pisze...

O tym co piszesz to nie wiem czy są jakieś akademickie artykuły, ale są np. o mixinach i traitach:
http://scg.unibe.ch/research/traits

oodventurer pisze...

@Paweł

Nie musimy chyba biernie oczekiwać na ludzi nauki - do porównania elementów składni można spróbować wykorzystać artykuł Martina Fowlera
akceptując to, że w konkretnych wypadkach mogą pojawić się różnice w ocenach, czy dany element składni zalicza się w tym wypadku do szumu.

oodventurer pisze...

@iirekm
Napisałeś: "...dobrze napisane mixiny (=traity) wymagają wiele tzw. 'glue code', a tego za programistę kompilator nie zrobi". Co rozumiesz pod słowami "dobrze napisane" i "glue code"?

Jeśli dobrze napisany mixin to taki, który umożliwia dynamiczną zmianę zachowania, to można łatwo wykazać zbędność 'glue code'. Napisałem
taki przenosząc na Scalę Twój przykład oparty na Template Method. Glue code ogranicza się tam do jednej linijki implementacji abstrakcyjnej metody oraz klauzuli with w deklaracji klasy. Nie ma potrzeby generowania metod delegujących wywołania. Trait Flying może więc nabyć np. 16 nowych metod, a klasa Eagle pozostać niezmieniona. To jest bardzo ciekawa cecha traitów w Scali, wydatnie zwiększająca czytelność. Dzięki temu, że trait Flying nie deklaruje "self type" można go miksować z czymkolwiek w dowolnych konfiguracjach bez troszczenia się o linearyzację.

iirekm pisze...

Odwołuję się do tego artykułu:
http://scg.unibe.ch/archive/papers/Scha03aTraits.pdf

Dobrze napisany mixin to trait. Zasadnicza różnica jest taka, że trait nie zawiera stanu (stąd ten abstract getAnimal() zamiast pola private Animal animal ).

'glue code' to własnie implementacja tej metody: public void getAnimal() { return this; }
W większości przypadków kompilator nie byłby na tyle mądry by taką metodę zaimplementować.

Zaletą Scalowego:
class Bat extends Mammal with Flying {
def getAnimal = this
}
... jest mniej pisania (ale tak czy owak musisz napisać to 'glue code'). Wadą - potencjalne konflikty. Co by było jakbyśmy mieli 'A extends B with C with D', i gdyby było do zaimplementowania 'C.getCośtam()' i 'D.getCośtam()' - ta sama nazwa a zupełnie różne przeznaczenia.
Takie konflikty raczej nie zdarzają się przy 'normalnych' klasach, bo każda klasa zna swoją nadklasę i wie jakie nazwy zostały użyte, ale mogą zdarzać się tu - trait nie zna swojego miejsca w hierarchii klas.
Stosując strategię czy template method takie konflikty rozwiążesz łatwo - każda strategia czy klasa szablonowa jest w sama w sobie osobnym 'namespacem'. W rozwiązaniu opartym na dziedziczeniu (jak w Scali) wszystko jest pakowane do jednego worka i zaczynają się kłopoty.

O zaśmiecaniu namespace'a klasy przez mixiny trochę piszą tu:
http://www.artima.com/weblogs/viewpost.jsp?thread=246483

Ogólnie: mixiny typu 'Scalowego' czy 'Pythonowego' nadużywają dziedziczenia, a jak wiadomo w przypadku kodu większych rozmiarów, dziedziczenie jest paskudne.

Sławek Sobótka pisze...

Z tego co widzę, to na problem można patrzeć z różnych perspektyw, np pod względem pisania/konstruowania: http://art-of-software.blogspot.com/2010/05/konstruktor-destruktor.html

Sławek Sobótka pisze...

nowa porcja informacji o DCI: http://www.infoq.com/interviews/coplien-dci-architecture

oodventurer pisze...

@Sławek
> http://art-of-software.blogspot.com/2010/05/konstruktor-destruktor.html

Eh... :( Tyle starań o utrzymanie wizerunku "writera" na marne. Zdemaskowałeś mnie ;)

A na poważnie, ciekawa mogłaby być dyskusja, jakie cechy obu rozwiązań lepiej wyrażają wartości (readable, minimal, self-documenting, expressive) z artykułu Jakuba?

< dygresja historyczna >
Przed Javą 1.5 bardziej świadomi programiści posługiwali się wzorcem typesafe enum. Od Javy 1.5 stało się to w większości wypadków zbędne, dzięki nowemu słowu kluczowemu. Analogicznie, większość użyć iteratora w pętlach ludzie zaczęli zastępować rozszerzoną składnią pętli for.
< /dygresja historyczna >

Czy jest to pogoń za nowinkami? Nie. Czy zmniejsza to czytelność i zrozumiałość kodu? Wręcz przeciwnie.
Identycznie jest z traitami Scali. Enumy nie są wolne od wad (sam czasem wybieram stary typesafe enum pattern z modyfikacjami), traity także nie są idealne. Kluczem jest umiejętność dokonania właściwej oceny, kiedy zastosować dane rozwiązanie.

@iirekm
Fakt, liberalne używanie traitów niesie ze sobą zagrożenie namespace pollution. Jest to jednak trochę jak z nożem: można pokroić chleb, a można się zranić. With great power comes great responsibility. Gdy klasa w Javie implementuje wiele interfejsów (pośrednio lub bezpośrednio), problem zanieczyszczenia przestrzeni nazw też wystąpi. Przy korzystaniu z delegacji objętość boilerplate'u będzie jednak bardziej kłuć w oczy i być może odwiedzie niektórych od złych wyborów (nie da im się zranić). Zakłuje niestety też w przypadkach, gdy mixin jest idealnym rozwiązaniem (ciężko pokroić chleb).

Sławek Sobótka pisze...

@oodventurer akurat podejście Twoje i Pawła B. bardziej mi pochodzi pod podejście writera.

Sam też jestem zdania, że o ile wszystko można i tak zrobić w assmeblerze czy bytecodzie to jednak nie o to się rozchodzi. Chodzi o konstrukcje, które wspierają siłę wyrazu dla mentalnych modeli.

iirekm pisze...

My tu sobie pierniczymy coby teoretycznie było gdyby... Z samą teorią daleko się nie zajedzie.
Znacie jakieś dobre, praktyczne, opensourcowe przykłady kodu, większe niż helloworld, napisane z użyciem mixinów i/lub traitów i/lub DCI?
Jeden przykład lepszy niż tysiąc postów. :-)

Odnośnie Scali - podobają mi się niektóre ficzery tam wprowadzone, ale jestem do niej sceptycznie nastawiony bo integracja z IDE jest dla Scali gorsza niż dla Javy (a przynajmniej tak było z rok temu jak próbowałem coś w Scali napisać). Wolę napisać trochę więcej 'boilerplate code', ale mieć przynajmniej zajebiaszcze wsparcie do refaktoryzacji, narzędzia jak EMMA, Checkstyle, Findbugs i podobne.
Jeśli społeczność Scali twierdzi, że ich język jest taki superzajebiaszczy, to niech to udowodnią: Niech na przykład za jego pomocą napiszą pluginy do IDE dorównujące lub przewyższające jakością pluginy do Javy.