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

👍 3
seancorfield18:06:47

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

👍 3
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!

thanks2 3
🙂 3
phronmophobic18: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

phronmophobic19:06:43

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

phronmophobic19: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

🙌 3
👍 3
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

phronmophobic19: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

phronmophobic19:06:19

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

👍 3
phronmophobic19:06:51

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

phronmophobic19: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

phronmophobic19:06:55

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

phronmophobic19:06:17

my point is that your application is already broken

phronmophobic19: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

phronmophobic19: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

😆 6
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.

😍 3
🤯 3
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.

phronmophobic18:06:31

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

phronmophobic18: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.

👍 3
phronmophobic19: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 🙂

phronmophobic19: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

phronmophobic19: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