Fork me on GitHub
#code-reviews
<
2020-06-30
>
seancorfield18:06:15

(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).

seancorfield18:06:47

Look at multimethods as a more idiomatic way to write apply-event -- since it is essentially polymorphic on :event/name

seancorfield18:06:01

(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?

enrico.teterra18:06:00

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

seancorfield19:06:49

That sounds good. Having an anonymous spec just for s/valid? seemed a bit odd.

seancorfield18:06:05

Overall, nice and clean and easy to read. Good job!

smith.adriane18:06:28

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)))

smith.adriane18:06:28

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)))

enrico.teterra19:06:27

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

smith.adriane19:06:43

apply-event combines validation and the event processing. my intuition is that those should be separate

smith.adriane19:06:10

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

enrico.teterra19:06:16

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

enrico.teterra19:06:26

i'll have a think where i could put it

smith.adriane19:06:27

what's the reluctance to throwing an error?

enrico.teterra19:06:35

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

enrico.teterra19:06:56

i prefer to reserve exceptions for things that should stop the application

smith.adriane19:06:19

you could apply-events return some error-value rather than throwing an exception

smith.adriane19:06:51

the issue with ignoring invalid events is that ignoring events is unlikely to be the right course of action

smith.adriane19:06:16

it means there's probably an issue that needs to be addressed and silencing the problem just makes it harder to track down

enrico.teterra19:06:55

makes sense, at the very least a warning should be logged somewhere and maybe someone gets an alert

enrico.teterra19:06:15

i was thinking of leaving the event store open, so another producer could add a new type of event at any point

enrico.teterra19:06:41

but that would break my application then if i returned an error

smith.adriane19:06:55

for a more rigorous approach, I recommend http://erlang.org/download/armstrong_thesis_2003.pdf very highly

smith.adriane19:06:17

my point is that your application is already broken

smith.adriane19:06:26

ignoring the error doesn't fix it

enrico.teterra19:06:03

haha 🙂 thats a fair point, added it to the reading list thanks

smith.adriane19:06:57

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

enrico.teterra19:06:59

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

seancorfield19:06:44

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.

enrico.teterra19:06:40

ooh thats a nice idea

seancorfield19:06:48

I have quite a bit of code that does something similar to (try body (catch Throwable t {:exception (Throwable->map t)}))

seancorfield19:06:16

Then you can pass it around like data and still tell exceptions from "real" data.

dominicm08:07:21

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

seancorfield18:06:12

Oh, good catch! I missed that completely.

smith.adriane18:06:31

not sure whether it's good practice or not, but I often have comments for else statements in conds and cases

smith.adriane18:06:08

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

seancorfield19:06:58

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.

smith.adriane19:06:37

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.

seancorfield19:06:44

^ although adding error handling throughout the app is whole 'nother layer of complexity which is probably a learning exercise in its own right 🙂

smith.adriane19:06:30

right. the simplest error handling implementation would be to simply remove the case's else statement and let the generic error handling take over

smith.adriane19:06:13

it's not the best error handling, but at least it would indicate something went wrong to the API's user

enrico.teterra19:06:18

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

enrico.teterra19:06:34

thanks for the pointers! really appreciate the help