Kurs:Neueste Internet- und WWW-Technologien/Web-Entwicklung mit Rails

Einleitung Bearbeiten

Ruby on Rails, kurz RoR, ist ein durch David Heinemeier Hannson entwickeltes quelloffenes Web Application Framework. Es basiert auf den Designgrundlagen:

  • CoC - Convention over Configuration
  • DRY - Don't repeat yourself

Des Weiteren werden alle Objekte mittels Scaffolding angelegt, d.h. RoR stellt Baupläne für die Erstellung von u.a. Models, Controllern, Tests, usw. bereit, welche durch den Nutzer verwendet werden können und somit einfach und schnell neue Elemente im System implementiert werden können.

Architektur Bearbeiten

Die Architektur von Ruby on Rails ist der Architektur von Grails gleich. Dies kommt daher, dass Grails eine Portierung von RoR in Java ist. Eine genauere Erläuterung der Architektur beider Webframeworks wird in der Web-Entwicklung mit Grails dargestellt. Hierbei ist lediglich zu beachten, dass Java durch Ruby und Grails durch Rails zu ersetzen ist.

Beispielimplementierung Bearbeiten

 
Beziehungen innerhalb der Beispielimplementierung

Im Weiteren wird folgendes Beispielszenario in den ersten Schritten implementiert und alle Designpattern beispielshaft an dem Szenario erläutert. Bei diesem Szenario handelt es sich um ein Mannschaftsverwaltungssystem. Hierbei gelten die folgenden einfachen Abhängigkeiten im zu entwickelnden System:

  • jede Mannschaft soll dabei genau einen Trainer haben und jeder Trainer gehört nur zu genau einer Mannschaft
  • jede Mannschaft hat n Spieler, aber ein Spieler gehört nur zu genau einer Mannschaft

Vorbereitung Bearbeiten

Möglichkeiten zum Aufsetzten einer Entwicklungsumgebung:

  • Virtualrails [1]
    • Schneller Einstieg, da nur die virtuelle Maschine in einem Player gestartet werden muss
    • Aufgesetzes System mit Rails 1.8.X, dies ist veraltet und man findet kaum Hilfestellungen (neuste Version 1.9.3 [Stand: 06.07.2012])
  • Railsinstaller [2]
    • Unterstützt die Betriebssysteme MacOS & Windows
    • Einfache Installation, System voll aufgesetzt
  • Ubuntu VM
    • ausführlicher Installationsguide (incl. kleinem Tutorial) [3]

Beispielimplementierung Bearbeiten

Die ersten Lebenszeichen Bearbeiten

  1. Erstellung eines neuen Projekts mit Kommandozeilenbefehl: rails new <appname>
    Es wird die Grundstruktur von jedem Railsprojekt angelegt und diverse Ordner und Dateien im Verzeichniss erstellt. Nachdem alle vollständig angelegt wurde, wird automatisch bundle install ausgeführt. Es installiert alle bereits vorgegebenen Gems.
  2. Zur Ausführung des Programms benötigt Ruby on Rails Javascript, deshalb muss in der Datei Gemfile im Hauptverzeichnis um folgende Zeilen ergänzt werden:
    gem ´execjs´
    gem ´therubyracer´
    Danach ist eine erneute Ausführung von bundle install notwendig, um die beiden Gems im System zu integrieren.
  3. Anlegen der benötigten Modelle, durch Nutzung von Scaffolds. Innerhalb jedes Befehls werden für jede "Klasse" diverse Dateien angelegt, u.a. das Model, ein Kontroller, mehere Dateien für die Ansicht sowie Tests und eine Migration für die Datenbank.
    1. rails generate scaffold Team name:string shortcut:string
    2. rails generate scaffold Player name:string number:integer
    3. rails generate scaffold Trainer name:string
  4. Die Modelle und das Schema für die Datenbank sind bereits vorhanden, aber die Datenbank wurde noch nicht auf das Schema geupdatet. Um das Updaten der Datenbank anzupassen muss folgender Kommandozeilenbefehl ausgeführt werden: rake db:migrate
  5. Nun ist alles soweit vorbereitet, dass ein erster Testlauf möglich ist. Zum Starten des Servers kann nun folgender Befehl ausgeführt werden: rails s
    Wenn der Server erfolgreich gestartet ist, dann kann z.B. mittels der URL http://localhost:3000/teams auf die Übersicht der Teams zugegriffen werden.

