fraktalio / fmodel

Functional, Algebraic and Reactive domain modeling with Kotlin (Multiplatform)
https://fraktalio.com/fmodel/
Other
253 stars 11 forks source link

Composition, pipelines between abstractions #117

Closed darky closed 2 years ago

darky commented 2 years ago

Hello @idugalic! Awesome work! :+1:

Abstractions, provided by this library, so good on their own. But feels lack of effective way how to compose abstractions together in common pipeline.

For example, I want to pass some AR to Saga and then receive flow of A And then, use this flow of A as events for multiple View for evolving their State And then, so on... Something like:


pipeline(
  saga.react(AR),
  evolveView(view1, view2, ...),
  catch {e -> ...}, // maybe some error will be occured, need to handle
  // and so on...
)

:point_up: It's just example for View and Saga. Composition should work with any direction with any abstraction

Sources of inspiration:


https://effector.dev/docs/api/effector/effector Effector provides only 4 abstractions: Event, Effect, Store and Domain, and huge count of utils how to compose abstractions with each other


https://gcanti.github.io/fp-ts/modules/ fp-ts provides Haskell like ADT and many ways how compose one data type to another


https://github.com/ReactiveX/RxJava RxJava provides huge count of operators how to compose one Observable to another

idugalic commented 2 years ago

Hi @darky, I'm glad you like it!

I suggest reading the series of blog posts I wrote to get a better impression of how to compose a business pipeline/flow out of these three components (Decider, View, Saga): https://fraktalio.com/blog/ (you can start from the first). Second post is very interesting, teaching us how to structure the various data of our domain (commands, events, state) as ADTs (Kotin Sealed inheritance is effectively modeling OR/SUM, and data classes are modeling AND/PRODUCT).

Decider

The decider is a data type that represents the main decision-making algorithm. These functions are available on the Decider component. The most interesting one is a combine function which is effectively making a Decider a Monoid. By using this combine function you can merge/aggregate/combine two (or more) deciders into one bigger decider.

The logic execution will be orchestrated (composed and combined) by the outside (application layer) components that use the domain components to do the computations. This project is proposing these two application layer components:

Saga

The saga is usually used for mapping different events from one 'decider' into the commands of other 'decider'.

Similar functions/methods exist on the Saga as well. combine included, making it a monoid.

View

The view component represents the Query model of the CQRS pattern. It is usually used with event sourcing (in the case of Event-sourcing aggregate) because Event-sourcing aggregate is modeling the Command side only. In the case of State-stored aggregate, the View is not needed because in this traditional approach you model the command and the query side altogether.

Similar functions/methods exist on the Saga as well. combine included, making it a monoid.

Similar to Decider, we have an 'application layer' component available( materialized view ) enabling you to compose and combine diferent View components/computations under it.

To your example:

Let's imagine someone is streaming to you Events of some sealed type MyEvent.

Let's imagine you have to View components modeled independently: View1<MyState1, MyEvent1?>and View2<MyState2, MyEvent2?>. Notice that: MyEvent1 extends MyEvent and MyEvent2 extends MyEvent.

Now you can construct a materialized view by using this function:

fun <S, E> materializedView(
    view: IView<S, E>,
    viewStateRepository: ViewStateRepository<E, S>,
): MaterializedView<S, E> =
    object : MaterializedView<S, E>, ViewStateRepository<E, S> by viewStateRepository, IView<S, E> by view {}

in where the view parameter is going to be: view = view1.combine(view2).

Saga is not needed in this case. The MaterializedView is going to subscribe to these events (streamed via Kafka, for example) and delegate them to your combination of View components in a type-safe way (with exhaustive pattern matching in place).

You can also use other methods that are defined on the View: mapLeftOnEvent (Contravariant) or dimapOnState (Profunctor).

view = view1.combine(view2).mapLeftOnEvent().dimapOnState

I hope to write more about this (time is always an issue :) )

This library should also help with the transition from Traditional (State-stored) to Event Sourced information systems.

It makes sense to consider our opinionated application module at first. We are showing the way how these 'domain' components could be combined and composed, taking into account two contexts: state-stored and event-sourced

This library depends on Kotlin Coroutines and the Flow (you might compare that to https://github.com/ReactiveX/RxJava), and it is optimized for streaming from the start (backpressure included).

It is way easier to create async and concurrent programs in Kotlin than in Java (so far), IMHO. Creating custom Flow operators is easy ;)

Best.

idugalic commented 2 years ago

@darky I am closing this issue. Thanks for the feedback!

Feel free to initiate further discussions on https://github.com/fraktalio/fmodel/discussions