项目结构与实践 #

一、项目结构 #

1.1 标准目录结构 #

text
my-project/
├── project.clj
├── README.md
├── resources/
│   ├── public/
│   └── config.edn
├── src/
│   └── my_project/
│       ├── core.clj
│       ├── utils.clj
│       └── handlers/
│           └── api.clj
└── test/
    └── my_project/
        ├── core_test.clj
        └── utils_test.clj

1.2 目录说明 #

目录 用途
src/ 源代码
test/ 测试代码
resources/ 资源文件
dev/ 开发工具代码
doc/ 文档

1.3 命名空间与文件 #

text
src/
└── my_project/
    ├── core.clj          -> my-project.core
    ├── utils.clj         -> my-project.utils
    └── handlers/
        ├── api.clj       -> my-project.handlers.api
        └── web.clj       -> my-project.handlers.web

二、命名空间设计 #

2.1 命名空间声明 #

clojure
(ns my-project.core
  "Core functionality of the project."
  (:require [clojure.string :as str]
            [my-project.utils :as utils])
  (:import [java.util Date])
  (:gen-class))

2.2 require风格 #

clojure
(ns my-project.core
  (:require [clojure.string :as str]
            [clojure.set :refer [union intersection]]
            [my-project.utils :as u]))

(str/upper-case "hello")

