collectiveidea / interactor

Interactor provides a common interface for performing complex user interactions.
MIT License
3.36k stars 212 forks source link

Recursive workflow in organizer #97

Closed mengqing closed 7 years ago

mengqing commented 9 years ago

How would I organize organizers if my business logic is as follows

  1. Task A
  2. Task B
  3. If [2] true then Task C, else Task D
  4. If Task C true then Task B and recur step [2], else Task D
mingan commented 9 years ago

We have some specific flows in our codebase as well and interactors help with a lot of them. There are however some operations which are not that obvious and one of them is recursion.

For your example, it's apparent that tasks B+C+D form a logical unit. I assume they're complex enough that it makes sense to make them separate interactors instead of a single one which would encapsulate the if-else logic and the recursion. Therefore I'd group them in an organizer.

Going from there, I don't see any easy way to avoid independence of the individual interactors. I'd simply write a value into the context in B and check for that value in C and D. You can still test them separately nicely (because you control the context) but it's dangerous because there's no protection of the context in between - someone can later come, put a new interactor in the middle and that might overwrite the value, so be aware of that.

For recursion you'll need another interactor in the process, which will call the B+C+D organizer again. There two key questions here: is the context the same or does it change? and how granular are any data written to the context? For example in our application, we have a tree structure of objects and need to bubble some operations from a leaf to the root. In that case, it's good, that we change the context every time. In your case, you might need to run everything in the same context and that will be harder (but I can't imagine when this would happen and if interactors are good fit for that problem). The interactor in charge of recursion needs to handle the output written to the context. Generally we don't use it very often, but you'll probably want to propagate at least failure of the interactors and possibly pass some errors up. This, however, heavily depends on your usecase and the granularity of the data.

Personally, I don't feel really good about passing flags or data controlling the flow through the context, but it definitely beats some other options we're getting away from. Maybe the authors of the library or someone else might have a better and cleaner suggestion.

jamesconant commented 8 years ago

tl;dr vol. 1: not all logic needs to live in an interactor

If you, or at this point some other developer - as this issue is 5 months old, comes across this sort of problem and determine that recursion is the right approach, keep in mind that you don't need to store all actual business logic in the Interactor / Organizer structure and thus solely rely on them alone.

For example, In your case you might try encapsulating all of the recursive business in a handful of smaller service classes, which are then coordinated by either another model, service, or interactor. The point is, you don't have to do all business logic control flow in the Interactor / Organizer pattern. Just the bits that are pragmatically useful and structurally reasonable.

tl;dr vol. 2: recursion is hard.

I would be curious what the specific case being solved is. More specifically, is this a case where recursion is actually the best (or even a good) approach to a solution. Much less meta-programming and regex, using these tools in cases where they aren't clearly and defensibly the best solution (not just 'a' solution) leads to a world of pain for not only the current developer but all those who come down the line later only to discover, Here There Be :dragon:.

Pedantry aside, a recursive organizer structure seems a little practically pragmatic in this case. For instance, @Mingan mentions error message handing, but there is also I think a larger consideration of the (very handy) transactional nature of Interactors and Organizers. If some recursive branch reaches an exception or failure state, there is no clear way for this recursive tree to unwind itself and undo everything that should be undone. This is most especially true if you are over-riding state. Going a step further, I'm not even sure it would programmatically feasible to do this as you would have un-reasonable number data points to store while maintaining a recursive tree of unknown length.

Hope this helps.

laserlemon commented 7 years ago

The pending Interactor 4.0 roadmap includes the ability to pass options to the organize method which could allow you to build all sorts of conditional branching structures if it suits your use case. Closing in anticipation of those additions.