Skip to content

Instantly share code, notes, and snippets.

@Solaxun
Created July 3, 2020 19:23
Show Gist options
  • Save Solaxun/6f6e56052ed473b15846c26b297d6110 to your computer and use it in GitHub Desktop.
Save Solaxun/6f6e56052ed473b15846c26b297d6110 to your computer and use it in GitHub Desktop.
(comment
Multimethods are an open dispatch mechanism, but is closed in it's
dispatch function. If the set of dispatch types is open, so is
the multimethod.
Protocols are always open because you can always add a new type.
Multimethods require you to pick something to dispatch on that
remains open so any user can create a new one.
For dependency injection, protocols work great because you can
construct the thing implementing the protocol ahead of time with
whatever data you need, and calling code only needs to call the
protocol method. Different implementations might have different
data stored in the record implementing the protocol, but the
protocol method signature remains consistent. For example, a
ISave might save data to SQL, which needs several arguments
(conn string, db, etc.) or a CSV, which needs one argument (path).
Constructing the record ahead of time to house the appropriate info
and then providing a simple protcol method `save` means calling
code doesn't have to change or know about the different required
data.
The object corrollary to this is similar to protocols, construct an
object with the required data in advance, and then provide methods
on that object with consistent signatures across subclasses to do the
work needed.
For multimethods, you can call with diff args as below, but then
for dependeny injection the calling code has to know that the argument
structure could change, which breaks DI. The way to make it like
protocols is to make sure that you embed all information you need
inside a collection/map that the multimethod receives, so that the
function signatures are consistent, and you just construct whatever
map/collection you need for the respective multi-method to do it's
work ahead of time. This is basically the same as protcols, where the
collection/map for multimethods corresponds to a record or reification
etc. implementing a protocol.
Usually you only dispatch on type, so defmulti is probably best for
dispatching on more than one type.)
;; works but notice different function signatures across multimethods
;; means calling code has to know about this - bad for DI, defeats the
;; point of using multimethods.
(defmulti save
(fn [storage & args] storage))
(defmethod save :sql
[_ conn-string table db]
{:db db :conn conn-string :table table})
(defmethod save :csv
[_ fpath]
fpath)
(save :sql "blah@dc1:foo" "table1" "db1")
(save :csv "marksstuff/saved/here.csv")
;; protcol version - object implementing protocol has
;; all the data you need, that is constructed in advance
;; and passed into the calling code so the calling code
;; is unchanged as the method signatures are consistent.
;; in this case, the signature only takes the type, but
;; it could take more args so long as they are consistent
(defprotocol ISave
(save [this]))
(defn sql-data [conn-string db table]
(reify ISave
(save [_]
{:db db :conn conn-string :table table})))
(defn csv-data [fpath]
(reify ISave
(save [_]
fpath)))
(save (csv-data "marksstuff/saved/here.csv"))
(save (sql-data "blah@dc1:foo" "table1" "db1"))
;; making the multimethod signature consistent basically
;; mirroring the protocol version above. Map constructed
;; in advance by whoever wants to extend the persistence
;; layer with a new type to save, code that calls save
;; multimethod is unchanged, all data each implmentation
;; needs is part of the map.
(defmulti save
(fn [storage] (:storage-type storage)))
(defmethod save :sql
[{:keys [conn-string db table] :as storage}]
storage)
(defmethod save :csv
[storage-type]
(:fpath storage-type))
(save {:storage-type :sql :db "db1" :conn "blah@dc1:foo" :table "table1"})
(save {:storage-type :csv :fpath "marksstuff/saved/here.csv"})
@Solaxun
Copy link
Author

Solaxun commented Jan 16, 2021

Another way to make multi-method signatures consistent is to have a rest args which is ignored. So in the above example:

(defmethod save :csv
  [_ fpath & args]                         ; rest args to be ignored
  fpath)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment