[GiM logo] gim.org.pl is down || odświeżony jogger (v.0.4) GiMa

D singleton Dlaczego taki tytuł? Bo singleton, jest powszechnie krytykowanym wzorcem projektowym. Dzieje się tak głównie z powodu początkująch, którzy za wszelką cenę starają się by ich programy były 'obiektowe', a przecież wiadomo, że w programowaniu obiektowym zmienne globalne są ZŁE. Niewiele mysląc młodzi adepci sztuki, opakowują zmienne globalne w singleton i są z siebie zadowoleni ;).
"Singleton nie jest odpowiedzią na zmienne globalne, odpowiedzią jest 'Na jaką cholerę Ci zmienne globalne?'"
Na początek pokażę więc, co można zrobić ze zmiennymi globalnymi w D, a dalej dla tych którzy wiedzą do czego MOŻNA i NALEŻY używać singletona, różne sposoby implementacji w D.

Wskazówka dla osób nowych, to jest któryś post z kolei na temat programowania w D, pozostałe znajdziesz w kategorii programowanie.
Mały spis treści, dla tych którzy zdążyli już przeczytać:

  • Ex1 - moduły jako opakowanie dla zmiennych globalnych
  • Ex2 - singleton - prosty sposób, XKCDRnd
  • Ex3 - singleton z użyciem mixinów szablonów
  • Ex4 - dziedziczenie po szablonie singletonu
  • Ex5 - singleton a wielowątkowość
  • Ex6 - dziedziczenie singletonów

Przykład 1

W D w 90% przypadków, singleton jako klasa nie jest potrzebny. Jeśli na prawdę nie wiesz co zrobić ze zmiennymi globalnymi, lub masz w kodzie metody, które chciałbyś mieć dostępne z dowolnego miejsca w kodzie, najprostszym rozwiązaniem jest wrzucenie tego wszystkiego w jeden moduł.
D ma jasny podział na moduły i klasy. Moduł odpowiada pojedyńczemu plikowi źródłowemu. Moduł ma jedną statycznie alokowaną instancję.
W pierwszym przykładzie mamy dwa pliki: IdManager.d - w którym będzie metoda do której chcemy mieć dostęp, oraz Ex1main.d, który będzie korzystał z tego modułu.

  1.  // zawartość IdManager.d
  2.  module IdManager;
  3.  
  4.  private {
  5.      uint[char[]] identifiers;
  6.      uint idCounter = 0x4d6947;
  7.  
  8.      uint nextCounter() {
  9.          asm {
  10.              mov EAX,idCounter;
  11.              shr EAX,1;
  12.              jnc NOXOR;
  13.              xor EAX,0xa3000000;
  14.  NOXOR:      mov idCounter,EAX;
  15.          }
  16.      }
  17.  }
  18.  uint getIdent(char[] name)
  19.  {
  20.      if ((name in identifiers) is null)
  21.          identifiers[name] = nextCounter;
  22.  
  23.      return identifiers[name];
  24.  }
  25.  
  26.  // zawartość Ex1main.d
  27.  import tango.io.Stdout;
  28.  static import IdManager;
  29.  
  30.  void main()
  31.  {
  32.      Stdout.formatln ("Kowalski: {:x8}", IdManager.getIdent("Kowalski"));
  33.      Stdout.formatln ("Nowak:    {:x8}", IdManager.getIdent("Nowak"));
  34.      Stdout.formatln ("Kowalski: {:x8}", IdManager.getIdent("Kowalski"));
  35.  }

W module IdManager mamy dwie zmienne prywatne, oraz prywatną funkcję do zmiany wartości licznika. Podobnie jak w przypadku klas, elementy prywatne modułu mogą być używane jedynie w samym module. Dodatkowo jest publicznie dostępna funkcja, która, kolejnym podanym ciągom przypisuje identyfikatory i zapamiętuje je.
W pliku, gdzie korzystamy z modułu, robimy statyczny import, dzięki czemu, wszystkie odwołania do funkcji getIdent, będą musiały być poprzedzone nazwą modułu.

Oczywiście kod jest mało sensowny, ma tylko prezentować, jak można to zrobić: wrzucamy rzeczy do osobnego modułu, co niepotrzebne opakowujemy w private, w modułach które będą korzystać robimy statyczny import.
Acha, oczywiście wujek Dobra Rada, przypomina, że zmienne globalne są _ZŁE_ ;) tak więc stosując takie rozwiązanie miej świadomość konsekwencji.
Jeżeli szukałeś sposobu na opakowanie zmiennych globalnych, to NIE CZYTAJ DALEJ, bo najprawdopodobniej staniesz się jedną z tych osób, która przyczynia się do krytyki Singletonów :P

