Ruby - wprowadzenie, część 4 - nareszcie coś poważnego

niedziela, 19 luty 2006, w kategoriach: Programowanie, Ruby

Zgodnie z zapowiedzią, bierzemy się za praktyczne programowanie. Przez następne pare odcinków będziemy rozwijać prostą, konsolową aplikacje będącą czymś w rodzaju PIMa, z naciskiem na wsparcie dla GTD (pasujące do tematyki strony). W trakcie pisania programu zaczną nam oczywiście wypływać na powierzchnię pewne niezbędne sprawy o których dotąd nie mówiliśmy, tak więc po drodze poruszymy mnóstwo pobocznych aspektów Ruby’ego. Wszystko będzie się działo dość spontanicznie, nie mam napisanych 10 odcinków tutoriala do przodu, więc asekuracyjnie proszę o wyrozumiałość :)

Odpuścimy sobie GUI, bo jego pisanie jest nudne, pozatym pojawia się problem znalezienia crossplatformowej biblioteki, trzeba opisać jej instalcję… Nudy, nudy, nudy. Skupimy się na esencji, na logice programu. Jeśli ktoś chcę, może potem w ramach ćwiczeń dopisać GUI, nie powinno być to szczególnie trudne.

Punktem wyjściowym naszych rozważań niech będzie bardzo prosta aplikacja, zarządzająca listą zadań do zrobienia. Po jej uruchomieniu pojawia nam się ekran wyglądający tak:

---
Lista zadań do zrobienia:
1 Wynieść śmieci (Dodano: 01.01.01)
2 Kupić kwiaty dla dziewczyny (Dodano: 02.01.01) (Termin: 02.01.01)
3 Zadzwonić do Piotrka (Dodano: 03.01.01) (Termin: 07.01.01)

Co chcesz zrobić? [ Wpisz odp. polecenie ]
dodaj - Dodaj nowe zadanie
usun - Usuń istniejące zadanie
koniec - Koniec pracy
---

Jak widać po powyższym projekcie, głównym obiektem z którym będziemy mieli doczynienia, jest zadanie. Zadanie ma swój numer identyfikacyjny, treść (tytuł), date wprowadzenia i opcjonalnie termin, w jakim musi zostać zakończone.

Mamy więc pierwszą klasę:

class Zadanie
  attr_accessor :numer, :tresc, :data_wprowadzenia, :termin_ukonczenia

  def wyswietl
    print "#{@numer}: #{@tresc} (Dodano: #{@data_wprowadzenia})"
    print " (Termin: #{@termin_ukonczenia})" if @termin_ukonczenia != nil
    puts
  end

  def Zadanie.zbuduj
    puts "Podaj tresc zadania: "
    tresc = gets.chop

    puts "Podaj termin ukonczenia zadania (format dd.mm.yyyy): "
    termin = gets.chop

    zadanie = Zadanie.new
    zadanie.tresc = tresc
    zadanie.termin_ukonczenia = termin
    zadanie.data_wprowadzenia = DateTime.now

    return zadanie
  end
end

Widzimy tu kilka ciekawych rzeczy. Po pierwsze, stworzyliśmy statyczną metodę do tworzenia obiektu, pobierającą dane od użytkownika i zwracającą nową instancję Zadania. Będziemy więc tworzyć instancję klasy w ten sposób:

zadanie = Zadanie.zbuduj

Zwróćmy też uwagę na powtarzający się wzorzec:

puts "Podaj cośtam"
costam = gets

Dobrze by było, gdyby język posiadał jakąś metodę, pozwalającą uprościć powyższe do np.:

costam = IO.prompt("Podaj cośtam")

