Skip to content

Latest commit

 

History

History
248 lines (190 loc) · 6.6 KB

Model.mdown

File metadata and controls

248 lines (190 loc) · 6.6 KB

The Story of how I built a Model

So in this text, I will actually build a very simple Webapp including data that is stored on the server. I will not use a database, so as not to complicate things.

The project is the following: I like to write stories in my free time, but with my notes all over time and space (I don't know, where they're getting lost), it gets quite annoying. Also the notes are completely disorganized, but that we will fix later.

So the idea is to have a web application that stores my stories. For now, lets assume, that the stories only have a title and a body.

In ruby, classes don't need no stupid type declarations, you just call it @variablename and the '@' tells us that it has instance scope and the variable can hold a string, a number, or any object. Similarily @@classvariable has class scope. The first, we won't even see, because Ruby can define getters and setters automatically with the attr_accessor function, to which you give symbols - strings that signify a name. The minimal class, that has a publicly accessable text and title is

class Text
  attr_accessor :text, :title
end

But we want a constructor, which is called initialize in ruby and some management of all the entities, e.g. displaying all Texts.

The constructor only passes the arguments to the setters:

class Text
  attr_accessor :text, :title

  def initialize( title, text )
    self.text  = text
    self.title = title
  end
  
  # ...
end

def defines methods (or functions) in the current scope and self is always bound to the current object.

Now we need a class scoped set of all the Texts, preferably only the ones we save (think of tests, where we generate Texts that should not appear in the set).

class Text
  # ...
  @@texts = []
  
  def save
    @@texts << self unless @@texts.include? self
  end
end

Ok, that's a bit to digest:

  • @@texts = [] initializes the class scoped variable with an empty list literal.

  • save does not take any arguments, we don't need to write the parens in method declarations.

  • The shift operator adds the element to the list.

  • The postfix unless (or if) is an idiomatic way to write one-line conditional statements. It is equivalent to

      unless @@texts.include? self
        @@texts << self
      end

    but is much more readable.

  • include? is a valid method name and indeed idiomatic for predicates (methods, that return true or false). Again, you don't need to write the parens on the method calls (fun fact: attr_accessor is a method too).

That said, the delete method should be simple

class Text
  # ...
  def delete
    @@texts.delete self
  end
  
  def self.all
    @@texts
  end
end

but the all method needs some explaining again:

  • Remember how self is always bound to the current object? In the body of a class, that object is the class you're currently defining! That's why def self.all defines the method all in the class, not the instances.

Finally, lets add a unique identifier with what we have learned:

class Text
  attr_accessor :text, :title
  
  @@text_count = 0
  
  def initialize( title, text )
    self.title = title
    self.text = text
    @id = @@text_count
    @@text_count += 1
  end
  
  def id
    @id
  end

  # ...
end

and add a lookup:

  def self.by_id id
    @@texts.detect {|txt| txt.id == id}
  end
  • The detect method takes a block, that is an executable part of code. This block is evaluated and the method returns the first element, for which the block returns true. In the same fashion, there are
    • select: returns a list of all the elements, for which the block is true.
    • collect: returns a list of all the return values for the block executed on the elements
    • inject: ... Lets not get into that.

This is all there is to do for the model part.

Now we can present that with Sinatra, we need to require the models file as well and pass the variables.

require 'rubygems'
require 'sinatra'
require './models.rb'

get "/" do
  haml :index, :locals => {:texts => Text.all}
end

get "/text/:id" do
	text = Text.by_id(params[:id].to_i)
  
  return 404 if text.nil?

  haml :show, :locals => {:text => text}
end
  • Note that I converted the parameter id to an integer before handing it to the #by_id method.
  • I can return the 'not found' code 404 directly, if... well... I didn't find the text with the id.
  • We can extract parameters from a get request (or any request really), by marking the variable with a colon (like a symbol).

Finally, we can display all that with two haml files:

# index.haml
%ul
  - for text in texts
    %li
      %a( href="/text/#{text.id}" )= text.title

This displays links to all texts in an unsorted list showing their title.

  • In a ruby string, you can put a ruby expression between #{ and }, and it will insert the value into the string.
  • In HAML, you can define attributes of a block (in this example a) by putting them in parens. The right hand side of the equal signs are ruby expressions.

Finally, we need to add a file to show a single text:

# show.haml
%h1
  = text.title
%div
  :markdown
    #{text.text}
  • In HAML, when we prepose a paragraph with a filter name, like markdown, then that is processed through that. Note however, that prefixing something with = does no longer insert it.

Of course, if we run that program, it will not show anything, in lack of any texts and also a way to add them. For the moment, we can fix that by adding a fixture to the controller file:

require 'rubygems'
require 'sinatra'
require 'rdiscount'
require './models.rb'

Text.new("Humpty Dumpty", "Humpty Dumpty sat on the wall...").save

get "/" do
  haml :index, :locals => {:texts => Text.all}
end

get "/text/:id" do
	text = Text.by_id(params[:id].to_i)

	return 404 if text.nil?

  haml :show, :locals => {:text => text}
end
  • Ruby objects are initialized by the #new method of the class, which in turn runs the #initialize method of the new object.
  • There is no magic "fixture" file, the fixture is just some code that adds objects to the collection.

That concludes this. We can now build a simple model and know how to build a in-memory database. The rest should be a breeze, right?

Next time, I'll show you, how to make the application understand request like "add this text" and "this text sucks, change it" and also "this text sucks so much, forget it ever existed". You will see, that the designers of the HTTP protocol had something like that in mind.