W tej części kursu omówimy wzorzec singletonu, dynamicznie dodawane metody oraz moduły i miksiny w języku Ruby.

Wzorzec singletonu

Założenia są proste: dana klasa może występować w co najwyżej jednym egzemplarzu.

Stwórzmy dla przykładu klasę implementującą konsolę - taką, jaką znamy choćby z Quake'a. Ponieważ w całym silniku gry nie potrzebujemy więcej, niż jednej konsoli - zastosujemy tutaj wzorzec singletonu:

class Console
  def self.instance
    return @@instance if defined? @@instance
    @@instance = new
  end
  
  private_class_method :new, :allocate
end

c1 = Console.instance
c2 = Console.instance

c1.object_id    # => 22691800
c2.object_id    # => 22691800

Widać tutaj, że aby zrealizować singleton w Rubim, należy:

  • zablokować metody new i allocate - uczynić je prywatnymi, by mogły być wywoływane tylko w odrębie klasy
  • udostępnić metodę, która zwraca (a wcześniej ewentualnie tworzy) instancję naszego singletonu

Oczywiście Ruby nie byłby sobą, gdyby nie pozwolił nam na realizację tego wszystkiego za pomocą paru stuknięć w klawiaturę :-).

require 'singleton'

class Console
  include Singleton
end

c1 = Console.instance
c2 = Console.instance

c1.object_id    # => 22691800
c2.object_id    # => 22691800

Metody singletonu

Nazwa może być nieco myląca, gdyż nie do końca jest związana ze wzorcem singletonu.

Ruby pokazuje swoją dynamikę w przykładzie, w którym rozszerzamy wbudowaną w język klasę:

class Integer
  def odd?
    (self % 2) == 1
  end
end

puts 5.odd?       # => true
puts 6.odd?       # => false
puts 7.odd?       # => true

To nie koniec, można posunąć się nawet dalej. Ruby oferuje nam możliwość dopisania metody do egzemplarza dowolnej klasy. Taką metodę nazywamy metodą singletonu.

class Dog
  def bark
    puts "Woof, woof!"
  end
  
  def method_missing(name)
    puts "Hey, man. I can't #{name}!"
  end
end

Rex, Azor = Dog.new, Dog.new

def Azor.kill
  puts "/me is killing"
end

Rex.kill
Azor.kill
Hey, man. I can't kill!
/me is killing

Metoda method_missing wywoływana jest w momencie, gdy następuje próba wywołania nieistniejącej metody. Więcej takich sztuczek postaram się omówić w kolejnej części tutoriala.

Przestrzenie nazw

...czyli gdy klasa niezbyt dobrze modeluje problem.

Przestrzenie nazw to jedno z dwóch zastosowań modułów (o drugim powiemy za chwilę) w Rubim. Poza umożliwieniem programiście logicznej organizacji struktury projektu, pozwalają dodatkowo uniknąć konfliktów nazw, które tak bardzo potrafią dokuczać w innych językach. O przykłady nietrudno. log(10) może oznaczać zarówno logarytm z dziesięciu, jak i zalogowanie błędu o kodzie 10. Takich niejednoznaczności należy unikać.

Jak?

module MyMath
  # ...
  def self.log(x)
    # ...
  end
end

module CriticalErrorLogger
  def self.log(error_num)
    # ...
  end
end

y = MyMath.log(10)			# Logarytm
CriticalErrorLogger.log(10)		# Logowanie błędu

Pamiętajmy, że konieczne jest poprzedzenie selfem nazwy metody, by zdefiniować metodę modułu, a nie instancji. Metody instancji mają zastosowanie w miksinach, o których dosłownie za chwilę.

Czasem jednak chcemy napisać program czysto matematyczny, a wtedy rzeczą naturalną jest, że sinus to sin(x) a nie Math.sin(x):

puts Math.sin(Math::PI/2)

include Math
puts sin(PI/2)

Widać tutaj, że includowaniu ulegają nie tylko metody, ale także stałe.

require

Chcąc użyć klas i metod z zewnętrznego pliku, konieczne jest użycie metody require (np. require 'complex' - jeśli chcemy użyć w programie liczb zespolonych). Jak widać, nie ma potrzeby pisania rozszerzenia ładowanego pliku.

A ścieżki?

Są, wszystkie dostępne w magicznej zmiennej $:

