Applied Web Heresies: ETech 2007


I really wanted to go to Putting the Fun in Functional: Applying Game Mechanics to Social Software by Amy Jo Kim, but my inner geek won out and I went to Applied Web Heresies with Avi Bryant (slides). I hope someone else took good notes.

The basis for the talk is Seaside, a web framework for Smalltalk that Avi wrote several years ago. The problem with Seaside is you're not going to use it! There are a lot of interesting ideas in Seaside that people should know, so this tutorial is way of spreading the ideas outside of Smalltalk.

Avi suggests using Seaside as a recipe. He tells the story of Primo Levy and onions in the varnish. There are a lot of "best practices" of Web development that were good decisions at the time but which are no longer needed.

Here's the basic requirement list:

  • OOP
  • Servlet-style app server
  • Blocks and closures

"First thing we do, let's kill all the templates." Templates were a good idea that have become useless and harmful. They're constraining or they're a bad programming language. The is a belief that templates are useful for model/view separation. But HTML is now a semantic layer and CSS is the real view layer (see Zen Garden). This is an interesting point of view and one I have a hard time arguing with.

So, you need a rich library for generating HTML (see AWT for inspiration). Each widget has a render method that gets a canvas passed to it.

The first thing we did was write a small framework to get things going. Different groups were working in different languages. I used Ruby (even though I don't really know Ruby). Here's mine:

require 'webrick' 
require 'stringio' 
server = WEBrick::HTTPServer.new(:Port => 2000) 
server.mount_proc("/heresy"){|req, res| Application.new.handle(req, res)} 
server.mount_proc("/favicon.ico"){|req,res| res.status = 404} 

class Application   
    def handle(req, res) 
        canvas = Canvas.new(res) 
        render_on(canvas) 
    end 
    
    def render_on(html) 
        html.heading("Hello World") 
    end 
end 
  
class Canvas 
  

  
  def initialize(res)
    @res = res
    res['Content-Type'] = 'text/html'
  end
  
  def heading(str, level=1) 
    @res.body = "<h1>"+ str + "</h1>"
  end 
  
end

trap("INT"){ server.shutdown } 
server.start

We're omitting tag objects here and just spitting out HTML, but a real API should do that.

The next heresy that Avi proposes is that sessions are two valuable to persist. In general, you can never marshall and unmarshall the stuff in memory reliably. All the good stuff's in memcached anyway. Keep the session in the memory of the application server

What about load balancing? Use sticky sessions. YAGNI Application servers going down is unusual. Users losing session data is a minor annoyance. Live with it.

NeXT's WebObjects is the inspiration for much of what Avi's done.

Next we move from our HelloWorld application to something that has stateful sessions. We're going to build a registry of sessions and push the canvas down a level so that the sessions hold the canvas. My Ruby coding skills were not up to keeping up, so I cheated and grabbed Avi's code (which includes a much more usable Canvas object):

require 'webrick' 
require 'stringio' 
server = WEBrick::HTTPServer.new(:Port => 2000) 
server.mount_proc("/heresy"){|req, res| Application.new.handle(req, res)} 
server.mount_proc("/favicon.ico"){|req,res| res.status = 404} 

class Registry 
    def initialize 
        @items = [] 
    end 
    
    def register(item) 
        @items << item 
        (@items.size - 1).to_s 
    end 
    
    def find(key) 
        @items[key.to_i] 
    end 
end


class Application   
    @@sessions = Registry.new
    
    def handle(req, res)
        session_cookie = req.cookies.detect{|c| c.name == "heresy"}
        if(session_cookie)
            session = @@sessions.find(session_cookie.value)
        end
        
        unless session
            session = Session.new
            res.cookies << WEBrick::Cookie.new("heresy", @@sessions.register(session))
        end
        
        session.handle(req, res)
    end
end 
  

class Canvas
    def initialize
        @io = StringIO.new
    end
    
    def tag(name, attrs={}, &proc)
        @io << "<"
        @io << name
        attrs.each{|k,v| @io << " #{k}='#{v}'"}
        @io << ">"
        proc.call
        @io << "</#{name}>"
    end
    
    def text(str)
        @io << str
    end
    
    def heading(txt, level=1)
        tag("h#{level}"){text(txt)}
    end
    
    def string
        @io.string
    end
end

class Session 
    def initialize
        @count = 0 
    end 

        
    def handle(req, res)
        @count += 1
        
        html = Canvas.new
        render_on(html)
        
        res.body = html.string
        res["Content-Type"] = "text/html"
    end
    
    def render_on(html)
        html.heading("Hello World: #{@count}")
    end

end

trap("INT"){ server.shutdown } 
server.start

What's left to do on session? Lots, including:

  • Unguessable session keys
  • Session keys as query params
  • Session locks
  • Expiration from registry

The next piece of heresy: meaningful URLs don't carry enough meaning. People put a lot of energy trying to create names that describe the particular point in an application. Not every page in an application is a meaningful part of an API. URLs, particularly query parameters are classic place where people repeat themselves. Don't repeat yourself. Lots of meaningless names create namespace collisions.

Avi proposes using a registry to store IDs against page names. The inspiration for this is TCL/TK: Register closures/blocks as callback objects. Here's the code that implements this refinement (I did it almost all by myself--I had to peak to see how WeBrick handled requests):

require 'webrick' 
require 'stringio' 
server = WEBrick::HTTPServer.new(:Port => 2000) 
server.mount_proc("/heresy"){|req, res| Application.new.handle(req, res)} 
server.mount_proc("/favicon.ico"){|req,res| res.status = 404} 

class Registry 
  def initialize 
    @items = [] 
  end 
  
  def register(item) 
    @items << item 
    (@items.size - 1).to_s 
  end 
  
  def find(key) 
    @items[key.to_i] 
  end 
end


class Application   
  @@sessions = Registry.new
  
  def handle(req, res)
    session_cookie = req.cookies.detect{|c| c.name == "heresy"}
    if(session_cookie)
      session = @@sessions.find(session_cookie.value)
    end
    
    unless session
      session = Session.new
      res.cookies << WEBrick::Cookie.new("heresy", @@sessions.register(session))
    end
    
    session.handle(req, res)
  end
end 


class Canvas
  
  def initialize(cbs)
    @io = StringIO.new
    @callbacks = cbs
  end
  
  def tag(name, attrs={}, &proc)
    @io << "<"
    @io << name
    attrs.each{|k,v| @io << " #{k}='#{v}'"}
    @io << ">"
    proc.call
    @io << "</#{name}>"
  end
  
  def text(str)
    @io << str
  end
  
  def link(name, &proc)
    id = @callbacks.register(proc)
    tag("a",{ :href=>"?#{id}"}){text(name)}
  end
  
  def space
    @io << " "
  end
  
  def heading(txt, level=1)
    tag("h#{level}"){text(txt)}
  end
  
  def string
    @io.string
  end
end

class Counter 
  def initialize
    @count = 0 
  end 
  
  def render_on(html) 
    html.heading("Hello World: #{@count}") 
    html.tag("p"){html.text("this is fun!!!")}
    html.link("--"){@count -= 1} 
    html.space 
    html.link("++"){@count += 1} 
  end 
end

class Session 
  def initialize 
    @callbacks = Registry.new 
    @root = Counter.new 
  end 
  
  def handle(req, res)
    
    req.query.each do |k,v|
      if callback = @callbacks.find(k)
        callback.call(v)
      end
    end

    html = Canvas.new(@callbacks)
    @root.render_on(html)
    
    res.body = html.string
    res["Content-Type"] = "text/html"
  end
end

trap("INT"){ server.shutdown } 
server.start

As we finished this segment, I said "I get what we're doing, but why are we doing it?" In other words, why go to all this trouble to create a registry of callback methods and URLs that point to it uniquely? The answer is in the next little exercise. Here's the code I produced:

require 'webrick' 
require 'stringio' 
server = WEBrick::HTTPServer.new(:Port => 2000) 
server.mount_proc("/heresy"){|req, res| Application.new.handle(req, res)} 
server.mount_proc("/favicon.ico"){|req,res| res.status = 404} 

class Registry 
  def initialize 
    @items = [] 
  end 
  
  def register(item) 
    @items << item 
    (@items.size - 1).to_s 
  end 
  
  def find(key) 
    @items[key.to_i] 
  end 
end


class Application   
  @@sessions = Registry.new
  
  def handle(req, res)
    session_cookie = req.cookies.detect{|c| c.name == "heresy"}
    if(session_cookie)
      session = @@sessions.find(session_cookie.value)
    end
    
    unless session
      session = Session.new
      res.cookies << WEBrick::Cookie.new("heresy", @@sessions.register(session))
    end
    
    session.handle(req, res)
  end
end 


class Canvas
  
  def initialize(cbs)
    @io = StringIO.new
    @callbacks = cbs
  end
  
  def tag(name, attrs={}, &proc)
    @io << "<"
    @io << name
    attrs.each{|k,v| @io << " #{k}='#{v}'"}
    @io << ">"
    proc.call
    @io << "</#{name}>"
  end
  
  def text(str)
    @io << str
  end
  
  def link(name, &proc)
    id = @callbacks.register(proc)
    tag("a",{ :href=>"?#{id}"}){text(name)}
  end
  
  def space
    @io << " "
  end
  
  def heading(txt, level=1)
    tag("h#{level}"){text(txt)}
  end
  
  def string
    @io.string
  end
end

class MultiCounter
  def initialize
    @counters = [Counter.new, Counter.new, Counter.new]
  end
  
  def render_on(html)
    @counters.each{|ea| ea.render_on(html)}
  end
end

class Counter 
  def initialize
    @count = 0 
  end 
  
  def render_on(html) 
    html.heading("Hello World: #{@count}") 
    html.tag("p"){html.text("this is fun!!!")}
    html.link("--"){@count -= 1} 
    html.space 
    html.link("++"){@count += 1} 
  end 
end

class Session 
  def initialize 
    @callbacks = Registry.new 
    @root = MultiCounter.new 
  end 
  
  def handle(req, res)
    
    req.query.each do |k,v|
      if callback = @callbacks.find(k)
        callback.call(v)
      end
    end

    html = Canvas.new(@callbacks)
    @root.render_on(html)
    
    res.body = html.string
    res["Content-Type"] = "text/html"
  end
end

trap("INT"){ server.shutdown } 
server.start

The only changes here are the addition of a MultiCounter class that creates a three item array of counter objects and a render_on object that calls render_on on each member of the array. Then, instead of putting a Counter object at the root, I put a MultiCount object. And you get three of the Counter widgets on the same page. This shows how easy it is to use the objects as Web widgets. Avi says you can't do this with named URLs.

Avi remarks that pages are a lousy unit of reuse and partials ain't much better. "But every piece of my application is a beautiful and unique snowflake." Again, with the CSS.

What aren't we doing?

  • Splitting callback registries up by page view
  • Tracking the back-button
  • Redirecting after side-effects
  • Forms!

Overall this is a very interesting set of thoughts about Web development. Clearly he's espousing a lot of new ideas, which generated a lot of "How do you ...?" questions. Very good stuff. This tutorial made me think, which is the best kind.


Please leave comments using the Hypothes.is sidebar.

Last modified: Thu Oct 10 12:47:19 2019.