Beziehungen herstellen Bearbeiten

In der einleitenden Erläuterung sind zwei Beziehungsarten identifizierbar. Zum Einen eine 1:n und zum Anderen eine 1:1 Beziehung. In RoR sind Beziehungsangaben in Modellen zu spezifizieren. In der Datei app/models/Team.rb sieht nach der Bearbeitung wie folgt aus:

class Team < ActiveRecord::Base

  has_many :players
  has_one :trainer

end

Die anderen beiden Modelle brauchen jeweils die Zeile:
belongs_to :team

Nun kann Ruby on Rails beispielsweise den Zugriff auswerten: player.team.name. Dadurch würde der Name der Mannschaft des Spielers ausgegeben werden. Ein Problem besteht aber immernoch, die Referenzen zwischen Objekten müssen in der Datenbank gespeichert werden, doch dafür sind noch keine Spalten definiert. Da sich RoR an Convention over Configuration orientiert, gibt es auch hierzu eine Konvention. Die Spalten gehört zu dem Model, welches belongs_to beinhaltet. In RoR werden Datenbankveränderungen über Migrations vorgenommen, d.h. um die zwei Spalten hinzuzufügen wird eine neue Migrationsdatei benötigt. Der Kommandozeilenbefehl rails generate migration addTeamID erzeugt eine neue Datei, sie befindet sich in db/migrate/<rnd-number>_add_team_id.rb. Diese muss nach der Bearbeitung folgenden Inhalt aufweisen:

class add_team_id < ActiveRecord::Migration

  def up
    add_column :players, :team_id, :integer
    add_column :trainers, :team_id, :integer
  end

  def down
    remove_column :players, :team_id
    remove_column :trainers, :team_id
  end

end

In dem Code ist sowohl eine up, als auch eine down Methode zu erkennen. Der Befehl rake db:migrate führt alle up Methoden von Migrationen aus, die im aktuellen System noch nicht ausgeführt wurden. Die down Methode dient dazu um Migrationen rückgängig zu machen.

Validatoren Bearbeiten

Falls über das Interface neue Objekte hinzugefügt werden, dann ist es nicht zwangsläufig notwendig, dass Attribute Werte erhalten. Nun kann es aber passieren, dass Attribute gesetzt werden sollen. Dazu gibt es in Rails Validatoren, welche bei der Erzeugung eines neuen Objekts überprüfen, ob das neue Objekt valide ist. Beispielshaft sind im folgenden Validatoren für den Spieler angelegt wurden.

class Player < ActiveRecord::Base
  validates_presence_of :name, :number
  validates :number, :numericality => true
  validates :team_id, :numericality => true
  
  belongs_to :team
end

Veränderung der Ansicht Bearbeiten

Beim Anlegen eines neuen Spielers tritt mit den aktuellen Validatoren ein Problem auf. Es geht nicht mehr! Der einfache Grund: Es wird eine TeamID als Input erwartet, doch das Interface stellt eine Möglichkeit dazu bereit. Eine Lösung wäre eine neue Textbox, worin der Nutzer die ID des Teams eintragen kann. Für ein Produktivsystem ist das keine akzeptable Lösung. Ein Auswahlmenu ist eine elegantere Lösung. Die Ansicht, welche gerendert wird, ist in der Datei app/views/players/_form.html.erb definiert. Durch die Zeile <%= f.select(:team_id, @teams.collect{ |t| [t.name, t.id] }) %> wird eine Auswahlbox gerendert, welche alle angelegten Teams im System enthält und bei der Erzeugung eines Spielers die ausgewählte TeamID als Inputparameter mitsendet.

