Rule of Separation
The Rule of Separation is one of my favorites because it tells me how to modularize code. I’ve heard lots of advice over the years to “modularize” and that “it’s an art” (which is a fancy way of people saying they don’t know). With this rule, we do know, and by the end of this piece, you’ll understand how to modularize your code.
In this piece we will:
define the Rule of Separation
use the rule to improve real code
discuss how to use the principle when architecting
discuss how to use the principle to refactor code.
Rule of Separation
Seperate policy from mechanism; separate interfaces from engines
In this section, we’ll only focus on the first half of the rule as the second half is very similar. Here are a couple of quick definitions:
Policy: Restricts functionality (ex. fail if username > 20 chars)
Mechanism: Adds functionality (ex. the ability to create users)
Therefore, the rule tells us that code which adds functionality should be ‘seperate’ from code which restricts functionality. A quick example would be backend code responsible for user creation where usernames must be <= 20 characters.
The rule of seperation tells us that the “policy” to restrict usernames should be separate from the code which actually creates the user. Therefore the code should look like:
def create_user_api(username, password):
# policy
if len(username) > 20:
raise ApplicationException("USERNAME_TOO_LONG")
# mechanism
create_user_internal(username, password)
(In this case, it seems almost obvious. Later we’ll see a case where it’s less obvious.) create_user_internal is a mechanism which creates users. What create_user_internal does with usernames > 20 characters doesn’t matter. For example, it could work or it could fail with a SQL error because it tries to fit 21 characters into varchar(20). As fast coders, we generally prefer mechanisms which always work because then policy changes do not require mechanism changes, but sometimes for performance reasons we’re forced to use varchar(20).
Exceptions to the rule
None. Seriously. I’ve never seen a time where policy and mechanism had to be intertwined.
Example
So…last week I messed up and broke this rule. These things are hard, and even us experts get it wrong sometimes.
I was working on a leetcode problem (warning: spoilers): Sentence Screen Fitting
The problem asks how many times can you write a sentence on a fixed width screen. My solution was straightforward (with an optimization unrelated to this post), note the current position and try to write a sentence, returning the new position. If the new position is valid, note that a sentence was written and reloop. Or in code:
def words_typing(a, rows, cols):
max_p = [rows - 1, cols - 1]
p = [-1, max_p[1]]
sentences = 0
while True:
p = write_sentence(a, p, max_p)
if p is None or p[0] > max_p:
break
sentences += 1
return sentences
Inside, there’s a mechanism, write a sentence. Then there’s a policy, if writing the sentence is “impossible” (p is None) or spills off of the page (p[0] > max_p[0]), then abort.
Seems straightforward, except…why is it “impossible”? Why check for None? The mechanism should always be able to write a sentence.
If we look into the write_sentence code (with the optimization removed), we see:
def write_sentence(a, p, max_p):
for i in range(len(a)):
new_p = write_word(a[i], p, max_p)
if new_p is None or new_p[0] > max_p[0]:
return new_p
p = new_p
return new_p
There it is again, apparently write_word is also saying it’s impossible to write a word. What’s write word doing?
# assume l <= max_p[1] + 1
def write_word(l, p, max_p):
# write on current line
if p[1] + 1 + l <= max_p[1]:
return [p[0],p[1]+1+l]
# write on next line and next line is a valid line
if p[1] + 1 + l > max_p[1] and p[0] + 1 <= max_p[0]:
return [p[0]+1,l-1]
# write on next line but next line is not a valid line
if p[1] >= 0 and p[1] + 1 + l > max_p[1] and p[0] + 1 > max_p[0]:
return None
Oh, now we see the culprit, the write word API is making a policy decision on whether to write or not. write_word is supposed to be a mechanism which computes positions given the number of columns (this is needed to know whether to write on this line or the next line). But it’s going beyond and checking the row, and if the row is “invalid”, it’s returning None. Once it returns none, now every method that calls it has to check if it’s None, leading to unnecessary complexity.
Instead, let’s separate policy and mechanism. The policy is whether writing a word or sentence is “valid”. The mechanism is writing the word/sentence. Let’s rewrite the mechanisms without the policy
# assume l <= cols
def write_word(l, p, cols):
# write on current line
if p[1] + 1 + l < cols:
return [p[0],p[1]+1+l]
else:
return [p[0]+1,l-1]
Great, the three way if statement is now a single if-else, much simpler.
def write_sentence(a, p, cols):
for i in range(len(a)):
p = write_word(a[i], p, cols)
return p
Originally this had to use a temp var new_p, had a break in the loop, and made two checks. Now, it’s just calling write_word multiple times! There is an optimization opportunity to abort early if p[0] >= rows, but there’s a much better optimization which doesn’t complicate the code as much.
Lastly, let’s fix up the top level code.
def words_typing(a, rows, cols):
sentences = 0
p = [-1, max_p[1]]
while True:
p = write_sentence(a, p, cols)
if p[0] >= rows:
break
sentences += 1
return sentences
Even the top level code which handles policy is simplified now that only the top level code is handling policy. We can get rid of max_p and the if check only checks 1 attribute of p, not 2.
As we can see, separating policy and mechanism in this code lead to shorter, cleaner code. Shorter, cleaner code is faster to write and faster to read and debug which sounds good to us!
System Architecture
System architecture benefits greatly from separating policy and mechanism. In particular, it allows individual services to be split into a “front-end” which handles the service API’s policy and a “back-end” which handles the API’s mechanism. By being freed of policy concerns and complexities, mechanisms can often be designed more generally or given input guarantees from the policy, more simply.
Unfortunately the browser “front-end” or app “front-end” cannot be where policy is enforced because client code is untrustworthy. Therefore policy front-ends must be implemented in the first service to handle a client’s http request.
Although this section is short, it’s hugely important when architecting systems.
Architecture implementation
Separating policy and mechanism makes implementation more parallizable, so you can throw more engineers at it! The policy and mechanism components only interact with eachother through the mechanism’s API. Otherwise, they’re completely independent. Once the API of the mechanism is established, the policy and mechanism can be implemented in parallel.
Code Refactoring
The violation of the rule of separation involves policy decisions being made too low in the call stack.
In our example above, this was when write_word (which was beneath write_sentence and words_typing) returned None.
Therefore, look for when policy can be pushed up. Especially look for checks which bubble up (or down) or errors. If so, see if that code can be moved up to the top and the code beneath it can remain blissfully ignorant. If so, then you’ve pulled policy out of the mechanism, thereby separating the two.
Policy is much easier to move around than mechanism, and code without policy is mechanism. Therefore, once the policy has been bubbled as high as possible in the call stack, everything beneath it is mechanism which is now separate.
Conclusion
We saw the rule of representation.
We saw an example of how to apply it to real code.
We saw how to apply it to system architecture.
We saw how to apply it to system implementation.
We saw how to apply it to refactor code (we saw a lot).
Next week, we’ll see the Rule of Silence. Because of it’s quiet nature, it’s an unappreciated rule. If you like what you read, share it with a friend or give this post a like, it really makes my day.