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]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]- 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. - 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. - 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.
- rails generate scaffold Team name:string shortcut:string
- rails generate scaffold Player name:string number:integer
- rails generate scaffold Trainer name:string
- 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
- 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:
- Basecamp
- github
- Groupon
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]- ↑ VirtualRails, vorinstalliertes "Ruby on Rails"-System in einer Ubuntu VM.
- ↑ Railsinstaller, Installationspaket für Windows und MacOS.
- ↑ Installationsguide für einen Ubuntu VM mit RoR, ausführlicher Guide mit Ruby on Rails Tutorial.
- ↑ Quotes, Ruby on Rails Quotes