Nie jest to może wielki skrót, ale zwiększa czytelność. Pozatym, pozwoli nam zilustrować ważną koncepcję, a także jej zastosowanie w Rubym - przydałaby nam się taka a taka konstrukcja w języku, a nie mamy jej - więc ją dopiszemy. W zależności od tego, co chcemy osiągnąć nie tylko zmieniamy treść naszego programu, ale także modyfikujemy sam język. Na tym polega programowanie “od dołu” (bottom-up programming) i jest to świetnie możliwe w Rubym, m. in. dlatego, że nie ma tu wielkiego odróżnienia pomiędzy kodem samego Ruby’ego, a tym użytkownika, a klasa nigdy nie jest zamknięta. Możemy więc roszerzyć istniejący moduł IO, o pożądaną metodę:

class
  def prompt(text)
    puts text
    return gets.chop
  end
end

Niekiedy pomysł ten posunięty zostaje na tyle daleko, że otrzymujemy praktycznie całkiem nowy język, specyficzny dla danej dziedziny, tzw. DSL (Domain Specific Language). I tak w Railsie relacje pomiędzy częściami modelu opisujemy niby za pomocą Ruby’ego, ale tak naprawdę jest to również jakiś DSL - dialekt Ruby’ego na potrzeby opisywania tego typu danych.

Nasza metoda wygląda więc teraz tak:

def Zadanie.zbuduj
  zadanie = Zadanie.new
  zadanie.tresc = IO.prompt("Podaj tresc zadania: ")
  zadanie.termin_ukonczenia = IO.prompt("Podaj termin ukonczenia
                                    zadania (format dd.mm.yyyy): ")
  zadanie.data_wprowadzenia = DateTime.now

  return zadanie
end

Używamy, tu modułu Date, który trzeba najpierw załadować gdzieś wcześniej linią:

require 'date'

W ten sam sposób można załadować także naszą własną klasę:

require 'plik.rb'

Następny komponent którego potrzebujemy, to lista wszystkich zadań jakie użytkownik stworzył, z możliwością zapisu i odczytu z pliku. Aż przyjemnie będzie to zaimplementować:

class ListaZadan
  attr_accessor :zadania

  def initialize
    @zadania = []
  end
  def dodaj(zadanie)
    @zadania.push(zadanie)
  end
  def usun(nr_zadania)
    @zadania.delete_at(nr_zadania)
  end
  def wczytaj
    File.open("data", "a+") { |file| @zadania = YAML.load(file) }
    if @zadania == false
      @zadania = []
    end
  end
  def zapisz
    File.open("data", "a+") { |file| YAML.dump(@zadania, file) }
  end
end

Tak tak, serializacja klasy w Rubym to kwestia dwóch linijek kodu. Użyliśmy w tym celu YAMLa - języka znaczników o dość prostej konstrukcji, tak więc zapisane obiekty moglibyśmy spokojnie edytować w edytorze tekstu. Gdyby ktoś chciał uniemożliwić użytkownikowi grzebanie w danych w ten sposób może użyć formatu binarnego, z klasy Marshall. Tu także musimy załadować odpowiedni moduł:

require 'yaml'

Musimy sobie też poradzić z sytuacją kiedy plik mamy pusty, a YAML.load zwraca false i ponownie uczynić tablice tablicą:

if @zadania == false
  @zadania = []
end

Pojawia się tu też kolejne zastosowanie klauzur:

File.open("plik", tryb) { |plik| zrob_cos_z_plikiem }

Ma to za zadanie automatycznie zamknąć plik, zaraz po wykonaniu przekazanego bloku kodu. Kolejny raz oszczędzamy palce :) Listing prezentuje też kilka nowych wiadomości na temat tablic, po pierwsze, są one całkowicie dynamiczne, zawsze możemy więc dodać do nich nowy obiekt (póki starczy nam pamięci), tak jak tu:

@zadania.push(zadanie)

Czy usunąć element z podanym indeksem:

@zadania.delete_at(nr_zadania)

A teraz spróbujemy przygotować szkielet głównego interfejsu:

class Interfejs
  def initialize
    @lista = ListaZadan.new
    @lista.wczytaj
  end

  def start
    puts "Lista zadan do zrobienia: "
    puts "--------------------------"
    @lista.zadania.each_index do |idx|
      puts "#{idx} "
      @lista.zadania[idx].wyswietl
    end
    puts

    puts "Co chcesz zrobić? [ Wpisz odp. polecenie ]"
    puts "dodaj - Dodaj nowe zadanie"
    puts "usun - Usun istniejące zadanie"
    puts "koniec - Koniec pracy"
  end
end

Ten kod:

@lista.zadania.each_index do |idx|
  puts "#{idx} ";
  @lista.zadania[idx].wyswietl
end

Robi mniej więcej to samo co each, tyle że zamiast iterować po samych elementach tablicy, iteruje po ich indeksach.

Mamy już zaimplementowane wyświetalnie zadań, ale najpierw muszą się one jakoś pojawić w pliku, który ładujemy, pozatym w paru miejscach nasz kod jest bardzo wątpliwej klasy… Ciąg dalszy nastąpi.

P.S. Zdaje sobie sprawę, że listingi są odrobine nieczytelne, myśle właśnie co by tu z nimi zrobić

Dodaj do del.icio.us | Dodaj do wykop.pl

Komentarze ():

lopex, 21 luty 2006, 3:02 pm

mam tylko dwie małe uwagi:
class > #
a to nie jest potrzebne w tym przypadku, można za to:

class IO
# tutaj self lub IO - to w końcu to samo w kontekście klasy
def self.prompt(text)

end
end

lub po prostu:

def IO.prompt(text)

end

natomiast:

if @zadania == false
@zadania = []
end

dzięki powerowi operatora || który zwraca false lub prawą wartość, można zmienić na:

@zadania ||= []

lopex, 21 luty 2006, 4:02 pm

oj zjadlo cos :(

na górze miało być:

class

lopex, 21 luty 2006, 4:02 pm

znowu zjadło :O
jak mam zrobić operator przekierowania ?

sztywny, 21 luty 2006, 6:02 pm

Co do IO.prompt - zgoda, a czemu zjada to nie wiem, wordpress ma tendencje do ssania :(

Skrót z || jest mi znany, ale staram się stopniowo wprowadzać takie rzeczy, żeby zbytnio nie szokować czytelnika :)

lopex, 21 luty 2006, 7:02 pm

może to przejdzie

class

Arkadiusz Młynarczyk, 12 kwiecień 2006, 5:04 pm

Czy przypadkiem nie jest tak, że klasa Zadanie ma niepotrzebnie zadeklarowane pole numer i niepotrzebnie je wypisuje?
Patrząc na listing wydaje mi się, że nigdzie nie jest ono wykorzystywane.

sztywny, 12 kwiecień 2006, 5:04 pm

Racja, fajnie że ktoś uważnie śledzi listingi :) Miałem jakiś zamysł z tym numerem na początku, a potem wogóle o tym zapomniałem.

papaj, 1 czerwiec 2006, 7:06 pm

W komentarzy do kodu źródłowego podajesz jak mozna usunąć zadanie z podanym indeksem… @zadania.delete_at(zadanie)
A chyba powinien byc przykład @zadania.delete_at(nr_zadania) ??

sztywny, 1 czerwiec 2006, 8:06 pm

Fakt, poprawiono.

Maciej Piechotka, 8 wrzesień 2007, 6:30 pm

> a klasa nigdy nie jest zamknięta.

A po zamroźeniu?

salciarz, 9 marzec 2008, 10:51 pm

YAML się nadaje, ale tylko do pojedynczych ‘zrzutów.’ Każda kolejna serializacja jest w naszej bazie oddzielona trzema spacjami. Ładujemy zatem wszystko co się znajduje pomiędzy — a —, albo tylko — a

Patryk Gruszka, 21 czerwiec 2008, 10:33 pm

Mógłbyś załączyć paczkę z source?

Skomentuj