(union #{1 2} #{2 3})

(u/helper)

2.3 避免过度引用 #

clojure
(ns my-project.core
  (:require [clojure.string :as str]))

(str/split "a,b,c" #",")

(ns my-project.core
  (:require [clojure.string :refer [split upper-case lower-case]]))

(split "a,b,c" #",")

2.4 命名空间组织 #

clojure
(ns my-project.core
  (:require [my-project.config :as config]
            [my-project.db :as db]
            [my-project.api :as api]
            [my-project.utils :as utils]))

(defn -main []
  (config/load!)
  (db/connect!)
  (api/start!))

三、代码风格 #

3.1 缩进 #

clojure
(defn my-function
  [arg1 arg2]
  (let [result (+ arg1 arg2)]
    (when (pos? result)
      (println "Result:" result)
      result)))

(defn another-function
  [x]
  (if (even? x)
    "even"
    "odd"))

3.2 函数定义 #

clojure
(defn function-name
  "Doc string describing the function."
  [param1 param2]
  (body))

(defn multi-arity
  ([x] (multi-arity x 1))
  ([x y] (+ x y)))

3.3 let绑定 #

clojure
(let [a 1
      b 2
      c (+ a b)]
  (* c 2))

3.4 线程宏 #

clojure
(-> value
    function1
    (function2 arg)
    function3)

(->> collection
     (map function1)
     (filter predicate)
     (reduce +))

3.5 条件表达式 #

clojure
(if condition
  then-expr
  else-expr)

(when condition
  expr1
  expr2)

(cond
  condition1 expr1
  condition2 expr2
  :else default)

四、测试实践 #

4.1 测试文件组织 #

clojure
(ns my-project.core-test
  (:require [clojure.test :refer [deftest is testing]]
            [my-project.core :as core]))

(deftest add-test
  (testing "basic addition"
    (is (= 4 (core/add 2 2))))
  (testing "edge cases"
    (is (= 0 (core/add 0 0)))
    (is (= -1 (core/add 1 -2)))))

4.2 测试断言 #

clojure
(deftest assertions-test
  (is (= 1 1))
  (is (true? true))
  (is (thrown? Exception (throw (Exception. "error"))))
  (is (instance? String "hello"))
  (is (.startsWith "hello" "hel")))

4.3 测试夹具 #

clojure
(use-fixtures :each
  (fn [f]
    (setup-db)
    (f)
    (cleanup-db)))

(deftest with-fixture
  (is (db/connected?)))

4.4 属性测试 #

clojure
(require '[clojure.test.check :as tc]
         '[clojure.test.check.generators :as gen]
         '[clojure.test.check.properties :as prop])

(def prop-add-commutative
  (prop/for-all [a gen/int
                 b gen/int]
    (= (core/add a b)
       (core/add b a))))

(tc/quick-check 100 prop-add-commutative)

五、错误处理 #

5.1 异常处理 #

clojure
(defn safe-divide [a b]
  (try
    (/ a b)
    (catch ArithmeticException e
      :division-by-zero)
    (catch Exception e
      (throw (ex-info "Division failed"
                      {:a a :b b}
                      e)))))

5.2 自定义异常 #

clojure
(defn validate-input [data]
  (when (empty? data)
    (throw (ex-info "Input cannot be empty"
                    {:input data}))))

(defn process [data]
  (try
    (validate-input data)
    (do-work data)
    (catch clojure.lang.ExceptionInfo e
      (let [{:keys [input]} (ex-data e)]
        (handle-error input)))))

5.3 Either模式 #

clojure
(defn either [value]
  (if (instance? Exception value)
    {:error value}
    {:ok value}))

(defn bind [result f]
  (if (:ok result)
    (f (:ok result))
    result))

(defn safe-parse [s]
  (try
    {:ok (Integer/parseInt s)}
    (catch Exception e
      {:error e})))

(-> (safe-parse "42")
    (bind #(hash-map :ok (* % 2))))

六、配置管理 #

6.1 配置文件 #

resources/config.edn:

clojure
{:database {:host "localhost"
            :port 5432
            :name "mydb"}
 :server {:port 3000
          :mode :dev}}

6.2 加载配置 #

clojure
(ns my-project.config
  (:require [clojure.edn :as edn]
            [clojure.java.io :as io]))

(defn load-config []
  (-> "config.edn"
      io/resource
      slurp
      edn/read-string))

(def config (load-config))

6.3 环境变量 #

clojure
(defn env [key default]
  (or (System/getenv key) default))

(def db-host (env "DB_HOST" "localhost"))
(def db-port (Integer/parseInt (env "DB_PORT" "5432")))

七、日志记录 #

7.1 使用tools.logging #

clojure
(ns my-project.core
  (:require [clojure.tools.logging :as log]))

(defn process [data]
  (log/info "Processing data:" data)
  (try
    (let [result (do-work data)]
      (log/debug "Result:" result)
      result)
    (catch Exception e
      (log/error e "Processing failed")
      (throw e))))

7.2 日志级别 #

clojure
(log/trace "Very detailed info")
(log/debug "Debug information")
(log/info "General information")
(log/warn "Warning message")
(log/error "Error message")

八、性能优化 #

8.1 类型提示 #

clojure
(defn ^String process [^String s]
  (.toUpperCase s))

(defn add [^Long a ^Long b]
  (+ a b))

8.2 避免反射 #

clojure
(set! *warn-on-reflection* true)

(defn bad [s]
  (.length s))

(defn good [^String s]
  (.length s))

8.3 惰性序列 #

clojure
(defn process-large-data [data]
  (->> data
       (map expensive-op)
       (filter valid?)
       (take 100)))

九、最佳实践总结 #

9.1 代码组织 #

  • 一个命名空间一个职责
  • 相关函数放在一起
  • 公共API放在顶层

9.2 命名约定 #

  • 使用kebab-case
  • 谓词函数以 ? 结尾
  • 副作用函数以 ! 结尾
  • 私有函数使用 defn-

9.3 文档 #

clojure
(defn my-function
  "Brief description of the function.
  
  Parameters:
    x - description of x
    y - description of y
  
  Returns:
    Description of return value.
  
  Example:
    (my-function 1 2) => 3"
  [x y]
  (+ x y))

9.4 测试原则 #

  • 测试公共API
  • 边界条件测试
  • 错误情况测试
  • 保持测试简单

十、总结 #

项目实践要点:

方面 建议
结构 遵循标准目录结构
命名空间 单一职责,清晰命名
代码风格 一致的缩进和格式
测试 覆盖公共API
错误处理 使用ex-info
配置 外部化配置
日志 使用tools.logging

关键点:

  1. 遵循标准项目结构
  2. 命名空间职责单一
  3. 保持代码风格一致
  4. 编写充分的测试
  5. 外部化配置和日志

Clojure完全指南系列完成!

最后更新:2026-03-27