Przykład 2

Kolejne przykłady będą różnymi sposobami implementacji Singletonu.
Tuż przed publikacją tego artykułu trafiłem na taki obrazek, wzięty stąd.
W ogólności od singletonu oczekujemy leniwej instancjacji no i oczywiście najwyżej jednej instancji w pamięci. Tradycyjnie najpierw kod, potem komentarze.

  1.  // zawartość XKCDRnd.d
  2.  module XKCDRnd;
  3.  
  4.  Menazeria getInstance()
  5.  {
  6.      if (Menazeria._inst is null)
  7.          Menazeria._inst = new Menazeria;
  8.      return Menazeria._inst;
  9.  }
  10.  
  11.  final class Menazeria
  12.  {
  13.      private static typeof(this) _inst;
  14.      private this() {}
  15.      // dalsze zmienne i metody
  16.      uint random() { return 4; }
  17.  }
  18.  
  19.  // zawartość Ex2main.d
  20.  import tango.io.Stdout;
  21.  static import XKCDRnd;
  22.  
  23.  void main()
  24.  {
  25.      auto a = XKCDRnd.getInstance, b = XKCDRnd.getInstance;
  26.      Stdout (a.random).newline;
  27.      Stdout (XKCDRnd.getInstance.random).newline;
  28.      assert (a != b, "Wszystko ok :>");
  29.  }

Kod powinien być dosyć jasny. Klasa Menazeria zawiera statyczne pole typu Menazeria (typeof(this)), które będzie przechowywać instancję naszego obiektu, prywatny konstruktor, żeby klasy nie można było stworzyć 'z zewnątrz', oraz metodę, którą nasz Singleton udostępnia. Dodatkowo całą klasę możnaby uczynić prywatną. Klasa ma typ składowania final, po klasach final nie można dziedziczyć.
Moduł jest importowany statycznie, by wymusić podawanie pełnej nazwy i zapobiec ewentualnym konfliktom nazw. Widać, że obiekt klasy Menazeria jest tworzony dopiero przy pierwszym wywołaniu funkcji getInstance() (leniwa instancjacja), kolejne jej wywołania zwracają referencję, do zaalokowanego wcześniej obiektu.

Przykład 3

Oczywiście pisanie za każdym razem singletona, może być dość nudne, a raczej nikt się nie zgodzi, by do języka włączyć np. słówko singleton, bo to zupełnie bez sensu. Stworzymy więc szablon, następnie skorzystamy z czegoś co się nazywa mixiny szablonów. Mixiny szablonów, pozwalają na umieszczenie treści szablonu w podanym miejscu (a ponieważ szablony moga być parametryzowane, daje to ułatwiające życie możliwości). Mixiny szablonów są ewaluowane w miejscu w którym pojawia się mixin. Dzięki mixinom, można (oczywiście między innymi super możliwościami ;)) 'przykrywać' zmienne, co normalnie nie jest dozwolone.

  1.  // zawartość Ex3Man.d
  2.  module Ex3Man;
  3.  
  4.  //dummy plug
  5.  class Font
  6.  {
  7.      private char[] name;
  8.      this(char[] a) { name = a; }
  9.      char[] toString() { return "<font:"~name~">"; }
  10.  }
  11.  
  12.  template MeyersSingletonMix(T)
  13.  {
  14.      private static T _inst;
  15.      private this() {}
  16.      static this()
  17.      {
  18.          _inst = new T;
  19.      }
  20.      public static T inst()
  21.      {
  22.          return _inst;
  23.      }
  24.  }
  25.  
  26.  class FontManager
  27.  {
  28.      mixin MeyersSingletonMix!(FontManager);
  29.  
  30.      private Font[char[]] loadedFonts;
  31.  
  32.      Font loadFont(char[] name)
  33.      {
  34.          if ((name in loadedFonts) is null)
  35.              loadedFonts[name] = new Font(name);
  36.          return loadedFonts[name];
  37.      }
  38.  }
  39.  
  40.  // zawartość Ex3main.d
  41.  import tango.io.Stdout;
  42.  import Ex3Man;
  43.  
  44.  void main()
  45.  {
  46.      Stdout (FontManager.inst.loadFont("verdana")).newline;
  47.      Stdout (FontManager.inst.loadFont("helvetica")).newline;
  48.      Stdout (FontManager.inst.loadFont("arial")).newline;
  49.  }

