Инкапсуляция, namespaces и друзья, в Clojure

Переодически меня мучают вопросы инкапсуляции и отделения интерфейсов от реализаций в Clojure. В этом куске мыслыей хочу на конкретном примере показать возможные варианты решения задачи.

Представим часть системы "Добро", в "Добро" должна быть сущность которая возвращает добро из какого-то источника, мы хотим реализовать систему так что бы у нас был слой абстракции отделяющий нас от конкретного способа получения добра.

Посмотрим как мы можем решить эту задачу используя Java а потом используя Clojure

Мы хотим что бы пользователи просто взаимодействовали c GoodMaker-ом

//GoodApp.java

package app;

import app.retriever.GoodMaker;

public class GoodApp {
    GoodMaker goodMaker = new GoodMaker();

    public void app(){
        goodMaker.retrieveGood();

    }
}

GoodMaker инкапсулирует логику получения получения добра в приватном методе private Good databaseGoodRetriever()

//GoodMaker.java
package app.retriever;

import app.Good;

public class GoodMaker {

    public Good retrieveGood() {
        return databaseGoodRetriever();
    }

    private Good databaseGoodRetriever(){
//        ... Достать добро из базы и возвратить
        return new Good();

    }
}

Мы можем пойти дальше - выделить логику по получению добра в отдельную сущность, и делегировать запрос получения конкретной реализации;

//GoodMaker.java

package app.retriever;

import app.Good;

public class GoodMaker {
    private GoodRetriver goodRetriver = new DatabaseGoodRetirver();

    public Good retrieveGood() {
        return goodRetriever.retriveGood();
    }

}

Теперь попробуем описать то же в Clojure

Какие возмжности у нас есть:

  • namespace и private/public функции
  • closures (замыкания)

(and "namespaces" "private/public functions")

Так мы хотим получать добро:

#good-app.clj

(ns good-app.core
  (:require [good-app.retriever :as retriever]))

  (defn app [] (retriever/retrieve-good))

Namespace good-app.retriever содержит публичный API для доступа retrieve-good. Публичный API делегирует задачу приватной функции, которорая является конкретной реализацией, в нашем случаее приватная функция вытягивает "Добро" из базы:

#good-retriever.clj
(ns good-app.retriever)

  (defn- database-retriever []
  ;;.... Достаем из базы 
    some-good)

  (defn retrieve-good [] (database-retriever))

defn- создает приватную функцию в неймспейсе, за приделами неймспейса ее не никто не сможет вызвать.
Нууууу...
Почти никто, на самом деле, при желании, ее сможет вызвать кто угодно из любого пакета вот так:

(#' good-app.retriever/database-retriever)

Хорошо это или плохо однозначно сказать трудно, с одной сторон если есть возможность ей обязательно кто то воспользуется, с другой стороны если человек так делает он делает это намеренно и должен понимать к чему такие вещи могут привести. Еще, таким способом можно покрывать тестами приватные функции.

Можно сделать подругому, можно открытй API описывать в одном namespace а функции с конкретной реализцией поместить в другой неймспейс, с именем говорящим что это конкретная реализация

Попробуем, переместим приватную функцию database-retriever из good-app.retriever в good-app.retriever.impl и будем вызывать ее оттуда. Важно что database-retriever теперь стала публична.

#good-retriever.clj

(ns good-app.retriever
  (:require [good-app.retriever.impl :as impl]))

  (defn retrieve-good [] (impl/database-retriever))
#good-retriever.clj

(ns good-app.retriever.impl)

  (defn database-retriever []
  ;;.... Достаем из базы 
    some-good)

Что у нас получилось в таком варианте? Мы разделили неймспесы с API и реализацией. Но теперь вообще кто угодно может вызвать функции из пакета good-app.retriever.impl, опять же человека должно смутить что он пользуется конкретной реализацие ане абстрактным API, но это же все от человека зависит.

Такой подход описан в книке The Joy of Clojure, кстати, часть с описанием этого подхода доступна для скачивания, страница 182 в книге, или 9 в бесплатном куске.

Еще можно попробовать описать интерфейс и реализацию в одном неймспейсе, но в разных файлах, таким образом функции с реализацией можно сделать приватными, и при этом логически разделить api и реализацию по файлам. Я так не делал, как это сделать написано тут

closures

Можно замкнуть нужные функции в сущности которую вернет публичный API, для илюстрации этого нам прийдется немножко поменять интерфейс доступа.

Мы пулучаем мапу с api функциями через (retriever/api), а потом выбираем нужную функцию и вызываем ее:

#good-app.clj

(ns good-app.core
  (:require [good-app.retriever :as retriever]))


  (defn app [] ((:retrieve-good (retriever/api))))
#good-retriever.clj

(ns good-app.retriever)

(defn api [] 
   (let [database-retriever (fn [] 
                               ;;.... Достаем из базы
                               some-good)]
    {:retrieve-good database-retriever}))

В таком случаее никто не сможет вызвать database-retriever не используя наш публичный api.

Все. Стало легче.

Материалы:

Written on October 5, 2013