irb(main):001:0> $:
=> ["c:/ruby/lib/ruby/site_ruby/1.8", "c:/ruby/lib/ruby/site_ruby/1.8/i386-msvcrt", "c:/ruby/lib/ruby/site_ruby", "c:/ru by/lib/ruby/1.8", "c:/ruby/lib/ruby/1.8/i386-mswin32", "."

Tablica (a raczej kobyła) z plikami, które includowaliśmy (wiednie lub bezwiednie) dostępna jest pod magiczną nazwą $"

irb(main):002:0> $"
=> ["rbconfig.rb", "rubygems/rubygems_version.rb", "thread.so", "thread.rb", "rbconfig/datadir.rb", "rubygems/user_inter action.rb", "socket.so", "timeout.rb", "net/protocol.rb", "uri/common.rb", "uri/generic.rb", "uri/ftp.rb", "uri/http.rb" , "uri/https.rb", "uri/ldap.rb", "uri/mailto.rb", "uri.rb", "net/http.rb", "stringio.so", "yaml/error.rb", "syck.so", "y aml/ypath.rb", "yaml/basenode.rb", "yaml/syck.rb", "yaml/tag.rb", "yaml/stream.rb", "yaml/constants.rb", "rational.rb", "date/format.rb", "date.rb", "yaml/rubytypes.rb", "yaml/types.rb", "yaml.rb", "zlib.so", "rubygems/remote_fetcher.rb", " forwardable.rb", "digest.so", "digest.rb", "digest/sha2.rb", "parsedate.rb", "time.rb", "rubygems/source_index.rb", "rub ygems/version.rb", "rubygems/specification.rb", "openssl.so", "openssl/bn.rb", "openssl/cipher.rb", "openssl/digest.rb", "openssl/buffering.rb", "fcntl.so", "openssl/ssl.rb", "openssl/x509.rb", "openssl.rb", "rubygems/gem_openssl.rb", "ruby gems/security.rb", "rubygems/custom_require.rb", "rubygems.rb", "ubygems.rb", "e2mmap.rb", "irb/init.rb", "irb/workspace .rb", "irb/context.rb", "irb/extend-command.rb", "irb/output-method.rb", "irb/notifier.rb", "irb/slex.rb", "irb/ruby-tok en.rb", "irb/ruby-lex.rb", "readline.so", "irb/input-method.rb", "irb/locale.rb", "irb.rb"]

Ups...

Metoda require powoduje załadowanie pliku docelowego tylko raz. Niestety, przed nieopatrznym powtórzeniem procesu Ruby broni się niezbyt mądrze.

irb(main):001:0> require 'mathn'
=> true
irb(main):002:0> require 'mathn'
=> false
irb(main):003:0> require 'C:/ruby/lib/ruby/1.8/mathn.rb'
C:/ruby/lib/ruby/1.8/mathn.rb:118: warning: already initialized constant Unify
C:/ruby/lib/ruby/1.8/mathn.rb:306: warning: already initialized constant Unify
=> true

W tym przypadku wygląda to niewinnie, ale bardzo łatwo można sobie zrobić tym krzywdę. Prawdopodobnie zostanie to poprawione w przyszłych wersjach, ale póki co - beware!

Miksiny

Drugie zastosowanie modułów. Wspaniała możliwość będąca doskonałym połączeniem zalet dziedziczenia wielobazowego (znanego chociażby z C++) z przejrzystością i jednoznacznością definicji klas.

Prześledźmy poniższy przykład:

module Witness
  def create_witness
    @witness = "Been there. Seen it. Got the scars."
  end
  
  def has_witness?
    (defined? @witness) ? true : false
  end
end

class Accident
  include Witness
  
  def to_s
    "An accident with " + (self.has_witness? ? 
            "a witness saying \'#{@witness}\'" : "no witness")
  end
end

acc1, acc2 = Accident.new, Accident.new
acc2.create_witness

puts acc1, acc2
An accident with no witness
An accident with a witness saying 'Been there. Seen it. Got the scars.'

Jak to wszystko funkcjonuje?

  • moduły nigdy nie mają instancji - mogą co najwyżej zostać włączone do klasy; wywołać można jedynie metody modułu (np. Math.sin(0)
  • argumentem metody include jest zawsze nazwa widocznego z danego miejsca modułu; jeśli moduł nie jest widoczny, należy załadować odpowiedni plik za pomocą metody require
  • klasa, do której dołączany jest moduł, przejmuje także jego zmienne instancji, modułu oraz stałe

Pamiętać należy o kolejności poszukiwania metody w momencie wywołania:

  1. metody oryginalnej klasy
  2. metody wmiksowane
  3. metody przodka
  4. metody wmiksowane w przodka

Oczywiście w momencie znalezienia metody, jest ona wywoływana, a samo przeszukiwanie przerywane.

Rubinowe miksiny

Ruby dostarcza parę ciekawych modułów, które wmiksowane są w wiele standardowych klas. Tylko czekają, aż wmiksujemy je i my :). Na przykładzie omówimy Comparable i Enumerable.

Enumerable

Zasada jest prosta. Implementujemy w naszej klasie metodę each, a Ruby zajmuje się resztą. Znaczy się: dopisuje za nas takie metody, jak all?, any?, map, min, sort itp. (ri Enumerable!).

Dla przykładu zaimplementujmy kolekcję, która zawiera ciągi kropek. Ilość kropek kolejnych elementów jest równa wartości kolejnych elementów tablicy podanej do konstruktora.

class Dotter
  include Enumerable
  
  def initialize(val)
    @collection = val.to_a
  end
  
  def each
    @collection.each { |e| yield '.'*e }
  end
  
end

d1 = Dotter.new(2..7)
# ..
# ...
# ....
# .....
# ......
# .......

d2 = Dotter.new([5, 4, 6, 1, 7, 3])
# .....
# ....
# ......
# .
# .......
# ...


d1.all? { |v| v.length > 8} # false
d2.any? { |v| v.length%2}   # true
d2.include?('..')           # false
d2.max                      # "......."
d2.min                      # "."
d2.sort                     # [".", "...", "....", ".....", 
                            # "......", "......."]

Comparable

Użycie tego modułu jest równie proste. Implementujemy metodę <=>, a w efekcie otrzymujemy metody porównania oraz between?.

Dla przypomnienia, metoda <=> zwraca -1, gdy pierwszy element jest mniejszy od drugiego, 0 - gdy oba elementy są równe oraz 1, gdy pierwszy element jest większy od drugiego.

Czas na praktykę. Weźmy przykład z dokumentacji Rubiego. Poza tym, że rozmiar jednak ma znaczenie, jeszcze coś innego jest w nim interesujące.

class SizeMatters
  include Comparable
  
  attr :str
  def <=>(anOther)
    str.size <=> anOther.str.size
  end
  
  def initialize(str)
    @str = str
  end
  
  def inspect
    @str
  end
end

s1 = SizeMatters.new("Z")
s2 = SizeMatters.new("YY")
s3 = SizeMatters.new("XXX")
s4 = SizeMatters.new("WWWW")
s5 = SizeMatters.new("VVVVV")

s1 < s2                       # => true
s4.between?(s1, s3)           # => false
s4.between?(s3, s5)           # => true
[ s3, s2, s5, s4, s1 ].sort   # => [Z, YY, XXX, WWWW, VVVVV]

Czy wiesz, że...

Gdy zdefiniujemy pustą klasę:

class Empty
end

e = Empty.new
puts e.methods.sort.join(", ")

==, ===, =~, __id__, __send__, class, clone, display, dup, eql?, equal?, extend, freeze, frozen?, gem, hash, id, inspect, instance_eval, instance_of?, instance_variable_defined?, instance_variable_get, instance_variable_set, instance_variables, is_a?, kind_of?, method, methods, nil?, object_id, private_methods, protected_methods, public_methods, require, require_gem, respond_to?, send, singleton_methods, taguri, taguri=, taint, tainted?, to_a, to_s, to_yaml, to_yaml_properties, to_yaml_style, type, untaint

zauważamy sporą ilości metod, których nie definiowaliśmy. Tak naprawdę są to metody klas nadrzędnych do naszej: Object, Module, Class.

Za to zamieszanie Ruby jest krytykowany przez wielu sceptyków. Okazuje się, że może być jeszcze gorzej :).

