Ruby - wprowadzenie, część 5 - programujemy inkrementalnie

środa, 1 marzec 2006, w kategoriach: Programowanie, Ruby

Efektem ostatniego odcinka jest ten podsumowujący listing. Zaimplementowaliśmy pewne podstawowe funkcje, poznaliśmy pewne “idiomy” Ruby’ego, ale wciąż nie bardzo jest co uruchamiać. Zaczniemy więc od wzięcia na tapetę klasy “Interfejs”:

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

  def start
  puts "Lista zadan do zrobienia: "
  puts "--------------------------"
  @lista.zadania.each_index do |idx|
    print "#{idx} "
    @lista.zadania[idx].wyswietl
  end
  puts "--------------------------"
  puts "Co chcesz zrobic? [ Wpisz odp. polecenie ]"
  puts "dodaj - Dodaj nowe zadanie"
  puts "usun - Usun istniejace zadanie"
  puts "koniec - Koniec pracy"
  end
end

W tej części będziemy chcieli przede wszystkim stworzyć główną pętle programu: wyświetlenie zadań do zrobienia i listy poleceń, pobranie polecenia od użytkownika, wykonanie go i rozpoczęcie całej pętli od początku (chyba że użytkownik wydał polecenie zakończenia pracy z programem).

Chcąc, uniknąć zbyt długich metod, zaczniemy od małego refactoringu - zmiany kodu w celu poprawienia jego czytelności, bez zmiany jego działania. Z metody start wydzielmy sobie dwie metody:

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

def wyswietl_polecenia
  puts "Co chcesz zrobic? [ Wpisz odp. polecenie ]"
  puts "dodaj - Dodaj nowe zadanie"
  puts "usun - Usun istniejace zadanie"
  puts "koniec - Koniec pracy"
end

Napiszemy sobie też na razie pustą metodę przetwarzającą zadania:

def przetworz_polecenie(polecenie)
end

I możemy w metodzie start umieścić już szkielet głównej pętli:

def start
  while true
    wyswietl_zadania
    wyswietl_polecenia
    przetworz_polecenie(gets.chop)
  end
end

Musimy teraz mieć jakiś sposób, żeby sprawdzić, czy użytkownik wpisał poprawne polecenie, czy nie i ewentualnie wyświetlić odpowiednie pouczenie, po czym ponownie pobrać standardowe wejście. Jako że pętle goto wyszły z mody kawałek czasu temu, użyjemy w tym celu wyjątków.

Wyjątki, są to specjalne klasy, które zostają zainicjalizowane i “wyrzucone” w “ciele” pewnej metody kiedy zaistnieje jakiś błąd, tak żeby użytkownik wywołujący tą metodę mógł taki wyjątek “złapać” i zorientować się że coś poszło nie tak. Nasza pusta klasa będzie sygnalizować wpisanie przez użytkownika nieprawidłowego polecenia:

class NieznanePolecenie RuntimeError
end

Dziedziczymy z RuntimeError, czyli standardowej klasy wyjątków. Teraz zmieniamy odrobinę główną pętle:

def start
  while true
    wyswietl_zadania
    wyswietl_polecenia
    begin
      przetworz_polecenie(gets.chop)
    rescue NieznanePolecenie
      puts "Wpisales zle polecenie, co chcesz zrobic?"
      retry
    end
  end
end

To co jest pomiędzy “begin” a “rescue”, w C++ czy Javie znalazłoby się w “try” i jest to blok kodu który może rzucić wyjątek. Następnie mamy “rescue NazwaKlasy” (odpowiednik catch), czyli obsłużenie takiego, a takiego wyjątku, w tym przypadku NieznanePolecenie. Specjalna instrukcja “retry” powtarza cały blok zawarty pomiędzy “begin” a “rescue”. Pozostaje nam tylko sprawić, żeby metoda przetworz_polecenie rzucała odpowiedni wyjątek, ale tym zajmiemy się chwile później.

Wracając do poleceń - rozbudowa programu będzie polegać głównie na dodawaniu właśnie nowych polecen, dlatego chcielibyśmy aby dało się to zrobić jak najłatwiej i jak najbardziej elegancko. Wykorzystamy w tym celu dwie rzeczy - wzorzec projektowy “Command” i dynamiczne właściwości Ruby’ego.