Po kolei, co się tutaj dzieje. Klasa Font to klasa pomocnicza, która służy do ładowania i przechowywania fontów. Fonty będą przechowywane przez singleton FontManager.
Na początku klasy jest umieszczony mixin, który jest parametryzowany typem FontManager, spójrzmy więc na kod szablonu. Nasza klasa (eee, to nie była aluzja żadna) FontManager będzie więc zawierała:

  • statyczne prywatne pole typu FontManager — _inst,
  • prywatny konstruktor oraz statyczny konstruktor,
  • publiczną metodę inst() zwracającą pole _inst.

Statyczne konstruktory klas są wywoływane po statycznych konstruktorach modułów, a przed funkcją main (Oczywiście statyczny konstruktor jest wywoływany dla całej klasy, nie dla poszczególnych obiektów).
Widać więc, że sam singleton zostanie stworzony zaraz na początku programu, a nie przy pierwszym wywołaniu metody inst(), sam obiekt nie jest więc leniwie tworzony. Ma to tę przewagę, że jeśli stworzymy statyczny destruktor, będziemy mieć pewność, że (i w którym dokładnie momencie) zostanie on wywołany.
Nasz singleton udostępnia metodę loadFont, która tworzy obiekty dla kolejnych czcionek i zapamiętuje je.

Przykład 4

Alternatywą dla poprzedniego rozwiązania, jest stworzenie szablonu klasy (pamiętając, o tym, że w D jeśli w deklaracji szablonu znajduje się dokładnie jeden element (klasa, funkcja, interfejs, itd.), to zapis szablonu można uprościć), następnie dziedziczenie po pewnej instancji tego szablonu.

  1.  // zawartość Ex4Man.d
  2.  module Ex4Man;
  3.  import tango.math.Random;
  4.  
  5.  class MeyersSingleton(T)
  6.  {
  7.      protected static T _inst;
  8.      static this()
  9.      {
  10.          _inst = new T;
  11.      }
  12.      public static T getInstance()
  13.      {
  14.          return _inst;
  15.      }
  16.  }
  17.  
  18.  class ManagersManager : MeyersSingleton!(ManagersManager)
  19.  {
  20.      private {
  21.          int[][] data;
  22.          this() { }
  23.          static ~this() { delete data; }
  24.      }
  25.      int[][] getData()
  26.      {
  27.          if (data is null)
  28.              // tablica dwuwymiarowa
  29.              data = new int[][](3,4);
  30.          foreach (a,ref b; data)
  31.              foreach (c,ref d; b)
  32.                  d = Random.shared.next(100);
  33.          return data;
  34.      }
  35.  }
  36.  
  37.  // zawartość Ex4main.d
  38.  import tango.io.Stdout;
  39.  import Ex4Man;
  40.  
  41.  void main()
  42.  {
  43.      auto x = ManagersManager.getInstance();
  44.      auto y = ManagersManager.getInstance();
  45.  
  46.      foreach (row; x.getData)
  47.      {
  48.          foreach (b; row)
  49.              Stdout.format ("{:d3} ", b);
  50.          Stdout.newline;
  51.      }
  52.      Stdout.newline;
  53.      foreach (row; x.getData)
  54.      {
  55.          foreach (b; row)
  56.              Stdout.format ("{:d3} ", b);
  57.          Stdout.newline;
  58.      }
  59.  }

Ponieważ sam singleton, podobnie jak w poprzednim przykładzie, jest tworzony w statycznym konstruktorze (patrz: szablon), w klasie dodałem statyczny destruktor który zwalnia pamięć, zaalokowaną w metodzie getData. Przykład nie jest jakiś specjalny, powinien być prostszy do zrozumienia niż poprzedni. Warto zwrócić, że na wszelki wypadek, typ dostępu zmiennej _inst jest zmieniony z prywatnego na protected (bo wcześniej był mixin, który wstawiał całą treść, a tu mamy dziedziczenie).

Przykład 5

