Idiomy, sztuczki i właściwości języka Ruby.

Dynamiczne tworzenie klas i obiektów (A.K.A. jedno wielkie oszustwo w celu poszerzenia horyzontów)

Wiele języków programowania pozwala na serializowanie obiektów, jednak w języku Ruby jak najbardziej możliwe jest serializowanie klas. Oznacza to, że można napisać fragment kodu, który zbuduje od nowa całą nieznaną klasę, doda do niej metody i pola oraz utworzy jej obiekt i wykona na nim jakiś kod.

Przyjmijmy, że nasza klasa opisana jest następującym, pseudo XMLowym kodem:

<?xml version="1.0" encoding="utf-8"?>
<class name="Printer">
  <public>
    <field name="field_one" reader="true" writer="true"/>
    <field name="field_two" reader="true" writer="true"/>
    <field name="field_three" reader="true" writer="true"/>
    <method name="print_args">
      <arguments>
        <argument name="arg1"/>
        <argument name="arg2"/>
        <argument name="arg3"/>
      </arguments>
      <body>
        puts arg1
        puts arg2
        puts arg3
      </body>
    </method>
  </public>
</class>

Z tego kodu chcemy:

  • utworzyć w trakcie wykonania skryptu nową klasę,
  • utworzyć obiekt
  • wykonać na utworzonym obiekcie różne metody
  • odziedziczyć po tej dynamicznie utworzonej klasie.

Kod tworzący klasę złożymy z trzech części. Pierwsza z nich to cztery funkcje pomocnicze:

require 'rexml/document'
include REXML
 
# przekształca tablicę napisów w ciąg tych napisów oddzielonych przecinkiem
# np. ["a", "b", "c"] => "a, b, c".
# Wykorzystana zostanie do przygotowania listy argumentów dla metody
def make_args_string(args_names)
  args_string = String.new
  args_names.each_with_index do |arg_name, index|
    if(0 == index)
      args_string = args_string + arg_name
    else
      args_string = args_string + ", #{arg_name}"
    end
  end
  return args_string
end
 
# dodaje metodę o nazwie name, ciele code 
# oraz tablicy nazw argumentów args_names do podanej klasy
def my_define_method(clazz, name, code, args_names)
  args_string = make_args_string(args_names)
  define_method_code = <<-EOS
    def #{name}(#{args_string})
      #{code}
    end
  EOS
  clazz.class_eval define_method_code, __FILE__, __LINE__
end
 
# analizuje element dotyczący metody z podanego wcześniej XMLa
# (pomiędzy znacznikami <method> i </method>)
# i wyciąga z niego wszystkie potrzebne informacje.
# Następnie tworzy kod metody i dodaje go do klasy
# za pomocą metody my_define_method zdefiniowanej powyżej.
def add_method_to_class_from_xml_element(clazz, element)
  method_name = element.attributes["name"]
  method_arguments = element.elements["arguments"]
  method_arguments_array = []
  method_arguments.each do |method_argument_element|
    if(method_argument_element.class != REXML::Text)
      method_arguments_array.push(method_argument_element.attributes["name"])
    end
  end
  method_code = element.elements["body"].text
  my_define_method(clazz, method_name, method_code, method_arguments_array)
end
 
# Analizuje element dokumentu XML odpowiedzialny za pole
# (znacznik <field/>) i w zależności od tego, czy pole ma być 
# udostępnione na zewnątrz do odczytu/zapisu używa
# metod attr_accessor, attr_writer i attr_reader do dodania pola
# do klasy oraz zdefiniowania dostępu do niego
def add_field_to_class_from_xml_element(clazz, element)
  field_name = element.attributes["name"]
  is_readable = ("true" == element.attributes["reader"])
  is_writeable = ("true" == element.attributes["writer"])
  clazz.class_eval do
    if(true == is_writeable)&&(true == is_readable)
      attr_accessor field_name
    elsif(true == is_writeable)
      attr_writer field_name
    elsif(true == is_readable)
      attr_reader field_name
    end
  end
