In my last post, I wrote a code-snippet that admittedly was not a very good example of well-refactored ruby. I'll let you go find it yourself, since I have no desire to link to something that embarassing ;), but I thought today I'd show that I learn from my mistakes by walking through some refactoring I did on my startup's app this morning.
First of all, what is refactoring? (Yes, somebody might be reading this who doesn't know). You can see this wikipedia entry for a complete description, but the short version is this: refactoring is changing the code without changing it's behavior. Ideally, after you refactor, the code does the exact same thing it did before. The difference is that it also:
(a) is easier to read (b) contains less duplication (c) is less complex, and (d) is more flexible towards future changes.
So without any further ado, here is my starting code:
class SomeModel < ActiveRecord::Base has_many :objectives has_many :activities
private def load_json_objects(collection,json) i = 1 collection.each do |item| json << "{display:\"#{item.name}\",value:\"#{item.id}\"}" i += 1 if i <= collection.length json << "," end end end end
This does what I want it to, it takes the objectives and activities collections from the ActiveRecord associations and transforms them into a simple JSON array with a "display" member and a "value" member for each object (this is useful for me when I'm generating
Good point! I like that idea, it does seem to be even cleaner. Correct me if I'm wrong, but wouldn't the correct version of what you wrote be more like this (without the "json" local variable inside the block):
json = collection.collect do |item| "{display:\"#{item.send(display_method)}\",value:\"#{item.send(value_method)}\"}" end.join(',')
def stats_to_json [self.objectives, self.activities].map do |a| a.map { |e| {:display => e[:name], :value => element[:id]} } end.to_json end
(In the last two, I left out the display_method and value_method abstraction, since we don't really need them anymore. If you're going to add new collections to this list, you'll have to modify the code in stats_to_json anyway.)
(I haven't tested any of the code here, so I'm not sure it works correctly.)
I'm not sure if all the changes you've made are the best practices of refactoring. Some of the changes you said you made were because you "might" want to do something with it later. I think this violates YAGNI. I've run into a lot of problems caused by this in the past.
There are two places in this post where I used the word "might". The first was this:
"I might want to use it on other collections later"
The second was one paragraph later, here:
"I might want this functionality in more than one model class (in fact, I needed it in another class immediately) "
Now, perhaps the wording was poorly chosen on my part, but I think that the section there in parens indicates clearly that I wasn't making these changes "just because", and that in fact I had an immediate need to reuse the code in another class.
Now, even though that is the case here, I think I would still stand by those changes even if I didn't need them elsewhere immediately because of another topic that is probably more important to me than YAGNI: cohesion (Cohesive means that a certain class performs a set of closely related actions. A lack of cohesion, on the other hand, means that a class is performing several unrelated tasks. Though lack of cohesion may never have an impact on the overall functionality of a particular class—or of the application itself—but the application software will eventually become unmanageable as more and more behaviours become scattered and end up in wrong places.). The purpose of the model object is not to transform collections into a JSON representation, and having that specific behavior in the model seems, in a word, wrong. Therefore, although YAGNI is certainly important to remember when someone is suggesting building seven different implementations of an XML parser because of the seven possible clients you MIGHT interact with, I don't think you have a leg to stand on in this case.
BTW, YAGNI almost always refers to building new functionality when it's unnecessary, not refactoring. If you check the article you linked to in your comment (wikipedia, YAGNI), you will find this statement corroborated. Out of all the problems that are listed as occurring by not adhering to YAGNI (which I have listed below), only the first could possibly be present as a result of over-enthusiastic refactoring:
-The time spent is taken from adding, testing or improving necessary functionality.
-The new features must be debugged, documented, and supported.
-Any new feature imposes constraints on what can be done in the future, so an unnecessary feature now may prevent implementing a necessary feature later.
-Until the feature is actually needed, it is difficult to fully define what it should do and to test it. If the new feature is not properly defined and tested, the unnecessary feature may not work right, even if it eventually is needed.
-It leads to code bloat; the software becomes larger and more complicated.
-Unless there are specifications and some kind of revision control, the feature may not be known to programmers who could make use of it.
-Adding the new feature may suggest other new features. If these new features are implemented as well, this may result in a snowball effect towards creeping featurism.
Ethan likes software. He could dress that up with all kinds of long words like "engineer" and "architect", but really he just likes coding. His current specialty is webapps with Ruby-on-Rails, because in his own anecdotal experience it just hurts less.
He also has other interests for which he keeps other blogs. If you're reading this on one of those other blogs right now, you already know what they are, and are probably already at the blog that interests you, so unless you're bored don't feel like you have to go read the others.
11 comments:
json = collection.collect do |item|
json << {display:\"#item.send(display_method)}\","
json << "value:\"#item.send(value_method)}\"}"
end.join(',')
"[#{json}]"
I like something like that, using join instead of doing it yourself, then chopping.
@chris schneider
Good point! I like that idea, it does seem to be even cleaner. Correct me if I'm wrong, but wouldn't the correct version of what you wrote be more like this (without the "json" local variable inside the block):
json = collection.collect do |item|
"{display:\"#{item.send(display_method)}\",value:\"#{item.send(value_method)}\"}"
end.join(',')
"[#{json}]"
Following with what chris said, how about:
def collection_to_json(collection, display_method=:name, value_method=:id)
json_array = collection.map do |element|
%Q|{display: "#{element.send(display_method)}", value: "#{element.send(value_method)}"}|
end
"[#{json_array.join(",")}]"
end
Even better, when you include the JSON library, you can directly convert arrays and hashes into JSON. So, you could write collection_to_json() as:
def collection_to_json(collection, display_method=:name, value_method=:id)
collection.map do |e|
{:display => e.send(display_method), :value => element.send(value_method)}
end
return collection.to_json
end
Or, the whole thing in stats_to_json() as:
def stats_to_json
stats = []
stats << self.objectives.map { |e| {:display => e[:name], :value => element[:id]} }
stats << self.activities.map { |e| {:display => e[:name], :value => element[:id]} }
stats.to_json
end
Or even:
def stats_to_json
[self.objectives, self.activities].map do |a|
a.map { |e| {:display => e[:name], :value => element[:id]} }
end.to_json
end
(In the last two, I left out the display_method and value_method abstraction, since we don't really need them anymore. If you're going to add new collections to this list, you'll have to modify the code in stats_to_json anyway.)
(I haven't tested any of the code here, so I'm not sure it works correctly.)
I'm not sure if all the changes you've made are the best practices of refactoring. Some of the changes you said you made were because you "might" want to do something with it later. I think this violates YAGNI. I've run into a lot of problems caused by this in the past.
@amos king
There are two places in this post where I used the word "might". The first was this:
"I might want to use it on other collections later"
The second was one paragraph later, here:
"I might want this functionality in more than one model class (in fact, I needed it in another class immediately) "
Now, perhaps the wording was poorly chosen on my part, but I think that the section there in parens indicates clearly that I wasn't making these changes "just because", and that in fact I had an immediate need to reuse the code in another class.
Now, even though that is the case here, I think I would still stand by those changes even if I didn't need them elsewhere immediately because of another topic that is probably more important to me than YAGNI: cohesion (Cohesive means that a certain class performs a set of closely related actions. A lack of cohesion, on the other hand, means that a class is performing several unrelated tasks. Though lack of cohesion may never have an impact on the overall functionality of a particular class—or of the application itself—but the application software will eventually become unmanageable as more and more behaviours become scattered and end up in wrong places.). The purpose of the model object is not to transform collections into a JSON representation, and having that specific behavior in the model seems, in a word, wrong. Therefore, although YAGNI is certainly important to remember when someone is suggesting building seven different implementations of an XML parser because of the seven possible clients you MIGHT interact with, I don't think you have a leg to stand on in this case.
@amos king
BTW, YAGNI almost always refers to building new functionality when it's unnecessary, not refactoring. If you check the article you linked to in your comment (wikipedia, YAGNI), you will find this statement corroborated. Out of all the problems that are listed as occurring by not adhering to YAGNI (which I have listed below), only the first could possibly be present as a result of over-enthusiastic refactoring:
-The time spent is taken from adding, testing or improving necessary functionality.
-The new features must be debugged, documented, and supported.
-Any new feature imposes constraints on what can be done in the future, so an unnecessary feature now may prevent implementing a necessary feature later.
-Until the feature is actually needed, it is difficult to fully define what it should do and to test it. If the new feature is not properly defined and tested, the unnecessary feature may not work right, even if it eventually is needed.
-It leads to code bloat; the software becomes larger and more complicated.
-Unless there are specifications and some kind of revision control, the feature may not be known to programmers who could make use of it.
-Adding the new feature may suggest other new features. If these new features are implemented as well, this may result in a snowball effect towards creeping featurism.
Why not just do something like this? Not only is it easier, but personally I prefer the API this way.
class SomeModel < ActiveRecord::Base
has_many :objectives
has_many :activities
def stats
conversion = lambda { |record| { :display => record.name, :value => record.id } }
objectives.map(&conversion) + activities.map(&conversion)
end
end
SomeModel.find(1).stats.to_json
Uh, where are the tests?
You might want to try something like this instead.
class SomeModel < ActiveRecord::Base
has_many :objectives
has_many :activities
def stats
[objectives.to_json(:only => [:name]), activities.to_json(:only => [:name])]
end
end
class SomeModelTest < Test::Unit::TestCase
def setup
@some_model = SomeModel.new
valid_stats = [@some_model.objectives.to_json(:only => [:name]),@some_model.activities.to_json(:only => [:name])]
end
def test_stats
assert_equal @some_modle.stats, valid_stats
end
end
http://pastie.org/646732
Post a Comment