Ruby on Rails II: jeszcze o języku Ruby
Opublikował siefca
W tym odcinku podręcznika zajmę się gramatyką i składnią Ruby’ego, bo chciałbym dokończyć omawianie przykładowego kodu, w celu zapoznania się z najczęstszymi konstrukcjami języka. Dopiero w następnym artykule poświęconym środowisku Ruby on Rails zamierzam pokazać to, co widzi użytkownik w trakcie generowania aplikacji i powiedzieć do czego to służy.
Znajomość Ruby’ego jest bardzo potrzebna, żeby wiedzieć co się „psuje”. W zasadzie robienie czegoś w Rails to robienie czegoś w Rubym. Jeżeli poszukujesz łatwego systemu zarządzania zawartością, który zwolni Cię z konieczności kodowania, to przykro mi, lecz RoR nie jest w stanie Ci w tym pomóc. Ruby on Rails to wygoda dla programisty, ale nie zwolnienie z konieczności programowania przy tworzeniu aplikacji webowej.
Jeszcze o Rubym
W poprzedniej części używałem przykładu klasy Pieniadz, aby zademonstrować najpowszechniejsze konwencje programowania i zaprezentować takie mechanizmy jak duck typing, dziedziczenie, czy przeciążanie metod i operatorów. Zauważyłem przy okazji, że podane tam opisy składni języka mogą być przez niektórych odebrane jako nieco monotonne. Podawane przeze mnie przykłady są co prawda tłumaczone, ale w taki sposób, że czytający musi przenosić wzrok z kodu na objaśnienie. Spróbuję więc nieco innej konwencji, polegającej na przedstawianiu większego fragmentu kodu, a następnie osobnym tłumaczeniu go niemal linijka po linijce. Jeśli pomyliłem się i wcale lepiej się tego nie przyswaja, to proszę o informację zwrotną.
Wróćmy więc do naszych przykładów. Wyróżnię tutaj trzy etapy: założenia, kod i objaśnienia.
Założenia
Chcemy stworzyć nowy typ danych (klasę), który będzie przechowywał informację o liczbie jednostek monetarnych (pieniążków). Nazwa tej klasy to Pieniadz. Informacje jakie będą w niej przechowywane to liczba większych jednostek monetarnych (wieksze), mniejszych jednostek monetarnych (mniejsze), a także waluta tych jednostek (waluta). Większe jednostki to po prostu specjalistyczne określenie na całkowite części waluty (na przykład złote), a mniejsze to części ułamkowe (na przykład grosze). Aby przechowywać informacje o walucie stworzymy nowy typ danych o takiej właśnie nazwie.
Z perspektywy klasy obsługującej środki pieniężne nie musimy wiedzieć jak działa klasa odpowiedzialna za walutę – musimy jedynie zaufać, że przechowuje ona potrzebne informacje i daje odpowiednie metody służące do zarządzania nimi. Klasa Pieniadz powinna zawierać metody służące do ustawiania jednostek monetarnych i waluty: do ich odczytywania, konwersji do łańcucha znaków lub do liczby zmiennoprzecinkowej, a także do przeliczania liczby jednostek monetarnych na inną walutę. Poniższy schemat prezentuje tę klasę – kolorem jasnoszarym oznaczyłem pola, a bananowym metody:
Zamierzamy też stworzyć nowy typ danych, który będzie przechowywał informacje o walucie. Nazwą będzie Waluta, a przechowywane w nim dane to na pewno jej trzyliterowe oznaczenie (np. PLN) i kurs po jakim jest wymieniana na bazowe jednostki. Za bazę przyjmiemy sobie złotówki. Klasa ta powinna udostępniać metody pozwalające ustawiać i odczytywać oznaczenie i kurs, a także dokonywać przewalutowania po otrzymaniu argumentu oznaczającego inną walutę; wynik konwersji na inną walutę dla ułatwienia będzie prezentowany jako liczba zmiennoprzecinkowa i nie będzie zmieniał żadnego z pól należących do klasy. Poniższy schemat prezentuje pola i metody tej klasy:
Kod
class Waluta attr_accessor :kod attr_accessor :kurs attr_reader :baza # konstruktor def initialize(kod=:PLN, kurs=1) @baza = :PLN ustaw(kod, kurs) end # trzy pierwsze litery # i internalizacja oznaczenia def ladna(w) w.to_s[0..2].upcase.to_sym end # ustawianie kursu i oznaczenia waluty def ustaw(w=baza, kurs=1) @kod = ladna(w) @kurs = kurs.to_f end # metody konwersji do łańcucha znaków def to_s; @kod.to_s; end def kurs_s; @kurs.to_s + " " + @baza.to_s; end # metoda konwersji kursu między walutami def konwersja(ile, z_waluta, do_waluta=nil) r = z_waluta.kurs * ile.to_f if do_waluta == nil r else r / do_waluta.kurs end end # konwersja kursu między bieżącą a inną walutą def przelicz(ile, do_waluta=nil) konwersja(ile, self, do_waluta) end end # Definiujemy klasę Pieniadz. class Pieniadz attr_accessor :wieksza attr_accessor :mniejsza attr_accessor :waluta # konstruktor def initialize(w=0, m=0, wal=:PLN) if w.is_a?(self.class) @mniejsza = w.mniejsza @wieksza = w.wieksza @waluta = w.waluta else if wal.is_a?(Waluta) @waluta = wal elsif m.is_a?(Waluta) @waluta = m m = 0 else @waluta = Waluta.new(wal); end @wieksza = 0 @mniejsza = 0 if (m != 0) @wieksza = w.to_i; @mniejsza = m.to_i; else dodaj(w) end end end # metoda dodawania liczby do pieniądza # duck typing def dodaj(p) if p.is_a?(self.class) @wieksza += p.wieksza @mniejsza += p.mniejsza elsif p.respond_to? :to_f @wieksza += p.to_f.to_i @mniejsza += ((p.to_f - @wieksza) * 100).round.to_f.to_i else @wieksza += p.to_i end end # metoda konwersji do liczby zmiennoprzecinkowej def to_f @wieksza.to_f + @mniejsza.to_f / 100.to_f end # metody konwersji do łańcucha znaków def to_str "#{@wieksza.to_s},#{@mniejsza.to_s} #{@waluta.to_s}" end def to_s; self.to_str; end # metody przewalutowywania def przelicz!(w) r = @waluta.przelicz(self.to_f, w) @waluta = w @wieksza = @mniejsza = 0 dodaj r self end def przelicz(w) n = self.class.new(self) n.przelicz! w end end ######################################## pln = Waluta.new :PLN eur = Waluta.new :EUR, 4 usd = Waluta.new :USD, 3 pieniadz = Pieniadz.new 2.40, eur puts "Pieniądz:\t\t" + pieniadz pieniadz.przelicz!(usd) puts "Po przeliczeniu:\t" + pieniadz pieniadz.przelicz!(pln) puts "Po przeliczeniu:\t" + pieniadz pln.ustaw("polska") puts "Po zmianie waluty:\t" + pieniadz usd.kurs = 2 puts "\nZmiana kursu #{usd} na:\t" + usd.kurs_s pieniadz.przelicz!(usd) puts "Po przeliczeniu:\t" + pieniadz puts "\nNieinwazyjne\nprzeliczenie:\t\t" + pieniadz.przelicz(pln)
Zależności między klasami przedstawia poniższy uproszczony schemat, na którym umieściłem też przykładowe wartości pól:
Objaśnienia
Klasa Waluta
class Waluta attr_accessor :kod attr_accessor :kurs attr_reader :baza
W powyższym fragmencie tworzymy nowy typ danych (klasę) o nazwie Waluta i umieszczamy w niej metody pozwalające na zapisywanie i odczytywanie pól klasy przechowujących dane. Dla zmiennej instancji (zwanej też polem klasy) o nazwie kod, w której będziemy trzymali tekstowe oznaczenie waluty, i dla zmiennej instancji kurs zawierającej wartość przelicznika waluty na PLN definiujemy metody (czyli funkcje składowe) umożliwiające zapis i odczyt (makro attr_accessor). Z kolei dla zmiennej baza, w której dla wygody będziemy mieli nie zmieniające się, tekstowe oznaczenie bazowej waluty, w jakiej jest przelicznik, będziemy mieli tylko akcesor pozwalający na odczyt.
Akcesory
Przypomnij sobie, że w Rubym pola składowe klas nie są widoczne z zewnątrz i żeby móc się do nich odwołać, np. pisząc waluta.kurs należy zdefiniować metody o takich samych jak one nazwach, czyli właśnie akcesory. Gdybyśmy chcieli ten sam fragment zapisać samodzielnie, nie korzystając z makr, to dla pola kod wyglądałby on w najprostszej postaci tak:
# metoda odczytu pola @kod def kod @kod end # metoda zapisu pola @kod def kod=(v) @kod = v end
Metoda: Waluta::initialize()
Kolejny fragment to metoda zwana konstruktorem, wykonywana za każdym razem, gdy tworzona jest instancja danej klasy, czyli nowy obiekt o zdefiniowanym przez nas typie (tu Waluta). Chcemy, żeby tworząc nowy obiekt, można było ustawić oznaczenie waluty i kurs przeliczania w złotówkach.
def initialize(kod=:PLN, kurs=1) @baza = :PLN ustaw(kod, kurs) end
Przyjmuje ona dwa argumenty, dla których ustawiono wartości domyślne. Gdyby podczas tworzenia obiektu typu Waluta programista nie podał w nawiasie tych wymaganych parametrów, to zostaną przyjęte: symbol PLN dla zmiennej lokalnej kod i przelicznik 1 dla zmiennej kurs. Gdyby podano tylko jeden argument, to interpreter uzna, że wprowadzono pierwszy z lewej, a pominięto następny i skorzysta z domyślnej wartości zmiennej kurs, a zmienną kod przyjmie z wywołania.
W konstruktorze ustawiamy pole baza na symbol PLN – nie ma to specjalnego znaczenia i jest używane jedynie podczas tworzenia ładnych, tekstowych reprezentacji przelicznika kursu. Następnie wywoływana jest metoda ustaw() klasy Waluta. Łatwo się domyślić, że to tamta metoda odpowiedzialna będzie za wpisanie przekazanych argumentów do odpowiednich pól klasy.
Metoda: Waluta::ladna()
Kolejna metoda to „upiękniacz” wprowadzonego symbolu waluty, spójrz:
def ladna(w) w.to_s[0..2].upcase.to_sym end
Powyższa funkcja składowa klasy Waluta przyjmuje argument w postaci obiektu w. Następnie wywołuje jego metodę to_s (konwersja do łańcucha tekstowego), z którego wybierany jest zestaw znaków określony przedziałem 0..2 (trzy pierwsze litery). Kolejnym krokiem jest wywołanie metody upcase tak powstałego obiektu – wynikiem jej działania jest stworzenie obiektu, którego wszystkie litery zamienione są na wielkie. Zauważ, że nie wspominam, o tym, że już w momencie wywołania to_s mamy do czynienia z obiektem typu String. Byłoby to istotne, gdybyśmy mieli do czynienia z językiem, gdzie nie istnieje kacze typowanie, natomiast w Rubym nie powinniśmy się przejmować jakiego typu jest to obiekt, obchodzi nas jedynie to, że potrafi obsłużyć wycinanie fragmentu tekstu określonego przedziałem i ma zdefiniowaną metodę upcase. Wracając do przykładu; ostatnim elementem łańcuszka jest wywołanie metody to_sym należącej do obiektu zwróconego przez wywołanie upcase. Metoda ta konwertuje dane do tak zwanego typu symbolicznego.
Typ symboliczny zdefiniowano w jednej z wbudowanych w język Ruby klas. Jest to prosty sposób na przechowywanie łańcuchów znaków, które mają być identyfikatorami czegoś. W skrócie chodzi o to, że w przypadku symboli (bo tak skrótowo nazywa się obiekty tego typu) ich identyfikatory są tożsame z ich zawartością. Proces przekształcania łańcucha znaków do symbolu nosi nazwę internalizacji. W Rubym można użyć w tym celu metody to_sym lub jej synonimu intern, a pisząc program można zastosować składnię polegającą na użyciu symbolu dwukropka, np.: :łańcuch lub :"łańcuch". Wybrałem ten sposób przechowywania danych o oznaczeniu waluty, zgodnie z konwencją, że tekstowe identyfikatory lub indeksy warto pamiętać jako symbole.
Wywołując metodę jako ladna("euro") (łańcuch jako parametr) lub jako ladna(:euro) (symbol jako parametr), możemy spodziewać się, że w wyniku jej działania zostanie zdefiniowany i zwrócony symbol EUR. Przekazywanie jako parametru symbolu też jest możliwe, ponieważ jego klasa również udostępnia metodę to_s, a my tą metodę profilaktycznie wywołujemy w funkcji ladna(). Od podawania łańcucha w praktyce różni się to tym, że symbol EUR zostanie trochę wcześniej dopisany do wewnętrznej, globalnej tablicy symboli utrzymywanej przez interpreter. Przy okazji: gdybyśmy chcieli odwołać się do tego symbolu bezpośrednio w kodzie, to odpowiednim zapisem byłby :EUR.
Metoda: Waluta::ustaw()
Kolejna metoda klasy Waluta to ustaw(). Przyjmuje ona takie same argumenty jak konstruktor i służy do wypełniania wewnętrznych struktur danymi. W pole klasy kod wpisywany jest upiększony funkcją ladna() symboliczny identyfikator waluty, a w pole kurs wpisywana jest zmiennoprzecinkowa (wywołanie metody konwersji to_f) reprezentacja podanego przelicznika.
def ustaw(w=baza, kurs=1) @kod = ladna(w) @kurs = kurs.to_f end
Metody: Waluta::to_s i Waluta::kurs_s
Poniżej kolejny fragment. Są to dwie proste metody, których zadaniem jest produkowanie łańcuchów znaków. Pierwsza metoda (to_s) zwraca łańcuch (klasa String) zawierający tekstową reprezentację symbolu umieszczonego w polu kurs. Mogą z niej niejawnie korzystać na przykład funkcje próbujące wyświetlić obiekt na ekranie. Pisząc na przykład puts Waluta.new(:EUR) na ekranie pojawi się tekst EUR. Druga metoda służy do tego, aby stworzyć łańcuch zawierający przelicznik kursu waluty i przyjętą na stałe walutę przelicznika. Wywołanie puts Waluta.new(:EUR,4).kurs_s wyświetli napis 4 PLN, co będzie oznaczało, że kurs tej waluty ustalono na 4 PLN.
def to_s; @kod.to_s; end def kurs_s; @kurs.to_s + " " + @baza.to_s; end
Metoda: Waluta::konwersja()
Teraz trochę matematyki. Poniższa metoda odpowiada za przeliczanie kursu między różnymi walutami. Mimo, że jest zdefiniowana w klasie Waluta, to z powodzeniem mogłaby być samodzielną funkcją. Przyjmuje trzy argumenty: ile oznaczający liczbę jednostek monetarnych do przeliczenia, z_waluta oznaczający walutę tych jednostek (obiekt typu Waluta) i do_waluta oznaczający walutę, do której dokonujemy przeliczenia. Ostatni argument może być pominięty przy wywoływaniu i wtedy jego domyślną wartością będzie nil. Jest to specjalny obiekt języka Ruby, który oznacza pustą wartość.
Pierwszym krokiem jest przemnożenie jednostek monetarnych przez kurs wymiany waluty źródłowej i przekształcenie (dla pewności) uzyskanego wyniku do liczby zmiennoprzecinkowej. W tym miejscu pamiętamy po prostu w zmiennej lokalnej r wartość będącą wynikiem przeliczenia podanych jednostek na złotówki.
Kolejnym krokiem jest wyrażenie warunkowe, które sprawdza, czy podana waluta docelowa została ustawiona. Jeśli nie została (jest obiektem nil), to przyjmujemy, że chciano uzyskać przelicznik w złotówkach (domyślnym kursie wymiany) i zwracamy wynik wyrażenia obliczonego wcześniej (a pamiętanego w zmiennej r). Jeśli jednak ustawiono walutę docelową, to konieczna jest jeszcze jedna operacja, czyli przeliczenie złotówek na jednostki określone jej kursem wymiany. Ich ilość uzyskamy dzieląc tymczasowy wynik r przez kurs wymiany zapisany w polu kurs obiektu waluty docelowej do_waluta.
def konwersja(ile, z_waluta, do_waluta=nil) r = z_waluta.kurs * ile.to_f if do_waluta == nil r else r / do_waluta.kurs end end
Metoda: Waluta::przelicz()
Aby ułatwić sobie życie potrzebujemy jeszcze jednej prostej funkcji. Wspomniałem wcześniej, że poprzednia metoda konwersji mogłaby z powodzeniem być zdefiniowana poza klasą, ponieważ nie czyni użytku z żadnego z pól, chociaż realizuje obliczenia. Zrobimy sobie metodę wywołującą funkcję składową konwersja(), ale nie będzie ona wymagała podania waluty źródłowej, ponieważ będzie korzystała z własnego obiektu. Cała funkcja to po prostu wywołanie poprzedniej. Różnica polega na tym, że zamiast pobierać obiekt reprezentujący walutę źródłową jako argument uzyskuje go ona przez odwołanie się do instancji klasy. W celu uzyskania referencji do bieżącego obiektu klasy Waluta używane jest słowo kluczowe self:
def przelicz(ile, do_waluta=nil) konwersja(ile, self, do_waluta) end
Klasa Pieniadz
Klasa Pieniadz jest typem danych, który służy do przechowywania informacji o jednostkach monetarnych i ich walucie. Liczba jednostek większych przechowywana jest w polu wieksza, a mniejszych w polu mniejsza. Waluta określona jest w polu waluta. Dla każdego z tych pól istnieje stworzony za pomocą makra akcesor umożliwiający jego zapis i odczyt.
class Pieniadz attr_accessor :wieksza attr_accessor :mniejsza attr_accessor :waluta
Metoda: Pieniadz::initialize()
Konstruktor klasy Pieniadz przyjmuje trzy argumenty, z których każdy ma przypisaną wartość domyślną: w oznacza liczbę jednostek większych (domyślnie 0), m oznacza liczbę jednostek mniejszych (domyślnie 0), wal oznacza walutę (domyślnie symbol PLN, ale nie obiekt typu Waluta!).
def initialize(w=0, m=0, wal=:PLN) if w.is_a?(self.class) @mniejsza = w.mniejsza @wieksza = w.wieksza @waluta = w.waluta else if wal.is_a?(Waluta) @waluta = wal elsif m.is_a?(Waluta) @waluta = m m = 0 else @waluta = Waluta.new(wal); end @wieksza = 0 @mniejsza = 0 if (m != 0) @wieksza = w.to_i; @mniejsza = m.to_i; else dodaj(w) end end end
Pierwsze wyrażenie warunkowe sprawdza czy pierwszy argument jest obiektem typu Pieniadz (self.class jest metodą pobierającą odwołanie do klasy bieżącego obiektu). Jeśli tak jest, to każde z pól jest kopiowane. W przypadku jednostek pieniężnych wykonywane jest ich przepisanie, a w przypadku waluty, do pola waluta wpisana zostaje referencja do obiektu typu Waluta, do którego odwołuje się także instancja klasy, z której kopiujemy. Ten przypadek przypomina coś, co nazywa się konstruktorem kopiującym – konstruktor wypełnia zmienne składowe obiektu wartościami uzyskanymi z zaglądnięcia do tych samych pól w obiekcie podanym jako argument. Gdy to wyrażenie warunkowe nie jest spełnione, to wykonywane są wszystkie pozostałe.
Pierwsze warunkowe wyrażenie w bloku pozostałych sprawdza czy podana waluta jest obiektem typu Waluta. W tym celu używana jest dziedziczona z klasy Object metoda is_a?(). Jeśli tak jest, to w polu waluta umieszczana jest referencja do obiektu określającego konkretną walutę. Co to znaczy? Oznacza to, że w wyrażeniu @waluta = wal nie jest tworzony nowy obiekt, a tylko odwołanie do obiektu określonego w wal. Jeśli utworzymy obiekt reprezentujący walutę, przekażemy referencję do niego obiektowi reprezentującemu pieniądza, a potem zmienimy na przykład kurs waluty w pierwszym obiekcie, to nasz pieniądz również zmieni kurs. Jest to domyślne zachowanie interpretera języka Ruby: poza literałami liczbowymi, znakowymi, logicznymi oraz obiektem nil wszystkie rezultaty przekazywane są przez referencje. Literały o których mowa to na przykład: 123, true, 'x'. Dzięki temu oszczędza się pamięć i ułatwia zarządzanie obiektami. Jeśli istnieje potrzeba stworzenia kopii obiektu, zamiast odwołania do niego, to korzysta się z metody dup lub clone. Referencję możesz sobie wyobrazić tak, że zamiast obiektu przekazywany jest identyfikator miejsca w pamięci, w którym ten obiekt się znajduje. Nie ufaj wymienionym funkcjom klonowania i duplikowania obiektu zanadto, bo one nie wykonują tzw. głębokiego kopiowania. Jeśli na przykład w jakimś polu instancji klasy będzie przechowywana referencja do obiektu (np. do tablicy, tablicy asocjacyjnej czy nawet do napisu tekstowego), to skopiowane będą referencje. Aby skopiować zawartość obiektu w głęboki sposób stosuje się sztuczkę z serializowaniem, o której opowiem przy innej okazji.
Warto zauważyć, że w Rubym przyjęto konwencję, według której nazwy metod zwracających wartość logiczną (prawdę lub fałsz) kończą się znakiem zapytania. Nie ma to wpływu na sposób obliczania wyrażeń i w praktyce możesz nazwać tak każdą ze swych funkcji, jednak wtedy nikt nie zechce czytać Twojego kodu. :-)
Kolejny warunek pierwszego wyrażenia (m.is_a?(Waluta)) to sprawdzenie, czy przypadkiem w miejscu argumentu m nie podano obiektu waluty. Taka sytuacja może się zdarzyć, gdy ktoś spróbuje zainicjować obiekt z użyciem tylko dwóch parametrów: ilością jednostek monetarnych wyrażonych liczbą zmiennoprzecinkową i walutą. Ta konstrukcja kompensuje przesunięcie, które ma miejsce, gdy w takim przypadku „ucieka” trzeci argument.
Gdy powyższe warunki nie są spełnione (ani na drugi, ani trzeci argument nie jest obiektem typu Waluta), to znaczy, że programista zamiast podawać walutę wprowadził tylko jej oznaczenie (na przykład jako tekst lub symbol) lub nie podał nic (wtedy przyjmowany jest domyślnie symbol PLN). W takim przypadku tworzona jest instancja klasy Waluta, czyli nowy obiekt, a referencja do niego zostaje zapamiętana w polu waluta. Odpowiada za to zapis: @waluta = Waluta.new(wal).
Kolejny krok to wyzerowanie wartości pól przechowujących liczby jednostek monetarnych i sprawdzenie czy w argumencie m przekazano liczbę jednostek mniejszych (m != 0). Jeśli tak się stało, to wartości określające liczbę większych i mniejszych jednostek są kopiowane do pól wieksza i mniejsza. W przeciwnym razie (podano tylko liczbę jednostek większych) zakładamy, że wprowadzona może być wartość zmiennoprzecinkowa i wywoływana jest metoda dodaj(), która powinna poradzić sobie z taką sytuacją i rozbić liczbę na całości i części setne.
Metoda: Pieniadz::dodaj()
A oto i metoda dodaj() klasy odpowiedzialnej za przechowywanie informacji o pieniążkach. Służy ona do tego, aby wewnętrzne pola przechowujące ilość większych i mniejszych jednostek powiększyć na podstawie przekazanej wartości. Ta wartość może być liczbą zmiennoprzecinkową, liczbą całkowitą, albo obiektem typu Pieniadz.
def dodaj(p) if p.is_a?(self.class) @wieksza += p.wieksza @mniejsza += p.mniejsza elsif p.respond_to? :to_f @wieksza += p.to_f.to_i @mniejsza += ((p.to_f - @wieksza) * 100).round.to_f.to_i else @wieksza += p.to_i end end
Widzimy, że wita nas wyrażenie warunkowe, które sprawdza, czy podany jako argument obiekt nie jest czasami obiektem stworzonym w oparciu o klasę Pieniadz lub o klasę po niej dziedziczącą (zapis: p.is_a?(self.class)). Jeżeli tak jest, to pola mniejsza i wieksza są powiększane o wartości uzyskane z tych samych pól w przekazanym obiekcie.
W przeciwnym wypadku sprawdzamy, czy przekazany obiekt wyposażony jest w metodę to_f odpowiedzialną za konwersję do liczby zmiennoprzecinkowej. Jeśli to prawda, to jest ona wywoływana, a wynik działania jest zamieniany na liczbę całkowitą z użyciem metody to_i. W ten sposób do pola wieksza trafia część całkowita i pozostaje tylko wyliczenie setnych części wprowadzonej wartości. Dokonujemy tego używając nieco enigmatycznego zapisu:
@mniejsza += ((p.to_f - @wieksza) * 100).round.to_f.to_i
Oznacza on mniej więcej tyle: powiększ pole mniejsza o wartość wyrażenia powstałego w wyniku przekształcenia obiektu p do postaci zmiennoprzecinkowej, od której odjęto wszystkie całkowite jednostki, a pozostawiony ułamek pomnożono przez 100, aby następnie zaokrąglić uzyskaną wartość, zmienić ją na liczbę zmiennoprzecinkową, a następnie na całkowitą. Te kombinacje z zaokrąglaniem (metoda round) są obejściem na znane zachowanie mechanizmu obsługi liczb zmiennoprzecinkowych, który może dawać fałszywe wyniki odejmowania powstałe na skutek błędów operacji elementarnych.
Jeśli wszystkie powyższe warunki nie są spełnione, to przyjmujemy, że podany obiekt ma metodę umożliwiającą przekształcenie do liczby całkowitej i wykonujemy ją zmieniając tylko wartość pola wieksza.
Zauważ, że jest to zupełnie bezpieczną praktyką, aby używać konwerterów w stylu to_f czy to_i, nawet jeśli wiemy, że obiekt może być już właśnie takiego typu. Także, gdy tworzysz swoją własną klasę i umowną nazwą jej typu jest na przykład obrazek, to możesz stworzyć w niej metodę to_obrazek, która zwraca referencję do obiektu instancji (self) – to nic nie szkodzi, a może pomóc, szczególnie przy intensywnym korzystaniu z dobrodziejstw duck typingu.
Zwróć też uwagę na kolejność warunków w metodzie dodaj(). Gdybyśmy najpierw sprawdzali, czy przekazany w argumencie obiekt jest wyposażony w metodę to_f, to nawet jeśli byłaby to instancja klasy Pieniadz, wyrażenie obsługujące tę okoliczność nigdy nie zostałoby wykonane, ponieważ klasa ta również posiada metodę to_f. W tym momencie uświadomiłem sobie, że elastyczniejszym rozwiązaniem byłoby dodanie metody to_pieniadz w klasie Pieniadz i zastąpienie pierwszego testu sprawdzeniem, czy przekazywany obiekt udostępnia taką metodę. Byłoby to rozwiązanie bardziej czytelne i uniwersalne w kontekście przyjętej konwencji.
Metoda: Pieniadz::to_f
def to_f @wieksza.to_f + @mniejsza.to_f / 100.to_f end
Ten prosty konwerter ma za zadanie skonstruować liczbę zmiennoprzecinkową złożoną z części całkowitych i setnych przechowywanych w polach wieksza i mniejsza. Chyba dodatkowy komentarz jest tu zbędny.
Metody: Pieniadz::to_str i Pieniadz::to_s
Poniższe metody służą do ładnego prezentowania tego, co w obiekcie siedzi. Zwracany jest łańcuch znaków (lub jak kto woli obiekt typu String) zawierający przekształcone do tekstowej postaci wartości wszystkich pól klasy. Użyłem tu wyrażeń zagnieżdżonych, które oznacza się zapisem #{ }. Wewnątrz nawiasów klamrowych możesz umieścić kod Ruby’ego, który zostanie wykonany i przekształcony do tekstu (tak, zwracany obiekt musi mieć odpowiedni konwerter). Wyrażenia te przeznaczone są do umieszczania bezpośrednio w literałach tekstowych.
# metody konwersji do łańcucha znaków def to_str "#{@wieksza.to_s},#{@mniejsza.to_s} #{@waluta.to_s}" end def to_s; self.to_str; end
Różnica między metodami to_str a to_s jest taka, że według konwencji, obecność tej pierwszej wskazuje na to, iż obiekt ją udostępniający może być traktowany jako łańcuch znaków, natomiast obecność drugiej oznacza, że posiadający ją obiekt można reprezentować jako łańcuch znaków. Przydaje się to w używaniu duck typingu. Tylko kilka klas Ruby’ego udostępnia konwerter to_str, co znaczy, że tylko kilka klas tworzy obiekty zachowujące się dokładnie tak jak obiekty klasy String. Z kolei wiele klas ma metodę to_s, która oznacza, że ich obiekty mogą być przedstawiane w tekstowej postaci, chociaż nie muszą implementować metod służących do operowania na łańcuchach znaków. Tu użyłem małego oszustwa i zdefiniowałem to_str, tylko po to, aby zadziałał operator dodawania tak skonstruowanego wyniku z innym łańcuchem. Nie powinno się tak robić, ale inaczej trudno byłoby wspomnieć tą ciekawostkę.
Metody: Pieniadz::przelicz!() i Pieniadz::przelicz()
Poniższa metoda służy do wykonywania operacji zmiany waluty i przeliczenia liczby jednostek monetarnych. Przyjmuje ona argument typu Waluta, który oznacza walutę docelową. W celu wykonania obliczeń wywoływana jest metoda przelicz() przechowywanego w polu waluta obiektu waluty bieżącej. Przekazywane jej argumenty to zmiennoprzecinkowa reprezentacja jednostek pieniężnych zapisanych w polach bieżącej instancji (self.to_f) i przekazany do funkcji obiekt określający walutę docelową. Wynik przewalutowania (będący liczbą zmiennoprzecinkową) jest umieszczany w polach klasy z użyciem metody dodaj(), a odwołanie do nowego obiektu waluty zastępuje odwołanie wcześniej przechowywane w polu waluta.
def przelicz!(w) r = @waluta.przelicz(self.to_f, w) @waluta = w @wieksza = @mniejsza = 0 dodaj r self end
Zauważ, że powyższa funkcja składowa dokonuje przewalutowania w taki sposób, że waluta i liczby jednostek w bieżącym obiekcie są zastępowane uzyskanymi wynikami. Właśnie z tego powodu, zgodnie z konwencją, nazwa metody kończy się znakiem wykrzyknika.
Czasem może przydać się funkcja, która przelicza walutę, ale zamiast nadpisywać wewnętrzne struktury instancji klasy zwraca referencję do nowo stworzonego obiektu typu Pieniadz. Oto ona:
def przelicz(w) n = self.class.new(self) n.przelicz! w end end
Najpierw tworzymy nowy obiekt, który jest kopią naszego, chociaż zajmuje zupełnie inny obszar w pamięci. Zapis self.class jest dynamicznym synonimem nazwy klasy Pieniadz, a metoda new każde stworzyć nowy obiekt podanego typu. Do konstruktora powstającej instancji przekazujemy słowo kluczowe self oznaczające referencję do bieżącego obiektu. Właśnie z niego skorzysta konstruktor, gdy będzie kopiował poszczególne pola między obiektami klasy Pieniadz, tak jak to wcześniej zaprogramowaliśmy.
Kolejne kroki to wywołanie inwazyjnej metody przelicz!() w odniesieniu do stworzonego przed momentem obiektu i zwrócenie do niego referencji. Zauważ, że zwracamy tylko wynik działania metody przelicz!(), bo jest to wystarczające dla uzyskania odniesienia do bieżącego obiektu.
Wywoływanie
Gotowy program możemy zapisać do pliku lub wkleić do interaktywnej powłoki (polecenie irb) i zacząć testy:
pln = Waluta.new :PLN eur = Waluta.new :EUR, 4 usd = Waluta.new :USD, 3
Powyższe linijki tworzą trzy obiekty typu Waluta. Każdemu z nim przypisujemy identyfikator, a dwóm ostatnim kurs wymiany na złotówki. Zauważ, że w przypadku pierwszego obiektu kursu nie podajemy i w tym przypadku konstruktor tej klasy użyje wartości domyślnej, czyli 1 – dla PLN będzie to prawidłowy kurs.
W kolejnej linii tworzymy obiekt przechowujący informację na temat pieniądza:
pieniadz = Pieniadz.new 2.40, eur
Instancja będzie zainicjowana zmiennoprzecinkową wartością 2.40 i obiektem waluty, do którego odwołanie przechowywane jest w zmiennej eur.
Spróbujmy wyświetlić na ekranie nasz pieniądz. W tym celu używamy funkcji puts, która oczekuje, że podane jej obiekty (czy mówiąc precyzyjniej: obiekty, do których podano odwołania) dadzą się przekształcić do łańcucha znaków (są wyposażone w odpowiednie metody konwertujące). Poza tym mamy tutaj sumowanie łańcuchów, które jest możliwe dzięki naszej niecnej sztuczce z konwerterem to_str:
puts "Pieniądz:\t\t" + pieniadz
Następne instrukcje to testy inwazyjnej metody przeliczającej, która jako argument przyjmuje obiekt waluty (precyzyjnie: odwołanie do obiektu waluty):
pieniadz.przelicz!(usd) puts "Po przeliczeniu:\t" + pieniadz pieniadz.przelicz!(pln) puts "Po przeliczeniu:\t" + pieniadz
A poniżej dowód na to, że pole waluta obiektu pieniadz jest w istocie tylko referencją. Wywołując metodę waluta::ustaw() możemy zmienić symbol reprezentujący walutę, a podczas wyświetlania zawartości obiektu Pieniadz zmiana się uwidoczni:
pln.ustaw("polska") puts "Po zmianie waluty:\t" + pieniadz
Poniżej zabawa w zmianę kursu dolara:
usd.kurs = 2 puts "\nZmiana kursu #{usd} na:\t" + usd.kurs_s pieniadz.przelicz!(usd) puts "Po przeliczeniu:\t" + pieniadz
Ta konstrukcja może być niebezpieczna, jeśli dokonujemy przeliczenia między tą samą walutą. Dlaczego? Jeśli klasa pieniądza odwoływałaby się do obiektu waluty (np. usd) przez referencję, zmiana kursu spowoduje nie tylko ustawienie waluty podawanej jako argument, ale wpłynie również na kurs pośredniego przeliczania na złotówki. W związku z tym zmiana kursu i wykonanie przeliczenia sprawią, że wartość pośrednia zostanie policzona według nowego kursu. Aby tego uniknąć należy w omawianych przypadkach zastosować duplikowanie obiektu do stworzonej wcześniej z użyciem metody new tymczasowej instancji waluty.
I na koniec demonstracja nieinwazyjnego przewalutowania:
puts "\nNieinwazyjne\nprzeliczenie:\t\t" + pieniadz.przelicz(pln)
Kiedy o Rails?
O Ruby on Rails w następnej części. Jeśli udało Ci się przebrnąć przez przykłady tłumaczące podstawy programowania obiektowego na przykładzie języka Ruby, to będziesz w stanie łatwo zrozumieć więcej niż połowę wszystkiego, o czym będę pisał. Oczywiście dotyczy to wszystkich, którzy jeszcze nie mieli kontaktu z tym językiem, chociaż elementarne techniki programowania są im znane, bo do takich osób adresuję ten podręcznik. Wczoraj wieczorem miałem pokusę sporządzenia kompletnego podręcznika objaśniającego wszystkie konstrukcje języka, ale z pomysłu zrezygnowałem. Przypomniało mi się, gdy lata temu pisałem w technikum pracę dyplomową o Linuksie. Pierwsze sto stron klepałem parę miesięcy, a drugie sto stron dwa tygodnie. Ten drugi przedział czasowy to był moment, gdy „połknąłem bakcyla” i nie czułem ani żadnego zmęczenia, ani potrzeby robienia czegokolwiek innego. Było to pasjonujące i samo robienie tego w pełni mnie satysfakcjonowało. Problem jaki byłby teraz, to praca i inne obowiązki, które uniemożliwiałyby mi takie zaszycie się gdzieś z tematem. Gdyby nie było dobrych tutoriali, to pewnie spróbowałbym coś napisać, ale przy okazji pisania artykułu o internowaniu łańcuchów do Wikipedii trafiłem na świetnie opracowany tekst Aleksandra Pohla napisany w języku polskim. Zapomniałem o tym, ale gdy wczoraj szukałem więcej informacji na temat singletonów, to Google zaprowadziło mnie znowu do jego podręcznika. Jeśli chcesz poznać gramatykę i składnię języka Ruby, to gorąco polecam publikację „Ruby intro”.



Dzięki wielkie, na razie przeczytałem tylko połowę pierwszej części i zapowiada się całkiem ciekawa lektura ;)
Pieniądze pod żadnym pozorem nie powinny być przechowywane w typach zmiennoprzecinkowych. Należey: 1. Użyć BigDecimal. 2. Samodzielnie zaimplementować wszystkie operatory jako działania na liczbie podjednostek (grosze lub 1/100 groszy).
Polecam wykonać poniższy kod aby przekonać się dlaczego typ float się nie nadaje do liczenia pieniędzy.
dla porównania