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.