Continuing on the series about SOLID principles on functional programming, the next one is the Open/Closed Principle. The definition from the Wikipedia:

The open/closed principle states “software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification”; that is, such an entity can allow its behavior to be extended without modifying its source code.

This is kinda interesting on its own way: what’s an “extension”? Considering the context when it was written, and future interpretations of the principle, the idea is that any program should not be re-compiled (re-written, modified, etc) to be extended. The idea of this principle is that local changes should not propagate to other parts of the program: make entities as self-contained as possible, write then in a way that extensions would not depend on modifications on these entities, then “close” then to modifications. I can see two cases for the “open-closed principle” violation, and the first one is the most common:

(defn as-int [some-str]
  (when (re-matches #"\d+" some-str)
    (Integer/parseInt some-str)))

This code returns an Integer if it can parse a string as a number, and nil if it can’t. Now, suppose we want to “extend” this functionality by accepting other objects like Double (truncates to integer) or nil (returns 0). The only way to do it is to change the when to a case, but that means that for every new implementation I’ll have to change this function. Now, a better way is to use protocols:

(defprotocol IntegerLike 
  (as-int [self]))

(extend-protocol IntegerLike
  String
  (as-int [self] 
    (when (re-matches #"\d+" self)
      (Integer/parseInt self))))

(extend-protocol IntegerLike
  Double
  (as-int [self] (.intValue self)))

;; If you want a "default" implementation
(extend-protocol IntegerLike
  Object
  (as-int [_] nil))

Now, the second violation of this principle is the propagation of modifications to other parts of the program. Using Chlorine as an example, the first implementation of the Evaluator protocol (the protocol that captures the evaluations, results, handling of code, and other things) was:

(defprotocol Evaluator
  (evaluate [this command opts callback])
  (break [this id]))

;; After a while it was changed to:
(defprotocol Evaluator
  (evaluate [this command opts callback])
  (break [this id])
  (autocomplete [this prefix opts callback]))

It’s easy to see that every code that implements the Evaluator had to add the autocomplete part. But it becomes even worse: because autocomplete depends on the capabilities of each REPL, and libraries installed on the classpath, it means that this command was not self-contained. Also, if I wanted other features, I was thinking on adding then to this protocol, but then every single change would propagate to multiple parts of the program. In the end, I ended up removing this autocomplete function from the protocol, as it should be “closed” for modification, and it also would obey the I from the SOLID pattern, that I’ll talk more later.


0 Comments

Leave a Reply

Your email address will not be published.

%d bloggers like this: