Closed henry-hz closed 7 years ago
Roughly you would look at turning your Commands
into effects. The following structures can be converts into effects with defeffect
:
defmodule Commands do
defmodule OpenAccount, do: defstruct [:account_number, :initial_balance]
defmodule DepositMoney, do: defstruct [:account_number, :transfer_uuid, :amount]
defmodule WithdrawMoney, do: defstruct [:account_number, :transfer_uuid, :amount]
defmodule CloseAccount, do: defstruct [:account_number]
end
defeffect open_account (account_number) do
%OpenAccount{account_number: account_number, initial_balance: 0}
end
# ...
You would do similar things with DepositMoney
, CloseAccount
and so forth. Once you have the effects created you can start composing them together. Here is an example that opens an account, deposits money into it, and then, if the account balance is less than 15, closes it.
# pure, ~> and ~>> are from the effects library
action = open_account(1234) ~> deposit_money(1234, 10.22) ~>> fn account ->
if account.balance < 15 do
close_account(account.id)
else
pure(account)
end
end
The action
you have is just an effect that represents a sequence of things. No "dangerous" code has actually taken place. You then need to build an interpreter for your effects (i.e. what does DepositMoney
mean).
# queue_apply is from the effects library
def interpret(%Effect{effect: %OpenAccount{account_id: account_id}, next: next}) do
new_account = %Account{id: account_id, balance: 0}
interpret(queue_apply(next, new_account))
end
def interpret(%Effect{effect: %DepositMoney{account_id: id, amount: a}, next: next}) do
# TODO: Get previous account from DB or test env or whatever
previous_account = get_account(id)
updated_account = {previous_account | balance: previous_account.balance + a}
interpret(queue_apply(next, updated_account))
end
# ...
You need to define what action is taken on each effect. There are lots of fancier ways to define your interpret
function, examples of which can found in the test folder in the project. 😄
Once you have an interpreter you can send your effects/actions through it.
final_account = interpret(action)
You can then define interpreters for any kind of behaviour on your effects that you wish, whether it be effects that are no-ops for testing or calls to databases or whatever.
NOTE: This library while fairly complete isn't necessarily super polished. You may find some odd things. e.g. queue_apply
isn't the most appropriately named. Best source of knowledge is the tests folder.
As an additional resource you may find the following repository useful: https://github.com/metalabdesign/effects-workshop and its corresponding slides: https://docs.google.com/presentation/d/1JwS5ZrjnFucpMSRyjIt5Sd_rHmUJoyIKN3vGJa8YrzA/edit?usp=sharing
@izaakschroeder thanks for the complete and fast feedback. the example you sent is very good for multi-scenario modeling with gherkin,
action = open_account(1234) ~> deposit_money(1234, 10.22) ~>> fn account ->
if account.balance < 15 do
close_account(account.id)
else
pure(account)
end
end
but, from the CRQR/ES implementation, we must have the pattern "command" -> create a new "event" -> apply(event) into one defmodule scope i.e. we will use this structure into gen_servers to receive commands from command handlers. See how commanded run the domain model. For complex business applications, a semantic boundary protects the conceptual model from ambiguities, and a 'lego' application is built, using eventual consistency to update different contexts or services around. the point is that all the business logic should be into the aggregate scope. @slashdotdash made another lib, eventsourced that solves it partially, but it's not used in commanded, once the 'update' function is too verbose, he decided to move this into the gen_server, and use this
defp apply_events(aggregate_module, aggregate_state, events)do Enum.reduce(events, aggregate_state, &aggregate_module.apply(&2, &1)) end
to replay events and rebuild the state. if we could 'point' to aggregate_module.apply and let that scope apply the events to change the aggregate state, using effects, would be the ideal solution, pls, if you can help us with that :100:
this is the Monadex suggestion: https://github.com/rob-brown/MonadEx/issues/6 in CQRS/ES, Events are the cause of state mutation, and commands are causing events to be created
The event handlers just map 1:1 to interpreters in this kind of scenario. Except instead of using gen_server
, an interpreter keeps track of its own state. You can observe this pattern in the following example:
def interpret(
previous_state,
%Effect{effect: %OpenAccount{account_id: account_id}, next: next}
) do
new_account = %Account{id: account_id, balance: 0}
next_state = %{previous_state | accounts: [new_account]}
interpret(next_state, queue_apply(next, new_account))
end
if we could 'point' to aggregate_module.apply and let that scope apply the events to change the aggregate state, using effects
I'm not sure what this means. What you're referring to seems to be analogous to a "process manager" here: https://github.com/slashdotdash/commanded#process-managers. As far as effects are concerned, this can be modelled as another interpreter exactly as above.
def apply(
%TransferMoneyProcessManager{} = transfer,
%MoneyTransferRequested{
transfer_uuid: transfer_uuid,
debit_account: debit_account,
credit_account: credit_account,
amount: amount
}) do
%TransferMoneyProcessManager{transfer |
transfer_uuid: transfer_uuid,
debit_account: debit_account,
credit_account: credit_account,
amount: amount,
status: :withdraw_money_from_debit_account
}
end
Becomes:
def interpret(
local_state,
%Effect{next: next, effect: %MoneyTransferRequested{
transfer_uuid: transfer_uuid,
debit_account: debit_account,
credit_account: credit_account,
amount: amount
}}
) do
next_state = %{local_state |
transfer_uuid: transfer_uuid,
debit_account: debit_account,
credit_account: credit_account,
amount: amount,
status: :withdraw_money_from_debit_account
}
interpret(next_state, queue_apply(next, some_result_here))
end
yes, this I got it, the problem is that we are implementing using the
def apply(
%TransferMoneyProcessManager{} = transfer,
%MoneyTransferRequested{
transfer_uuid: transfer_uuid,
debit_account: debit_account,
credit_account: credit_account,
amount: amount
}) do
%TransferMoneyProcessManager{transfer |
transfer_uuid: transfer_uuid,
debit_account: debit_account,
credit_account: credit_account,
amount: amount,
status: :withdraw_money_from_debit_account
}
end
because only once this code should be written, and this is how the aggregate_instance is handling them. we are looking for a way to be possible to write this aggregates in a way that they continue to be a pure data structure (without macros, monads, etc...), only defstruct + apply, as you see here . So we can have one team dealing with the business modeling + testing, and the same 'business modeling' will be pasted into the 'side effects' environment. In less words, we can't refactor commanded to support monads, we have to make a work around to inject aggregates and process_manager data structure with their respective apply's into 'effects' and have a 'pure' environment to test. in the future, a GUI could make diagrams and simulate this business models.
I guess a sensible thing to do in this case might be just to make another implementation of a gen_server
for testing that responds to all the same messages? I'm not sure of other options given the desire not to change the interface commanded
is using. You could then configure it to respond to messages deterministically using the instance's initial state which seems reasonable enough, bypassing the need for any kind of monad. It just means that your gen_server
should only be responsible for executing actions and not doing business logic (analog to the interpreter here). You can configure which gen_server
your app uses with @variables
. e.g. @manager System.get_env(:myapp, :manager_type)
and then set :manager_type
to be SomeModule.TestServer
or SomeModule.RealServer
where appropriate.
As far as testing going you could create additional handlers on your mock gen_server
instance to respond back to the caller with the server's current state and to programatically alter/inject the server's state.
You could make the state an effect itself but I don't think that would be very useful – just another layer of unnecessary abstraction since whatever the server returns to caller's would then also have to be an effect and I imagine the callee wants to do something useful with the data.
Yes, this is the same path suggested by @slatdodash, the commanded author, to build a black box for system testing and modeling. I will still looking for an opportunity to use effect in our projects :smiley:
No worries happy to help! 😄
Dear @izaakschroeder , amazing lib! please, how should we use Effects to test Aggregate Roots data structures (that contains an apply function to change it's state) ?
see: https://github.com/slashdotdash/commanded/blob/master/test/example_domain/bank_account/bank_account.ex
We would like to make a complete pure test framework for Aggregates and Process Managers.
I have isolated and organized them in a Pure CQRS/ES project, https://github.com/work-capital/ev_sim , pls, see account aggregate