pointfreeco / episode-code-samples

💾 Point-Free episode code.
https://www.pointfree.co
MIT License
973 stars 294 forks source link

[Question] Why Actions are nested? #55

Closed ghost closed 3 years ago

ghost commented 4 years ago

Hi! I watched a few episodes regarding app architecture so I probably miss something. But my question is why actions in your architecture are nested? JS Redux uses global actions and they are plain for a reason.

Here is an example from your code: .offlineCounterView(.primeModal(.saveFavoritePrimeTapped))

This is breaking the Law of Demeter which leads to tight coupling and makes refactoring pretty hard (you will always have to refactor N-1 and N+1 levels). Also how it will be possible to handle some action on more than one reducer? Let's say logOutAction - it should probably be handled by different reducers.

Thanks in advance!

mbrandonw commented 4 years ago

Hi there @Ankoma22.

Actions are nested because it aids in modularity. For example, if you have:

enum AppAction {
 case screen1(Screen1Action)
 case screen2(Screen2Action)
}

Then the domain, reducer and view for screen 1 and 2 can each be broken out into their own modules with no dependencies on each other, and they can be pulled back and combined into a main app reducer that powers the entire application. This makes it possible to run those screens in isolation (without building the full app), and can help with compile times as well as many other benefits.

Redux may use global actions, but that could also be due to the fact that JS does not have proper enums, and so they really have no choice. You certainly can use global actions in the architecture by creating a "marker" protocol that your actions conform to:

protocol Action {}

struct MyState { ... }
struct MyAction: Action { ... }

func reducer(state: inout MyState, action: Action) {
  switch action {
  case _ as MyAction:
    // Do things with the action here
    break

  default:
    // Do nothing here?
    break
  }
}

We do not recommend this approach since it makes composition more difficult (you can't pullback), and you lose exhaustivity of handling actions. Notice that we have a default in the switch, and it's not clear what we should do there. Should it be an error if that ever executes? Exhaustivity means we never have to worry about actions coming in that we don't expect, and means we can refactor actions in far, deep corners of the code base and understand how those changes impact the entire application.

And we don't believe that nested actions goes against the law of Demeter. That law is really only applicable to reference types, where knowledge of far away units can cause you to make incorrect assumptions about how the system behaves. Value types don't have this problem, they are just inert data with no behavior.

Hope that helps clear up some of your questions!