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

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

Who is the downstream customer of the code you write?

Usually, that downstream customer is you. If it isn’t, its one of your friends or colleagues, or the person that might be cursing your name for the code you left them to decipher.

A discrete solution approach

I have been working with some of our University interns doing some extra-curriculum work learning programming languages. Currently we are learning Ruby by doing a combination of book work and problem solving on the exercism.io platform. One of the first problems to be solved in the Ruby track is the Hamming distance problem. That is to say the difference in character placement between two strings of identical length. A discrete problem based approach might look like this:

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

Towards domain thinking

Domain based problem solving focuses more on outcome and future state than the immediate resolution. Clearly there is also a danger to ‘over-engineer’ for the future and pre-load the solution with too much future proofing.

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

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

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
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
raise ArgumentError.new unless strand1.same_length?(strand2)
self.zip(other).count{|(a,b)| a.point_mutation?(b) }

Limitations

Type safety is an obvious concern when using delegation patterns that assume a specific inbound type. Whether or not this is acceptable is purely a mode of the total solution. For me, its often a great place to start, and we add guards later as we need them to reason about the remainder of the system.

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

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