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.