Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Comparing changes

Choose two branches to see what's changed or to start a new pull request. If you need to, you can also compare across forks.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also compare across forks.
base fork: steveklabnik/maze_solver
base: procedural
...
head fork: steveklabnik/maze_solver
compare: objectoriented
  • 19 commits
  • 7 files changed
  • 0 commit comments
  • 1 contributor
Commits on Dec 07, 2011
@steveklabnik Extract a simple maze class.
This doesn't do a whole lot yet. But it does give me an object to
handle the current state, as well as actually performing the transition
between states.
f8ef051
@steveklabnik Moving top-level methods inside of Room.
Top level methods are icky. Besides, they should really be helping the
Room do its job, anyway. Notice how the main logic is a bit more clear
now, with the can_go? and finished? methods.
651a45a
@steveklabnik Hiding current_xml.
There's no reason to expose this, and we weren't even using it outside
of the Room.
387f02c
@steveklabnik making current_xml lazily loaded
We really only change current_xml when we change URI. We just want the
xml to match the current URI anyway, so why not just lazily load it?
5b6ec81
@steveklabnik 'curry' the xml
Now that we only ever use it as a parameter, we can actually remove it
as a parameter and put it directly into our xml methods.

This is a reference to curried functions in Haskell:

add :: Integer -> Integer -> Integer
add x y = x + y

add_five = add 5

add_five is a 'curried' version of foo. You can do this in Ruby too,
but it's so much more elegant in Haskell.
42c8024
@steveklabnik Extract Request class.
Really, requesting stuff from net::http has nothing else to do with
anything else, so we'll sequester it off into its own deal-e-o.
e6bcc2d
@steveklabnik Normalizing names of xml_* methods
One of the things I like to do before extracting a class is make the
names all the same. This makes it easier to extract.
424e8ea
@steveklabnik Extracting XML from xml_*
This is pretty straightforward, after looking at the last commit. We
just move all of the methods that started with xml into a class.

After all, xml_foo(xml) is really just xml.foo in disguise...
206a9e1
@steveklabnik Simple instance variable caching.
The problem with the last commit is that we ended up making a LOT MORE
requests than we should have, due to making excessive XML objects. Then
inside of the XML object, we referred to the virtual attribute that
makes a Request multiple times. Ack! Now we don't do that any more.

