Hey there,

While reading about refactoring to collections in another programming language recently, I was curious how the concept could be applied in Dart.

Consider the following example which is taken from a recently published cqrs library:



part of 'cqrs_example.dart' ; /// /// AccountAggregate, a unit for data manipulation and management of /// consistency(*1) using the account model as aggregate root. /// class AccountAggregate extends Aggregate < Account > { final String name = 'account' ; @override Account initializeModel () => Account (); /// /// Applies domain events to the account domain model emitted by the commands /// @override Future < void > apply ( Account model , DomainEvent event ) async { if ( event is AccountCreatedEvent ) { model . id = event . id ; model . owner = event . owner ; model . amount = 0.0 ; } else if ( event is DepositPerformedEvent ) { model . amount += event . amount ; } else if ( event is WithdrawalPerformedEvent ) { model . amount -= event . amount ; } else { throw Exception ( "Unknown event!" ); } } /// /// Handles commands to the account domain model /// @override Future < void > handleCommand ( Command cmd , Account model , CommandOutput out ) async { if ( cmd is CreateAccountCmd ) { if ( model . id != null ) { out . setError ( "Model with id ${cmd.modelId} already exists!" ); return ; } out . addEvent ( AccountCreatedEvent ( id: cmd . modelId , owner: cmd . owner )); } else if ( cmd is DepositCmd ) { out . addEvent ( DepositPerformedEvent ( id: cmd . modelId , amount: cmd . amount )); } else if ( cmd is WithdrawCmd ) { if ( model . amount < cmd . amount ) { out . setError ( "Not enough balance!" ); return ; } out . addEvent ( WithdrawalPerformedEvent ( id: cmd . modelId , amount: cmd . amount )); return ; } else { throw UnsupportedError ( cmd . runtimeType . toString ()); } } }

The class AccountAggregate exposes two public methods to handle commands and apply domain events. Both methods heavily rely on conditional checks to perform the desired business logic.

Imagine we want to add more commands and events in the future resulting in a bigger and more unreadable if/else trunk. How can we beautify this?

Lookup tables to the rescue:



part of 'cqrs_example.dart' ; class AccountAggregate extends Aggregate < Account > { final String name = 'account' ; final Map < String , Function > _events = { " $AccountCreatedEvent " : _applyAccountCreated , " $DepositPerformedEvent " : _applyDepositPerformed , " $WithdrawalPerformedEvent " : _applyWithdrawalPerformed , }; final Map < String , Function > _commands = { " $CreateAccountCmd " : _handleCreateAccount , " $DepositCmd " : _handleDeposit , " $WithdrawCmd " : _handleWithdraw , }; @override Account initializeModel () => Account (); @override Future < void > apply ( Account model , DomainEvent event ) async => _events . containsKey ( event . runtimeType . toString ()) ? await Function . apply ( _events [ event . runtimeType . toString ()], [ model , event ]) : throw Exception ( "Unknown event!" ); @override Future < void > handleCommand ( Command cmd , Account model , CommandOutput out ) async => _commands . containsKey ( cmd . runtimeType . toString ()) ? await Function . apply ( _commands [ cmd . runtimeType . toString ()], [ cmd , model , out ]) : throw UnsupportedError ( cmd . runtimeType . toString ()); static Future < void > _handleCreateAccount ( CreateAccountCmd cmd , Account model , CommandOutput out ) async { if ( model . id != null ) { out . setError ( "Model with id ${cmd.modelId} already exists!" ); return ; } out . addEvent ( AccountCreatedEvent ( id: cmd . modelId , owner: cmd . owner )); } static Future < void > _handleDeposit ( DepositCmd cmd , Account model , CommandOutput out ) async { out . addEvent ( DepositPerformedEvent ( id: cmd . modelId , amount: cmd . amount )); } static Future < void > _handleWithdraw ( WithdrawCmd cmd , Account model , CommandOutput out ) async { if ( model . amount < cmd . amount ) { out . setError ( "Not enough balance!" ); return ; } out . addEvent ( WithdrawalPerformedEvent ( id: cmd . modelId , amount: cmd . amount )); } static Future < void > _applyAccountCreated ( Account model , AccountCreatedEvent event ) async { model . id = event . id ; model . owner = event . owner ; model . amount = 0.0 ; } static Future < void > _applyDepositPerformed ( Account model , DepositPerformedEvent event ) async { model . amount += event . amount ; } static Future < void > _applyWithdrawalPerformed ( Account model , WithdrawalPerformedEvent event ) async { model . amount -= event . amount ; } }

In the refactored version two maps are introduced for each possible commands and the corresponding events, using a separate method for a specific command to be handled and event to be applied.

This will not only make it easier to add new commands and events, we are also able to mock and test every command and event method on its own.

Feedback and thoughts appreciated :)

P.S.: Thanks Ravi Teja Gudapati for the cqrs library!

Links

(*1) NoSQL Distilled: A Brief Guide to the Emerging World of Polyglot Persistence