Skip to main content

Command Palette

Search for a command to run...

namespaces, require, tests, project structure y domain modeling

Updated
5 min readView as Markdown
namespaces, require, tests, project structure y domain modeling

Esta semana vamos a pasar de “escribir funciones” a organizar un proyecto real. La meta es entender cómo se reparte el código en namespaces, cómo se conectan con require, cómo se ordenan src y test, y cómo se modela un dominio con funciones puras.

La idea no es memorizar una receta. Es entender por qué esta estructura hace el proyecto más fácil de leer, probar y mantener.

1) Namespaces: la manera de ordenar el código

Un namespace (ns) es una forma de agrupar funciones relacionadas. En Clojure, normalmente cada archivo tiene un namespace, y ese namespace debe coincidir con la ruta del archivo.

;; src/clojure_saveme/week3/domain.clj
(ns clojure-saveme.week3.domain)

Piensa en esto así:

  • el archivo dice dónde vive el código

  • el namespace dice cómo lo llamas

  • juntos te ayudan a no mezclar responsabilidades

Regla práctica:

  • src/clojure_saveme/week3/domain.clj

  • clojure-saveme.week3.domain

La carpeta usa _ y el namespace usa - porque así se traduce la ruta al nombre lógico del módulo.

2) require: conectar módulos entre sí

Cuando un namespace necesita usar funciones de otro, lo trae con :require.

;; src/clojure_saveme/week3/core.clj
(ns clojure-saveme.week3.core  
  (:require [clojure-saveme.week3.domain :as domain]))

(defn checkout  
  [raw-order min-total discount-percentage]
  (let [order (domain/build-order raw-order)]
    (if (domain/eligible-for-discount? order min-total)
      (domain/mark-paid (domain/apply-discount order discount-percentage))
      (domain/mark-paid order))))

Qué está pasando aquí:

  1. core no reescribe la lógica del negocio

  2. core llama a domain

  3. :as domain evita repetir el nombre completo cada vez

Eso mismo se ve en los tests: un namespace de pruebas también hace require del código que quiere verificar.

3) Estructura del proyecto: separar por responsabilidad

Cuando el proyecto crece, ya no conviene poner todo en un solo archivo. La idea es separar por intención:

src/clojure_saveme/
  week3/
    domain.clj
    core.clj
test/clojure_saveme/
  week3/
    domain_test.clj
    core_test.clj

Interpretación simple:

  • domain.clj = reglas y modelado

  • core.clj = orquestación del caso de uso

  • *_test.clj = pruebas del namespace correspondiente

Así encuentras rápido qué hace cada pieza y dónde cambiarla.

4) Tests: probar comportamiento, no duplicar lógica

Un test importa el namespace que quiere probar igual que cualquier otro archivo.

(nsclojure-saveme.week3.domain-test  
  (:require [clojure.test :refer :all]
            [clojure-saveme.week3.domain :as domain]))
(deftest order-total-test  
  (is (=300 (domain/order-total {:items [{:price 100 {:price 200}]})))
  (is (=0 (domain/order-total {:items []}))))

La clave es esta: el test no “recalcula” el resultado por otro camino. Solo verifica que la función haga lo esperado.

Cuando pruebas core, pruebas el flujo completo:

(deftest checkout-test  (testing "applies discount when order qualifies"    
   (let [result (core/checkout {:id 2 :customer "Luis" :items [{:price 500}]} 300 10)]
      (is (= :paid (:status result)))
      (is (= [{:price 450}] (:items result))))))

testing sirve para dar contexto cuando un mismo test cubre varios escenarios.

5) Funciones puras: la base para probar fácil

Una función pura:

  • devuelve siempre lo mismo para la misma entrada

  • no depende de cosas externas como archivos, base de datos o impresión en consola

(defn order-total  
  [order]
  (reduce +0 (map :price (:items order))))

Esto es ideal para tests porque solo necesitas entrada y salida.

Contraejemplo:

(defn order-total-impuro!  
  [order]
  (println "calculando total...")
  (reduce +0 (map :price (:items order))))

Ese println mete un efecto secundario y complica la prueba.

6) Domain modeling: tres tipos de funciones

Para modelar bien un dominio, ayuda separar el código en tres grupos.

1. Constructores / normalización

Toman datos crudos y devuelven una forma consistente.

(defn build-order  
  [{:keys [id customer items]}]
  {:id id
   :customer customer
   :items items
   :status:pending})

2. Reglas de negocio

Preguntan algo sobre el dominio.

(defn eligible-for-discount?  
  [ordermin-total]
  (>= (order-total order) min-total))

3. Transformaciones

Devuelven una nueva versión del dato sin mutarlo.

(defn apply-discount  
  [order percentage]
  (update order :items          
          (fn [items]
            (map #(update % :price * (-1 (/ percentage 100))) items))))
(defn mark-paid  
  [order]
  (assoc order :status :paid))

Separar así hace que el código sea más claro: si cambia una regla, no tienes que tocar todo el flujo.

7) core.clj: orquestar, no inventar reglas

core.clj funciona como el coordinador del caso de uso. Su trabajo es combinar piezas, no concentrar toda la lógica.

(defn checkout  
  [raw-order min-total discount-percentage]
  (let [order (domain/build-order raw-order)]
    (if (domain/eligible-for-discount? order min-total)
      (domain/mark-paid (domain/apply-discount order discount-percentage))
      (domain/mark-paid order))))

Piensa en esto como un flujo:

  1. recibo datos crudos

  2. los normalizo

  3. consulto una regla

  4. aplico transformaciones

  5. devuelvo el resultado final

8) Qué código vive esta semana

  • src/clojure_saveme/week3/domain.clj — dominio y funciones puras

  • src/clojure_saveme/week3/core.clj — orquestación y casos de uso

  • test/clojure_saveme/week3/domain_test.clj — pruebas del dominio

  • test/clojure_saveme/week3/core_test.clj — pruebas de orquestación

9) Cómo correr las pruebas

clojure -M:test -m cognitect.test-runner

10) Ejercicios para practicar

Básico

  • crea un namespace clojure-saveme.week3.reports y haz require de domain

  • escribe una función pura product-discount que reciba un producto y un porcentaje

Intermedio

  • agrega una regla overdue? para pedidos y su test

  • separa checkout en dos funciones: una que decide si aplica descuento y otra que aplica la transformación

Avanzado

  • crea clojure-saveme.week3.reports que dependa de core y domain

  • escribe una función que resuma pedidos: total general, cantidad pagada, cantidad pendiente

  • agrega tests con lista vacía como caso borde

11) Meta de la semana

Al terminar, deberías poder explicar:

  • por qué la ruta del archivo determina el namespace

  • cómo se usa :require con :as

  • cómo ordenar src y test cuando el proyecto crece

  • por qué las funciones puras son más fáciles de probar

  • cómo separar dominio y orquestación

Si entiendes eso, ya estás pensando como alguien que puede trabajar en un backend Clojure real: módulos claros, reglas bien separadas y tests que acompañan al código.

Enjoy!

Joe