<%= form_for(@player) do |f| %>
  <% if @player.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(@player.errors.count, "error") %> prohibited this player from being saved:</h2>

      <ul>
      <% @player.errors.full_messages.each do |msg| %>
        <li><%= msg %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= f.label :name %><br />
    <%= f.text_field :name %>
  </div>
  <div class="field">
    <%= f.label :number %><br />
    <%= f.number_field :number %>
  </div>
  <div class="field">
    <%= f.label :team_id %><br />
    <%= f.select(:team_id, @teams.collect{ |t| [t.name, t.id] }) %>
  </div>
  <div class="actions">
    <%= f.submit %>
  </div>
<% end %>

Die Variable @teams, welche alle Teams im System enthalten soll, muss noch gesetzt werden. Dazu muss in den entsprechenden Kontroller die folgende Zeile in die new Methode eingefügt werden @teams = Team.all. Auszugsweise sollte der Kontroller dann folgendermaßen aussehen:

class PlayersController < ApplicationController

  ...

  # GET /players/new
  # GET /players/new.json
  def new
    @teams = Team.all
    @player = Player.new

    respond_to do |format|
      format.html # new.html.erb
      format.json { render json: @player }
    end
  end

  ...

end

Nun können Spieler angelegt werden, aber die Informationen werden in der Spielerübersicht und der Detailansicht nicht angezeigt, dazu müssen die beiden Dateien app/views/teams/index.html.erb und app/views/teams/show.html.erb so erweitert werden:

<h1>Listing players</h1>

<table>
  <tr>
    <th>Name</th>
    <th>Number</th>
    <th>Team</th>
    <th></th>
    <th></th>
    <th></th>
  </tr>

<% @players.each do |player| %>
  <tr>
    <td><%= player.name %></td>
    <td><%= player.number %></td>
    <td><%= player.team.shortcut %></td>
    <td><%= link_to 'Show', player %></td>
    <td><%= link_to 'Edit', edit_player_path(player) %></td>
    <td><%= link_to 'Destroy', player, confirm: 'Are you sure?', method: :delete %></td>
  </tr>
<% end %>
</table>

<br />

<%= link_to 'New Player', new_player_path %>
<p id="notice"><%= notice %></p>

<p>
  <b>Name:</b>
  <%= @player.name %>
</p>

<p>
  <b>Number:</b>
  <%= @player.number %>
</p>

<p>
  <b>Team:</b>
  <%= @player.team.name %>
</p>


<%= link_to 'Edit', edit_player_path(@player) %> |
<%= link_to 'Back', players_path %>

Neue Spieler können nun angelegt und die Teams über eine Selectbox ausgewählt werden. Die Daten zu einem Spieler werden sowohl in der Übersicht, in Form der Abkürzung des Teams, als auch in der Detailansicht, in Form des ganzen Namens, angezeigt. Das Letzte was in diesem kleinen Tutorial behandelt wird, ist eine Ansicht innerhalb des Teams, sodass auf der Detailansichtsseite alle Mitglieder einer Mannschaft angezeigt werden. Hierzu kann die Datei app/views/teams/show.html.erb so verändert werden:

<p id="notice"><%= notice %></p>

<p>
  <b>Name:</b>
  <%= @team.name %>
</p>

<p>
  <b>City:</b>
  <%= @team.city %>
</p>

<p>
  <b>Players:</b>
  <% @team.players.all.each do |p| %> 
    <%=link_to p.name, p %> </br>	
  <% end %>

</p> 


<%= link_to 'Edit', edit_team_path(@team) %> |
<%= link_to 'Back', teams_path %>

Zu erkennen ist, dass nicht nur alle Spieler aufgelistet werden, sondern auch Links zu den Spielern gerendert werden.

Designpattern Bearbeiten

Probleme bei Software ohne Pattern Ziel der Nutzung von Pattern
Instabil
Komplex
Undurchsichtig
Wartungsressistent
Codewiederholungen
Verständlich
Flexibel
Effektiv
Wartbar
Testbar

