Clojure defprotocol yields a object, interface and functions, the object is a map representing a specification of the protocol, the interface can use to define the type, the functions is the dispatching function like as Multimethods using the class function. (In the protocol, A default dispatch value is the ‘Object’.)

(defprotocol Animal
  (cry [this]))
#_=> Animal

Animal
#_=> {:on user.Animal,
      :on-interface user.Animal,
      :sigs {:cry {:name cry, :arglists ([this]), :doc nil}},
      :var #'user/Animal,
      :method-map {:cry :cry},
      :method-builders {#'user/cry #function[user/eval70478/fn--70479]}}

(meta #'cry)
#_=> {:name cry,
      :arglists ([this]),
      :doc nil,
      :protocol #'user/Animal,
      :ns #namespace[user]}

(clojure.reflect/reflect user.Animal)
#_=> {:bases nil,
      :flags #{:interface :public :abstract},
      :members
      #}}

When defining a type, you can use the interface generated by defprotocol. You can call the ‘cry’ function directly or indirectly(by the dispatching function). There is a slight performance difference.

(deftype Bird []
  Animal
  (cry [this] "Tweet tweet!"))
#_=> user.Bird

user.Bird
#_=> user.Bird

(clojure.reflect/reflect Bird)
#_=> {:bases #{java.lang.Object clojure.lang.IType user.Animal},
      :flags #{:public :final},
      :members
      #
        {:name getBasis,
         :return-type clojure.lang.IPersistentVector,
         :declaring-class user.Bird,
         :parameter-types [],
         :exception-types [],
         :flags #{:public :static}}
        {:name user.Bird,
         :declaring-class user.Bird,
         :parameter-types [],
         :exception-types [],
         :flags #{:public}}}}

( cry (Bird.))
#_=> "Tweet tweet!"

(.cry (Bird.))
#_=> "Tweet tweet!"

(let [x (Bird.)]
  (time
   (dotimes [_ 100000]
     (.cry x)))
  (time
   (dotimes [_ 100000]
     (.cry x)))
  (time
   (dotimes [_ 100000]
     (cry x)))
  (time
   (dotimes [_ 100000]
     (cry x))))
"Elapsed time: 1.741576 msecs"
"Elapsed time: 1.634831 msecs"
"Elapsed time: 6.305106 msecs"
"Elapsed time: 5.010498 msecs"
#_=> nil

You can extend a existed type using extend-protocol.

(deftype Dog [])
#_=> user.Dog

user.Dog
#_=> user.Dog

(clojure.reflect/reflect Dog)
#_=> {:bases #{java.lang.Object clojure.lang.IType},
      :flags #{:public :final},
      :members
      #
        {:name user.Dog,
         :declaring-class user.Dog,
         :parameter-types [],
         :exception-types [],
         :flags #{:public}}}}

(extend-protocol Animal
  Dog
  (cry [this] "Woof woof!"))
#_=> nil

Animal
#_=> {:on user.Animal,
      :on-interface user.Animal,
      :sigs {:cry {:name cry, :arglists ([this]), :doc nil}},
      :var #'user/Animal,
      :method-map {:cry :cry},
      :method-builders {#'user/cry #function[user/eval70478/fn--70479]},
      :impls {user.Dog {:cry #function[user/eval70562/fn--70563]}}} ; <== added by extend-protocol

( cry (Dog.))
#_=> "Woof woof!"

(.cry (Dog.))
#_=> IllegalArgumentException No matching field found: cry for class user.Dog ...

In fact, the dispatching function retrieve a function as follows, but use the cache.

((get-in Animal [:impls Dog :cry]) nil)
#_=> "Woof woof!"

(.-__methodImplCache cry)
#_=> #object[clojure.lang.MethodImplCache 0x8d08ba "clojure.lang.MethodImplCache@8d08ba"]

(let [x (Dog.)]
  (time
    (dotimes [_ 100000]
      (cry x)))
  (time
    (dotimes [_ 100000]
      (cry x)))
  (time
    (dotimes [_ 100000]
      ((get-in Animal [:impls Dog :cry]) x)))
  (time
    (dotimes [_ 100000]
      ((get-in Animal [:impls Dog :cry]) x))))
"Elapsed time: 5.480076 msecs"
"Elapsed time: 4.04941 msecs"
"Elapsed time: 21.866702 msecs"
"Elapsed time: 20.10644 msecs"

We dive into extend-protocol to see what happened in the dispatching function of the protocol. extend-protocol macro use extend function, extend add a function to the protocol object and replace the dispatching function with a new dispatching function.

We will do what extend function does.

(deftype Cat [])
#_=> user.Cat

;; Add a function to the protocol object.
(alter-var-root #'Animal assoc-in [:impls Cat :cry] (fn [this] "meow"))
#_=> {:on user.Animal,
      :on-interface user.Animal,
      :sigs {:cry {:name cry, :arglists ([this]), :doc nil}},
      :var #'user/Animal,
      :method-map {:cry :cry},
      :method-builders {#'user/cry #function[user/eval74564/fn--74565]},
      :impls {user.Dog {:cry #function[user/eval74581/fn--74582]},
              user.Cat {:cry #function[user/eval74590/fn--74591]}}}

;; MethodImplCache capture the protocol object,
;; so we have to replace a cache object to use a changed protocol object.
(set! (.-__methodImplCache cry) (clojure.lang.MethodImplCache. Animal :cry))
#_=> #object[clojure.lang.MethodImplCache 0x763b44ce "clojure.lang.MethodImplCache@763b44ce"]

(cry (Cat.))
#_=> "meow"

;; `extend` function generate a dispatching function.
(((get-in Animal [:method-builders (intern 'user 'cry)])
        (clojure.lang.MethodImplCache. Animal :cry))
       (Cat.))
#_=> "meow"
;; and replace it with new.
(.bindRoot (intern 'user 'cry)
           (comp (partial str "new dispatching function: ")
                 ((get-in Animal [:method-builders (intern 'user 'cry)])
                  (clojure.lang.MethodImplCache. Animal :cry))))
#_=> nil

(cry (Cat.))
#_=> "new dispatching function: meow"

If you want know how does defprotocol work in ClojureScript, see Clojurescript defprotocol’s secret.