brianium/yoose

A small library for use case driven development


Keywords
async, clojure, usecase
License
MIT

Documentation

yoose

Clojars Project

Finn Says Yussssss

Yoose attempts to encourage use case centered applications.

What is a use case?

In software and systems engineering, a use case is a list of actions or event steps typically defining the interactions between a role and a system to achieve a goal. The actor can be a human or other external system.

Yoose attempts to define a use case as a process created with input and output ports (currently via core.async channels). A use case should be able to gather input from it's input port to accomplish it's goal, and send output to it's output port to notify interested parties. Interested parties you say??!?!? An interested party might be an HTTP handler, a CLI application, a robotic arm? - the list goes on and on.

Usage

The following contrived example is inspired by the clean todos project.

(ns yoose.todos
  (:require [brianium.yoose.async :refer :all]))
  
(defusecase create-todo [this db]
  (let [entity (<in this)]
    (->> entity
         (save db)
         (create-message :todo/create)
         (>out this))))

The defusecase is an optional bit of syntactic sugar, but one I find useful. defusecase defines a function that is used for creating use cases. A use-case-factory if you will. These factories expect to be called with an input and output channel a la core.async, and any other dependencies needed to do the job. Or in clojure.spec parlance:

(s/def ::use-case-factory
  (s/fspec :args (s/cat
                   :in   ::in
                   :out  ::out
                   :deps (s/* any?))
           :ret  yoose.spec/use-case))

And so one might actually create an instance of a use case like so:

(def input (chan))

(def output (chan))

(def db (create-a-db))

(def use-case (create-todo input output db))

Notice how dependencies defined after this in the defusecase example are passed in after input and output.

Any number of additional arguments can be defined this way:

(defusecase create-todo [this db arg2 arg3] ...)

While this is possible, I would recommend a single dependency map. This is super convenient when passing around dependencies - say with a super cool library like mount - and destructuring is always your friend.

(defusecase create-todo [this {:keys [db arg2 arg3]}] ...)

**Note: use cases are currently built with core.async in mind, but it may not always be the case. It's possible that some day yoose.manifold or yoose.queue might burst onto the scene and define use case factories with different expectations. When in doubt - refer to the specs ;)

Documentation

brianium.yoose

brianium.yoose defines the api for exercising use cases. A use case is something that implements the brianium.yoose/UseCase protocol. Most of the functions in brianium.yoose just implement the functions defined by the protocol.

push!

Places a value into the use case input port

(push! use-case "some value!")

pull!

Calls the given function with the next value taken from the output port

(pull! use-case #(println %))

pull!!

Takes a value from the output port and returns it. Blocks until output is received

(def response (pull!! use-case))

<in

Takes a value from the input port. The use of this function is encouraged only in the context of defining a use case

(let [input (<in use-case)])

>out

Puts a value into the output port. The use of this function is encouraged only in the context of defining a use case

(>out use-case "an output message")

in

Returns the input port of the use case

(let [port (in use-case)])

out

Returns the output port of the use case

(let [port (out use-case)])

close!

Closes input and output ports

(close! use-case)

trade!!

Pushes a values into the use case and blocks until output is available.

(def result (trade!! use-case "hello"))

use-case?

Check if the given value implements the brianium.yoose/UseCase protocol

(use-case? value)

For more information - see the spec

brianium.yoose.async

Provides a core.async implementation of the brianium.yoose/UseCase protocol.

make-use-case

Creates a new use case backed by core.async

(require '[clojure.core.async :refer [chan]])

(def input (chan))

(def output (chan))

(def use-case (make-use-case input output))

<in

brianium.yoose/<in redefined as a macro. Since go macro translation stops at function creation boundaries - brianium.yoose/<in can't opt into a wrapping go block. Using brianium.yoose.async/<in circumvents this problem.

;;; BAD - throws error for using <! outside of go block
(go
  (let [input (brianium.yoose/<in use-case)]))
  
;;; OK
(go
  (let [input (brianium.yoose.async/<in use-case)]))

>out

brianium.yoose/>out redefined as a macro. See brianium.yoose/<in rationale above.

defusecase

Defines an async use case. A use case is really just a function that executes it's body in the context of a go loop.

(defusecase create-todo [this db]
  (let [entity (<in this)]
    (->> entity
         (save db)
         (create-message :todo/create)
         (>out this))))
		 
;; is equivalent to

(defn create-todo [in out db]
  (let [use-case (make-use-case in out)]
    (go-loop []
      (let [entity (<in this)]
        (->> entity
             (save db)
             (create-message :todo/create)
             (>out this)))
    (recur))
  use-case))