Wzorzec projektowy to pewne rozwiązanie umożliwiające łatwe rozwiązanie jakiejś szerokiej gamy problemów czy to programistycznych, czy architektonicznych , czy jeszcze innych. Więcej o wzorcach można poczytać np. w Wikipedii.

Wzorzec Command, o którym mowa, każe nam zbudować obiekt dla każdego polecenia w systemie. Dodatkowo zrobimy sobie hash, który przyporządkuje polecenia użytkownika, odpowiadającym klasom poleceń. Rozwiązanie które otrzymamy będzie dużo bardziej eleganckie niż to co mamy do tej pory. Mamy więc pare nowych klas:

class Polecenie
  attr_accessor :opis
  def to_s
    return @opis
  end
end

class PolecenieDodaj Polecenie
  attr_accessor :lista_zadan

  def initialize
    @opis = "Dodaj nowe zadanie"
  end

  def wykonaj
    @lista_zadan.dodaj(Zadanie.zbuduj)
  end
end

class PolecenieUsun Polecenie
  attr_accessor :lista_zadan

  def initialize
    @opis = "Usun istniejace zadanie"
  end

  def wykonaj
    puts "Podaj numer zadania: "
    @lista_zadan.usun(gets.to_i)
  end
end

class PolecenieKoniec Polecenie
  attr_accessor :lista_zadan

  def initialize
    @opis = "Koniec pracy"
  end

  def wykonaj
    @lista_zadan.zapisz
    puts "Dziekujemy za prace z programem!"
    exit
  end
end

Potrzebujemy teraz listy wszystkich polecen w systemie, więc w klasie Interfejs wprowadzamy dodatkowy hasz - gdzie kluczem jest nazwa polecenie wydanego przez użytkownika, a wartością obiekt reprezentujący to polecenie.

def initialize
  @lista = ListaZadan.new
  @lista.wczytaj

  @polecenia = {}

  @polecenia["dodaj"] = PolecenieDodaj.new
  @polecenia["dodaj"].lista_zadan = @lista

  @polecenia["usun"] = PolecenieUsun.new
  @polecenia["usun"].lista_zadan = @lista

  @polecenia["koniec"] = PolecenieKoniec.new
  @polecenia["koniec"].lista_zadan = @lista
end

No i zmienia nam się metoda wyświetlająca możliwe polecenia:

def wyswietl_polecenia
  puts "Co chcesz zrobic? [ Wpisz odp. polecenie ]"
  @polecenia.each_key do |klucz|
    puts "#{klucz} - #{@polecenia[klucz].opis}"
  end
end

Zostało nam jeszcze tylko napisanie metody przetwarzającej podane polecenie i otrzymamy pierwszą jako tako działająco wersję programu:

def przetworz_polecenie(polecenie)
  do_wykonania = @polecenia[polecenie]
  if(do_wykonania != nil)
    do_wykonania.wykonaj
  else
    raise NieznanePolecenie
  end
end

Linia “do_wykonania = @polecenia[polecenie]” wyszukuje w naszym haszu polecenie wydane przez użytkownika i jeśli zostanie ono znalezione, pobierany jest odpowiadający mu obiekt, poczym zostanie ono wykonane. Jeśli zaś takiego polecenia nie ma, metoda rzuca wyjątek NieznanePolecenie.

W tej chwili nasz kod powinien przedstawiać się tak, jak w listingu 2. Można to już uruchomić, pobawić się i dostrzec pewne dość oczywiste niedociągnięca i pomyłki w programie - spowodowane głównie tym, że nie da się czytelnie w tutorialu wprowadzać na raz zbyt wielu nowych koncepcji. Dlatego w następnym odcinku zajmiemy się m. in. naprawieniem tych usterek i poprawieniem ogólnego kształtu programu. Do zobaczenia :)

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

Komentarze ():

n-kamil, 6 lipiec 2008, 9:43 pm

Zamiast a+ powinno być odpowiednio r i w

Marek, 30 styczeń 2009, 12:43 am

Nie działają linki z lisingami :(

Skomentuj