Dwa poprzednie przykłady zamiast lewniwej instancjacji, tworzyły obiekt w statycznym konstruktorze. Ma to tę przewagę nad przykładem drugim (XKCDRandom :)), że nie trzeba się było martwić czy nie zostanie stworzonych kilka instancji szablonu w przypadku aplikacji wielowątkowej. Z kolei sam dostęp do danych w obu poprzednich przykładach nie jest thread-safe.
W następnym przykładzie pokażę w jaki sposób można stworzyć singleton, którego tworzenie będzie leniwa i bezpieczna dla wątków (thread-safe), i w którym sekcję krytyczną opakujemy, by również była bezpieczna dla wątków. Klasa Image jest klasą pomocniczą, tylko dla przykładu, podobnie jak wcześniej Font

  1.  // zawartość Ex5Man.d
  2.  module Ex5Man;
  3.  import tango.util.log.Trace;
  4.  //dummy plug
  5.  class Image
  6.  {
  7.      private char[] name;
  8.      this(char[] a) { name = a; }
  9.      char[] toString() { return "<img:"~name~">"; }
  10.  }
  11.  
  12.  
  13.  class ThreadSafeSingleton(T)
  14.  {
  15.      protected static T _inst;
  16.  
  17.      public static synchronized T getInstance()
  18.      {
  19.          if (!_inst)
  20.          {
  21.              Trace.formatln("creating new instance");
  22.              _inst = new T;
  23.          }
  24.          return _inst;
  25.      }
  26.  }
  27.  
  28.  class SuperManager : ThreadSafeSingleton!(SuperManager)
  29.  {
  30.      private {
  31.          Image[char[]] images;
  32.          this() { }
  33.      }
  34.      Image imgLoad(char[] name)
  35.      {
  36.          synchronized (SuperManager.classinfo) {
  37.              Trace.formatln("adding element {}", name);
  38.              if ((name in images) is null)
  39.                  images[name] = new Image(name);
  40.          }
  41.          return images[name];
  42.      }
  43.  }
  44.  

Widać, że nasz singleton SuperManager dziedziczy po instancji szablonu ThreadSafeSingleton (parametryzowanej klasą SuperManager). Przeanalizujmy więc najpierw szablon klasy ThreadSafeSingleton. Widać, że szablon nie ma już statycznego konstruktora, zamiast tego instancja obiektu jest leniwie (przy pierwszym dostępie tworzona w metodzie getInstance. Ponadto metoda ta została opatrzona atrybutem synchronized.
Jak pisałem (? chyba w którymś z wcześniejszych wpisów, nie jestem pewien), atrybut synchronized stosowany przy metodzie działa per-class-object (co oznacza, że do metody danej instancji obiektu, może się odwoływać, tylko jeden wątek w danym czasie ), ponieważ metoda ta jest statyczna, to nie jest to problemem i powinno dobrze działać.
W metodzie imgLoad natomiast, pokazana jest konstrukcja, która powinna być stosowana, jeżeli do danego fragmentu kodu (np. używającego jakiejś funkcji z C, która nie jest thread-safe), niezależnie od ilości obiektów danej klasy, powinien mieć dostęp tylko jeden wątek w danym czasie.
Co prawda, tutaj mamy pewność, że obiekt bedzie miał tylko jedną instancję (no w końcu to singleton, nie? :>), więc możnaby podobnie jak przy metodzie getInstance użyć słowa synchronized przy samej nazwie metody, jednakże chciałem pokazać samą konstrukcję :)

  1.  // zawartość Ex5main.d
  2.  import tango.io.Stdout;
  3.  import tango.util.log.Trace;
  4.  import tango.core.Thread;
  5.  import Ex5Man;
  6.  
  7.  void func1()
  8.  {
  9.      char[] blah = "blurp".dup;
  10.      //foreach (i; 32..127)
  11.      Thread.yield;
  12.      for (int i=32; i<128; i++)
  13.      {
  14.          blah[3] = i;
  15.          for (int j=32; j<128; j++)
  16.              blah[4] = j;
  17.          Trace.formatln("loading element {}", blah);
  18.          SuperManager.getInstance.imgLoad(blah);
  19.      }
  20.      Thread.yield;
  21.  }
  22.  
  23.  void func2()
  24.  {
  25.      char[] blah = "zxcvb".dup;
  26.      for (int i=127; i>31; i--)
  27.      {
  28.          blah[3] = i;
  29.          for (int j=127; j>31; j--)
  30.              blah[4] = j;
  31.          Trace.formatln("loading element {}", blah);
  32.          SuperManager.getInstance.imgLoad(blah);
  33.      }
  34.      Thread.yield;
  35.  }
  36.  
  37.  
  38.  void main()
  39.  {
  40.      Thread a = new Thread(&func1);
  41.      Thread b = new Thread(&func2);
  42.  
  43.      Trace.formatln ("Starting threads");
  44.      a.start;
  45.      b.start;
  46.      thread_joinAll;
  47.      Trace.formatln ("Threads stopped");
  48.  }

