Wygodny cache
Czasem tworząc fragment kodu już w momencie jego wklepywania do edytora zdajemy sobie sprawę, że wykonywane przezeń operacje będą kosztowne (mam tu na myśli wydajność...). Ja zwykle w tym momencie myślę „oo wyniki działania tej metody warto będzie przechowywać w cache’u – dodam go za chwilę”. Często jednak ta chwila nie nadchodzi nigdy...
Natrafiłem na projekt (no dobra projekt to trochę za obszerna nazwa na jedną klasę), który w wielu przypadkach upraszcza obsługę cache’owania wyników. Projekt nazywa się cache-decorator i jak nie trudno się domyślić implementacja opiera się wzorcu projektowym dekorator. Instalacja sprowadza się do umieszczenia pliku cache_dekorator.rb w takimi miejscu, z którego będzie go można pobrać za pomocą require (w przypadku aplikacji railsowej może to być katalog np. lib/)
Przykład wykorzystania cache-decoratora:
-
require "cache_decorator"
-
-
time = CacheDecorator.new(Time)
-
# dekorujemy klase Time i wskazujemy że chcemy cachowac
-
# wyniki metody now(), timeout dla cache'u to 5 sekund
-
time.cache(:now, 5)
-
-
# sprawdzamy dzialanie
-
10.times do
-
puts time.now
-
sleep(1)
-
end
-
-
#wynik:
-
Thu Aug 03 22:24:39 środkowoeuropejski czas letni 2006
-
Thu Aug 03 22:24:39 środkowoeuropejski czas letni 2006
-
Thu Aug 03 22:24:39 środkowoeuropejski czas letni 2006
-
Thu Aug 03 22:24:39 środkowoeuropejski czas letni 2006
-
Thu Aug 03 22:24:39 środkowoeuropejski czas letni 2006
-
Thu Aug 03 22:24:39 środkowoeuropejski czas letni 2006
-
Thu Aug 03 22:24:45 środkowoeuropejski czas letni 2006
-
Thu Aug 03 22:24:45 środkowoeuropejski czas letni 2006
-
Thu Aug 03 22:24:45 środkowoeuropejski czas letni 2006
-
Thu Aug 03 22:24:45 środkowoeuropejski czas letni 2006
Ponieważ w Ruby mamy domknięcia (closures), możemy cache_decorator wykorzystać jeszcze ciekawiej:
-
# fragment kosztownego kodu zamieniam na lambde
-
# (tu jest to znow niesmiertelny czasomierz)
-
tick = lambda { Time.now }
-
-
# tworze cache do tej procedury
-
cached_tick = CacheDecorator.new(tick)
-
# metoda cachowana to oczywiscie call
-
cached_tick.cache(:call, 3)
-
-
# no i sprawdzenie w dzialaniu
-
6.times do
-
puts cached_tick.call
-
sleep(1)
-
end
-
-
# wynik to:
-
Thu Aug 03 22:43:28 środkowoeuropejski czas letni 2006
-
Thu Aug 03 22:43:28 środkowoeuropejski czas letni 2006
-
Thu Aug 03 22:43:28 środkowoeuropejski czas letni 2006
-
Thu Aug 03 22:43:28 środkowoeuropejski czas letni 2006
-
Thu Aug 03 22:43:32 środkowoeuropejski czas letni 2006
-
Thu Aug 03 22:43:32 środkowoeuropejski czas letni 2006
Działanie tego dekoratora sprowadza się do owinięcia cache’owanego obiektu i filtrowaniu komunikatów jakie do niego wysyła klient (przypominam ze w Rubym wywołanie metody to tak naprawdę wysłanie komunikatu do obiektu).
Jeżeli dla danego komunikatu i zestawu jego argumentów istnieje już rezultat w wewnętrznym cache’u ten rezultat jest zwracany, jeżeli nie, komunikat jest przekazany do owiniętego obiektu i uzyskany wynik po zapisaniu w cache’u jest zwracany klientowi.
Ciekawie z poznawczego punktu widzenia jest rzucić okiem pod maskę tego dekoratora. Autor sprytnie zaimplementował metodę ‘cache’. Zamiast sprawdzania za każdym razem jaki komunikat jest wysyłany do udekorowanego obiektu (co mogłoby być zrobione w metodzie method_missing) tworzona jest dynamicznie metoda która stanowi wrapper do metody właściwej (zawartej w owiniętym obiekcie). Opis może być zagmatwany wiec rzut oka na kod (dodałem kilka komentarzy dla jasności):
-
def cache(m, timeout=nil)
-
# idiom pozwalajacy na tworzenie singleton method(s)
-
(class <<self; self; end).class_eval do
-
# no to definiujemy metode o nazwie takiej jak cachowana metoda
-
define_method(m) do |*args|
-
invalidate_methods_from_invalidating_method m
-
@cached_items[m] ||= {}
-
cache = @cached_items[m][args.inspect]
-
if cache.nil? or cache.expired?
-
# @obj.send(m, *args) to wywolanie metody wlasciwiej - tylko jezeli
-
# nie ma wyniku w cache'u albo wynik sie przedawnil
-
cache = CachedItem.new(@obj.send(m, *args),
-
timeout.nil? ? nil : Time.now + timeout)
-
end
-
@cached_items[m][args.inspect] = cache
-
cache.data
-
end
-
end
-
end
Na zakończnie dodam tylko, że dla mnie, człowieka karmionego ‘dynamiką’ zawartą w java.lang.Reflection takie podejście jak powyżej stanowi powiem świeżego powietrza i namacalny dowód na istnienie ruby-way.
Miłego cachowania ;)