Note that HTTP caching would be the 'right' thing to do here. I don't
want to get into that yet, so we're just using instance variables.
bf52b1f
@steveklabnik Cleanup and rename various things.
Okay! These classes are pretty solid. Basically, this commit renames
some methods, classes, whatever. No major changes. The most complicated
bit is using Forwardable to make the Request class a bit nicer.
6087bd6
@steveklabnik Breaking up into files.
This is almost a real project or something. Lots of little files to go
with lots of little classes.
46c4bd8
@steveklabnik README -> Markdown
Before, it was just some text. Now we introduce a tiny bit of
Markdown so it shows up nicer on GitHub.
11412d0
@steveklabnik Extract Solver object.
The contents come from about 90% of the loop. Which makes sense, our
solver should decide what to do with the maze.
2bbe22c
@steveklabnik Move Solver into its own file, simplify loop.
Two things: I moved the solver out into its own file. Self-explanatory.
Secondly, a loop with only one statemnt is better written in one line.
feed719
@steveklabnik rename total_steps
We refer to it as total_steps outside of the class, and steps inside.
Let's make that more consistent.
a4436d0
@steveklabnik Various renaming and extractions.
This is just a bunch of little extract methods, renamings, etc.
Nothing complicated.
9f474d6
@steveklabnik reworking requires.
There's no reason to require everything up front. With this, each file
requires the things it needs. If we were *cough* writing tests, this
would make each file testable on its own.
aba7b26
@steveklabnik small style changes
I like to use ' instead of " wherever possible, and not use () unless
it's required.
3b8a28a
@steveklabnik Beefing up README.
Okay, now that we're done, let's make sure we have some good documentation.
4b4ca90
View
50 README.md
@@ -1,3 +1,49 @@
-Some experiments with maze+xml.
+# Maze Solver
-You're gonna need to 'gem install nokogiri' to get this going.
+Primarily, this repository is a hypermedia client for applications that
+serve [maze+xml](http://amundsen.com/media-types/maze/).
+
+Secondarily, this repository demonstrates refactoring from a simple procedural
+Ruby program to an OO one.
+
+Well, as [much as 'refactoring' is without any tests](http://hamletdarcy.blogspot.com/2009/06/forgotten-refactorings.html),
+anyway.
+
+## Running
+
+First of all, I ran this with Ruby 1.9.3. 1.9.2 should work just fine, too.
+
+Secondly, you need some [nokogiri](http://nokogiri.org) to get going, so
+`gem install` that right-off.
+
+Finally, just `ruby solver.rb` to solve the maze.
+
+## The procedural version
+
+I started with a simple task: "request some XML with Net::HTTP" and slowly built
+up some code. I pulled out some methods, and before you know it, I had a little
+procedural maze solver.
+
+You might be intereseted in following from the beginning. If so, check it [here](https://github.com/steveklabnik/maze_solver/tree/beginning)
+
+You can read along with the commits to see how I built it by going [here](https://github.com/steveklabnik/maze_solver/compare/beginning...procedural).
+
+You can see this version [here](https://github.com/steveklabnik/maze_solver/tree/procedural).
+
+## The Refactoring
+
+Commit early, commit often, that's what I always say! I tried to keep the
+commits really tiny so you could see my process with each one. The easiest
+way to follow along is probably on GitHub, you can see the list of commits
+by going [here](https://github.com/steveklabnik/maze_solver/compare/procedural...objectoriented).
+Check each one in order, as they build upon each other.
+
+## The OO version
+
+After the Great Refactoring, what's left is a shiny new version with lots of
+objects. It happens to all of us eventually... anyway, you can check it
+out [here](https://github.com/steveklabnik/maze_solver/tree/objectoriented).
+
+## The Alternate TDD universe
+
+... coming soon...
View
27 lib/maze.rb
@@ -0,0 +1,27 @@
+require 'xml'
+
+class Maze
+ def initialize(uri)
+ @xml_cache = {}
+ @uri = uri
+ @uri = xml.href_for('start')
+ end
+
+ def visit(uri)
+ @uri = uri
+ end
+
+ def can_go?(direction)
+ xml.href_for(direction)
+ end
+
+ def finished?
+ xml.has_node?('completed')
+ end
+
+ private
+
+ def xml
+ @xml_cache[@uri] ||= XML.new(@uri)
+ end
+end
View
19 lib/request.rb
@@ -0,0 +1,19 @@
+require 'net/http'
+require 'uri'
+require 'forwardable'
+
+class Request
+ extend Forwardable
+ def_delegators :@uri, :request_uri, :hostname, :port
+
+ def initialize(url)
+ @uri = URI(url)
+ end
+
+ def get
+ req = Net::HTTP::Get.new(request_uri)
+ req['Accept'] = 'application/xml'
+
+ Net::HTTP.start(hostname, port) {|http| http.request(req) }.body
+ end
+end
View
50 lib/solver.rb
@@ -0,0 +1,50 @@
+require 'maze'
+require 'forwardable'
+
+class Solver
+ DIRECTIONS = ['start', 'east', 'west', 'south', 'north', 'exit']
+
+ extend Forwardable
+ def_delegators :@maze, :finished?
+
+ attr_accessor :total_steps
+
+ def initialize(uri)
+ @visited = {}
+ @path = []
+ @total_steps = 0
+ @maze = Maze.new(uri)
+ end
+
+ def next_step
+ link = next_unvisited_direction || backtrack
+
+ visit(link)
+ end
+
+ private
+
+ def next_unvisited_direction
+ possible_directions.find {|href| !@visited[href] }
+ end
+
+ def possible_directions
+ DIRECTIONS.collect do |direction|
+ @maze.can_go?(direction)
+ end.compact
+ end
+
+ def visit(link)
+ @total_steps += 1
+
+ @visited[link] = true
+ @path << link
+
+ @maze.visit(link)
+ end
+
+ def backtrack
+ @path.pop
+ @path.pop
+ end
+end
View
25 lib/xml.rb
@@ -0,0 +1,25 @@
+require 'nokogiri'
+require 'request'
+
+class XML
+ def initialize(uri)
+ @uri = uri
+ end
+
+ def href_for(rel)
+ doc = Nokogiri::XML(self.to_s)
+
+ node = doc.xpath("//link[@rel='#{rel}']").first
+ #uuuuuuuuugh, node isn't a hash, or else I'd #fetch
+ return nil unless node
+ node[:href]
+ end
+
+ def has_node?(node)
+ Nokogiri::XML(self.to_s).xpath("//#{node}").first
+ end
+
+ def to_s
+ @request ||= Request.new(@uri).get
+ end
+end
View
67 maze.rb
@@ -1,67 +0,0 @@
-# This code solves one of Mike Amundsen's maze+xml mazes: http://amundsen.com/media-types/maze/
-#
-# The code isn't great. I just wanted to see how easy it'd be. So I built up a little test, extracted
-# some stuff into a method... next thing you know I've got a little procedural application.
-#
-# Backtracking implementation borrowed from https://github.com/caelum/restfulie/blob/master/full-examples/mikemaze/maze_basic.rb because I'm lazy.
-
-
-require 'uri'
-require 'net/http'
-require 'nokogiri'
-
-def request_xml(url)
- uri = URI(url)
-
- req = Net::HTTP::Get.new(uri.request_uri)
- req['Accept'] = "application/xml"
-
- res = Net::HTTP.start(uri.hostname, uri.port) {|http|
- http.request(req)
- }
-
- res.body
-end
-
-def xml_has_completed?(xml)
- Nokogiri::XML(xml).xpath("//completed").first
-end
-
-def extract_href_from_xml(rel, xml)
- doc = Nokogiri::XML(xml)
-
- node = doc.xpath("//link[@rel=\"#{rel}\"]").first
- return nil unless node
- node[:href] #uuuuuuuuugh can't use Hash#fetch because it's not a hash.
-end
-
-def start_uri
- xml = request_xml('http://amundsen.com/examples/mazes/2d/five-by-five/')
- extract_href_from_xml('start', xml)
-end
-
-xml = request_xml(start_uri)
-visited = {}
-path = []
-steps = 0
-
-until(xml_has_completed?(xml))
-
- link = ["start", "east", "west", "south", "north", "exit"].collect do |direction|
- extract_href_from_xml(direction, xml)
- end.find {|href| href && !visited[href] }
-
- if !link
- path.pop
- link = path.pop
- end
-
- visited[link] = true
- path << link
- xml = request_xml(link)
-
- steps = steps + 1
-end
-
-puts "you win in #{steps}!"
-
View
9 solver.rb
@@ -0,0 +1,9 @@
+$:.unshift("lib")
+
+require 'solver'
+
+solver = Solver.new('http://amundsen.com/examples/mazes/2d/five-by-five/')
+
+solver.next_step until solver.finished?
+
+puts "Solved in #{solver.total_steps}."

No commit comments for this range

Something went wrong with that request. Please try again.