RESTful Bearbeiten

Die Architektur der Controller sollte dem REST-Interface entsprechen.

Probleme bei der folgenden Implementierung

  • Entspricht nicht dem REST-Interface
  • CoC wurde nicht eingehalten / Namespaces in RoR würde aufrufe vereinfachen
class GameEventController < ApplicationController

def add_goal_to_stats	def exchange_player	def start_game
end			end			end

def add_comment		def add_injury		def end_game
end			end			end

def destroy_comment	def edit_comment	def add_goal_to_player
end			end			end

end

Lösungsansatz:

  • Erstellung neuer Controller und Auslagerung der Methoden zum entsprechenden Controller (REST entsprechend)
class GameController < ApplicationController

class PlayerController < ApplicationController

class CommentController < ApplicationController

class GameStatsController < ApplicationController

class TeamController < ApplicationController

Model Bearbeiten

Im folgenden werden drei Refactorings für das Model eines Objekts vorgestellt. Diese sind nur ein Teil von Verbesserungsmöglichkeiten.

Law of Demeter Bearbeiten

Problem: In der View wird gegen das LoD verstoßen, welches besagt, dass in objektorientierter Softwareentwicklung Objekte nur mit Objekten in ihrer unmittelbaren Umgebung kommunizieren und interagieren sollen. So wird die Kopplung zwischen Objekten verringert und die Wartbarkeit erhöht.

class Person < ActiveRecord::Base
   belongs_to :team
end
<%= @person.team.position %>
<%= @person.team.name %>

In der Ansicht wird in diesem Beispiel u.a. auf den Namen des Teams zu dem ein Spieler gehört zugegriffen. Bei diesem Zugriff wird gegen das LoD verstoßen. Ein Lösungsmöglichkeit wird im nachfolgenden Code aufgezeigt.

class Person < ActiveRecord::Base
   belongs_to :team
   delegate :name, :position,
		:to => :team,
		:prefix => true
end
<%= @person.team_position %>
<%= @person.team_name %>

Durch den delegate Befehl können Variablen des Teams so behandelt werden, als ob sie innerhalb der Person gespeichert sind. RoR übernimmt die Verknüpfung der Objekte über die ID und leitete Anfragen, egal ob lesend oder schreibend, an das richtige Objekt weiter, in dem Fall das Team.

Finder-Methoden Bearbeiten

Problem: Methoden, welche zum Auffinden von Objekten genutzt werden, gehören in das Model des Objekts, welches gefiltert wird.

class Team < ActiveRecord::Base
   has_many :players

   def find_players_without_card
      self.players.find(:all, :conditions => {:cards => 0})
   end
end

class Player < ActiveRecord::Base
  belongs_to :team
end
class Controller < ApplicationController
   def index
      @players = @team.find_players_without_card
   end
end

Innerhalb des Codes existiert eine Methode, welche alle Spieler eines Teams heraussucht, die noch keine Karte erhalten haben. Im Controller des Teams wird diese Methode aufgerufen, um in der Ansicht weiter verarbeitet werden zu können. Wie bereits in der Problemstellung angesprochen, gehören Filtermethoden in das zu filternde Objekt. Im Folgenden ist eine Möglichkeit zur Lösung des "Problems" aufgezeigt.

class Team < ActiveRecord::Base
   has_many :players
end

class Player < ActiveRecord::Base
   belongs_to :team

   named_scope :without_card, :conditions => {:cards => 0}
end
class Controller < ApplicationController
   def index
      @players = @team.players.without_card
   end
end

Durch den named_scope im Playermodel ist es möglich im Controller des Teams eine "Methode" auf allen Spielern aufzurufen und so nur die Spieler zu erhalten, welche der Bedingung genügen, dass sie keine Karte besitzen.

Code-Duplikation Bearbeiten

