Clojure-программисты хорошо знают, насколько Clojure – мощный язык по сравнению с Java и прочими языками промышленной разработки. Выразительность Clojure позволяет писать очень компактный и красивый код. Но, на мой взгляд, не это главное: важно, чтобы код был понятным. Зачем вся эта мощь и выразительность, если на выходе мы получаем всё ту же кучу известной субстанции, будто бы написанной code monkey?

Антипаттерн

В этой статье я бы хотел разобрать несколько фрагментов кода из изветной библиотеки Selmer. Давайте взглянем вот на эту функцию:

(defn make-resource-path
  [path]
  (cond
    (nil? path)
      nil
    (instance? java.net.URL path)
      (append-slash (str path))
    :else
      (append-slash
       (try
         (str (java.net.URL. path))
         (catch java.net.MalformedURLException err
           (str "file:///" path))))))

Взгляните на похожий код на java:

public static URL toURL(URI u, ClassLoader loader) throws MalformedURLException {
    if ("classpath".equals(u.getScheme())) {
        String path = u.getPath();
        if (path.startsWith("/")){
            path = path.substring("/".length());
        }
        return loader.getResource(path);
    }
    else if (u.getScheme() == null && u.getPath() != null) {
        //Assume that its a file.
        return new File(u.getPath()).toURI().toURL();
    }
    else {
        return u.toURL();
    }
}

Что Java-версия, что Clojure-версия не очень-то читабельны. Нарушен главный принцип single responsibility: функция делает слишком много. В Java-версии это усугубляется еще и вложенными if-ами: метод принимает слишком много решений. Для Java более-менее красивого решения здесь не написать в виду ограниченности самого языка, а вот Clojure предоставляет прекрасную альтернативу: мультиметоды. Вот версия make-resource-path, переписанная при помощи мультиметодов:

(declare path->URL)

(defmulti make-resource-path2
  (fn [path]
    (cond
      (nil? path) :nil
      (instance? java.net.URL path) :url)))

(defmethod make-resource-path2 :nil
  [path]
  nil)

(defmethod make-resource-path2 :url
  [path]
  (-> path
      str
      append-slash))

(defmethod make-resource-path2 :default
  [path]
  (-> path
      path->URL
      append-slash))

(defn path->URL [ path ]
  (try
    (str (java.net.URL. path))
    (catch java.net.MalformedURLException err
      (str "file:///" path))))

Вначале при помощи defmulti объявляется функция-диспетчер, которая передает управление в соответствующий метод. Обратите внимание, что в зарефакторенной версии каждая функция делает ровно одну вещь, и поэтому её значительно легче понимать, чем изначальный комбайн.

Паттерн

Впринципе, если пройтись по коду, то легко заметить, что подобный паттерн встречается и в других местах, например: split-by-args, consume-block и т.д.

Я вовсе не призываю отказываться от оператора cond, но полагаю, что не стоит загружать его слишком большим количеством работы. Если он делает еще что-то, кроме распознавания условия и возврата значения – стоит рассмотреть альтернативу в виде мультиметода.