I'm also curious as to what the best way is to recommend refactorings to maintainers of open source code. If somebody on the internet sent me a pull request of refactorings, I think I'd probably find it a bit of a bother to go through someone else's code and figure out what they're doing and why. Do you know what I mean? But then again that's never happened, so I don't know first hand what that would be like, and I'm pretty confident of the merits of my refactoring.
Here's the original method that was way too big for my liking:
Note: the method is parsing a file that contains, say "an_id_of_video and sorting out the "tag info" and converting it into proper html code for displaying a youtube video in this case.
(https://github.com/veger/ruby-bbcode/blob/4a491cd315d4ec6c8c12e858b5be83ecad76ec8f/lib/ruby-bbcode.rb)
def self.parse(text, tags = {})
tags = @@tags if tags == {}
tags_list = []
@bbtree = {:nodes => []}
bbtree_depth = 0
bbtree_current_node = @bbtree
text.scan(/((\[ (\/)? (\w+) ((=[^\[\]]+) | (\s\w+=\w+)* | ([^\]]*))? \]) | ([^\[]+))/ix) do |tag_info|
ti = find_tag_info(tag_info) # NOTICE HERE
if ti[:is_tag] and !tags.include?(ti[:tag].to_sym)
# Handle as text from now on!
ti[:is_tag] = false
ti[:text] = ti[:complete_match]
end
if !ti[:is_tag] or !ti[:closing_tag]
if ti[:is_tag]
tag = tags[ti[:tag].to_sym]
unless tag[:only_in].nil? or (tags_list.length > 0 and tag[:only_in].include?(tags_list.last.to_sym))
# Tag does to be put in the last opened tag
err = "[#{ti[:tag]}] can only be used in [#{tag[:only_in].to_sentence(@@to_sentence_bbcode_tags)}]"
err += ", so using it in a [#{tags_list.last}] tag is not allowed" if tags_list.length > 0
return [err]
end
if tag[:allow_tag_param] and ti[:params][:tag_param] != nil
# Test if matches
return [tag[:tag_param_description].gsub('%param%', ti[:params][:tag_param])] if ti[:params][:tag_param].match(tag[:tag_param]).nil?
end
end
if tags_list.length > 0 and tags[tags_list.last.to_sym][:only_allow] != nil
# Check if the found tag is allowed
last_tag = tags[tags_list.last.to_sym]
allowed_tags = last_tag[:only_allow]
if (!ti[:is_tag] and last_tag[:require_between] != true and ti[:text].lstrip != "") or (ti[:is_tag] and (allowed_tags.include?(ti[:tag].to_sym) == false))
# Last opened tag does not allow tag
err = "[#{tags_list.last}] can only contain [#{allowed_tags.to_sentence(@@to_sentence_bbcode_tags)}] tags, so "
err += "[#{ti[:tag]}]" if ti[:is_tag]
err += "\"#{ti[:text]}\"" unless ti[:is_tag]
err += ' is not allowed'
return [err]
end
end
# Validation of tag succeeded, add to tags_list and/or bbtree
if ti[:is_tag]
tag = tags[ti[:tag].to_sym]
tags_list.push ti[:tag]
element = {:is_tag => true, :tag => ti[:tag].to_sym, :nodes => [] }
element[:params] = {:tag_param => ti[:params][:tag_param]} if tag[:allow_tag_param] and ti[:params][:tag_param] != nil
else
text = ti[:text]
text.gsub!("\r\n", "\n")
text.gsub!("\n", "<br />\n")
element = {:is_tag => false, :text => text }
if bbtree_depth > 0
tag = tags[bbtree_current_node[:tag]]
if tag[:require_between] == true
bbtree_current_node[:between] = ti[:text]
if tag[:allow_tag_param] and tag[:allow_tag_param_between] and (bbtree_current_node[:params] == nil or bbtree_current_node[:params][:tag_param] == nil)
# Did not specify tag_param, so use between.
# Check if valid
return [tag[:tag_param_description].gsub('%param%', ti[:text])] if ti[:text].match(tag[:tag_param]).nil?
# Store as tag_param
bbtree_current_node[:params] = {:tag_param => ti[:text]}
end
element = nil
end
end
end
bbtree_current_node[:nodes] << element unless element == nil
if ti[:is_tag]
# Advance to next level (the node we just added)
bbtree_current_node = element
bbtree_depth += 1
end
end
if ti[:is_tag] and ti[:closing_tag]
if ti[:is_tag]
tag = tags[ti[:tag].to_sym]
return ["Closing tag [/#{ti[:tag]}] does match [#{tags_list.last}]"] if tags_list.last != ti[:tag]
return ["No text between [#{ti[:tag]}] and [/#{ti[:tag]}] tags."] if tag[:require_between] == true and bbtree_current_node[:between].blank?
tags_list.pop
# Find parent node (kinda hard since no link to parent node is available...)
bbtree_depth -= 1
bbtree_current_node = @bbtree
bbtree_depth.times { bbtree_current_node = bbtree_current_node[:nodes].last }
end
end
end
return ["[#{tags_list.to_sentence((@@to_sentence_bbcode_tags))}] not closed"] if tags_list.length > 0
true
end
Huge method, right? So after lots of thinking, I realized what was potentially the optimal fix. Instead of using ti as just a plain old hash, I thought it would be better to refactor it into an object that works just like a hash, but also has methods attached to it that perform most of the work of the #parse method.
So the beginnings of my refactoring look like this:
(original)
.
.
.
text.scan(/((\[ (\/)? (\w+) ((=[^\[\]]+) | (\s\w+=\w+)* | ([^\]]*))? \]) | ([^\[]+))/ix) do |tag_info|
ti = find_tag_info(tag_info)
if ti[:is_tag] and !tags.include?(ti[:tag].to_sym)
# Handle as text from now on!
ti[:is_tag] = false
ti[:text] = ti[:complete_match]
end
.
.
.
end
.
.
.
(refactored)
.
.
.
text.scan(/((\[ (\/)? (\w+) ((=[^\[\]]+) | (\s\w+=\w+)* | ([^\]]*))? \]) | ([^\[]+))/ix) do |tag_info|
ti = TagInfo.new(tag_info, tags)
ti.handle_unregistered_tags_as_text # if the tag isn't in the tags list, then treat it as text
.
.
.
end
.
.
.
gist: https://gist.github....onymous/5145597
(lib/ruby-bbcode/tag_info.rb)
module RubyBBCode
class TagInfo
def initialize(tag_info, tags)
@tag_data = find_tag_info(tag_info)
@tags = tags
@tag = @tags[@tag_data[:tag].to_sym] unless @tag_data[:tag].nil?
end
def [](key)
@tag_data[key]
end
def []=(key, value)
@tag_data[key] = value
end
def handle_unregistered_tags_as_text
if element_is_tag? and tag_included_in_tags_list?
# Handle as text from now on!
self[:is_tag] = false
self[:text] = self[:complete_match]
end
end
def allowed_outside_parent_tags?
@tag = @tags[@tag_data[:tag].to_sym]
@tag[:only_in].nil?
end
def constrained_to_within_parent_tags?
@tag = @tags[@tag_data[:tag].to_sym]
!@tag[:only_in].nil?
end
def element_is_tag?
self[:is_tag]
end
def element_is_opening_tag?
!self[:closing_tag]
end
def element_is_text?
!self[:is_tag]
end
def tag_included_in_tags_list?
!@tags.include?(self[:tag].to_sym)
end
def find_tag_info(tag_info)
ti = {}
ti[:complete_match] = tag_info[0]
ti[:is_tag] = (tag_info[0].start_with? '[')
if ti[:is_tag]
ti[:closing_tag] = (tag_info[2] == '/')
ti[:tag] = tag_info[3]
ti[:params] = {}
if tag_info[4][0] == ?=
ti[:params][:tag_param] = tag_info[4][1..-1]
elsif tag_info[4][0] == ?\s
#TODO: Find params
end
else
# Plain text
ti[:text] = tag_info[8]
end
ti
end
end
end
Is that a good start? Also, I'm noticing an awkwardness to the code. The parse method loops through a glob of text, and creates TagInfo units... But TagInfo units are all instantiated with the same "tags" variable, which serves as kind of a global variable. That must imply that maybe I should refactor the code "above" the ti unit, so it can 'harbor' these TagInfo units along side the 'tags' data? So if anyone sees the code located at https://github.com/v.../ruby-bbcode.rb and has any insights into what a good refactoring would involve, I'm all ears on that.
This post has been edited by NotarySojac: 12 March 2013 - 11:36 AM

New Topic/Question
Reply




MultiQuote





|