In my mind, the difference between domain thinking and discrete problem thinking is how considerate you are of others.

Image for post
Image for post
A rose from our garden. At some level its all about strands of DNA and individual nucleic acids. But I just like the color.

Even if you don’t intend anybody else to read your code, there’s still a very good chance that somebody will have to stare at your code and figure out what it does: That person is probably going to be you, twelve months from now. — Raymond Chen

Who is the downstream customer of the code you write?

When that downstream customer is you, you’re likely on the phone with one of your clients trying to align the process they are describing with the one that you have encoded. In that moment, its often really nice to have a 1:1 mapping between the grammar they are using in their domain, and the way that you have described the execution of the code in your source code.

A discrete solution approach

class Hamming
def self.compute(strand1, strand2)
raise ArgumentError.new unless strand1.length == strand2.length
s1, s2 = [strand1, strand2].map { |x| x.split(//)}
s1.zip(s2).count {|(a,b)| a != b }
end
end

I challenged my students to think about talking through this code with a subject matter expert facing the problem and how much they would have to translate primitives and mechanics of the language in that conversation. Because we are using base types in the language, we must model our problem more directly around those types rather than the grammar that describes the domain.

Towards domain thinking

Returning to Ruby after some time away, I am yet again happy with the tools available within the language to produce a reasonable representation that models your domain, without the ceremony required for normal object composition.

Specification: A Point Mutation is difference between to Nucleotides in the same position within a strand:

class Nucleotide < SimpleDelegator
alias :point_mutation? :!=
end

Nucleotide.new("A").point_mutation?(Nucelotide.new("B")) => true

We accomplish this abstraction by using the SimpleDelegator class to delegate all messages to the object passed into the constructor. In the example use case "A" and "B" are used as the delegate objects. alias delegates the message passed to point_mutation? to the != method, which in turn delegates this to the delegate object. We can then use this base class in the next level of our abstraction.

Specification: A strand of DNA is made up of an ordered series of Nucleotides

class Strand < SimpleDelegator
def self.parse(str)
arr = str.split("")
new(arr.map {|x| Nucleotide.new(x)})
end
end

Strand.parse("AA").inspect # => => "[\"A\", \"A\"]"
Strand.parse("AA").class # => Strand
Strand.parse("AA")[0].class # => Nucleotide

Again, we use SimpleDelegator to decorate the Array passed to the constructor in parse. And so doing, we can now decorate the base data structure with our own implementation needs, and allows us to continue with the next specification.

Specification: hamming difference is the total difference between strands of a similar length.

class Strand < SimpleDelegator
def self.parse(str)
arr = str.split("")
new(arr.map {|x| Nucleotide.new(x)})
end

def same_length?(other)
self.length == other.length
end

def -(other)
self.zip(other).count{|(a,b)| a.point_mutation?(b) }
end

alias :hamming_distance :-
end

Strand.parse("A").same_length?(Strand.parse("B")) # => true
Strand.parse("A") - Strand.parse("B") # => 1
Strand.parse("A").hamming_difference(Strand.parse("B")) # => 1

As shown in the example above, we have two ways to describe difference, both mathematically oriented, with , and also with a named method. We can now implement our Hamming.compute solution using a more domain oriented grammar.

class Hamming
def self.compute(str1, str2)
strand1 = Strand.parse(str1)
strand2 = Strand.parse(str2)
raise ArgumentError.new unless strand1.same_length?(strand2) strand1.hamming_distance(strand2)
# alternatively
# strand1 - strand2
end
end

And, at least I believe, it would be easier to have a meaningful conversation over the phone with my biologist subject matter expert.

“Yes, the way we solve the hamming distance is to validate the incoming strands are the same length.”

raise ArgumentError.new unless strand1.same_length?(strand2)

“Then we count the aggregate difference where there is a point mutation in the second strand when compared to the first.”

self.zip(other).count{|(a,b)| a.point_mutation?(b) }

Limitations

Complete Solution

class Nucleotide < SimpleDelegator
alias :point_mutation? :!=
end

class Strand < SimpleDelegator
def self.parse(str)
arr = str.split("")
new(arr.map {|x| Nucleotide.new(x)})
end

def same_length?(other)
self.length == other.length
end

def -(other)
self.zip(other).count{|(a,b)| a.point_mutation?(b) }
end

alias :hamming_distance :-
end


class Hamming

def self.compute(str1, str2)
strand1 = Strand.parse(str1)
strand2 = Strand.parse(str2)
raise ArgumentError.new unless strand1.same_length?(strand2)

strand1.hamming_distance(strand2)
end

end

Thanks for reading. Sharing and applause appreciated!

Written by

former pro cyclist turned polyglot programmer, husband and father. Influencer, Tech Leader, and Automator.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store