end

Drugi fragment kodu to przetworzenie metadanych klasy:

# Wczytanie dokumentu i jego korzenia
file = File.new("Class.xml")
doc = Document.new(file, {:respect_whitespace => []})
root = doc.root
 
# Uzyskanie informacji o nazwie klasy
class_name = root.attributes["name"]
 
# Dodanie nowej klasy do tablicy symboli interpretera
clazz = Object.const_set(class_name, Class.new)
 
# Dostanie się do środka ciała klasy, by dodać do niej nowy kod
clazz.class_eval do
public() #Rozpoczęcie sekcji elementów publicznych i pobranie odpowiedniej sekcji z dokumentu
  public_elements = root.elements["public"]
 
  #pętla iterująca po elementach klasy (pola i metody) i dodająca je do klasy
  public_elements.elements.each do |element|
    case element.name
    when "field" then
      add_field_to_class_from_xml_element(clazz, element)
    when "method" then
      add_method_to_class_from_xml_element(clazz, element)
    end
 
  end
end

Dziedziczenie dynamiczne i warunkowe

W języku Ruby nie ma deklaratywnego kodu - cały kod jest wykonywalny. Również dziedziczenie jest wykonywane dynamicznie. Nie musimy zatem podawać "na sztywno" nazwy klasy - po operatorze '<' może znaleźć się jakiekolwiek wyrażenie zwracające obiekt klasy Class. Poniższy przykład przedstawia wykorzystanie tej własności tak, by w trakcie działania programu zadecydować, po jakiej klasie dziedziczy nasza własna klasa.

father = "James"
 
class SomebodysSon <
  case father
    when "Johhny" then Integer
    when "James" then String
    when "Andy" then Regexp
  end
 
public
  def mothers_name
    puts superclass.name #=> "String"
  end
end

Nadpisywanie metod w klasach rdzennych

Ruby pozwala na ponowne otwarcie ciała dowolnej klasy i dopisanie bądź nadpisanie w niej dowolnej metody. Ta wolność dochodzi do absurdu - można w ten sposób zawiesić interpreter, co przedstawia poniższy fragment kodu.

class Object
  def self.new
  end
end
 
puts "this is the end of all hope..." #=> Segmentation fault (core dumped)

Nadpisywanie metody przez nią samą

Jak już wspomniałem, w Ruby praktycznie cały kod jest wykonywalny, także instrukcja def, definiująca nową metodę w aktualnej klasie. Instrukcji tej można także używać wewnątrz metody. To, w połączeniu z możliwością redefinicji (nadpisania) każdej metody w każdej klasie daje możliwość nadpisania metody przez nią samą. Przykład takiej metody można zaobserwować poniżej.

class SuicideBomber
  def do_suicide_bombing
    def do_suicide_bombing
      puts "I'm dead already"
    end
    puts "Whaaaaaaaah!"
  end
end
 
achmed = SuicideBomber.new
achmed.do_suicide_bombing #=> Whaaaaaaaah!
achmed.do_suicide_bombing #=> I'm dead already

Dynamiczne dodawanie kodu do klasy

Elastyczność składni Rubiego

#!/usr/bin/ruby -w
temperature = 2
eggs = 2
it = 2
 
def add(*ingredients) ingredients end
 
def mix(arg) arg + "aaa" end
 
def ready true end
 
def equals(num) 1 end
 
def everything(arg) 2 end
 
def blows! "lalalala" end
 
##############################################################################
add eggs and mix it until ready unless temperature.equal? 100 or everything blows!
##############################################################################
 
puts "OK, computer!"

Definiowanie metody przez inną metodę (czyli proteza stereotypów)

Obsługa nieznalezionych metod.

O ile nie zaznaczono inaczej, treść tej strony objęta jest licencją Creative Commons Attribution-Share Alike 2.5 License.