Na koniec został sam kod, który coś z tym robi. Kod jest jasny i prosty. Jedynie kilka uwag:

  • po stringach w funkcjach func1, func2, jest .dup, bo jak już kiedyś pisałem, literały znaków są read-only, więc musimy sobie zrobić ich kopię by móc je modyfikować (pod Windowsem ten kod może działać bez .dup, jednak w D 2.0, literały znaków mają być na obu platformach read-only, więc lepiej nie nabierać złych nawyków),
  • do wypisywania użyte jest Trace, bo wyjście przy pomocy Stdout, nie jest bezpieczne dla wątków,
  • te 'fory' tam są brzydkie, w D2.0 jest konstrukcja foreach(i; start..stop), mi osobiście przypominająca pythonowy xrange(),

Przykład 6

Uch już mi się nie chce, ale na koniec jeszcze jeden przykład, żeby zebrać wszystko do kupy.
No więc, niektórych dziwnych ludzi nachodzą takie dziwne pomysły, że chcieliby mieć możliwość dziedziczenia po singletonie. Oczywiście zamysł jest taki, by klasa dziedzicząca, również była singletonem.
Wykorzystamy sposób z przykładu trzeciego z mixinami. Szablon zmienimy by umożliwiał leniwą instancjację. Dalszy opis poniżej.

  1.  // zawartość Ex6Man
  2.  module Ex6Man;
  3.  import tango.io.Stdout;
  4.  
  5.  template MeyersSingletonMix(T)
  6.  {
  7.      private static T _inst;
  8.      static T getInstance()
  9.      {
  10.          if (_inst is null)
  11.              _inst = new T;
  12.          return _inst;
  13.      }
  14.  }
  15.  
  16.  private class Menazeria
  17.  {
  18.      mixin MeyersSingletonMix!(Menazeria);
  19.      private this() {
  20.          Stdout ("men\n");
  21.      }
  22.      char[] planet() { return "Mars"; }
  23.  }
  24.  
  25.  private class Womenazeria : Menazeria
  26.  {
  27.      mixin MeyersSingletonMix!(Womenazeria);
  28.      private this() {
  29.          Stdout ("women").newline;
  30.          super();
  31.      }
  32.      override char[] planet() { return "Venus"~Menazeria.planet; }
  33.  }
  34.  import Ex6Man;
  35.  import tango.io.Stdout;
  36.  
  37.  void main()
  38.  {
  39.      auto a = Menazeria.getInstance;
  40.      auto b = Womenazeria.getInstance;
  41.      Stdout (a.planet).newline;
  42.      Stdout (b.planet).newline;
  43.  }