Adam Hoscilo said,
sierpień 3, 2006 @ 23:42
Moze w kilka slow pod katem Rails. RoR ma dosc przyjemy wbudowany system cache’owania : http://ap.rubyonrails.com/classes/ActionController/Caching.html
i w aplikacjach Railsowych polecalbym korzystanie wlasnie z niego (chyba, ze komus fragment’s caching nie wystarcza).
daniel said,
sierpień 3, 2006 @ 23:45
Masz racje, to co tu starałem sie pokazać, to rozwiazanie troche bardziej uniwersalne - do wykorzystania np przy w głebszych warstwach aplikacji.
Adam Hoscilo said,
sierpień 3, 2006 @ 23:55
Czesc osob mysli Ruby w kontekscie Rails i to mogloby ich troche “zbic z tropu”.
Ogolnie w tym rozwiazaniu widac dynamike i sile Ruby’ego
ciukes said,
sierpień 4, 2006 @ 00:40
Rozumiem twój zachwyt Ruby’m ale przesadzasz demonizując Jave.
Po pierwsze, od wersji 1.3 jest klasa:
http://java.sun.com/j2se/1.5.0/docs/api/java/lang/reflect/Proxy.html
dzięki ktorej możesz przechwytywać wywołania metod. Naprędce skleciłem kod który pokazuje że nie jest to żadna masakra:
http://www.ciukes.com/jarmark.org/code/CacheExample.java
Kod ustawiający cache jest krótki i prosty. Przykladowy rezultat jest zgodny z oczekiwanym.
Po drugie od czasu wprowadzenia do świata Javy pojecia aspektów, nikt nie przejmuje się cache bo zawsze go można dokleić ;)
To pokazuje, że Java nie jest takim betonem za jaki sie ją uważa.
daniel said,
sierpień 4, 2006 @ 08:36
Hmm, nie pisałem że tego sie nie da w Javie zaimplementować to po pierwsze. Dobrze wiesz, że akurat dekorator jako wzorzec jest w Javie bardzo intensywnie uzywany (choćby w samym API - wszystkie bufory IO), chodzilo mi raczej o zupełnie inne podejscie - zamiast analizować co się dzieje tworzy \’dedykowaną\’ metodę o odpowiednio zmodyfikowanej zawartosci. Druga spawa to wykorzystanie domknięć (closures) których efekt działania można prosto objąć cache\’m.
lopex said,
sierpień 4, 2006 @ 17:57
Może się czepiam, ale nie rozumiem dlaczego cały czas wszyscy mylą domknięcia: http://en.wikipedia.org/wiki/Closure_%28computer_science%29 z klauzulami: http://en.wikipedia.org/wiki/Clause. Wreszcie, w przykładzie z lambda { Time.now } domknięcie nie jest nigdzie wykorzystywane, więc wyjdzie na to samo jak przewrapujesz to w najzwyklejszej metodzie ;)
daniel said,
sierpień 4, 2006 @ 18:26
Rzeczywiście masz absolutną rację z tym nazewnictwem, tak się jakoś zafiksowałem, że nie sprawdzałem nigdy- wiec jeszcze raz wielkimil literami CLOSURE - to domknięcie. Chyba błąd bierze sie z fałszywego podbieństwa w brzmieniu do klauzla… Dzieki za zwórcenie uwagi, poprawiłem już błąd w tekscie.
Co do przykładu z Time - absolutnie sie z Tobą zgdzam, problem troche w tym jak wykombinować przykład ktory będzie prosty (a może nawet uproszczony) tak, żeby moć sie skupić na mięsku własciwym… Umówmy się sensowność cachowania wyników Time.now jest taka jak posiadanie zegarka który zawsze wskazuje jedną godzine… (btw: ponoć rosyjskie zegarki są najszybsze :))
lopex said,
sierpień 5, 2006 @ 00:31
Tak mi się jescze przypomniało, w tych przykładach tego może nie widać bo cachinghandlera w Javie można było wyrzucić do osobnej klasy, ale są przypadki gdy widać słabostki Javy. Głównie chodzi o intersekcję i modyfikację eigenklass czy nawet metaklas - tutaj nie pomoże żaden aspectj, janino, asm itp., bo po prostu jvm na to nie pozwala. Druga sprawa to właśnie domknięcia - można je w Javie w pewnym zakresie symulować za pomocą klas anonimowych, ale to będzie tylko namiastka bo jvm jest maszyną stosową i niestety zmienne lokalne będą musiały być final. A to że przy okazji zajmie to 10x więcej kodu to jest już inna sprawa ;)
Nazgul's Weblog said,
marzec 24, 2007 @ 20:15
Mechanizm cachowania w RoR…
Cache w Ruby on Rails
Czasami tworząc aplikacje internetowe, wymagające dużej wydajności, okazuje się, że wykonywanie jakiegoś kodu od nowa dla każdego odwiedzającego jest nieekonomiczne. Można temu zaradzić umieszczając taki kod w cache (…