(s/invalid? (s/conform :todo/task-event event))
-- it's probably clearer to check (s/valid? :todo/task-event event)
(and switch the if/else clauses). You normally only use s/conform
if you actually want to conform the data (i.e., get back a data structure labeled with how it matches the spec).
Look at multimethods as a more idiomatic way to write apply-event
-- since it is essentially polymorphic on :event/name
(when (s/valid? (s/keys :req [:event/name]) event)
-- I'm curious why this isn't checking against :todo/task-event
like the apply-event
function?
Overall, nice and clean and easy to read. Good job!
the formatting on apply-event
is confusing to me. tasks
is on the same line as the result of "task-completed" even though it's an else statement. I prefer a format like:
(defn apply-event [tasks event]
(if (s/invalid? (s/conform :todo/task-event event))
tasks
(case (:event/name event)
"task-added"
(->> (select-keys event [:task/uri :task/title])
(conj (remove #(= (:task/uri event) (:task/uri %)) tasks)))
"task-completed"
(remove #(= (:task/uri %) (:task/uri event)) tasks)
;; else
tasks)))
Just to expand on this, exceptions are the only error handling mechanism clojure has. Also nil punning for some cases. In clojure, exceptions are not reserved for shutdown/panic situations
Oh, good catch! I missed that completely.
not sure whether it's good practice or not, but I often have comments for else statements in conds and cases
ah good catch!
i wanted to leave the event store open to also process other types of events that might be added in the future
(there's no requirement on :task/uri
or :task/title
)
maybe i could add a smaller spec which i then compose into :todo/task-event
and reuse there for the check
additionally, I don't think apply-event
should return the tasks unmodified if an unrecognized event is sent. I would probably either remove the else statement, throw an exception, or return some sort of error
My (deleted) comment about adding a docstring still stands (I think everything should have a docstring really, including ns
) -- but in task-event-handler
I'd name the argument send-fn
to avoid shadowing the built-in send
(in clojure.core
) which confused me when I read the body.
if I send an event to an API and I send it an invalid or unrecognized event, I would hope to receive an error rather than it silently do nothing.
That sounds good. Having an anonymous spec just for s/valid?
seemed a bit odd.
^ although adding error handling throughout the app is whole 'nother layer of complexity which is probably a learning exercise in its own right 🙂
right. the simplest error handling implementation would be to simply remove the case
's else statement and let the generic error handling take over
it's not the best error handling, but at least it would indicate something went wrong to the API's user
good point! you're right if the application reducer encounters an error it could lead to an incorrect state being created from the events - i'll have a think about that
thanks for the pointers! really appreciate the help
that is a lot more readable, yeah on the if
its not all that clear what's happening sometimes, adding comments is a good idea. thanks for the tip
apply-event
combines validation and the event processing. my intuition is that those should be separate
currently, apply-event
will continue to grow in size if you keep adding more events. check out https://clojuredocs.org/clojure.core/defmulti for an alternate approach
right - in other languages I would sometimes start a function with some guards that exit early (like using :pre i guess? - but i didn't want to throw an error) that was my intention there
i'll have a think where i could put it
what's the reluctance to throwing an error?
i'm not sure if its an exceptional (unexpected) thing, it could happen that the event store contains some old events or stale events, or events with an old schema that shouldn't be reduced
i prefer to reserve exceptions for things that should stop the application
you could apply-events return some error-value rather than throwing an exception
the issue with ignoring invalid events is that ignoring events is unlikely to be the right course of action
it means there's probably an issue that needs to be addressed and silencing the problem just makes it harder to track down
makes sense, at the very least a warning should be logged somewhere and maybe someone gets an alert
i was thinking of leaving the event store open, so another producer could add a new type of event at any point
but that would break my application then if i returned an error
for a more rigorous approach, I recommend http://erlang.org/download/armstrong_thesis_2003.pdf very highly
my point is that your application is already broken
ignoring the error doesn't fix it
haha 🙂 thats a fair point, added it to the reading list thanks
I was working on a mobile game where you run a virtual restaurant. if the game received a recipe it didn't recognize, rather than crashing , it just used the first recipe in the list of recipes. one day, we pushed an incorrect config and all our player's dishes turned into veggie burritos
🌯 🌯
i have to admit i'm not sure what the best practice is here in the event sourcing case, i'll need to look into it
You can always reify exceptions and treat them as events, by which I mean you can catch them, call Throwable->map
on them, and transform them into events -- if you want.
ooh thats a nice idea
I have quite a bit of code that does something similar to (try body (catch Throwable t {:exception (Throwable->map t)}))
Then you can pass it around like data and still tell exceptions from "real" data.