Skip to content

Instantly share code, notes, and snippets.

@jkthorne
Created April 2, 2014 00:19
Show Gist options
  • Save jkthorne/9925739 to your computer and use it in GitHub Desktop.
Save jkthorne/9925739 to your computer and use it in GitHub Desktop.
<strong>The Problem</strong>
I recently have come across and interesting problem with an inadequate solution. This a project which has gone through converting the names on there domain objects but has maintain there Types. An object called Foo would previously be called foo but now has a name of bar. Along with this rename there was an external reference that had to be changes to represent the same object we will call this an external_id. This exteranl_id was added but pointed to existing domain concepts.
<code># pre rename
foo = Foo.new
foo.name #=&gt; "foo"
foo.id #=&gt; 1
# post rename
foo = Foo.new
Foo.name #=&gt; "bar"
foo.id #=&gt; 1</code>
<strong>Currently</strong>
The Current solution to this mapping problem is to implement a converter class that takes a key which is the new external_id and give back all objects that would be mapped to that. Also there is a method that takes and exteral_id and returns a instance of the object.
<code>foo = Mapper.key_to_instance(12)
foo.name #=&gt; "foo"
foo.id #=&gt; 1
foo = Mapper.instance_to_key(Foo.new)
foo.inspect #=&gt; 12</code>
<strong>Problem</strong>
Their is a problem with this approach and that is there are four different attributes you can map too ex. "name", "id", "external_id", Instance. If you have an external id which you can search by but you want to find what objects would have that name you would need to call each object and find that name. since the name is what was converted pre process it would be nice to implement a conversion function for it.
<code>foo = Mapper.key_to_instace(12)
foo.name #=&gt; "baz"</code>
<strong>solution</strong>
The first step in this solution is to rewrite a portion of the current implementing. Using an internal data structure of an array of arrays to represent the mappings. this give a quick iteration over all mappings and a simple interface to examine the mapping.
<code>require "minitest/autorun"
class Converter
MAPPING = [
[1, "one"],
[2, "two"],
[3, "three"]
]
def self.to_num(string)
MAPPING.find do |variable|
variable[1] == string
end.first
end
def self.to_string(number)
MAPPING.find do |variable|
variable[0] == number
end.last
end
end
describe Converter do
it "must converter strings" do
Converter.to_string(1).must_equal "one"
Converter.to_string(2).must_equal "two"
Converter.to_string(3).must_equal "three"
end
it "must converter number" do
Converter.to_num("one").must_equal 1
Converter.to_num("two").must_equal 2
Converter.to_num("three").must_equal 3
end
end</code>
There are 2 class methods to convert bi directional. Since there is only 2 way conversion this makes sense. In writing this class I noticed on the conversion functions a type of duplication. there is a MAPPING that is being iterated through with a find. The conditional is consistent in asserting a position in the array to the arguments value. and lastly once the mapping is found the return value is the opposite of the incoming value. This has taught us a few lessons but it is time to step it up a notch. There are four degrees of conversion in the original example but in TDD we see the duplication and recognize the next degree of complexity with intruding one more mapping. Firstly I recognize the need to abstract the mapping into concept and value this is most easily abstracted into a hash and requires little code change. Then I condense both implementation into one method missing. This surprising works on the first try. Even my test to see if a key of the same mapping would break worked fine in implementation.
<code>require "minitest/autorun"
class Converter
MAPPER = [
{foo: "foo1", bar: "bar1", baz: "baz1"},
{foo: "foo3", bar: "bar3", baz: "baz3"},
{foo: "foo5", bar: "bar5", baz: "baz5"}
]
def self.method_missing(name, id)
key, mapping = name.to_s.split("_to_").map(&amp;:to_sym)
MAPPER.find do |hash|
hash[key] == id
end[mapping]
end
end
describe "Converter" do
it "must converter (kay) to (mapping) and return its value" do
Converter.foo_to_bar("foo1").must_equal "bar1"
Converter.bar_to_baz("bar5").must_equal "baz5"
Converter.foo_to_foo("foo3").must_equal "foo3" # because people
end
end</code>
<strong>Conclusion</strong>
The work that was done before I started on this project had gone miles and had documented and discovered all the domain knowledge but lacked the dynamic requests that it found itself trying to implement. The need that fist arose is we had a list of names and needed external_ids and then had a list of external_ids and needed a list of names. This conversion technique will continue to support the old requirements and go on to implement further mapping requirements as the domain concepts expend with a relatively easy to understand mapping. This was an interesting problem and it came from a situation that seems like it would happen naturally from iterating on a project and maintaing existing features which made it a great candidate for exploration.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment