Obiekty w Rubim, część 2: singletony, moduły, miksiny
(8 komentarzy)W kategoriach: Ruby , Ruby tutorial , Techblog / 29 października 2007 [01:20:48]
Tagi technorati: OOP mixins modules namespaces programming ruby singleton pattern
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
newiallocate- 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
includejest zawsze nazwa widocznego z danego miejsca modułu; jeśli moduł nie jest widoczny, należy załadować odpowiedni plik za pomocą metodyrequire - 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:
- metody oryginalnej klasy
- metody wmiksowane
- metody przodka
- 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!
Radarek
29 października 2007, 08:40:58No 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ę:
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 :).
Jeden rysunek jest więcej wart niż 1000 słów
RazorJack
29 października 2007, 08:57:38Patrzą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ć.
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 :).Anonim
29 października 2007, 14:23:13„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)
psionides
29 października 2007, 19:53:58Moż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ł:
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? :)
RazorJack
29 października 2007, 20:09:21Z 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?).anOtherto oczywiście jaja ;).Radarek
29 października 2007, 20:13:29Pewnie Razorjack 10min przed pisaniem programował w C++ i stąd mu się ubzdurało iterowanie za pomocą indeksu :D:D.
RazorJack
29 października 2007, 20:15:50To nie jest śmieszne. Bo trafne :D. Nienawidzę studiów ;).
maly
01 listopada 2007, 10:17:30„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 :)