module Kernel
  def are_you_there?
    "Yeah, fuc*in' everywhere!"
  end
end

5.are_you_there?        # "Yeah, fuc*in' everywhere!"
"test".are_you_there?   # "Yeah, fuc*in' everywhere!"

Publiczna metoda modułu Kernel jest wmiksowana w każdą klasę. Zdefiniowanie metody poza jakąkolwiek klasą definiuje ją w module Kernel. Includowanie modułu poza jakąkolwiek klasą powoduje inkludowanie go do modułu Kernel.

Podany wyżej przykład pokazuje skalę tego, co dopiszemy do Kernela. Niby nic... ale nie bawmy się w Lenina!

Komentarze: Skocz na dół, na górę

  1. No cóż, dla mnie może nic nowego, ale dobry kawał roboty ;).

    Pozwolę sobie, przyczepisz się do tego co nie do końca jest jasne :P.

    „klasa, do której dołączany jest moduł, przejmuje także jego zmienne instancji, modułu oraz stałe”

    Jeśli masz na myśli taką sytuację:

    module Test
      def gogo
        @gogo ||= 0
        @gogo += 1
        puts "Gogo: #{@gogo}"
        p [self, self.class]
      end
    end
    class MyClass
      include Test
    end
    o = MyClass.new
    o.gogo
    p o.instance_variables #=> ["@gogo"]
    

    To sprawa jest troszkę inna. Skoro metoda jest wywoływana w kontekście obiektu klasy (self.class == MyClass) to nie ma „przejmowania” zmiennych instancji, a jest po prostu ich zwykłe tworzenie w momencie wywołania metody (u nas gogo).

    „Pamiętać należy o kolejności poszukiwania metody w momencie wywołania:

    1. metody oryginalnej klasy
    2. metody wmiksowane
    3. metody przodka
    4. metody wmiksowane w przodka”

    Najlepiej chyba napisać:
    1. metody oryginalnej klasy
    2. metody wmiksowane
    3. następnie rekurencyjne szukane są metody w nadklasie jeśli występuje (wracamy do pkt 1 i 2, ale w kontekście nadklasy)

    „Okazuje się, że każda nowo definiowana klasa zawiera wbudowane w język miksiny, m. in.: Object, Module, Class.”

    Zapędziłeś się trochę :). Object, Module, Class – wszystkie są klasami, a więc nie może być miksowania :).

    p [Object.class, Module.class, Class.class] #=> [Class, Class, Class]
    

    Jeden rysunek jest więcej wart niż 1000 słów

  2. To sprawa jest troszkę inna. Skoro metoda jest wywoływana w kontekście obiektu klasy (self.class == MyClass) to nie ma „przejmowania” zmiennych instancji, a jest po prostu ich zwykłe tworzenie w momencie wywołania metody (u nas gogo).

    Patrząc na definicję modułu i jego metod, tworzone są zmienne „instancji” danego modułu. Skoro klasa włączająca dany moduł do siebie przejmuje ciało metod modułu, można powiedzieć, że przejmuje także ich zmienne instancji :). Inny sposób zobrazowania, ale chodzi nam o dokładnie to samo :). Zmienne te muszą być stworzone w instancji klasy, bo w module nie mają prawa istnieć.

    Zapędziłeś się trochę :). Object, Module, Class – wszystkie są klasami, a więc nie może być miksowania :).

    Tak, masz całkowitą rację. Poprawiłem. W dokumentacji Rubiego też jest tego typu rysunek (ri Class) – oglądałem go podczas pisania posta, co mi nie przeszkodziło w palnięciu głupoty nie wiadomo skąd. Dzięki, Radarek, za wskazanie błędu :).

  3. „Za to zamieszanie Ruby jest krytykowany przez wielu sceptyków”. Nie widzę w tym nic złego, w końcu takie jest przeznaczenie modułu Kernel. Niepożądane jest natomiast zachowanie obiektu toplevel (tworzonego przez Object.allocate), którego metody zamiast stać się metodami singletona toplevel zatruwają klasę Object.

    def blah puts :foo
    end

    puts methods.grep(/blah/) # pusto :D
    puts Object.private_instance_methods.grep(/blah/) # niestety

    blah
    eval „blah”, TOPLEVEL_BINDING

    1.send(:blah)

  4. for i in (0..@collection.length – 1) do
    yield ‘.’*@collection[i]
    end

    Może ja się nie znam, bo jeśli chodzi o Rubiego to jestem totalny noob, ale jakoś mi to mało Rubiowo brzmi ;) Ja bym napisał:

    @collection.each {|i| yield '.'*i}
    

    A druga rzecz – czy w tym że w jednym miejscu jest „anOther” a nie another jest jakiś wyższy cel czy to tak dla jaj? :)

  5. Z tym forem masz rację. Zmieniłem. Fajnie jest mieć świadomość, że nie tylko ja tworzę i dbam o jakość wpisów. Dzięki :).

    A druga rzecz – przy tego typu metodach argument zwykle nazywa się other (taka… tradycja?). anOther to oczywiście jaja ;).

  6. Pewnie Razorjack 10min przed pisaniem programował w C++ i stąd mu się ubzdurało iterowanie za pomocą indeksu :D:D.

  7. To nie jest śmieszne. Bo trafne :D. Nienawidzę studiów ;).

  8. „Widać tutaj, że aby zrealizować signleton w Rubim, należy” singleton nie signleton :) Super, podoba mi się, ale … zawsze musi być jakieś ale hehe, więc co tak mało wpisów? opuszczasz się chłopie, codziennie sprawdzam czy jest coś nowego a tu lipa, więc pisz więcej :)

Dodaj komentarz na temat

Zanim skomentujesz...

W komentarzach działają znaczniki Textile.
Zastrzegam sobie prawo do edycji Twojego komentarza tylko i wyłącznie w celach estetycznych (naprawienie źle wstawionego kodu, itp). Nie zmieniam ich treści, ortografii, interpunkcji. Jeśli odczuwasz potrzebę edycji swojego komentarza, skontaktuj się ze mną, a zdziałamy co trzeba.