Jeder Softwareentwickler möchte es möglichst vermeiden, Code mehrmals zu schreiben, da Veränderungen an mehreren Stellen vorgenommen werden müssen und der Wartungsaufwand enorm steigt. Eine nicht ganz offensichtliche Code-Duplikation ist im folgenden Beispiel dargestellt.

class Player < ActiveRecord::Base
  validate_inclusion_of :status, :in => ['ready', 'injured', 'lazy']

  def self.all_ready                                          def ready?
    find(:all, :conditions => {:status => 'ready'})          self.status == 'ready'
  end                                                         end

  def self.all_injured                                        def injured?
    find(:all, :conditions => {:status => 'injured'})          self.status == 'injured'
  end                                                         end

  def self.all_lazy                                           def lazy?
    find(:all, :conditions => {:status => 'lazy'})             self.status == 'lazy'
  end                                                         end

end

Betrachtet man die Methoden, stellt man fest, dass der Code in den Methoden ähnlich ist. Wenn man nun einen weiteren Status hinzufügt, dann müsste man zwei neue Methoden, entsprechend dem Muster, hinzufügen. Im Folgenden wird Meta-Programmierung genutzt, um aus einer Liste von Status Methoden automatisch zu erzeugen.

class Player < ActiveRecord::Base
   STATUS = ['ready', 'injured', 'lazy']
   validate_inclusion_of :status, :in => STATUS
   
   class << self                                            STATUS.each do |s|
      STATUS.each do |s|                                      define_method "#{s}?" do
         define_method "all_#{s}" do                            self.status == s
            find(:all, :conditions => { :status => s})        end
         end                                                end
      end
   end

end

Durch diese Form der Meta-Programmierung ist es nun ohne Weiters möglich weitere Status einzutragen. Die entsprechenden Methoden sind automatisch im System verfügbar. Code-Duplikationen wurden weiterhin aus dem Model entfernt.

Controller Bearbeiten

Im Folgenden werden zwei Möglichkeiten vorgestellt, wie Controller schlankgehalten werden. Dies ist ein "good-practise" in Ruby on Rails.

Don't repeat yourself Bearbeiten

In dem nachfolgenden Controller-Code ist das Problem an Codewiederholungen beispielhaft dargestellt.

class PlayerController < ApplicationController

  def show
    @player = current_user.find_favorite
  end

  def edit
    @player = current_user.find_favorite
  end

  def update
    @player = current_user.find_favorite
    @player.update_like
  end

  def destroy
    @player = current_user.find_favorite
    @player.destroy
  end

end

Auffällig ist, dass innerhalb der Methoden immer die gleiche Zeile Code steht. Diese kann in Ruby on Rails mittels des Befehls before_filter vor jede Methode geschaltet werden. Hierbei kann definiert werden, vor welchen Methoden der Filter ausgeführt wird.

class PlayerController < ApplicationController

   before_filter :find_post, :only => [:show, :edit, :update, :destroy]

   def destroy			def update
      @player.destroy		   @player.update_like
   end				end

   def find_post
      @player = current_user.find_favorite
   end

end

Durch dieses Refactoring ist es weiterhin nicht notwendig die zwei, dann leeren, Methoden show und edit im Controller zu definieren. Dadurch ist der Controllercode kleiner geworden.

Inherited resources Bearbeiten

Im folgenden Beispiel gäbe es zwei Möglichkeiten Verbesserungen vorzunehmen. Zum Einen ist eine before_filter Methode anwendbar. Aber schaut man sich den Code etwas genauer an, dann stellt man noch was anderes fest.

class TeamController < ApplicationController
   
  def index
    @team = Team.all
  end

  def new
    @team = Team.new
  end

  def show
    @team = Team.find(params[:id])
  end

  def create
    @team.create(params[:id])
  end

  def edit
    @team = Team.find(params[:id])
  end

  def update
    @team = Team.find(params[:id])
    @team.update_attributes(params[:team])
  end

  def destroy
    @team = Team.find(params[:id])
    @team.destroy     
  end

end