Tworzymy dwie klasy, Womenazeria dziedziczy po Menazerii.

  • Istotnym jest, że w obu klasach wrzucamy mixin, gdyby nie to, w klasie Womenazeria, byłaby metoda getInstance, ale odziedziczona z Menazerii, czyli zwracająca typ Menazeria. Oczywiście metodę getInstance, można nadpisać, metodą z inną sygnaturą ponieważ jest statyczna.
    Gdyby jednak tak nie było, to nadal byłoby możliwe nadpisanie tej metody, gdyż D wspiera tzw. kowariantne typy 'zwracane' (covariant return types, podobnie jak w Javie od 1.5 (hi lami :>). Po szczegóły klikać wiki, albo ewentualnie dokumentację digitalmars,
  • W obu klasach jest mteoda planet, która zwraca jakąś tam nazwę. W klasie Womenazeria jest dodany jest atrybut override, który bardzo usprawnia pisanie kodu :). Atrybut ten powoduje, że gdyby sygnatura metody zmieniła się w klasie bazowej, kompilator poinformuje, że w klasie bazowej nie ma takiej metody. (W javie 1.5 jest @Override, a w C# (który przecież jest zrzyną jacy ;>) jest override.
    W klasie Womenazeria jest wywołanie metody klasy bazowej – Menazeria.planet, Menazeria możnaby w tym miejscu zastąpić słowem super, jednak mądrzejsi ode mnie, twierdzą, że tak jest bezpieczniej. (Podobnie jak to ma miejsce ze słówkiem outer, można robić kombinacje w stylu super.super.super.costam(..);),

Oczywiście nie jest to jakiś super sposób, może kiedyś będzie dało się to zrealizować w jakiś über-hax0rish sposób, przy użyciu cech (traits) i funkcji wykonywanych w czasie kompilacji. Chyba nawet możnaby na chwilę obecną, ale jestem zbyt leniwy, żeby próbować.

I to już (?) koniec przedstawienia na dzisiaj. Jeśli dotarłeś(/aś?) do końca, to znaczy że jesteś masochistą najprawdopodoniej zainteresują Cię pozostałe wpisy z kategorii programowanie
See you space cowboy...

Jeszcze implementacja h3r3tica wycięta z komentarza poniżej:

  1.  T Singleton(T)() {
  2.      static T singletonInstance;
  3.  
  4.      if (singletonInstance is null) {
  5.          synchronized (T.classinfo) {
  6.              if (singletonInstance is null) {
  7.                  singletonInstance = new T;
  8.                  static if (is (typeof (singletonInstance.initialize))) {
  9.                      singletonInstance.initialize();
  10.                  }
  11.              }
  12.          }
  13.      }
  14.      return singletonInstance;
  15.  }
  16.  ----
  17.  class FontManager {
  18.      ...
  19.  }
  20.  
  21.  alias Singleton!(FontManager) fontMngr;
  22.  ...
  23.  fontMngr.loadFont(...);

Dla tych, którzy się nie połapali Singleton jest tutaj szablonem funkcji. alias, tworzy alias na instancję tego szablonu, natomiast samo wywołanie funkcji (a tym samym leniwa instancjacja klasy) następuje dopiero w momentcie wywołania fontMngr.loadFont(..), które można inaczej zapisać jako fontMngr().loadFont(..);
(funkcja fontMngr zwraca obiekt klasy którą szablon jest parametryzowany, w tym przypadku FontManager).
W przykładzie tym ukryta jest jeszcze jedna właściwość o której nie wspominałem, mianowicie jeśli używamy kilka razy szablonu i za każdym razem parametryzujemy go takimi argumentami tego samego typu, to tworzona jest tylko jedna instancja szablonu.
Tak więc szablonu h3r3tica można bez obaw stosować mając kilka klas, które chcielibyśmy uczynić singletonami.

catz: [kom.puterowe] [programowanie w D] [Techblog]
tagz: [D] [D programming language] [dziedziczenie singletonów] [język D] [programowanie D] [singleton] [singleton in d] [singletony] [singletony i wątki] [wątki]
dnia piątek, 23 maj 2008, 222300 by Michał 'GiM' Spadliński

Komentarze:

Proszę wpisy pisane po angielsku komentować również w tym języku.

(Komentarz zmodyfikowany 24.05.2008 o 03:03)

A ja mam taka wersje:

Kod przeniesiony do wpisu


Co do tego slowka 'super', to chodzilo mi o to ze czasem nie dziala bo kompilator marudzi. Na przyklad:

class Foo {
void foo() {}
}

class Bar : Foo {
alias super.foo foo;
}

... nie zadziala. podobnie nie zadziala 'alias typeof(super).foo foo;' :S ... Ale juz:
typeof(super) SuperType;
alias SuperType.foo foo;
... zadziala bez problemu. Bug :P Ale przy overridingu jakiejs funkcji, jak chcesz wywolac wersje z klasy bazowej to jak najbardziej super.foo() jest wygodne i dziala jak trzeba.

Let's kick the beat! Mushrooom huuunting! :P

dnia sobota, 24 maj 2008, 022135 by h3r3tic

@h3: thx za komentarz :) pozwoliłem sobie przeedytować twój komentarz i przerzucić kod do wpisu (mam powyłączane textile dlatego się brzydko sformatował)

Ok, three, two, one let's jam.

dnia sobota, 24 maj 2008, 030529 by GiM

Dawno nie czytałem takiego kodu, trochę to już dziwne dla mnie. Ponieważ nacodzień pracuję w Ruby to najbliżej mi do przykładu z użyciem mixinów. Bardzo

dnia sobota, 24 maj 2008, 121453 by Seban

..tożsamość..:
..meritum..:
..lokum..:
Wpisz kod:code