# State Management Standards for Clojure
This document outlines the standards and best practices for managing state in Clojure applications. It's intended to guide developers in creating maintainable, performant, and robust systems. These guidelines reflect current Clojure best practices and are applicable to modern Clojure development.
## 1. Principles of State Management in Clojure
Clojure, as a functional programming language, emphasizes immutability. However, real-world applications require managing state. The key is to manage state in a controlled and predictable manner.
* **Immutability as the Default:** Favor immutable data structures whenever possible. This simplifies reasoning about code, avoids side effects, and enables concurrency.
* **Explicit State Management:** Make state transitions explicit and avoid implicit modification. This improves auditability and reduces the risk of unintended consequences.
* **Controlled Concurrency:** Clojure provides powerful concurrency primitives (atoms, refs, agents, vars). Choose the appropriate primitive based on the specific concurrency requirements.
* **Reactivity and Dataflow:** Consider using libraries like "integrant", Component, "re-frame", "Fulcro", or plain clojure.spec/watch for managing application lifecycle and data flow, especially in larger applications with complex dependencies and reactivity requirements.
## 2. Vars: Global Mutable State (Use with Caution!)
Vars are the simplest mechanism for managing state in Clojure, providing thread-local mutable storage. However, overuse can lead to global state issues.
### 2.1. Standards for Using Vars
* **Do This:** Use vars for configuration values that rarely change or for thread-local storage.
* **Don't Do This:** Use vars for application state that is frequently modified or shared between threads unless thread isolation is explicitly managed.
### 2.2. Why This Matters
Global mutable state (vars) can make code harder to reason about, especially in concurrent environments. Uncontrolled mutation can lead to race conditions and unpredictable behavior.
### 2.3. Code Examples
"""clojure
(ns my-app.config
(:defonce +config+ (atom {}))) ; Use defonce to initialize only once
(defn set-config! [key value]
(swap! +config+ assoc key value))
(defn get-config [key]
(@+config+ key))
;; Example Usage:
(set-config! :database-url "jdbc:...")
(println (get-config :database-url))
"""
### 2.4 Anti-patterns
* **Over-reliance on Vars:** Using vars for core application logic instead of local bindings and immutable data flow.
* **Uncontrolled Mutation:** Modifying vars without consideration for concurrency or data consistency.
## 3. Atoms: Mutable References
Atoms provide a mechanism for managing mutable state with atomic updates.
### 3.1. Standards for Using Atoms
* **Do This:** Use atoms for managing shared mutable state that requires atomic updates. Use "swap!" and "compare-and-set!" for modifying the atom's value safely.
* **Don't Do This:** Perform long-running or potentially blocking operations within "swap!" as this can degrade performance.
### 3.2. Why This Matters
Atoms ensure that updates to the state are atomic, preventing race conditions and data corruption in concurrent environments.
### 3.3. Code Examples
"""clojure
(def counter (atom 0))
(defn increment! []
(swap! counter inc))
(defn get-count []
@counter)
;; Example Usage:
(future (dotimes [_ 1000] (increment!)))
(future (dotimes [_ 1000] (increment!)))
(Thread/sleep 100) ; give futures some time to complete
(println (get-count)) ; should be close to 2000
"""
### 3.4. Anti-patterns
* **Direct Dereferencing without Atomic Updates:** Directly dereferencing the atom ("@atom-name") and then modifying and resetting it, leading to possible race conditions. Always use "swap!" or "compare-and-set!".
* **Using Atoms for Immutable Data:** Using atoms to hold immutable data structures needlessly.
* **Over-contention:** When multiple threads are constantly trying to update the same atom, it can lead to contention and performance issues.
## 4. Refs and Transactions
Refs provide transactional state management, ensuring that a series of operations are performed atomically.
### 4.1. Standards for Using Refs
* **Do This:** Use refs for managing shared state where consistency across multiple, related values is crucial. Update refs within "dosync" blocks to ensure transactional semantics.
* **Don't Do This:** Perform I/O operations or other side effects within "dosync" blocks as these can violate the atomicity and isolation guarantees.
### 4.2. Why This Matters
Refs guarantee ACID (Atomicity, Consistency, Isolation, Durability) properties for state updates, crucial for maintaining data integrity in complex systems.
### 4.3. Code Examples
"""clojure
(def account1 (ref 100))
(def account2 (ref 0))
(defn transfer! [amount]
(dosync
(if (>= @account1 amount)
(do
(alter account1 #(- % amount))
(alter account2 #(+ % amount)))
(throw (Exception. "Insufficient funds")))))
;; Example Usage:
(try
(transfer! 50)
(println "Transfer successful")
(catch Exception e
(println "Transfer failed:" (.getMessage e))))
(println "Account 1:" @account1) ; 50
(println "Account 2:" @account2) ; 50
"""
### 4.4. Anti-patterns
* **Performing I/O in Transactions:** Performing potentially failing I/O operations inside "dosync" blocks.
* **Long-running Transactions:** Holding transactions open for extended periods, leading to contention and reduced concurrency.
## 5. Agents: Asynchronous State Updates
Agents provide a mechanism for managing asynchronous state updates.
### 5.1. Standards for Using Agents
* **Do This:** Use agents for performing asynchronous, potentially long-running operations that update state.
* **Don't Do This:** Rely on agents for immediate, synchronous updates. Agents are designed for asynchronous processing.
### 5.2. Why This Matters
Agents allow decoupling state updates from the main thread, improving responsiveness and preventing blocking.
### 5.3. Code Examples
"""clojure
(def logger (agent []))
(defn log-message [message]
(send logger conj message)) ;; Use send to ensure proper agent queueing
(defn get-logs []
@logger)
;;Example Usage:
(log-message "Starting process...")
(log-message "Processing data...")
(log-message "Process completed.")
(await-for 100 logger) ; Wait for agent to process all messages
(println (get-logs))
"""
### 5.4. Anti-patterns
* **Synchronous Agent Use:** Using "send-off" when "send" is required, resulting in unintended asynchronous dispatch, or vice versa.
* **Ignoring Agent Errors:** Failing to handle exceptions thrown during agent actions; agents halt on exceptions by default. Use "set-error-handler!" on each agent, even if the handler just logs the error.
## 6. Component and Integrant: Managing Application Lifecycle and State Dependencies
Component and Integrant are libraries that provide a structured approach to managing application lifecycle and dependencies.
### 6.1. Standards for Using Component/Integrant
* **Do This:** Use Component or Integrant for applications with more than a few basic services/dependencies and especially when application shutdown is an important consideration. Define components as records implementing "Lifecycle" protocol (for Component) or use the component keys and init/halt functions as the core of Integrant.
* **Don't Do This:** Build complex and difficult-to-test systems manually managing startup/shutdown state.
### 6.2. Why This Matters
These libraries offer a declarative way to define application components, their dependencies, and their lifecycle (start/stop). This promotes modularity, testability, and maintainability. Integrant works with plain data and functions making it a great fit for Clojure.
### 6.3. Code Examples (Integrant)
"""clojure
(ns my-app.system
(:require [integrant.core :as ig]))
(defmethod ig/init-key :db/database [_ config]
(println "Initializing database with config:" config)
{:connection (atom {})}) ; Replace with actual DB initialization
(defmethod ig/halt-key :db/database [_ db]
(println "Closing database connection")
;; Close DB connection here
)
(defmethod ig/init-key :web/server [_ {:keys [db port]}]
(println "Starting web server on port" port)
{:server (atom {:db db :port port})}) ; Simulate a web server
(defmethod ig/halt-key :web/server [_ server]
(println "Stopping web server")
;; Stop web server here
)
(def system-config
{:db/database {:url "jdbc:..."}
:web/server {:db (ig/ref :db/database) :port 8080}})
;; Example usage:
(def system (ig/init system-config))
;; ... application runs ...
(ig/halt! system)
"""
### 6.4. Anti-patterns
* **Ignoring Component Lifecycle:** Failing to properly start and stop components, leading to resource leaks or unexpected behavior.
* **Tight Coupling:** Creating tight dependencies between components, reducing modularity and testability.
## 7. Managing Reactivity with "re-frame" and Fulcro
For UI-intensive applications, libraries like "re-frame" and Fulcro provide a structured way to manage application state and reactivity.
### 7.1. Standards for Using "re-frame" and Fulcro
* **Do This:** Adopt "re-frame"'s event-driven architecture or Fulcro's data-driven (normalized) approach for managing application state in complex UIs. Use subscriptions for deriving UI state from the central data store.
* **Don't Do This:** Mutate application state directly within UI components, bypassing the "re-frame" event handling or Fulcro mutations.
### 7.2. Why This Matters
These libraries provide a predictable and efficient mechanism for updating the UI in response to application state changes.
### 7.3. Code Example ("re-frame")
"""clojure
(ns my-app.re-frame
(:require [re-frame.core :as rf]))
;;Define an event handler
(rf/reg-event-fx
:increment
(fn [{:keys [db]} [_]]
{:db (update db :counter inc)}))
;; Define a subscription
(rf/reg-sub
:counter
(fn [db [_]]
(:counter db)))
;; Usage in a component (Om Next example)
(defn my-component []
(let [counter (rf/subscribe [:counter])]
(fn []
(dom/div
(dom/h1 (str "Counter: " @counter))
(dom/button {:on-click #(rf/dispatch [:increment])} "Increment")))))
"""
### 7.4. Anti-patterns
* **Direct State Mutation:** Modifying the "re-frame" or Fulcro application database directly instead of using events or mutations.
* **Complex Subscriptions:** Creating overly complex subscriptions that perform heavy computations, impacting UI performance.
## 8. Metadata and Watches
Clojure provides a mechanism for attaching metadata to data structures and setting up watches to observe changes to vars, atoms, refs, and agents.
### 8.1. Standards for metadata and watches
* **Do This:** Use metadata to attach auxiliary information to data structures without modifying their core functionality. Use watches to observe changes in state variables during development and debugging or react to state changes in production.
* **Don't Do This:** Overuse watches in production as they can negatively affect performance.
### 8.2. Why This Matters
Metadata allows associating additional information with data without altering its structure or semantics. Watches provides notifications when the value of a stateful value changes.
### 8.3. Code Examples
"""clojure
;; Using metadata
(def my-vector (with-meta [1 2 3] {:description "A simple vector"}))
(println (meta my-vector)) ;; Prints: {:description "A simple vector"}
;; Using watches
(def my-atom (atom 0))
(add-watch my-atom :my-watch
(fn [key atom old-state new-state]
(println "Atom" key "changed from" old-state "to" new-state)))
(swap! my-atom inc) ;; Output: Atom :my-watch changed from 0 to 1
(remove-watch my-atom :my-watch)
"""
### 8.4. Anti-patterns
* **Over-reliance on Watches for Core Logic:** Do not implement business logic inside of watch functions as they are for debugging.
* **Ignoring Watch Performance:** Using watches in performance-critical sections of code without considering their impact.
* **Abusing Metadata:** Attaching critical application data or logic to metadata, making it less discoverable and harder to maintain.
## 9. General Recommendations
* **Choose the Right Tool:** Select the appropriate state management mechanism based on the specific requirements of your application.
* **Keep State Local:** Minimize the scope of mutable state to reduce complexity and improve testability.
* **Embrace Immutability:** Favor immutable data structures whenever possible to simplify reasoning about code and prevent side effects.
* **Test Thoroughly:** Write comprehensive tests to ensure that state transitions are correct and that concurrent access is handled safely.
* **Monitor Performance:** Use performance monitoring tools to identify and address any bottlenecks related to state management.
* **Avoid Legacy Code Patterns:** Clojure evolves, avoid techniques that are out of date or have known drawbacks.
* **Always document the reason behind state selection**: Explain briefly why a specific approach for managing state has been selected in a particular context.
By following these standards, you can create Clojure applications that are maintainable, performant, and robust. Remember that this document should be adapted according to particular use cases and team conventions.
danielsogl
Created Mar 6, 2025
This guide explains how to effectively use .clinerules
with Cline, the AI-powered coding assistant.
The .clinerules
file is a powerful configuration file that helps Cline understand your project's requirements, coding standards, and constraints. When placed in your project's root directory, it automatically guides Cline's behavior and ensures consistency across your codebase.
Place the .clinerules
file in your project's root directory. Cline automatically detects and follows these rules for all files within the project.
# Project Overview project: name: 'Your Project Name' description: 'Brief project description' stack: - technology: 'Framework/Language' version: 'X.Y.Z' - technology: 'Database' version: 'X.Y.Z'
# Code Standards standards: style: - 'Use consistent indentation (2 spaces)' - 'Follow language-specific naming conventions' documentation: - 'Include JSDoc comments for all functions' - 'Maintain up-to-date README files' testing: - 'Write unit tests for all new features' - 'Maintain minimum 80% code coverage'
# Security Guidelines security: authentication: - 'Implement proper token validation' - 'Use environment variables for secrets' dataProtection: - 'Sanitize all user inputs' - 'Implement proper error handling'
Be Specific
Maintain Organization
Regular Updates
# Common Patterns Example patterns: components: - pattern: 'Use functional components by default' - pattern: 'Implement error boundaries for component trees' stateManagement: - pattern: 'Use React Query for server state' - pattern: 'Implement proper loading states'
Commit the Rules
.clinerules
in version controlTeam Collaboration
Rules Not Being Applied
Conflicting Rules
Performance Considerations
# Basic .clinerules Example project: name: 'Web Application' type: 'Next.js Frontend' standards: - 'Use TypeScript for all new code' - 'Follow React best practices' - 'Implement proper error handling' testing: unit: - 'Jest for unit tests' - 'React Testing Library for components' e2e: - 'Cypress for end-to-end testing' documentation: required: - 'README.md in each major directory' - 'JSDoc comments for public APIs' - 'Changelog updates for all changes'
# Advanced .clinerules Example project: name: 'Enterprise Application' compliance: - 'GDPR requirements' - 'WCAG 2.1 AA accessibility' architecture: patterns: - 'Clean Architecture principles' - 'Domain-Driven Design concepts' security: requirements: - 'OAuth 2.0 authentication' - 'Rate limiting on all APIs' - 'Input validation with Zod'
# Code Style and Conventions Standards for Clojure This document outlines the coding style and conventions to be followed when developing Clojure code. Adhering to these standards will ensure code readability, maintainability, and consistency across projects. The guidelines presented here align with modern Clojure practices and aim to leverage the language's strengths for effective software development. ## 1. General Principles * **Consistency:** Maintain a consistent style throughout the codebase. Prefer using established idioms and patterns over inventing new ones. * **Why:** Consistent code is easier to understand and modify. It also reduces cognitive load for developers working on the project. * **Readability:** Write code that is easy to read and understand. Prioritize clarity over brevity. * **Why:** Code is read much more often than it is written. Readable code reduces debugging time and makes it easier for new developers to join the project. * **Simplicity:** Keep code as simple as possible. Avoid unnecessary complexity. * **Why:** Simple code is easier to reason about and less prone to errors. It also makes it easier to refactor and maintain the codebase. * **Idempotence:** Functions should always return the same result if given the same arguments. Prefer immutable data structures to reduce side effects. * **Why:** Idempotent data structures promote easier reasoning about code. * **Testability:** Design code that is easy to test. Use dependency injection and avoid tight coupling. * **Why:** Testable code is more reliable and easier to maintain. It also allows for more confident refactoring. ## 2. Formatting ### 2.1. Indentation * Use 2 spaces for indentation. Avoid tabs. * **Why:** Consistent indentation makes code easier to read. Two spaces are a good balance between readability and line length. * Align code vertically where appropriate. * **Why:** Vertical alignment highlights the structure of the code and makes it easier to scan. * Use indentation to reflect the structure of the code. """clojure (defn calculate-total [price quantity discount] (let [discounted-price (- price (* price discount)) total-price (* discounted-price quantity)] total-price)) """ ### 2.2. Line Length * Limit lines to 80 characters. Consider breaking lines at logical points. * **Why:** Shorter lines are easier to read on a variety of screen sizes. They also make it easier to compare code side-by-side. * When breaking lines, indent the continuation lines by 2 spaces. """clojure (defn long-function-name [argument1 argument2 argument3] (let [result (+ argument1 argument2 argument3)] result)) """ ### 2.3. Whitespace * Use whitespace to separate logical sections of code. * **Why:** Whitespace improves readability and makes it easier to scan the code. * Add a newline after each top-level form. * **Why:** Clear separation of forms improves understanding. """clojure (ns my-project.core) (defn hello [name] (println (str "Hello, " name))) (hello "World") """ * Use a space after commas in collections. """clojure (def my-vector [1, 2, 3, 4]) ; Do this (def my-vector [1,2,3,4]) ; Don't do this """ ### 2.4. File Structure * Place namespaces in directories that match the namespace name. For example, the namespace "my-project.core" should be in the file "src/my_project/core.clj". * **Why:** This makes it easy to find the source code for a given namespace. * Keep related functions and data structures in the same namespace. * **Why:** This improves code organization and makes it easier to understand the dependencies between different parts of the application. * Use "-main" function to start the application if needed. ## 3. Naming Conventions ### 3.1. Namespaces * Use lowercase letters and underscores for namespace names. * **Why:** This is the convention in the Clojure community. * Use a hierarchical structure for namespace names (e.g., "com.example.my-project.core"). * **Why:** This helps to organize code and avoid naming conflicts. * Follow reverse domain name notation for the root namespace (e.g., "com.example.my-project"). """clojure (ns com.example.my-project.core) """ ### 3.2. Functions * Use lowercase letters and hyphens for function names. * **Why:** This is the convention in the Clojure community. * Use descriptive names that clearly indicate the function's purpose. * **Why:** Clear names make it easier to understand the code. * Use verbs for function names. * **Why:** Verbs indicate that the function performs an action. * When naming functions with side effects, end the name with an exclamation mark ("!"). * **Why:** This alerts developers that the function has side effects. Avoid this in most cases, prefer immutable data structures. """clojure (defn calculate-sum [numbers] (reduce + numbers)) (defn print-message! [message] (println message)) ; use with caution, prefer immutable structures """ ### 3.3. Variables and Constants * Use lowercase letters and hyphens for variable names. * **Why:** This is the convention in the Clojure community. * Use descriptive names that clearly indicate the variable's purpose. * **Why:** Clear names make it easier to understand the code. * Use "def" for constants and capitalize the name with underscores. * **Why:** Constants should be easily identifiable. """clojure (def PI 3.14159) (def MAX_VALUE 100) """ ### 3.4. Keywords * Use keywords for map keys. * **Why:** Keywords are interned, which makes them more efficient than strings. They also prevent naming conflicts. """clojure (def person {:name "John" :age 30}) """ ## 4. Stylistic Consistency ### 4.1. Idiomatic Clojure * Use Clojure's built-in functions and data structures whenever possible. * **Why:** Clojure's built-in functions are highly optimized. They also follow established conventions. * Use "destructuring" to extract values from data structures. * **Why:** Destructuring makes code more concise and readable. """clojure (defn greet [{:keys [name age]}] (println (str "Hello, " name "! You are " age " years old."))) (greet {:name "John" :age 30}) """ * Use "->" and "->>" for threading operations. * **Why:** Threading macros make code more readable by chaining operations together. """clojure (defn process-data [data] (-> data (map inc) (filter even?) (reduce +))) """ * Use Records and Types sparingly, opting for maps. While Records can provide performance benefits, the added complexity may outweigh the gains in many scenarios. * **Why:** Maps are more flexible data. ### 4.2. Immutability * Use immutable data structures whenever possible. * **Why:** Immutable data structures prevent side effects and make code easier to reason about. * Use "transient" to optimize operations that require modifying a data structure. * **Why:** Transients allow for in-place modification of data structures while still maintaining the illusion of immutability. """clojure (defn update-vector [v] (persistent! (reduce conj! (transient v) (range 10)))) """ ### 4.3. Error Handling * Use "try"/"catch"/"finally" for handling exceptions. * **Why:** Exception handling prevents the application from crashing and allows for graceful recovery from errors. * Use "assert" for validating input. * **Why:** Assertions can help catch errors early in the development process. """clojure (defn divide [a b] (assert (not= b 0) "Cannot divide by zero") (/ a b)) (try (divide 10 0) (catch ArithmeticException e (println "Error: " (.getMessage e)))) """ ### 4.4 Comments * Write meaningful comments. Explain the "why" not the "what". * **Why:** Code tells you what is happening, comments should explain why it is happening. * Use docstrings to document functions and namespaces. * **Why:** Docstrings are the primary way to document Clojure code. """clojure (ns my-project.core "This namespace contains the core functions for my project.") (defn calculate-sum "Calculates the sum of a collection of numbers." [numbers] (reduce + numbers)) """ ### 4.5 Testing * Write unit tests for all functions. * **Why:** Unit tests ensure that the code works as expected and prevent regressions. * Use a testing framework such as "clojure.test" or "Midje". * **Why:** Testing frameworks provide a structured way to write and run tests. * Write integration tests to verify that different parts of the application work together correctly. * **Why:** Integration tests ensure that the application as a whole works as expected. ## 5. Specific Anti-Patterns and Mistakes to Avoid * **Over-commenting trivial code:** Avoid stating the obvious. Comments should explain *why* the code is written, not *what* it does (unless the what is obfuscated). * **Using mutable state excessively:** Embrace Clojure's immutability. Minimize the use of "atoms", "refs", and "agents". For instance, if a function can be written without using an "atom", it generally *should* be. * **Ignoring REPL-driven development:** Clojure excels with REPL-driven development. Use it! Don't just write code in files and then execute them. Interact with your code, test functions directly, and explore the data flow in real-time. * **Premature optimization:** Don't optimize code before it's necessary. Focus on writing clear and correct code first. Profile before attempting any speed increases. ## 6. Modern Approaches and Patterns * **Data-Oriented Programming (DOP):** DOP is a core concept in Clojure. Treat data as immutable and first-class. Functions transform data. * Example: Configuration Management """clojure (def default-config {:db-host "localhost" :db-port 5432 :cache-size 1000}) (defn merge-config [base-config overrides] (merge base-config overrides)) (def prod-config (merge-config default-config {:db-host "prod.example.com" :cache-size 2000})) """ * **Component-Based Architecture:** Using libraries like "component" or "integrant" to manage the lifecycle of application components. * **Example (using Integrant):** """clojure (ns my-app.system (:require [integrant.core :as ig])) (defmethod ig/init-key :db/connection [_ config] (println "Connecting to DB:" config) ;; Actual DB connection code here {:connection config}) (defmethod ig/halt-key :db/connection [_ {:keys [connection]}] (println "Disconnecting from DB:" connection)) (def config {:db/connection {:host "localhost" :port 5432}}) (comment (def system (ig/init config)) (ig/halt! system)) """ * **Asynchronous Programming:** Using "core.async" for managing asynchronous operations and concurrency. * Example: """clojure (ns my-app.async (:require [clojure.core.async :as async])) (defn process-item [item] (Thread/sleep 1000) ; Simulate processing time (str "Processed: " item)) (defn start-processor [input-chan output-chan] (async/go-loop [] (when-let [item (async/<! input-chan)] (let [result (process-item item)] (async/>! output-chan result) (recur))))) (comment (let [input-chan (async/chan) output-chan (async/chan) _ (start-processor input-chan output-chan)] (async/>!! input-chan "Item 1") (async/>!! input-chan "Item 2") (async/close! input-chan) (println (async/<!! output-chan)) ; Prints "Processed: Item 1" (println (async/<!! output-chan)))) ; Prints "Processed: Item 2" """ * **Using Spec for Data Validation and Generation:** "clojure.spec.alpha" allows you to define specifications for your data and use these specifications for validation, data generation, and documentation. """clojure (ns my-app.spec (:require [clojure.spec.alpha :as s])) (s/def ::name string?) (s/def ::age pos-int?) (s/def ::person (s/keys :req-un [::name ::age])) (def valid-person {:name "Alice" :age 30}) (def invalid-person {:name "Bob" :age -5}) (comment (s/valid? ::person valid-person) ; true (s/valid? ::person invalid-person) ; false (s/explain ::person invalid-person)) """ ## 7. Performance Optimization Techniques * **Use transients for mutable operations on persistent data structures:** Transients allow for in-place modification of persistent data structures, which can significantly improve performance. Convert back to a persistent data structure after the modifications are complete. """clojure (defn modify-vector [v] (let [t (transient v)] (dotimes [i (count v)] (aset! t i (* i 2))) ; Mutating transient vector (persistent! t))) ; Convert back to persistent vector """ * **Minimize reflection by using type hints:** Clojure uses reflection to determine the types of arguments at runtime. Type hints can help the compiler avoid reflection, which can improve performance. """clojure (defn add [^long x ^long y] ; type hints for x and y (+ x y)) """ * **Use the "loop" and "recur" construct for tail-recursive functions:** Tail-recursive functions can be optimized by the compiler to avoid stack overflow errors. """clojure (defn factorial [n] (loop [n n acc 1] (if (<= n 1) acc (recur (dec n) (* acc n))))) """ * **Leverage Java Interop Judiciously:** While Clojure promotes functional programming, leveraging well-optimized Java libraries (while maintaining Clojure style) can boost performance for specific tasks. ## 8. Security Best Practices * **Input Validation:** Always validate user input to prevent injection attacks (e.g., SQL injection, XSS). Use "clojure.spec.alpha" or other validation libraries to ensure that input data conforms to expected formats and values. * **Output Encoding:** Properly encode output data to prevent XSS attacks. Use appropriate encoding functions for different output contexts (e.g., HTML, JavaScript, SQL). * **Authentication and Authorization:** Implement robust authentication and authorization mechanisms to protect sensitive data and resources. Use established libraries and frameworks for authentication and authorization, such as "friend" or "buddy". * **Dependency Management:** Keep dependencies up to date to address known security vulnerabilities. Regularly audit dependencies and update them to the latest versions. * **Avoid Using "eval":** Using "eval" can introduce security vulnerabilities if the input is not carefully validated. Avoid using "eval" whenever possible. If you must use it, ensure that the input is from a trusted source and that it is properly validated. * **Secrets Management:** Store secrets (e.g., passwords, API keys) securely and avoid hardcoding them in the codebase. Use environment variables or dedicated secrets management tools to store and manage secrets. * **Secure Random Number Generation:** Use secure random number generators for generating cryptographic keys and other security-sensitive data. Use the "java.security.SecureRandom" class or a dedicated security library for generating random numbers. This coding style and conventions document provides a comprehensive set of guidelines for developing Clojure code. By following these standards, developers can ensure that their code is readable, maintainable, and consistent. This leads to improved collaboration, reduced development time, and higher-quality software. Remember that these are guidelines, and there will be situations where it makes sense to deviate from them. Always use your best judgment and consider the specific context of the code you are writing.
# Core Architecture Standards for Clojure This document outlines the core architectural standards for Clojure projects, encompassing architectural patterns, project structure, and organization principles. Adhering to these standards enhances maintainability, performance, and security. ## 1. Architectural Patterns Selecting the right architectural pattern forms the foundation of a robust and scalable Clojure application. ### 1.1. Functional Core, Imperative Shell **Description:** This pattern separates the pure functional core of the application from the imperative shell that interacts with the outside world (e.g., databases, network services). **Do This:** * Maximize the amount of code in the functional core to leverage Clojure's strengths in immutability, concurrency, and testability. * Isolate side-effecting operations in the imperative shell using protocols/multimethods for abstraction. **Don't Do This:** * Mix side-effecting code directly within the core business logic. * Create large, monolithic functions that perform both pure computations and I/O operations. **Why:** Improves testability, reduces complexity, and allows for easier reasoning about the application's behavior. **Example:** """clojure ;; Functional Core (pure functions) (ns my-app.core (:require [my-app.domain :as domain])) (defn calculate-discount "Calculates the discount based on customer type and purchase amount." [customer purchase-amount] (domain/apply-discount customer purchase-amount)) ;; Imperative Shell (side-effecting operations) (ns my-app.handler (:require [my-app.core :as core] [ring.util.response :as response])) (defn handle-purchase "Handles a purchase request. This is the 'shell'." [request] (let [customer (get-in request [:body :customer]) purchase-amount (get-in request [:body :purchase-amount]) discounted-amount (core/calculate-discount customer purchase-amount)] (response/response (str "Discounted amount: " discounted-amount)))) """ **Anti-Pattern:** Embedding database calls within the "calculate-discount" function. ### 1.2. Component-Based Architecture (using "integrant" or "component") **Description:** Decomposes the application into independent, reusable components that can be easily assembled and configured. Use libraries like "integrant" or "component". **Do This:** * Define components as records that implement lifecycle protocols (e.g., "init", "start", "stop"). * Use configuration maps to specify component dependencies and settings. * Favor composition over inheritance. **Don't Do This:** * Create tightly coupled components with circular dependencies. * Hardcode component configurations. **Why:** Enhances modularity, testability, and reusability. Facilitates easier deployments and system evolution. **Example (using "integrant"):** """clojure (ns my-app.components (:require [integrant.core :as ig] [clojure.java.jdbc :as jdbc])) ;; Database Component (defmethod ig/init-key :db/h2 [_ config] (println "Initializing database...") (jdbc/get-connection config)) (defmethod ig/halt-key :db/h2 [_ db] (println "Closing database connection...") (.close db)) ;; HTTP Server Component (example) (defmethod ig/init-key :http/server [_ config] (println "Starting HTTP server...") ;; code to start the server {:server config}) ;; dummy server (defmethod ig/halt-key :http/server [_ {:keys [server]}] (println "Stopping HTTP server...") ;; code to stop the server nil) ;; dummy server ;; System Configuration (def config {:db/h2 {:dbtype "h2" :dbname "my-app"} :http/server {:port 3000}}) ;; Starting the system (def system (ig/init config)) ;; Stopping the system (ig/halt! system) """ **Anti-Pattern:** Creating a global database connection outside the component lifecycle. ### 1.3 Data-Oriented Architecture **Description:** Architecting the application around data transformations, where data flows through a series of functions. Emphasize the use of immutable data structures. **Do This:** * Model data using persistent data structures (maps, vectors, sets). * Use functions to transform data from one representation to another. * Leverage "clojure.spec.alpha" for data validation and generation. **Don't Do This:** * Rely on mutable state to track application data. * Mix data transformation logic with side-effecting operations. **Why:** Improves predictability, simplifies concurrency, and facilitates data provenance. **Example:** """clojure (ns my-app.data (:require [clojure.spec.alpha :as s])) ;; Define data specifications (s/def :user/id uuid?) (s/def :user/name string?) (s/def :user/email string?) (s/def :user/profile (s/keys :req [:user/id :user/name :user/email])) ;; Data transformation function (defn enrich-user-data "Enriches user data with additional calculated fields." [user] (assoc user :user/creation-date (java.util.Date.))) ;; Example usage (def raw-user {:user/id (java.util.UUID/randomUUID) :user/name "Alice" :user/email "alice@example.com"}) (def validated-user (s/conform :user/profile raw-user)) (def enriched-user (enrich-user-data validated-user)) (println enriched-user) """ **Anti-Pattern:** Modifying the "raw-user" map directly instead of creating a new, enriched map. ## 2. Project Structure and Organization A well-defined project structure is essential for maintainability and collaboration. ### 2.1. Standard Directory Layout **Do This:** * Follow the Leiningen/Boot standard directory layout: * "src/": Source code. * "test/": Test code. * "resources/": Static assets and configuration files. * Organize namespaces according to the project's domain. Use a hierarchical structure matching the directory structure reflecting the conceptual domain. **Don't Do This:** * Store source code in arbitrary locations. * Mix source code with test code. **Why:** Provides a consistent and predictable structure for all Clojure projects. **Example:** """ my-app/ ├── project.clj ├── src/ │ ├── my_app/ │ │ ├── core.clj │ │ ├── database.clj ; DB related operations are in a named namespace │ │ ├── api/ ; subfolder for API definitions │ │ │ └── routes.clj │ │ └── utils.clj ; Generic utility functions ├── test/ │ ├── my_app/ │ │ └── core_test.clj └── resources/ └── config.edn """ ### 2.2. Namespaces and Modules **Do This:** * Use meaningful namespace names that reflect the module's purpose ("com.example.my-app.db"). * Keep modules small and focused, each responsible for a single aspect of the application. * Use "require" and "use" judiciously to manage dependencies between modules. * Qualify function names when using "require" to improve readability ("(db/get-user user-id)"). * Expose only the necessary functions from a module using "defn-" for private functions. **Don't Do This:** * Create large, monolithic namespaces with hundreds of functions. * Use "use" excessively, as it can lead to namespace pollution. * Create circular dependencies between modules. **Why:** Promotes modularity, reduces cognitive load, and prevents naming conflicts. **Example:** """clojure ;; src/my_app/db.clj (ns my-app.db (:require [clojure.java.jdbc :as jdbc])) (def db-spec {:dbtype "postgresql" :dbname "my-database" :host "localhost" :user "db-user" :password "secret"}) (defn get-user "Retrieves a user from the database by ID." [user-id] (jdbc/query db-spec ["SELECT * FROM users WHERE id = ?" user-id])) (defn- internal-helper-function ;; private function "Only available within the my-app.db namespace" [x] (* x 2)) """ ### 2.3. Configuration Management **Do This:** * Store configuration settings in external files (e.g., EDN, YAML) or environment variables. * Use a library like "aero" or "environ" to load and manage configuration. * Avoid hardcoding configuration values in the source code. **Don't Do This:** * Store sensitive information (e.g., passwords, API keys) in plain text configuration files. Favor using environment variables or secure storage. * Load configuration settings directly into functions. **Why:** Simplifies deployment, allows for environment-specific configurations, and protects sensitive data. **Example (using "aero"):** """clojure ;; project.clj (add aero dependency) ;; (defproject my-app "0.1.0-SNAPSHOT" ;; ... ;; :dependencies [[org.clojure/clojure "1.11.1"] ;; [integrant "0.8.0"] ;; [aero "1.1.6"]]) ;; resources/config.edn {:db {:dbtype "postgresql" :dbname "my-database" :host "localhost"}} ;; src/my_app/config.clj (ns my-app.config (:require [aero.core :as aero])) (def config (aero/read-config "config.edn")) (println config) ;; Access configuration values (e.g., (:db config)) """ **Anti-Pattern:** Hardcoding database credentials directly in the "get-user" function. ## 3. Design Patterns Applying established design patterns can improve code structure and reusability. ### 3.1. Protocol-Oriented Programming **Description:** Defines abstract interfaces (protocols) and provides concrete implementations using records or types. **Do This:** * Define protocols for common operations or behaviors. * Implement protocols for different data types or components. * Use protocols to decouple components and promote code reuse. **Don't Do This:** * Create overly complex protocols with too many functions. * Implement protocols for trivial operations. **Why:** Enables polymorphism and extensibility, allowing you to add new functionality without modifying existing code. **Example:** Note the use of clojure.spec.alpha to validate that implementations of the protocol meet the expected contract. This is good practice to ensure protocol implementations are correct. """clojure (ns my-app.protocols (:require [clojure.spec.alpha :as s])) ;; Define a protocol (defprotocol Loggable (log [this message] "Logs a message.")) ;; Define a specification for the log function (s/fdef log :args (s/cat :this any? :message string?) :ret any?) ; Adjust 'any?' to a more specific return type if applicable ;; Implement the protocol for a record (defrecord User [id name email] Loggable (log [this message] (println (str "User " id ": " message)))) ;; Example usage (def user (->User 123 "Alice" "alice@example.com")) (log user "User logged in.") """ **Anti-Pattern:** Using concrete types directly instead of relying on protocols. ### 3.2. State Management with Atoms and Refs **Description:** Clojure provides built-in mechanisms for managing mutable state in a concurrent environment. Leverage "atom" for coordinated atomic updates, and "ref" for coordinated, transactional updates. Use "agent" sparingly, as it offers less control over timing. **Do This:** * Use "atom" for simple, independent state updates. * Use "ref" for complex, coordinated updates that require transactions. * Use "swap!" to update atoms atomically. * Use "dosync" to perform transactional updates on refs. * Consider using libraries like "mount" or "component" to manage application state. **Don't Do This:** * Use mutable global variables without proper synchronization. * Perform long-running or blocking operations within transactions. **Why:** Ensures data consistency and prevents race conditions in concurrent applications. **Example (using "atom"):** """clojure (def counter (atom 0)) (defn increment-counter "Increments the counter atomically." [] (swap! counter inc)) (increment-counter) (println @counter) """ **Example (using "ref"):** """clojure (def account1 (ref 100)) (def account2 (ref 50)) (defn transfer "Transfers money from one account to another." [amount from to] (dosync (alter from - amount) (alter to + amount))) (transfer 20 account1 account2) (println @account1) (println @account2) """ **Anti-Pattern:** Directly modifying the value of an atom without using "swap!". ### 3.3. Asynchronous Programming (using "core.async") **Description:** Clojure provides "core.async" for managing asynchronous operations using channels and go blocks. **Do This:** * Use "core.async" to handle I/O-bound operations without blocking the main thread. * Use channels to communicate between different parts of the application asynchronously. * Use "go" blocks to perform computations concurrently. * Utilize "alts!!" or "alts!" for non-blocking channel operations **Don't Do This:** * Perform CPU-intensive operations within "go" blocks. Use "pmap" or "future" for these. * Block the main thread with synchronous I/O operations. **Why:** Improves responsiveness and scalability by allowing the application to handle multiple requests concurrently. **Example:** """clojure (ns my-app.async (:require [clojure.core.async :as async :refer [chan go >! <! alts!]])) (defn process-data "Simulates a long-running process." [data] (Thread/sleep 1000) (str "Processed: " data)) (defn start-worker "Starts a worker that processes data from a channel." [input-chan output-chan] (go (loop [] (let [data (<! input-chan)] (when data (let [result (process-data data)] (>! output-chan result) (recur))))))) (defn example-async [] (let [input-chan (chan) output-chan (chan) _ (start-worker input-chan output-chan)] ; start worker in background (go (>! input-chan "Data 1") (>! input-chan "Data 2") (async/close! input-chan)) (go (loop [] (let [[val port] (alts! [output-chan (async/timeout 5000)])] (cond (= port output-chan) (do (println (str "Received: " val)) (recur)) :else (println "Timeout or channel closed"))))))) """ **Anti-Pattern:** Performing synchronous HTTP requests directly within a Ring handler. ## 4. Clojure Ecosystem and Tooling Leveraging the Clojure ecosystem's tools and libraries can significantly improve productivity and code quality. ### 4.1. Leiningen/Boot **Do This:** * Use Leiningen or Boot as the build tool for managing dependencies, running tests, and creating deployments. * Define dependencies explicitly in the "project.clj" or "build.boot" file. * Use Leiningen plugins to automate common tasks (e.g., linting, formatting). **Don't Do This:** * Manually manage dependencies by downloading JAR files. * Use outdated versions of Leiningen or Boot. **Why:** Provides a standardized and automated build process. ### 4.2. REPL-Driven Development **Do This:** * Use the REPL (Read-Eval-Print Loop) extensively for interactive development and experimentation. * Load code into the REPL using "load-file" or "require". * Use the REPL to test functions and explore data structures. * Use tools like "cider" or "refactor-nrepl" to enhance the REPL experience. **Don't Do This:** * Rely solely on unit tests without exploring the code in the REPL. * Restart the REPL frequently, instead leverage tools to reload your code. **Why:** Enables rapid prototyping, debugging, and iterative development. ### 4.3. Testing **Do This:** * Write comprehensive unit tests using "clojure.test". * Use mocking libraries like "with-redefs" or "mockfn" to isolate units of code. * Write integration tests to verify the interaction between different components. * Use generative testing libraries like "clojure.test.check" to automatically generate test cases. * Use "lein test" or "boot test" to run the test suite. * Run tests frequently during development. **Don't Do This:** * Skip writing tests altogether. * Write tests that are too brittle or tightly coupled to the implementation. **Why:** Ensures code correctness, prevents regressions, and facilitates refactoring. **Example:** """clojure (ns my-app.core-test (:require [clojure.test :refer :all] [my-app.core :as core])) (deftest calculate-discount-test (testing "Calculates discount for regular customer" (is (= 90 (core/calculate-discount :regular 100)))) (testing "Calculates discount for premium customer" (is (= 80 (core/calculate-discount :premium 100))))) """ ### 4.4. Linting and Formatting **Do This:** * Use a linter like Eastwood or Kibit to identify potential code issues. * Use a formatter like cljfmt to automatically format the code according to a consistent style. * Configure the IDE to automatically run the linter and formatter on save. * Configure your editor to automatically remove trailing whitespace on save. **Don't Do This:** * Ignore linting warnings or formatting issues. * Relying on developers to manually cleanup formatting when tools can automate this. **Why:** Improves code quality, consistency, and readability. ## 5. Conclusion Adhering to these core architecture standards provides a solid foundation for building robust, maintainable, and scalable Clojure applications. Consistent application of these guidelines leads to higher quality code, reduced technical debt, and increased developer productivity. This document should be reviewed and updated periodically to reflect the evolving best practices in the Clojure community.
# Component Design Standards for Clojure This document outlines the component design standards for Clojure, focusing on creating reusable, maintainable, and performant components. It is intended to guide developers and inform AI coding assistants to produce idiomatic and high-quality Clojure code. It is specifically tailored to component design patterns in Clojure, and should be viewed as a single rule in a larger set of standards. ## 1. General Principles ### 1.1. Definition of a Component In Clojure, a component is a self-contained unit of functionality that encapsulates data and behavior. Components should be designed to be composable, independent, and easily testable. They should have clear, well-defined interfaces and responsibilities. ### 1.2. Key Principles of Component Design * **Single Responsibility Principle (SRP):** Each component should have one, and only one, reason to change. * **Open/Closed Principle (OCP):** Components should be open for extension but closed for modification. * **Liskov Substitution Principle (LSP):** Subtypes must be substitutable for their base types without altering the correctness of the program. While less directly applicable in Clojure with its focus on data and protocols, this principle translates to ensuring that functions accepting one type of data can also reliably handle variations or extensions of that data. * **Interface Segregation Principle (ISP):** Clients should not be forced to depend on methods they do not use. In Clojure, this suggests favoring small, focused protocols over large, monolithic ones. * **Dependency Inversion Principle (DIP):** High-level modules should not depend on low-level modules. Both should depend on abstractions. ### 1.3. Why These Principles Matter These principles are crucial for: * **Maintainability:** Easier to understand, modify, and debug components. * **Reusability:** Components can be used in different parts of the application or in different applications altogether. * **Testability:** Independent components are easier to test in isolation. * **Scalability:** Well-designed components facilitate easier scaling and distribution of the application. ## 2. Component Composition Approaches ### 2.1. Function Composition Clojure's functional nature makes function composition a natural way to build components. **Do This:** * Use "comp" to combine functions into a new function. * Leverage transducers for efficient data transformations. **Don't Do This:** * Create overly complex, deeply nested function compositions that are hard to read. * Depend excessively on mutable state within composed functions. **Example:** """clojure (defn add-one [x] (+ x 1)) (defn square [x] (* x x)) ; Compose functions to square a number and then add one (def square-and-add-one (comp add-one square)) (println (square-and-add-one 5)) ; Output: 26 ; Using transducers for efficient composition (def increment-and-square (comp (map inc) (map #(* % %)))) (println (into [] increment-and-square [1 2 3])) ; Output: [4 9 16] """ **Explanation:** Function composition allows combining simple functions into more complex operations. Transducers enhance this by providing a way to compose transformations on collections without creating intermediate collections. ### 2.2. Data-Driven Composition Components can be designed to operate on data structures. **Do This:** * Use data structures (maps, vectors, sets) as the primary means of communication between components. * Define functions that operate on these data structures to perform specific tasks. * Use schema libraries like "clojure.spec.alpha" or "malli" to define the structure and validity of your data. **Don't Do This:** * Pass large, mutable stateful objects between components. * Rely on side effects to communicate between components. **Example:** """clojure (require '[clojure.spec.alpha :as s]) ; Define a spec for a user (s/def ::user (s/keys :req-un [::id ::name ::email])) (s/def ::id uuid?) (s/def ::name string?) (s/def ::email string?) ; A function to validate a user (defn validate-user [user] (s/valid? ::user user)) ; A function to format a user's name (defn format-user-name [user] (str "User: " (:name user))) (def user-data {:id (random-uuid) :name "Alice" :email "alice@example.com"}) (println (validate-user user-data)) ; Output: true (println (format-user-name user-data)) ; Output: User: Alice """ **Explanation:** This approach emphasizes data structures as the central point of interaction. Using schemas further solidifies the interfaces between components, making them more robust and easier to reason about. ### 2.3. Protocol-Based Composition Protocols define interfaces that components can implement. This allows for polymorphism and extensibility. **Do This:** * Define protocols that represent the capabilities of a component. * Implement these protocols for different data types or data structures. * Use protocols to abstract away implementation details. **Don't Do This:** * Create overly large protocols with many methods. * Violate the Liskov Substitution Principle by implementing protocols inconsistently. **Example:** """clojure (defprotocol Printable (to-string [this] "Converts the object to a string representation.")) (extend-protocol Printable java.lang.String (to-string [this] this) ; Strings are already printable clojure.lang.PersistentVector (to-string [this] (str "[" (clojure.string/join ", " (map to-string this)) "]")) java.lang.Integer (to-string [this] (str this))) (println (to-string "Hello")) ; Output: Hello (println (to-string [1 2 "World"])) ; Output: [1, 2, World] """ **Explanation:** Protocols enable polymorphism. Different data types can implement the same protocol, providing a consistent interface. "extend-protocol" provides a flexible way to add protocol implementations to existing types. ### 2.4. Component Libraries and Frameworks Several libraries and frameworks facilitate component-based development in Clojure. * **Component:** A library for managing component lifecycles (start, stop). * **Integrant:** A configuration-based system for building applications from components controlled by a data structure. * **Mount:** A simpler alternative to Component for managing application state. * **System:** A newer library aiming to combine the best aspects of Component and Integrant. **Do This:** * Choose a component library appropriate for your project's complexity. * Use the library's lifecycle management features to manage component dependencies and state. * Employ configuration-based systems like Integrant to externalize component configuration. **Don't Do This:** * Manually manage component lifecycles without a library. * Hardcode component dependencies within the component itself. **Example (using Integrant):** """clojure (ns my-app (:require [integrant.core :as ig])) ; Define a database component (defmethod ig/init-key ::db [_ {:keys [url]}] (println "Connecting to DB at" url) {:connection url}) ; In a real app, establish an actual DB connection (defmethod ig/halt-key ::db [_ db] (println "Disconnecting from DB at" (:connection db))) ; Close the connection ; Define an HTTP server component (defmethod ig/init-key ::server [_ {:keys [port db]}] (println "Starting server on port" port "using DB" db) {:port port :db db}) ; In a real app, start an HTTP server (defmethod ig/halt-key ::server [_ server] (println "Stopping server on port" (:port server))) ; Stop the server ; Define a configuration (def config {::db {:url "jdbc://localhost:5432/mydb"} ::server {:port 8080 :db (ig/ref ::db)}}) ; Start the system (def system (ig/init config)) ; Stop the system when done (ig/halt! system) """ **Explanation:** Integrant allows defining components (like "::db" and "::server") and their dependencies in a configuration map. "ig/init" starts the system, resolving dependencies using references (e.g., "(ig/ref ::db)"). "ig/halt!" shuts down the system in the reverse order. The "ig/init-key" and "ig/halt-key" multimethods define the initialization and termination logic for each component. This removes boilerplate from your component implementation and centralizes configuration. ## 3. Component Communication ### 3.1. Asynchronous Messaging For decoupled components, consider asynchronous messaging using libraries like "core.async" or message queues (e.g., RabbitMQ, Kafka). **Do This:** * Use channels ("core.async") or message queues for non-blocking communication. * Define clear message formats using data structures and schemas. **Don't Do This:** * Overuse asynchronous messaging when synchronous calls are sufficient. * Create complex message routing logic within components. **Example (using core.async):** """clojure (require '[clojure.core.async :as async]) ; Create a channel (def message-channel (async/chan)) ; Component 1 (producer) (defn send-message [message] (async/>!! message-channel message)) ; Blocking send ; Component 2 (consumer) (defn receive-message [] (async/<!! message-channel)) ; Blocking receive ; Example Usage (future (send-message "Hello from Component 1")) (println (receive-message)) ; Output: Hello from Component 1 """ **Explanation:** "core.async" provides channels for asynchronous communication. The "send-message" function sends a message to the channel, and "receive-message" retrieves it. The "future" macro runs the sender in a separate thread. Non-blocking versions of these operations are available (">!", "<!"). ### 3.2. Publish/Subscribe (Pub/Sub) For components that need to react to events, consider using a publish/subscribe pattern. Libraries like "clojure.tools.namespace.repl" use this pattern internally. **Do This:** * Use a dedicated pub/sub library (e.g., implementing a simple one with atoms and callbacks). * Define clear event types and data structures. **Don't Do This:** * Create tightly coupled event listeners. * Overuse global event buses. **Example (Simple Pub/Sub implementation):** """clojure (def event-bus (atom {})) (defn subscribe [event-type callback] (swap! event-bus update event-type (fn [callbacks] (conj (or callbacks []) callback)))) (defn publish [event-type event-data] (doseq [callback (get @event-bus event-type)] (callback event-data))) ; Example Usage (subscribe :user-created (fn [user] (println "User created:" user))) (subscribe :user-created (fn [user] (println "Sending welcome email to:" (:email user)))) (publish :user-created {:name "Bob" :email "bob@example.com"}) """ **Explanation:** This demonstrates a basic pub/sub system managed with an atom. "subscribe" registers a callback for a specific event type. "publish" triggers all callbacks associated with that event type, passing event data to each. ### 3.3. Services and APIs Expose component functionality as services or APIs using libraries like Ring/Compojure or shadow-cljs for frontend components. **Do This:** * Define clear API contracts using schemas or Swagger/OpenAPI. * Implement proper authentication and authorization. **Don't Do This:** * Expose internal component implementation details in the API. * Neglect security considerations. ## 4. Testing ### 4.1. Unit Testing Test individual components in isolation. **Do This:** * Use "clojure.test" or a testing framework like Midje. * Mock dependencies when necessary. * Write clear and concise test cases. **Don't Do This:** * Write tests that are tightly coupled to implementation details. * Neglect edge cases and error handling. **Example:** """clojure (ns my-app.core-test (:require [clojure.test :refer :all] [my-app.core :refer :all])) (deftest test-add-one (testing "Should add one to a number" (is (= (add-one 5) 6)) (is (= (add-one -1) 0)))) """ ### 4.2. Integration Testing Test how components interact with each other. **Do This:** * Test the entire system or a significant portion. * Use realistic test data. * Verify that components communicate correctly. **Don't Do This:** * Skip integration testing. * Assume that components work together correctly without testing. ### 4.3. Component Lifecycle Testing If using a component lifecycle library like "component" or "integrant", verify the start and stop behavior of components. **Do This:** * Write tests that start and stop components. * Verify that resources are acquired and released correctly. * Ensure that dependencies are started in the correct order. ## 5. Error Handling ### 5.1. Exceptions Use exceptions to signal exceptional conditions. **Do This:** * Throw exceptions when errors occur. * Catch exceptions at appropriate boundaries. * Provide informative error messages. **Don't Do This:** * Ignore exceptions. * Use exceptions for normal control flow. ### 5.2. Error Values Return error values (e.g., "nil", "false", or a tagged union) to indicate errors. **Do This:** * Use error values when exceptions are not appropriate. * Check for error values and handle them appropriately. **Don't Do This:** * Ignore error values. * Assume that all operations succeed. ### 5.3. Logging Log errors and other important events. **Do This:** * Use a logging library like "clojure.tools.logging". * Log at appropriate levels (e.g., "error", "warn", "info", "debug"). * Include relevant context in log messages. **Don't Do This:** * Log too much information. * Log sensitive information. ## 6. Modern Clojure Features ### 6.1. clojure.spec.alpha and Malli Use "clojure.spec.alpha" or "malli" extensively for data validation, generation, and documentation. **Do This:** * Define specs/schemas for all data structures used in your application. * Use "s/valid?" or "malli.core/validate" to validate data. * Use "s/gen" or "malli.core/generate" for property-based testing. **Don't Do This:** * Neglect data validation. * Assume that all data is valid. ### 6.2. Datafy/Nav Consider using "datafy" and "nav" (Clojure 1.10+) to provide a consistent way to inspect and navigate data structures. This can improve debugging and introspection. **Do This:** * Implement "datafy" and "nav" for custom data types. * Use the "datafy" function in debugging tools. ## 7. Anti-Patterns ### 7.1. God Object Avoid creating large, monolithic components that do too much. ### 7.2. Tight Coupling Minimize dependencies between components. ### 7.3. Premature Optimization Don't optimize components before they are needed. ### 7.4. Reinventing the Wheel Use existing libraries and frameworks when possible. ## 8. Performance Optimization ### 8.1. Immutability Leverage Clojure's immutable data structures for performance and concurrency benefits. ### 8.2. Laziness Use lazy sequences efficiently to avoid unnecessary computation. ### 8.3. Concurrency Use Clojure's concurrency features (e.g., atoms, refs, agents, core.async) to improve performance. Be extremely cautious and deliberate using these as they are easy to misuse. ### 8.4. Profiling Use profiling tools to identify performance bottlenecks. ## 9. Security Best Practices ### 9.1. Input Validation Validate all user input to prevent injection attacks. ### 9.2. Authentication and Authorization Implement proper authentication and authorization to protect sensitive data. ### 9.3. Dependency Management Keep dependencies up to date to patch security vulnerabilities. By adhering to these component design standards, Clojure developers can create applications that are maintainable, reusable, testable, and performant. This document provides a framework for building high-quality Clojure code.
# Performance Optimization Standards for Clojure This document outlines performance optimization standards for Clojure development. These standards aim to improve application speed, responsiveness, and resource utilization. They are tailored to Clojure's unique characteristics and leverage modern approaches. ## 1. Architectural Considerations ### 1.1 Data Structures and Algorithms **Standard:** Choose the right data structure and algorithm for the specific task. Consider time and space complexity trade-offs. **Why:** Selecting an appropriate data structure and algorithm can significantly impact performance. For example, using a "set" for membership testing is much faster than iterating through a "list". **Do This:** * Understand the performance characteristics of Clojure's data structures: "list", "vector", "map", "set". * Analyze algorithmic complexity (Big O notation) when choosing algorithms. * Profile code to identify performance bottlenecks. **Don't Do This:** * Use "list" for random access. Prefer "vector". * Use linear search on sorted data. Use a binary search implementation or leverage sorted sets/maps. **Example:** """clojure ;; Efficient membership testing using a set (def my-set (set (range 10000))) (time (contains? my-set 5000)) ; Evaluates very quickly ;; Inefficient membership testing using a list (def my-list (list (range 10000))) (time (some #(= 5000 %) my-list)) ; Considerably slower """ ### 1.2 Concurrency and Parallelism **Standard:** Utilize Clojure's concurrency features (atoms, refs, agents, and "pmap") appropriately to leverage multi-core processors. **Why:** Concurrency can dramatically improve performance for CPU-bound tasks. However, incorrect concurrency can introduce race conditions and deadlocks, harming performance. **Do This:** * Use "pmap" for embarrassingly parallel operations on collections (map-like operations where each element can be processed independently). * Use atoms for simple state management that doesn't require coordination between multiple operations. * Use refs with "dosync" for coordinated state changes that require transactions. * Use agents for asynchronous operations that don't require immediate results. * Carefully manage contention and locking. **Don't Do This:** * Overuse concurrency. Adding unnecessary threads can increase overhead. * Use "future" without understanding its limitations (e.g., eager evaluation). * Ignore exceptions thrown in concurrent computations. **Example:** """clojure ;; Parallel processing of a collection using pmap (defn square [x] (* x x)) (time (pmap square (range 1 10000))) ;; Atomic counter increment (def counter (atom 0)) (defn increment [] (swap! counter inc)) """ ### 1.3 Lazy Evaluation **Standard:** Leverage lazy evaluation for efficiency, but be aware of potential pitfalls. **Why:** Lazy evaluation can defer computation until needed, saving processing time and memory. However, it can also lead to unexpected performance issues if not managed correctly (e.g., holding onto the head of a sequence). **Do This:** * Use lazy sequences ("map", "filter", "range") for large or infinite data sets. * Realize only the necessary portion of a lazy sequence. * Use "doall" or "dorun" to force evaluation when side effects are required. **Don't Do This:** * Hold onto the head of a lazy sequence for extended periods, preventing garbage collection. * Chain together too many lazy operations without realizing intermediate results. This can create excessive stack usage. **Example:** """clojure ;; Lazy sequence example (def numbers (range 1000000)) (def even-numbers (filter even? numbers)) ;; Only realize the first 10 even numbers (take 10 even-numbers) ;; Force evaluation to prevent head retention: (dorun (map prn (take 10 even-numbers))) """ ### 1.4 Reducing Object Creation **Standard:** Minimize unnecessary object creation to reduce garbage collection overhead. **Why:** Frequent object creation and garbage collection can be a major performance bottleneck, especially in long-running applications. **Do This:** * Use transients for mutable operations on data structures within a local scope. * Prefer primitive operations ("unchecked-*") for numerical calculations when possible. * Reuse existing objects when applicable. **Don't Do This:** * Create temporary objects unnecessarily. * Rely solely on immutable data structures without considering the overhead of creating new copies. **Example:** """clojure ;; Using transients for efficient vector modification (defn modify-vector [v] (persistent! (reduce (fn [acc i] (assoc! acc i (* i 2))) (transient v) (range (count v))))) (def my-vector (vec (range 1000))) (time (modify-vector my-vector)) ;; Using unchecked math (defn sum-range [n] (loop [i 0 acc 0] (if (>= i n) acc (recur (inc i) (unchecked-add acc i))))) (time (sum-range 1000000));Evaluates much fasters than normal + function because of primtive math. """ ## 2. Coding Practices ### 2.1 Function Performance **Standard:** Write efficient functions that minimize unnecessary computations. **Why:** The performance of individual functions directly impacts the overall application performance. **Do This:** * Use memoization for pure functions with expensive calculations. * Employ tail-recursion for efficient looping. * Use function arities to improve dispatch performance. * Avoid unnecessary type checking. **Don't Do This:** * Perform redundant calculations within a function. * Create functions that are too large or complex. Break them down into smaller, more manageable units. **Example:** """clojure ;; Memoization example (def fibonacci (memoize (fn [n] (if (<= n 1) n (+ (fibonacci (- n 1)) (fibonacci (- n 2))))))) (time (fibonacci 30)) ; First call takes longer (time (fibonacci 30)) ; Subsequent calls are much faster ;; Tail-recursive function for summing a sequence (defn sum-seq [s] (loop [s s acc 0] (if (empty? s) acc (recur (rest s) (+ acc (first s)))))) """ ### 2.2 I/O Optimization **Standard:** Optimize input/output operations to minimize latency. **Why:** I/O operations are often the slowest part of an application. **Do This:** * Use buffered I/O for reading and writing files. * Batch multiple requests in database and network communications. * Use appropriate data serialization formats (e.g., Protocol Buffers, edn instead of JSON for Clojure-to-Clojure communication). Minimize the size of serialized data. * Use "slurp" and "spit" judiciously for small files, but use streaming approaches for large files. **Don't Do This:** * Perform frequent small I/O operations. * Read entire files into memory if only a portion is needed. * Block the UI thread with I/O operations. **Example:** """clojure ;; Buffered file reading (with-open [r (clojure.java.io/reader "large-file.txt" :encoding "UTF-8" :buffer-size 8192)] (doseq [line (line-seq r)] (println line))) ;; Asynchronous I/O using futures (defn process-data [data] (future (println "Processing data:" data))) (process-data "Some large data set") ;;Using streams (from clojure.java.io) (with-open [input-stream (clojure.java.io/input-stream "large-file.bin")] (let [buffer (byte-array 4096)] (loop [bytes-read (.read input-stream buffer)] (when (pos? bytes-read) ;; Process the buffer (byte-array) here (println "Read " bytes-read " bytes") (recur (.read input-stream buffer)))))); Avoids loading everything in memory """ ### 2.3 String Manipulation **Standard:** Use efficient string manipulation techniques. **Why:** String operations can be performance-intensive, especially when dealing with large strings or frequent concatenations. **Do This:** * Use "StringBuilder" for efficient string concatenation. * Use regular expressions carefully, being mindful of backtracking and compilation overhead. Consider caching compiled patterns using "re-pattern". * Prefer "subs" over creating new strings when extracting substrings. **Don't Do This:** * Repeatedly concatenate strings using "str" in a loop. * Use complex regular expressions unnecessarily. * Convert to and from strings excessively. **Example:** """clojure ;; Efficient string concatenation using StringBuilder (defn build-string [n] (let [sb (StringBuilder.)] (dotimes [i n] (.append sb i)) (.toString sb))) (time (build-string 10000)) ;; Cached regular expression pattern (def pattern (re-pattern "abc")) (re-find pattern "abcdefg") """ ## 3. Tooling and Debugging ### 3.1 Profiling **Standard:** Use profiling tools to identify performance bottlenecks. **Why:** Profiling helps pinpoint the areas of code that consume the most time and resources. **Do This:** * Use the "criterium" library for microbenchmarking. * Use profiling tools like VisualVM or YourKit for JVM-level profiling. * Use Clojure's built-in "time" macro for simple timing. **Don't Do This:** * Guess at performance bottlenecks. * Optimize prematurely without profiling. **Example:** """clojure ;; Using criterium for benchmarking (require '[criterium.core :as c]) (c/bench (reduce + (range 10000))) (c/quick-bench (reduce + (range 10000))) ;; Timing macro example (time (reduce + (range 10000))) """ ### 3.2 Memory Management **Standard:** Monitor memory usage to prevent memory leaks and excessive garbage collection. **Why:** Memory leaks and excessive garbage collection can lead to performance degradation and application instability. **Do This:** * Use memory profiling tools to identify memory leaks. * Be mindful of object lifetimes and release resources when no longer needed. * Avoid creating large, temporary data structures. **Don't Do This:** * Ignore memory usage patterns. * Assume that garbage collection will solve all memory-related problems. ### 3.3 Logging and Monitoring **Standard:** Implement adequate logging and monitoring to track application performance in production. **Why:** Logging and monitoring provide valuable insights into application behavior, enabling proactive identification and resolution of performance issues. **Do This:** * Use a logging library (e.g., "tools.logging") to record performance metrics. * Monitor key performance indicators (KPIs) such as response time, throughput, and CPU utilization. * Set up alerts for performance degradation. **Don't Do This:** * Log excessively, which can impact performance. * Fail to monitor application performance in production. ## 4. Clojure Specific Optimizations ### 4.1 AOT Compilation **Standard**: Use Ahead-of-Time (AOT) compilation for faster startup times. **Why:** AOT compilation compiles Clojure code to Java bytecode at compile time, reducing the amount of work needed at runtime, especially on application startup. **Do This**: * Enable AOT compilation in your "project.clj" file. * AOT compile namespaces that contain entry points or performance-critical functions. **Don't Do This**: * AOT compile everything blindly. Focus on critical namespaces. * Forget to recompile after changes. **Example**: In "project.clj": """clojure (defproject my-project "0.1.0-SNAPSHOT" :dependencies [[org.clojure/clojure "1.11.1"]] :main my-project.core :aot :all ; or a list of namespaces :uberjar-name "my-project.jar") """ ### 4.2 Type Hints **Standard**: Utilize type hints to improve performance of numerical operations and avoid reflection. **Why**: Clojure is dynamically typed, which can sometimes lead to reflection and decreased performance. Type hints provide the compiler with more information, allowing it to generate more efficient bytecode. **Do This**: * Add type hints to function arguments and local bindings where type information is known. * Use primitive type hints (e.g., "^long", "^double") for numerical operations. * Carefully consider the trade-off between verbosity and performance gains. **Don't Do This**: * Add type hints indiscriminately, which can clutter your code and provide minimal benefit. * Use incorrect type hints, which can lead to runtime errors. **Example**: """clojure (defn add-longs [^long x ^long y] (+ x y)) ; Without the hints, the Clojure compiler must use reflection to determine the types of x and y (defn vector-access [^clojure.lang.PersistentVector v ^long index] (.nth v index)) """ ### 4.3 Avoiding Reflection **Standard**: Minimize or eliminate reflection in performance-critical code. **Why**: Reflection is a runtime mechanism for inspecting and manipulating classes and objects. It's powerful but slow compared to direct method calls. **Do This**: * Use "*warn-on-reflection*" to identify reflection sites in your code during development. * Use type hints to guide the compiler. * Consider using protocols or interfaces to avoid reflection when dealing with Java classes. **Don't Do This**: * Ignore reflection warnings. * Rely on reflection for core logic. **Example**: """clojure (set! *warn-on-reflection* true) (defn print-length [^String s] (.length s)) ; Reflection warning will show unless type hinting is used. """ ### 4.4 Protocols and Multimethods **Standard:** Employ protocols and multimethods judiciously for extensibility and polymorphism while considering performance implications. **Why:** Protocols offer a performant way of defining interfaces, enabling polymorphism with compile-time dispatch. Multimethods provide flexible runtime dispatch based on arbitrary criteria, but can be slower than protocols. **Do This:** * Use protocols when you need high-performance polymorphism with known types. * Use multimethods for more complex dispatch logic where performance is less critical. * Cache multimethod dispatch functions when applicable. **Don't Do This:** * Overuse multimethods when protocols would suffice. * Ignore the performance impact of complex multimethod dispatch logic. **Example:** """clojure ;; Protocol example (defprotocol Shape (area [this])) (defrecord Circle [radius] Shape (area [this] (* Math/PI radius radius))) (defrecord Square [side] Shape (area [this] (* side side))) (defn print-area [x] (println (area x)));Very fast. ;; Multimethod example (defmulti describe (fn [x] (type x))) (defmethod describe java.lang.String [s] (str "This is a string: " s)) (defmethod describe :default [x] (str "This is something else: " x)) (println (describe "Hello")) (println (describe 123)); Slower than Protocols. """ ### 4.5 Libraries and Frameworks **Standard**: Carefully select and utilize Clojure libraries and frameworks, considering their performance characteristics. **Why**: The choice of libraries can dramatically impact performance. Some libraries are more optimized than others. **Do This**: * Benchmark libraries before using them in performance-critical code. * Choose libraries that are designed for performance and efficiency. * Understand the internal workings of the libraries you use. **Don't Do This**: * Use libraries blindly without considering their performance impact. * Rely on outdated or unmaintained libraries. **Example**: * "core.async" can be used for asynchronous operations, but benchmark it compared to simpler futures if performance is critical. * For JSON processing, consider "data.json" (part of clojure.data) or "cheshire" which are generally faster than "clojure.data.json" for certain workloads. * When working with numerical data, explore libraries like "tech.v3.datatype" for efficient array operations and interoperability with other JVM libraries. By adhering to these coding standards, developers can create Clojure applications that are both performant and maintainable. This document provides a solid foundation for building high-quality Clojure code that meets the demands of modern applications. Remember to continuously monitor and profile your application in production to identify and address any performance bottlenecks that may arise.
# Testing Methodologies Standards for Clojure This document outlines the standards for testing methodologies in Clojure projects. These standards promote maintainability, reliability, and code quality. They are designed to be used in conjunction with AI coding assistants such as GitHub Copilot and Cursor to promote adherence to these guidelines. ## 1. General Principles ### 1.1. Prioritize Testing * **Do This:** Treat testing as a first-class citizen in the development process. Write tests concurrently with, or even before, writing the function or module implementation itself. * **Don't Do This:** Neglect writing tests or consider it an "afterthought." * **Why:** Starting with tests (TDD – Test-Driven Development) fosters better design, reduces debugging time, and improves confidence in the code's correctness. ### 1.2. Test Pyramid * **Do This:** Structure tests in a pyramid shape: many unit tests, fewer integration tests, and even fewer end-to-end tests. * **Don't Do This:** Rely heavily on slow and brittle end-to-end tests at the expense of focused unit tests. * **Why:** Unit tests are fast, cheap to maintain, and provide quick feedback. Integration tests verify interactions between components, while end-to-end tests validate the entire system but are slower and more complex. ### 1.3. Test Isolation * **Do This:** Ensure tests are independent. Each test should set up its own context and tear it down after execution. * **Don't Do This:** Share mutable state between tests or rely on test execution order. * **Why:** Isolated tests ensure that failures are reproducible and easier to diagnose. They also prevent tests from interfering with each other. ### 1.4. Use Meaningful Assertions * **Do This:** Write assertions that clearly express the expected behavior. Use descriptive messages to communicate the purpose and context of the assertion. * **Don't Do This:** Use opaque or generic assertions that provide little insight into the underlying problem when a test fails. * **Why:** Clear assertions make it easier to understand what the test is verifying and aid in debugging when a test fails. ## 2. Unit Testing ### 2.1. Focus * **Do This:** Unit tests should verify the behavior of a single, isolated unit of code (usually a function or a small set of functions). * **Don't Do This:** Write unit tests that test multiple units of code or depend on external systems. * **Why:** Isolating units of code allows for precise bug identification; issues can be narrowed down and addressed at the foundational module level. ### 2.2. Mocking and Stubbing * **Do This:** Use mocking or stubbing to isolate the unit under test from its dependencies. Libraries like "clojure.test.check.clojure-test" and "mockfn" can be helpful. * **Don't Do This:** Directly test dependencies that are not part of the unit being tested. * **Why:** Isolating dependencies prevents failures in those dependencies from masking failures in the unit under test. It also leads to faster and more reliable tests. #### 2.2.1 Example with "mockfn" """clojure (ns my-project.core-test (:require [clojure.test :refer :all] [my-project.core :as core] [mockfn.clojure-test :refer [with-mock]])) (deftest test-calculate-total (testing "Calculate total with mocked discount" (with-mock [core/apply-discount (fn [price discount] 5.0)] ; Mock apply-discount function (let [items [{:price 10} {:price 20}] total (core/calculate-total items 0.2)] (is (= 100 total) "Total should be correct after applying mocked discount")))) (testing "Calculate total without mocks" (let [items [{:price 10} {:price 20}] total (core/calculate-total items 0.2)] (is (= 24 total) "Total should be correct without mocks")))) """ ### 2.3. Property-Based Testing * **Do This:** Use property-based testing, with libraries like "clojure.test.check", to automatically generate test data and verify that your functions satisfy certain properties. * **Don't Do This:** Rely solely on example-based testing (i.e., providing fixed inputs and outputs). * **Why:** Property-based testing can uncover edge cases and corner cases that are easily missed with example-based testing. #### 2.3.1 Example with "clojure.test.check" """clojure (ns my-project.core-test (:require [clojure.test :refer :all] [clojure.test.check.clojure-test :refer [defspec]] [clojure.test.check.properties :as prop] [clojure.test.check.generators :as gen] [my-project.core :as core])) (defspec square-non-negative 1000 ; Number of iterations (prop/for-all [x gen/nat] ; Generate natural numbers (>= (core/square x) 0))) ; Property: Square is non-negative """ In this example, "defspec" defines a property-based test named "square-non-negative". It uses the "gen/nat" generator to create random non-negative integers and verifies that the "square" function always returns a non-negative result. This tests a general property of "square", ensuring all non-negative results are >= 0. ### 2.4. Test Naming * **Do This:** Use descriptive names for tests that clearly indicate the scenario being tested. * **Don't Do This:** Use vague or ambiguous test names. * **Why:** Descriptive test names make it easier to understand the purpose of the test and to quickly identify failing tests. ### 2.5. Edge Cases * **Do This:** Always consider edge cases, boundary conditions, and invalid inputs when writing unit tests. * **Don't Do This:** Only test happy paths. * **Why:** Testing edge cases increases the robustness and reliability of your code. ## 3. Integration Testing ### 3.1. Focus * **Do This:** Integration tests should verify the interactions between two or more units of code. * **Don't Do This:** Use integration tests to test individual units in isolation (use unit tests for that). * **Why:** Integration tests ensure that components work together correctly and that data flows smoothly between them. ### 3.2. Database Interactions * **Do This:** Use a dedicated testing database for integration tests involving databases. * **Don't Do This:** Run integration tests against a production database. * **Why:** Using a separate testing database prevents tests from corrupting production data and ensures that tests are repeatable. ### 3.3. External APIs * **Do This:** Use mock services or stubs for external APIs when integration testing. Libraries like "clojure.test.check.clojure-test" alongside "mockfn" can be used. * **Don't Do This:** Directly call external APIs during integration tests, especially for APIs that might have rate limits or costs. * **Why:** Mocking external APIs provides control over the responses and eliminates dependencies on external systems, providing faster and more reliable tests. #### 3.3.1 Mocking External APIs with Component This is an example of mocking external APIs during integrations tests: """clojure (ns my-project.integration-test (:require [clojure.test :refer :all] [com.stuartsierra.component :as component] [my-project.system :as system] [my-project.api :as api])) (defn mock-api-component [value] (reify api/API (fetch-data [_ _] value))) (defn create-test-system [mock-api-value] (component/system-map :config (system/create-config) :api (mock-api-component mock-api-value) ; Use the mock API :service (system/create-service))) (deftest test-integration-with-mocked-api (let [test-system (component/start (create-test-system {:example "data"})) ; Provide desired return value service (:service test-system)] (try (is (= {:example "data"} (api/fetch-data (:api test-system) :some-param)) "Data fetched should match mock") (finally (component/stop test-system))))) """ ### 3.4. State Management * **Do This:** Carefully manage state during integration tests. Ensure that state is reset before each test. * **Don't Do This:** Allow state to accumulate between tests. * **Why:** Inconsistent state can lead to unpredictable test results and make it difficult to isolate bugs. ## 4. End-to-End (E2E) Testing ### 4.1. Focus * **Do This:** End-to-end tests should verify the entire system from the user's perspective. * **Don't Do This:** Use E2E tests to verify individual components or interactions (use unit and integration tests for that). * **Why:** E2E tests ensure that the system as a whole functions correctly and delivers the expected user experience. ### 4.2. Test Environment * **Do This:** Use a dedicated testing environment for E2E tests that closely resembles the production environment. * **Don't Do This:** Run E2E tests against a development environment or a shared testing environment. * **Why:** A dedicated testing environment provides a more realistic and reliable testing experience. ### 4.3. Automation * **Do This:** Automate E2E tests to ensure they can be run frequently and consistently. Use libraries like "selenium" or "playwright," wrapped as necessary. * **Don't Do This:** Manually run E2E tests. * **Why:** Automated E2E tests reduce the risk of human error and provide faster feedback on system-level issues. ### 4.4. Data Setup * **Do This:** Carefully manage test data for E2E tests. Ensure that data is set up and cleaned up before and after each test. * **Don't Do This:** Rely on existing data in the testing environment or leave data behind after tests are run. * **Why:** Consistent data management improves the reliability and repeatability of E2E tests. ### 4.5. Performance * **Do This:** Be mindful of the performance impact of E2E tests. Optimize tests to minimize execution time. Libraries like "perforate" or "criterium" can be used. * **Don't Do This:** Write E2E tests that are slow or inefficient. * **Why:** Fast E2E tests provide quicker feedback and reduce the overall testing time. ## 5. Tools and Libraries ### 5.1. "clojure.test" * Use the built-in "clojure.test" library for basic testing functionality. """clojure (ns my-project.core-test (:require [clojure.test :refer :all] [my-project.core :as core])) (deftest test-add (testing "Adding two positive numbers" (is (= 3 (core/add 1 2)))) (testing "Adding a positive and a negative number" (is (= -1 (core/add 1 -2))))) """ ### 5.2. "clojure.test.check" * Use "clojure.test.check" for property-based testing. """clojure (ns my-project.core-test (:require [clojure.test :refer :all] [clojure.test.check.clojure-test :refer [defspec]] [clojure.test.check.properties :as prop] [clojure.test.check.generators :as gen] [my-project.core :as core])) (defspec add-commutative 1000 (prop/for-all [x gen/nat y gen/nat] (= (core/add x y) (core/add y x)))) """ ### 5.3. "mockfn" * Use "mockfn" for mocking function calls during unit tests. """clojure (ns my-project.core-test (:require [clojure.test :refer :all] [my-project.core :as core] [mockfn.clojure-test :refer [with-mock]])) (deftest test-calculate-total (testing "Calculate total with mocked discount" (with-mock [core/apply-discount (fn [price discount] 5.0)] (let [items [{:price 10} {:price 20}] total (core/calculate-total items 0.2)] (is (= 100 total)))))) """ ### 5.4. "component" Testing * When working with components, tests often need to start and stop systems, mocking dependencies for isolation. """clojure (ns my-project.component-test (:require [clojure.test :refer :all] [com.stuartsierra.component :as component] [my-project.system :as system])) (deftest test-system-startup (let [test-system (component/start (system/new-system))] (try (is (not (nil? (:database test-system))) "Database should be started") (is (not (nil? (:web-server test-system))) "Web server should be started") (finally (component/stop test-system))))) """ ### 5.5 "stateful-testing" * For situations requiring in memory mocking instead of calls to external dependencies """clojure (ns my-project.state-test (:require [clojure.test :refer :all] [my-project.state :as state])) (deftest test-add-to-state (testing "Adding an item to the state" (state/reset-state) ; Reset the state before the test (state/add-item "item1") (is (= #{"item1"} @state/current-state) "State should contain the added item")) (testing "Adding another item to the state" (state/add-item "item2") (is (= #{"item1" "item2"} @state/current-state) "State should contain both items"))) """ ## 6. Test-Driven Development (TDD) ### 6.1. Red-Green-Refactor * **Do This:** Follow the Red-Green-Refactor cycle of TDD: * **Red:** Write a failing test. * **Green:** Write the minimum amount of code to make the test pass. * **Refactor:** Improve the code while keeping the test passing. * **Don't Do This:** Write code before writing tests or skip the refactoring step. * **Why:** TDD leads to better-designed code, reduces the risk of over-engineering, and increases confidence in the code's correctness. ### 6.2. Test Coverage * **Do This:** Aim for high test coverage (e.g., 80% or higher), but don't treat it as the sole metric of code quality. Libraries exist to determine line by line test coverage. * **Don't Do This:** Focus solely on achieving a certain test coverage percentage without considering the quality of the tests. * **Why:** High test coverage reduces the risk of regressions and makes it easier to maintain the code. However, meaningful tests count more than lines of code. ## 7. Reporting and CI/CD ### 7.1. Test Reporting * **Do This:** Use a test runner that provides detailed test reports with information about failures, errors, and code coverage. Use a variety of reporting formats, to allow multiple automated tools to process the results. * **Don't Do This:** Rely on manual inspection of test output. * **Why:** Automated test reporting makes it easier to track test results and identify potential issues. ### 7.2. Continuous Integration (CI) * **Do This:** Integrate tests into a CI/CD pipeline so that they are run automatically whenever code is changed. * **Don't Do This:** Run tests manually as part of the deployment process. * **Why:** CI/CD pipelines provide early feedback on code quality and ensure that changes are thoroughly tested before being deployed. ## 8. Code Examples ### 8.1. Asynchronous Testing When your Clojure code deals with asynchronous operations (e.g., using "core.async" or futures), testing requires special care to handle timing issues. """clojure (ns my-project.async-test (:require [clojure.test :refer :all] [clojure.core.async :as async :refer [>!! <!! timeout]] [my-project.async-operations :as ops])) (deftest test-async-operation (testing "Async operation completes successfully" (let [result-chan (async/chan) _ (ops/perform-async-task result-chan)] (let [result (<!! (async/timeout 1000) result-chan)] ; Timeout after 1 second (is (= :success result) "Async operation should return :success"))))) """ ### 8.2. Testing Exception Handling Ensuring that your code handles exceptions gracefully is critical. """clojure (ns my-project.exception-test (:require [clojure.test :refer :all] [my-project.error-handling :as eh])) (deftest test-divide-by-zero (testing "Divide by zero throws exception" (is (thrown? ArithmeticException (eh/divide 10 0))))) (deftest test-recover-from-exception (testing "Recovering from an exception using try-catch" (is (= :recovered (eh/recoverable-operation))))) """ ## 9. Conclusion Adhering to these testing standards will lead to more maintainable, reliable, and robust Clojure applications. Use these guidelines in tandem with AI coding assistants to promote consistent and high-quality testing practices within your development team. Regularly review and update these standards to align with the evolving Clojure ecosystem and your project's specific needs.