Domain Oriented Thinking

Jed Schneider
4 min readOct 20, 2017

--

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.

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?

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.

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

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

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

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.

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

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

Thanks for reading. Sharing and applause appreciated!

--

--

Jed Schneider

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