Bei näherer Betrachtung fällt auf, dass die Methoden die zu erwartenden Funktionen implementieren und zwar ohne Extras. In der ersten Zeile ist zu erkennen, dass der TeamController ein ApplicationController ist. Da die Methoden der Standartimplementierung entsprechen, ist es nicht notwendig sie zu implementieren, daraus folgt der folgende abgespeckte Controller.

class TeamController < ApplicationController

end

Nun kann es sein, dass nach einer erfolgreichen Erstellung eines Objekts die Umleitung nicht wie vorgesehen von statten gehen soll. Hierzu nutzt man die vererbte Methode und verändert nur die Umleitung, so wird Code-Duplikation vermieden. Ein Beispiel für eine veränderte Umleitung nach der Erstellung eines Objekts ist in folgender Methode dargestellt.

def create
   create! do |success, failure|
      success.html { redirect_to post_url(@team) }
      failure.html { redirect_to root_url }
end

View Bearbeiten

Im folgenden Abschnitt werden Verbesserungsmöglichkeiten für die View dargestellt. Als Grundregel für alle Ansichten gibt immer NO LOGIC IN VIEW. Folgend sind zwei Arten dargestellt Logik aus der View in andere Teile des Codes zu verschieben.

Helper Bearbeiten

Für jede View gibt es in Ruby on Rails ein Helperfile. Methoden, welche innerhalb dieser Datei definiert werden, können aus der View heraus aufgerufen werden. Beispiel für Logik in der View zur Anzeige einer Statusauswahlbox für Spieler.

<%= select_tag :state, options_for_select (   [[(:lazy), "draft"],
	                                      [(:injured), "injured"]], 
					      params[:default_state]      )   %>

Diese Methode sollte wie folgt in den Helper ausgelagert werden. Dieser befindet sich unter dem Pfad app/helpers/<modelname>_helper.rb.

def options_for_team_state (default_state)
   options_for_select (   [[(:lazy), "draft"],
			  [(:injured), "injured"]], 
			  default_state               )
end

Dadurch kann die View angepasst und die Logik ausgelagert werden.

<%= select_tag :state, options_for_team_state ( params[:default_state]  )  %>

Controller Bearbeiten

Logik kann aus der View auch in den entsprechenden Controller ausglagert werden. Beispielshaft ist im folgenden Viewcode dargestellt, welcher für jedes Team den Namen und die Abkürzung anzeigt.

<% @team = Team.find(:all) %>
<% @team.each do |team| %>
   <%= team.name %>
   <%= team.shortcut %>
<% end %>

Innerhalb dieses Codeabschnitts hat die erste Zeile nichts in der View zu suchen. Natürlich muss über die Variable iteriert werden, damit die Informationen für jedes Team angezeigt werden kann, doch hinter jeder Ansicht steckt eine Controller-Methode. Innerhalb dieser sollte die Variable gesetzt werden.

class TeamController < ApplicationController

  def index
    @team = Team.find(:all)
  end

end

Dadruch kann auf die erste Zeile innerhalb des View-Codes verzichtet werden und die Logik ist aus der View verschwunden.

Ruby on Rails im Einsatz Bearbeiten

Folgende Unternehmen / Webseiten nutzen Ruby on Rails:

  • Xing
  • Basecamp
  • github
  • Groupon
  • Twitter

Zitate[4] Bearbeiten

  • "Rails is the killer app for Ruby." Yukihiro Matsumoto, Creator of Ruby
  • "After researching the market, Ruby on Rails stood out as the best choice. We have been very happy with that decision. We will continue building on Rails and consider it a key business advantage." Evan Williams, Creator of Blogger, ODEO, and Twitter

Quellen & weiterführende Links Bearbeiten

  1. VirtualRails, vorinstalliertes "Ruby on Rails"-System in einer Ubuntu VM.
  2. Railsinstaller, Installationspaket für Windows und MacOS.
  3. Installationsguide für einen Ubuntu VM mit RoR, ausführlicher Guide mit Ruby on Rails Tutorial.
  4. Quotes, Ruby on Rails Quotes